Skip to content

Core functions

Soundscapy is a Python library for soundscape analysis and visualisation.

MODULE DESCRIPTION
audio

Provides tools for working with audio signals, particularly binaural recordings.

data

Soundscape data module.

databases

Soundscapy Databases Module.

isd

Module for handling the International Soundscape Database (ISD).

iso_plot

Main module for creating circumplex plots using different backends.

likert

Plotting functions for visualizing Likert scale data.

msn

Module for handling Multi-dimensional Skewed Normal (MSN) distributions.

plotting

Soundscapy Plotting Module.

processing

Soundscape survey data processing module.

satp

Module for handling the Soundscape Attributes Translation Project (SATP) database.

spi

Soundscapy Psychoacoustic Indicator (SPI) calculation module.

sspylogging

Logging configuration for Soundscapy.

surveys

Soundscapy Surveys Package.

CLASS DESCRIPTION
AnalysisSettings

Settings for audio analysis methods.

AudioAnalysis

A class for performing psychoacoustic analysis on audio files.

Binaural

A class for processing and analyzing binaural audio signals.

CentredParams

Represents the centered parameters of a distribution.

ConfigManager

Manage configuration settings for audio analysis.

DirectParams

Represents a set of direct parameters for a statistical model.

ISOPlot

A class for creating circumplex plots using different backends.

MultiSkewNorm

A class representing a multi-dimensional skewed normal distribution.

FUNCTION DESCRIPTION
add_iso_coords

Calculate and add ISO coordinates as new columns in the DataFrame.

add_results

Add results to MultiIndex dataframe.

cp2dp

Convert centred parameters to direct parameters.

create_iso_subplots

Create a set of subplots displaying data visualizations for soundscape analysis.

density

Plot a density plot of ISOCoordinates.

disable_logging

Disable all Soundscapy logging.

dp2cp

Convert direct parameters to centred parameters.

enable_debug

Quickly enable DEBUG level logging to console.

get_logger

Get the Soundscapy logger instance.

jointplot

Create a jointplot with a central distribution and marginal plots.

paq_likert

Create a Likert scale plot for PAQ (Perceived Affective Quality) data.

paq_radar_plot

Generate a radar/spider plot of PAQ values.

parallel_process

Process multiple binaural files in parallel.

prep_multiindex_df

Prepare a MultiIndex dataframe from a dictionary of results.

process_all_metrics

Process all metrics specified in the analysis settings for a binaural signal.

rename_paqs

Rename the PAQ columns in a DataFrame to standard PAQ IDs.

scatter

Plot ISOcoordinates as scatter points on a soundscape circumplex grid.

setup_logging

Set up logging for Soundscapy with sensible defaults.

AnalysisSettings

Bases: BaseModel

Settings for audio analysis methods.

PARAMETER DESCRIPTION
version

Version of the configuration.

TYPE: str

AcousticToolbox

Settings for AcousticToolbox metrics.

TYPE: LibrarySettings | None

MoSQITo

Settings for MoSQITo metrics.

TYPE: LibrarySettings | None

scikit_maad

Settings for scikit-maad metrics.

TYPE: LibrarySettings | None

METHOD DESCRIPTION
default

Create a default AnalysisSettings using the package default configuration file.

from_dict

Create an AnalysisSettings object from a dictionary.

from_yaml

Create an AnalysisSettings object from a YAML file.

get_enabled_metrics

Get a dictionary of enabled metrics.

get_metric_settings

Get the settings for a specific metric.

to_yaml

Save the current settings to a YAML file.

update_setting

Update the settings for a specific metric.

validate_library_settings

Validate library settings.

default classmethod

default()

Create a default AnalysisSettings using the package default configuration file.

RETURNS DESCRIPTION
AnalysisSettings

An instance of AnalysisSettings with default settings.

Source code in soundscapy/audio/analysis_settings.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
@classmethod
def default(cls) -> AnalysisSettings:
    """
    Create a default AnalysisSettings using the package default configuration file.

    Returns
    -------
    AnalysisSettings
        An instance of AnalysisSettings with default settings.

    """
    config_resource = resources.files("soundscapy.data").joinpath(
        "default_settings.yaml"
    )
    with resources.as_file(config_resource) as f:
        logger.info(f"Loading default configuration from {f}")
        return cls.from_yaml(f)

from_dict classmethod

from_dict(d)

Create an AnalysisSettings object from a dictionary.

PARAMETER DESCRIPTION
d

Dictionary containing the configuration settings.

TYPE: dict

RETURNS DESCRIPTION
AnalysisSettings

An instance of AnalysisSettings.

Source code in soundscapy/audio/analysis_settings.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
@classmethod
def from_dict(cls, d: dict) -> AnalysisSettings:
    """
    Create an AnalysisSettings object from a dictionary.

    Parameters
    ----------
    d : dict
        Dictionary containing the configuration settings.

    Returns
    -------
    AnalysisSettings
        An instance of AnalysisSettings.

    """
    return cls(**d)

from_yaml classmethod

from_yaml(filepath)

Create an AnalysisSettings object from a YAML file.

PARAMETER DESCRIPTION
filepath

Path to the YAML configuration file.

TYPE: str | Path

RETURNS DESCRIPTION
AnalysisSettings

An instance of AnalysisSettings.

Source code in soundscapy/audio/analysis_settings.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
@classmethod
def from_yaml(cls, filepath: str | Path) -> AnalysisSettings:
    """
    Create an AnalysisSettings object from a YAML file.

    Parameters
    ----------
    filepath : str | Path
        Path to the YAML configuration file.

    Returns
    -------
    AnalysisSettings
        An instance of AnalysisSettings.

    """
    filepath = _ensure_path(filepath)
    logger.info(f"Loading configuration from {filepath}")
    with Path.open(filepath) as f:
        config_dict = yaml.safe_load(f)
    return cls(**config_dict)

get_enabled_metrics

get_enabled_metrics()

Get a dictionary of enabled metrics.

RETURNS DESCRIPTION
dict[str, dict[str, MetricSettings]]

A dictionary of enabled metrics grouped by library.

Source code in soundscapy/audio/analysis_settings.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
def get_enabled_metrics(self) -> dict[str, dict[str, MetricSettings]]:
    """
    Get a dictionary of enabled metrics.

    Returns
    -------
    dict[str, dict[str, MetricSettings]]
        A dictionary of enabled metrics grouped by library.

    """
    enabled_metrics = {}
    for library in ["AcousticToolbox", "MoSQITo", "scikit_maad"]:
        library_settings = getattr(self, library)
        if library_settings:
            enabled_metrics[library] = {
                metric: settings
                for metric, settings in library_settings.root.items()
                if settings.run
            }
    logger.debug(f"Enabled metrics: {enabled_metrics}")
    return enabled_metrics

get_metric_settings

get_metric_settings(library, metric)

Get the settings for a specific metric.

PARAMETER DESCRIPTION
library

The name of the library.

TYPE: str

metric

The name of the metric.

TYPE: str

RETURNS DESCRIPTION
MetricSettings

The settings for the specified metric.

RAISES DESCRIPTION
KeyError

If the specified library or metric is not found.

Source code in soundscapy/audio/analysis_settings.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
def get_metric_settings(self, library: str, metric: str) -> MetricSettings:
    """
    Get the settings for a specific metric.

    Parameters
    ----------
    library : str
        The name of the library.
    metric : str
        The name of the metric.

    Returns
    -------
    MetricSettings
        The settings for the specified metric.

    Raises
    ------
    KeyError
        If the specified library or metric is not found.

    """
    library_settings = getattr(self, library)
    if library_settings and metric in library_settings.root:
        return library_settings.root[metric]
    logger.error(f"Metric '{metric}' not found in library '{library}'")
    msg = f"Metric '{metric}' not found in library '{library}'"
    raise KeyError(msg)

to_yaml

to_yaml(filepath)

Save the current settings to a YAML file.

PARAMETER DESCRIPTION
filepath

Path to save the YAML file.

TYPE: str | Path

Source code in soundscapy/audio/analysis_settings.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def to_yaml(self, filepath: str | Path) -> None:
    """
    Save the current settings to a YAML file.

    Parameters
    ----------
    filepath : str | Path
        Path to save the YAML file.

    """
    filepath = _ensure_path(filepath)
    logger.info(f"Saving configuration to {filepath}")
    with Path.open(filepath, "w") as f:
        yaml.dump(self.model_dump(by_alias=True), f)

update_setting

update_setting(library, metric, **kwargs)

Update the settings for a specific metric.

PARAMETER DESCRIPTION
library

The name of the library.

TYPE: str

metric

The name of the metric.

TYPE: str

**kwargs

Keyword arguments to update the metric settings.

TYPE: dict DEFAULT: {}

RAISES DESCRIPTION
KeyError

If the specified library or metric is not found.

Source code in soundscapy/audio/analysis_settings.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def update_setting(self, library: str, metric: str, **kwargs: dict) -> None:
    """
    Update the settings for a specific metric.

    Parameters
    ----------
    library : str
        The name of the library.
    metric : str
        The name of the metric.
    **kwargs
        Keyword arguments to update the metric settings.

    Raises
    ------
    KeyError
        If the specified library or metric is not found.

    """
    library_settings = getattr(self, library)
    if library_settings and metric in library_settings.root:
        metric_settings = library_settings.root[metric]
        for key, value in kwargs.items():
            if hasattr(metric_settings, key):
                setattr(metric_settings, key, value)
            else:
                logger.error(f"Invalid setting '{key}' for metric '{metric}'")
    else:
        logger.error(f"Metric '{metric}' not found in library '{library}'")
        msg = f"Metric '{metric}' not found in library '{library}'"
        raise KeyError(msg)

validate_library_settings classmethod

validate_library_settings(v)

Validate library settings.

Source code in soundscapy/audio/analysis_settings.py
140
141
142
143
144
145
146
@field_validator("*", mode="before")
@classmethod
def validate_library_settings(cls, v: dict | LibrarySettings) -> LibrarySettings:
    """Validate library settings."""
    if isinstance(v, dict):
        return LibrarySettings(root=v)
    return v

AudioAnalysis

AudioAnalysis(config_path=None)

A class for performing psychoacoustic analysis on audio files.

This class provides methods to analyze single audio files or entire folders of audio files using parallel processing. It handles configuration management, calibration, and saving of analysis results.

ATTRIBUTE DESCRIPTION
config_manager

Manages the configuration settings for audio analysis

TYPE: ConfigManager

settings

The current configuration settings

TYPE: dict

METHOD DESCRIPTION
analyze_file

Analyze a single audio file

analyze_folder

Analyze all audio files in a folder using parallel processing

save_results

Save analysis results to a file

update_config

Update the current configuration

save_config

Save the current configuration to a file

Initialize the AudioAnalysis with a configuration.

PARAMETER DESCRIPTION
config_path

Path to the configuration file. If None, uses default configuration.

TYPE: str, Path, or None DEFAULT: None

Source code in soundscapy/audio/audio_analysis.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def __init__(self, config_path: str | Path | None = None) -> None:
    """
    Initialize the AudioAnalysis with a configuration.

    Parameters
    ----------
    config_path : str, Path, or None
        Path to the configuration file. If None, uses default configuration.

    """
    self.config_manager = ConfigManager(config_path)
    self.settings = self.config_manager.load_config()
    logger.info(
        f"Psychoacoustic analysis initialized with configuration: {config_path}"
    )

analyze_file

analyze_file(file_path, calibration_levels=None, resample=None)

Analyze a single audio file using the current configuration.

PARAMETER DESCRIPTION
resample

TYPE: int | None DEFAULT: None

file_path

Path to the audio file to analyze.

TYPE: str or Path

calibration_levels

Dictionary containing calibration levels for left and right channels.

TYPE: dict DEFAULT: None

RETURNS DESCRIPTION
DataFrame

DataFrame containing the analysis results.

Source code in soundscapy/audio/audio_analysis.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def analyze_file(
    self,
    file_path: str | Path,
    calibration_levels: dict[str, float] | list[float] | None = None,
    resample: int | None = None,
) -> pd.DataFrame:
    """
    Analyze a single audio file using the current configuration.

    Parameters
    ----------
    resample
    file_path : str or Path
        Path to the audio file to analyze.
    calibration_levels : dict, optional
        Dictionary containing calibration levels for left and right channels.

    Returns
    -------
    pd.DataFrame
        DataFrame containing the analysis results.

    """
    file_path = ensure_input_path(file_path)

    logger.info(f"Analyzing file: {file_path}")
    return load_analyse_binaural(
        file_path,
        calibration_levels,
        self.settings,
        resample=resample,
    )

analyze_folder

analyze_folder(folder_path, calibration_file=None, max_workers=None, resample=None)

Analyze all audio files in a folder using parallel processing.

PARAMETER DESCRIPTION
resample

TYPE: int | None DEFAULT: None

folder_path

Path to the folder containing audio files.

TYPE: str or Path

calibration_file

Path to a JSON file containing calibration levels for each audio file.

TYPE: str or Path DEFAULT: None

max_workers

Maximum number of worker processes to use. If None, it will use the number of CPU cores.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
DataFrame

DataFrame containing the analysis results for all files.

Source code in soundscapy/audio/audio_analysis.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
@logger.catch
def analyze_folder(
    self,
    folder_path: str | Path,
    calibration_file: str | Path | None = None,
    max_workers: int | None = None,
    resample: int | None = None,
) -> pd.DataFrame:
    """
    Analyze all audio files in a folder using parallel processing.

    Parameters
    ----------
    resample
    folder_path : str or Path
        Path to the folder containing audio files.
    calibration_file : str or Path, optional
        Path to a JSON file containing calibration levels for each audio file.
    max_workers : int, optional
        Maximum number of worker processes to use.
        If None, it will use the number of CPU cores.

    Returns
    -------
    pd.DataFrame
        DataFrame containing the analysis results for all files.

    """
    folder_path = ensure_input_path(folder_path)
    audio_files = list(folder_path.glob("*.wav"))

    logger.info(
        f"Analyzing folder: {folder_path.name} of {len(audio_files)}"
        f"files in parallel (max_workers={max_workers})"
    ) if max_workers else logger.info(
        f"Analyzing folder: {folder_path}, {len(audio_files)} files"
    )

    calibration_levels = {}
    if calibration_file:
        calibration_file = ensure_input_path(calibration_file)
        with calibration_file.open() as f:
            calibration_levels = json.load(f)
        logger.debug(f"Loaded calibration levels from: {calibration_file}")

    all_results = []
    with ProcessPoolExecutor(max_workers=max_workers) as executor:
        futures = []
        for file in audio_files:
            future = executor.submit(
                load_analyse_binaural,
                file,
                calibration_levels,
                self.settings,
                resample,
                parallel_mosqito=False,
            )
            futures.append(future)

        for future in tqdm(
            as_completed(futures), total=len(futures), desc="Analyzing files"
        ):
            try:
                result = future.result()
                all_results.append(result)
            except Exception as e:  # noqa: BLE001, PERF203
                logger.error(f"Error processing file: {e!s}")

    combined_results = pd.concat(all_results)
    logger.info(
        f"Completed analysis for {len(audio_files)} files in folder: {folder_path}"
    )
    return combined_results

save_config

save_config(config_path)

Save the current configuration to a file.

PARAMETER DESCRIPTION
config_path

Path to save the configuration file.

TYPE: str or Path

Source code in soundscapy/audio/audio_analysis.py
213
214
215
216
217
218
219
220
221
222
223
224
def save_config(self, config_path: str | Path) -> None:
    """
    Save the current configuration to a file.

    Parameters
    ----------
    config_path : str or Path
        Path to save the configuration file.

    """
    self.config_manager.save_config(config_path)
    logger.info(f"Configuration saved to: {config_path}")

save_results

save_results(results, output_path)

Save analysis results to a file.

PARAMETER DESCRIPTION
results

DataFrame containing the analysis results.

TYPE: DataFrame

output_path

Path to save the results file.

TYPE: str or Path

Source code in soundscapy/audio/audio_analysis.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def save_results(self, results: pd.DataFrame, output_path: str | Path) -> None:
    """
    Save analysis results to a file.

    Parameters
    ----------
    results : pd.DataFrame
        DataFrame containing the analysis results.
    output_path : str or Path
        Path to save the results file.

    """
    output_path = ensure_path_type(
        output_path
    )  # If doesn't already exist, pandas will create the file.
    if output_path.suffix == ".csv":
        results.to_csv(output_path)
    elif output_path.suffix == ".xlsx":
        results.to_excel(output_path)
    else:
        msg = "Unsupported file format. Use .csv or .xlsx"
        raise ValueError(msg)
    logger.info(f"Results saved to: {output_path}")

update_config

update_config(new_config)

Update the current configuration.

PARAMETER DESCRIPTION
new_config

Dictionary containing the new configuration settings.

TYPE: dict

Source code in soundscapy/audio/audio_analysis.py
199
200
201
202
203
204
205
206
207
208
209
210
211
def update_config(self, new_config: dict) -> "AudioAnalysis":
    """
    Update the current configuration.

    Parameters
    ----------
    new_config : dict
        Dictionary containing the new configuration settings.

    """
    self.settings = self.config_manager.merge_configs(new_config)
    logger.info("Configuration updated")
    return self

Binaural

Bases: Signal

A class for processing and analyzing binaural audio signals.

This class extends the Signal class from the acoustic_toolbox library to provide specialized functionality for binaural recordings. It supports various psychoacoustic metrics and analysis techniques using libraries such as mosqito, maad, and acoustic_toolbox.

ATTRIBUTE DESCRIPTION
fs

Sampling frequency of the signal.

TYPE: float

recording

Name or identifier of the recording.

TYPE: str

Notes

This class only supports 2-channel (stereo) audio signals.

METHOD DESCRIPTION
__array_finalize__

Finalize the new Binaural object.

__new__

Create a new Binaural object.

acoustics_metric

Run a metric from the acoustic_toolbox library.

calibrate_to

Calibrate the binaural signal to predefined Leq/dB levels.

from_wav

Load a wav file and return a Binaural object.

fs_resample

Resample the signal to a new sampling frequency.

maad_metric

Run a metric from the scikit-maad library.

mosqito_metric

Run a metric from the mosqito library.

process_all_metrics

Process all metrics specified in the analysis settings.

pyacoustics_metric

Run a metric from the pyacoustics library (deprecated).

__array_finalize__

__array_finalize__(obj)

Finalize the new Binaural object.

This method is called for all new Binaural objects.

PARAMETER DESCRIPTION
obj

The object from which the new object was created.

TYPE: Binaural or None

Source code in soundscapy/audio/binaural.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def __array_finalize__(self, obj: "Binaural | None") -> None:
    """
    Finalize the new Binaural object.

    This method is called for all new Binaural objects.

    Parameters
    ----------
    obj : Binaural or None
        The object from which the new object was created.

    """
    if obj is None:
        return
    self.fs = getattr(obj, "fs", None)
    self.recording = getattr(obj, "recording", "Rec")

__new__

__new__(data, fs, recording='Rec')

Create a new Binaural object.

PARAMETER DESCRIPTION
data

The audio data.

TYPE: array_like

fs

Sampling frequency of the signal.

TYPE: float

recording

Name or identifier of the recording. Default is "Rec".

TYPE: str DEFAULT: 'Rec'

RETURNS DESCRIPTION
Binaural

A new Binaural object.

RAISES DESCRIPTION
ValueError

If the input signal is not 2-channel.

Source code in soundscapy/audio/binaural.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def __new__(
    cls, data: np.ndarray, fs: float | None, recording: str = "Rec"
) -> "Binaural":
    """
    Create a new Binaural object.

    Parameters
    ----------
    data : array_like
        The audio data.
    fs : float
        Sampling frequency of the signal.
    recording : str, optional
        Name or identifier of the recording. Default is "Rec".

    Returns
    -------
    Binaural
        A new Binaural object.

    Raises
    ------
    ValueError
        If the input signal is not 2-channel.

    """
    obj = super().__new__(cls, data, fs).view(cls)
    obj.recording = recording
    if obj.channels != ALLOWED_BINAURAL_CHANNELS:
        logger.error(
            f"Attempted to create Binaural object with {obj.channels} channels"
        )
        msg = "Binaural class only supports 2 channels."
        raise ValueError(msg)
    logger.debug(f"Created new Binaural object: {recording}, fs={fs}")
    return obj

acoustics_metric

acoustics_metric(metric, statistics=(5, 10, 50, 90, 95, 'avg', 'max', 'min', 'kurt', 'skew'), label=None, channel=('Left', 'Right'), metric_settings=None, func_args=None, *, as_df=True, return_time_series=False)

Run a metric from the acoustic_toolbox library.

PARAMETER DESCRIPTION
metric

The metric to run.

TYPE: (LZeq, Leq, LAeq, LCeq, SEL) DEFAULT: "LZeq"

statistics

List of level statistics to calculate (e.g. L_5, L_90, etc.). Default is (5, 10, 50, 90, 95, "avg", "max", "min", "kurt", "skew").

TYPE: tuple or list DEFAULT: (5, 10, 50, 90, 95, 'avg', 'max', 'min', 'kurt', 'skew')

label

Label to use for the metric. If None, will pull from default label for that metric.

TYPE: str DEFAULT: None

channel

Which channels to process. Default is ("Left", "Right").

TYPE: tuple, list, or str DEFAULT: ('Left', 'Right')

as_df

Whether to return a dataframe or not. Default is True. If True, returns a MultiIndex Dataframe with ("Recording", "Channel") as the index.

TYPE: bool DEFAULT: True

return_time_series

Whether to return the time series of the metric. Default is False. Cannot return time series if as_df is True.

TYPE: bool DEFAULT: False

metric_settings

Settings for metric analysis. Default is None.

TYPE: MetricSettings DEFAULT: None

func_args

Any settings given here will override those in the other options. Can pass any args or *kwargs to the underlying acoustic_toolbox method.

TYPE: dict DEFAULT: None

RETURNS DESCRIPTION
dict or DataFrame

Dictionary of results if as_df is False, otherwise a pandas DataFrame.

See Also

metrics.acoustics_metric acoustic_toolbox.standards_iso_tr_25417_2007.equivalent_sound_pressure_level : Base method for Leq calculation. acoustic_toolbox.standards.iec_61672_1_2013.sound_exposure_level : Base method for SEL calculation. acoustic_toolbox.standards.iec_61672_1_2013.time_weighted_sound_level : Base method for Leq level time series calculation.

Source code in soundscapy/audio/binaural.py
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
def acoustics_metric(
    self,
    metric: Literal["LZeq", "Leq", "LAeq", "LCeq", "SEL"],
    statistics: tuple | list = (
        5,
        10,
        50,
        90,
        95,
        "avg",
        "max",
        "min",
        "kurt",
        "skew",
    ),
    label: str | None = None,
    channel: str | int | list | tuple = ("Left", "Right"),
    metric_settings: MetricSettings | None = None,
    func_args: dict | None = None,
    *,
    as_df: bool = True,
    return_time_series: bool = False,
) -> dict | pd.DataFrame | None:
    """
    Run a metric from the acoustic_toolbox library.

    Parameters
    ----------
    metric : {"LZeq", "Leq", "LAeq", "LCeq", "SEL"}
        The metric to run.
    statistics : tuple or list, optional
        List of level statistics to calculate (e.g. L_5, L_90, etc.).
        Default is (5, 10, 50, 90, 95, "avg", "max", "min", "kurt", "skew").
    label : str, optional
        Label to use for the metric.
        If None, will pull from default label for that metric.
    channel : tuple, list, or str, optional
        Which channels to process. Default is ("Left", "Right").
    as_df : bool, optional
        Whether to return a dataframe or not. Default is True.
        If True, returns a MultiIndex Dataframe with
        ("Recording", "Channel") as the index.
    return_time_series : bool, optional
        Whether to return the time series of the metric. Default is False.
        Cannot return time series if as_df is True.
    metric_settings : MetricSettings, optional
        Settings for metric analysis. Default is None.
    func_args : dict, optional
        Any settings given here will override those in the other options.
        Can pass any *args or **kwargs to the underlying acoustic_toolbox method.

    Returns
    -------
    dict or pd.DataFrame
        Dictionary of results if as_df is False, otherwise a pandas DataFrame.

    See Also
    --------
    metrics.acoustics_metric
    acoustic_toolbox.standards_iso_tr_25417_2007.equivalent_sound_pressure_level :
        Base method for Leq calculation.
    acoustic_toolbox.standards.iec_61672_1_2013.sound_exposure_level :
        Base method for SEL calculation.
    acoustic_toolbox.standards.iec_61672_1_2013.time_weighted_sound_level :
        Base method for Leq level time series calculation.

    """
    if func_args is None:
        func_args = {}
    if metric_settings:
        logger.debug("Using provided analysis settings")
        if not metric_settings.run:
            logger.info(f"Metric {metric} is disabled in analysis settings")
            return None

        channel = metric_settings.channel
        statistics = metric_settings.statistics
        label = metric_settings.label
        func_args = metric_settings.func_args

    channel = ("Left", "Right") if channel is None else channel
    s = self._get_channel(channel)

    if s.channels == 1:
        logger.debug("Processing single channel")
        return acoustics_metric_1ch(
            s, metric, statistics, label, as_df, return_time_series, func_args
        )
    logger.debug("Processing both channels")
    return acoustics_metric_2ch(
        s,
        metric,
        statistics,
        label,
        channel,
        as_df,
        return_time_series,
        func_args,
    )

calibrate_to

calibrate_to(decibel, inplace=False)

Calibrate the binaural signal to predefined Leq/dB levels.

This method allows calibration of both channels either to the same level or to different levels for each channel.

PARAMETER DESCRIPTION
decibel

Target calibration value(s) in dB (Leq). If a single value is provided, both channels will be calibrated to this level. If two values are provided, they will be applied to the left and right channels respectively.

TYPE: float or List[float] or Tuple[float, float]

inplace

If True, modify the signal in place. If False, return a new calibrated signal. Default is False.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Binaural

Calibrated Binaural signal. If inplace is True, returns self.

RAISES DESCRIPTION
ValueError

If decibel is not a float, or a list/tuple of two floats.

Examples:

>>> # xdoctest: +SKIP
>>> signal = Binaural.from_wav("audio.wav")
>>> # Calibrate left channel to 60 dB and right to 62 dB
>>> calibrated_signal = signal.calibrate_to([60, 62])
Source code in soundscapy/audio/binaural.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def calibrate_to(
    self,
    decibel: float | list[float] | tuple[float, float] | np.ndarray | pd.Series,
    inplace: bool = False,  # noqa: FBT001, FBT002 TODO(MitchellAcoustics): Change to keyword-only in acoustic_toolbox.Signal
) -> "Binaural":
    """
    Calibrate the binaural signal to predefined Leq/dB levels.

    This method allows calibration of both channels either to the same level
    or to different levels for each channel.

    Parameters
    ----------
    decibel : float or List[float] or Tuple[float, float]
        Target calibration value(s) in dB (Leq).
        If a single value is provided, both channels will be calibrated
        to this level.
        If two values are provided, they will be applied to the left and right
        channels respectively.
    inplace : bool, optional
        If True, modify the signal in place.
        If False, return a new calibrated signal.
        Default is False.

    Returns
    -------
    Binaural
        Calibrated Binaural signal. If inplace is True, returns self.

    Raises
    ------
    ValueError
        If decibel is not a float, or a list/tuple of two floats.

    Examples
    --------
    >>> # xdoctest: +SKIP
    >>> signal = Binaural.from_wav("audio.wav")
    >>> # Calibrate left channel to 60 dB and right to 62 dB
    >>> calibrated_signal = signal.calibrate_to([60, 62])

    """
    logger.info(f"Calibrating Binaural signal to {decibel} dB")
    if isinstance(decibel, np.ndarray | pd.Series):  # Force into tuple
        decibel = tuple(decibel)
    if isinstance(decibel, list | tuple):
        if (
            len(decibel) == ALLOWED_BINAURAL_CHANNELS
        ):  # Per-channel calibration (recommended)
            logger.debug(
                "Calibrating channels separately: "
                f"Left={decibel[0]}dB, Right={decibel[1]}dB"
            )
            decibel = np.array(decibel)
            decibel = decibel[..., None]
            return super().calibrate_to(decibel, inplace)  # type: ignore[reportReturnType]
        if (
            len(decibel) == 1
        ):  # if one value given in tuple, assume same for both channels
            logger.debug(f"Calibrating both channels to {decibel[0]}dB")
            decibel = decibel[0]
        else:
            logger.error(f"Invalid calibration value: {decibel}")
            msg = "decibel must either be a single value or a 2 value tuple"
            raise TypeError(msg)
    if isinstance(decibel, int | float):  # Calibrate both channels to same value
        logger.debug(f"Calibrating both channels to {decibel}dB")
        return super().calibrate_to(decibel, inplace)  # type: ignore[reportReturnType]
    logger.error(f"Invalid calibration value: {decibel}")
    msg = "decibel must be a single value or a 2 value tuple"
    raise TypeError(msg)

from_wav classmethod

from_wav(filename, normalize=False, calibrate_to=None, resample=None, recording=None)

Load a wav file and return a Binaural object.

Overrides the Signal.from_wav method to return a Binaural object instead of a Signal object.

PARAMETER DESCRIPTION
filename

Filename of wav file to load.

TYPE: Path or str

calibrate_to

Value(s) to calibrate to in dB (Leq). Can also handle np.ndarray and pd.Series of length 2. If only one value is passed, will calibrate both channels to the same value.

TYPE: float or List or Tuple DEFAULT: None

normalize

Whether to normalize the signal. Default is False.

TYPE: bool DEFAULT: False

resample

New sampling frequency to resample the signal to. Default is None

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
Binaural

Binaural signal object of wav recording.

See Also

acoustic_toolbox.Signal.from_wav : Base method for loading wav files.

Source code in soundscapy/audio/binaural.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
@classmethod
def from_wav(
    cls,
    filename: Path | str,
    normalize: bool = False,  # noqa: FBT001, FBT002
    calibrate_to: float | list | tuple | None = None,
    resample: int | None = None,
    recording: str | None = None,
) -> "Binaural":
    """
    Load a wav file and return a Binaural object.

    Overrides the Signal.from_wav method to return a
    Binaural object instead of a Signal object.

    Parameters
    ----------
    filename : Path or str
        Filename of wav file to load.
    calibrate_to : float or List or Tuple, optional
        Value(s) to calibrate to in dB (Leq).
        Can also handle np.ndarray and pd.Series of length 2.
        If only one value is passed, will calibrate both channels to the same value.
    normalize : bool, optional
        Whether to normalize the signal. Default is False.
    resample : int, optional
        New sampling frequency to resample the signal to. Default is None

    Returns
    -------
    Binaural
        Binaural signal object of wav recording.

    See Also
    --------
    acoustic_toolbox.Signal.from_wav : Base method for loading wav files.

    """
    filename = ensure_input_path(filename)
    if not filename.exists():
        logger.error(f"File not found: {filename}")
        msg = f"File not found: {filename}"
        raise FileNotFoundError(msg)

    logger.info(f"Loading WAV file: {filename}")
    fs, data = wavfile.read(filename)
    data = data.astype(np.float32, copy=False).T
    if normalize:
        data /= np.max(np.abs(data))

    recording = recording if recording is not None else Path(filename).stem
    b = cls(data, fs, recording=recording)

    if calibrate_to is not None:
        logger.info(f"Calibrating loaded signal to {calibrate_to} dB")
        b.calibrate_to(calibrate_to, inplace=True)
    if resample is not None:
        logger.debug(f"Resampling loaded signal to {resample} Hz")
        b = b.fs_resample(resample)
    return b

fs_resample

fs_resample(fs, original_fs=None)

Resample the signal to a new sampling frequency.

PARAMETER DESCRIPTION
fs

New sampling frequency.

TYPE: float

original_fs

Original sampling frequency. If None, it will be inferred from the signal (Binaural.fs).

TYPE: float or None DEFAULT: None

RETURNS DESCRIPTION
Binaural

Resampled Binaural signal. If inplace is True, returns self.

See Also

acoustic_toolbox.Signal.resample : Base method for resampling signals.

Source code in soundscapy/audio/binaural.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def fs_resample(
    self,
    fs: float,
    original_fs: float | None = None,
) -> "Binaural":
    """
    Resample the signal to a new sampling frequency.

    Parameters
    ----------
    fs : float
        New sampling frequency.
    original_fs : float or None, optional
        Original sampling frequency.
        If None, it will be inferred from the signal (`Binaural.fs`).

    Returns
    -------
    Binaural
        Resampled Binaural signal. If inplace is True, returns self.

    See Also
    --------
    acoustic_toolbox.Signal.resample : Base method for resampling signals.

    """
    current_fs: float

    if original_fs is None:
        if hasattr(self, "fs") and self.fs is not None:
            current_fs = self.fs
        else:
            logger.error("Original sampling frequency not provided.")
            msg = "Original sampling frequency not provided."
            raise ValueError(msg)
    else:
        current_fs = original_fs

    if fs == current_fs:
        logger.info(f"Signal already at {current_fs} Hz. No resampling needed.")
        return self

    logger.info(f"Resampling signal to {fs} Hz")
    resampled_channels = [
        scipy.signal.resample(channel, int(fs * len(channel) / current_fs))
        for channel in self
    ]
    resampled_channels = np.stack(resampled_channels)
    return Binaural(resampled_channels, fs, recording=self.recording)

maad_metric

maad_metric(metric, channel=('Left', 'Right'), as_df=True, metric_settings=None, func_args={})

Run a metric from the scikit-maad library.

Currently only supports running all of the alpha indices at once.

PARAMETER DESCRIPTION
metric

The metric to run.

TYPE: (all_temporal_alpha_indices, all_spectral_alpha_indices) DEFAULT: "all_temporal_alpha_indices"

channel

Which channels to process. Default is ("Left", "Right").

TYPE: (tuple, list or str) DEFAULT: ('Left', 'Right')

as_df

Whether to return a dataframe or not. Default is True. If True, returns a MultiIndex Dataframe with ("Recording", "Channel") as the index.

TYPE: bool DEFAULT: True

metric_settings

Settings for metric analysis. Default is None.

TYPE: MetricSettings DEFAULT: None

func_args

Additional arguments to pass to the underlying scikit-maad method.

TYPE: dict DEFAULT: {}

RETURNS DESCRIPTION
dict or DataFrame

Dictionary of results if as_df is False, otherwise a pandas DataFrame.

RAISES DESCRIPTION
ValueError

If metric name is not recognised.

See Also

metrics.maad_metric_1ch metrics.maad_metric_2ch

Source code in soundscapy/audio/binaural.py
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
def maad_metric(
    self,
    metric: str,
    channel: int | tuple | list | str = ("Left", "Right"),
    as_df: bool = True,
    metric_settings: MetricSettings | None = None,
    func_args: dict = {},
) -> dict | pd.DataFrame:
    """
    Run a metric from the scikit-maad library.

    Currently only supports running all of the alpha indices at once.

    Parameters
    ----------
    metric : {"all_temporal_alpha_indices", "all_spectral_alpha_indices"}
        The metric to run.
    channel : tuple, list or str, optional
        Which channels to process. Default is ("Left", "Right").
    as_df : bool, optional
        Whether to return a dataframe or not. Default is True.
        If True, returns a MultiIndex Dataframe with ("Recording", "Channel") as the index.
    metric_settings : MetricSettings, optional
        Settings for metric analysis. Default is None.
    func_args : dict, optional
        Additional arguments to pass to the underlying scikit-maad method.

    Returns
    -------
    dict or pd.DataFrame
        Dictionary of results if as_df is False, otherwise a pandas DataFrame.

    Raises
    ------
    ValueError
        If metric name is not recognised.

    See Also
    --------
    metrics.maad_metric_1ch
    metrics.maad_metric_2ch

    """
    logger.info(f"Running maad metric: {metric}")
    if metric_settings:
        logger.debug("Using provided analysis settings")
        if metric not in {
            "all_temporal_alpha_indices",
            "all_spectral_alpha_indices",
        }:
            logger.error(f"Invalid maad metric: {metric}")
            raise ValueError(f"Metric {metric} not recognised")

        if not metric_settings.run:
            logger.info(f"Metric {metric} is disabled in analysis settings")
            return None

        channel = metric_settings.channel
    channel = ("Left", "Right") if channel is None else channel
    s = self._get_channel(channel)
    if s.channels == 1:
        logger.debug("Processing single channel")
        return maad_metric_1ch(s, metric, as_df)
    logger.debug("Processing both channels")
    return maad_metric_2ch(s, metric, channel, as_df, func_args)

mosqito_metric

mosqito_metric(metric, statistics=(5, 10, 50, 90, 95, 'avg', 'max', 'min', 'kurt', 'skew'), label=None, channel=('Left', 'Right'), as_df=True, return_time_series=False, parallel=True, metric_settings=None, func_args={})

Run a metric from the mosqito library.

PARAMETER DESCRIPTION
metric

Metric to run from mosqito library.

TYPE: (loudness_zwtv, sharpness_din_from_loudness, sharpness_din_perseg, sharpness_tv, roughness_dw) DEFAULT: "loudness_zwtv"

statistics

List of level statistics to calculate (e.g. L_5, L_90, etc.). Default is (5, 10, 50, 90, 95, "avg", "max", "min", "kurt", "skew").

TYPE: tuple or list DEFAULT: (5, 10, 50, 90, 95, 'avg', 'max', 'min', 'kurt', 'skew')

label

Label to use for the metric. If None, will pull from default label for that metric.

TYPE: str DEFAULT: None

channel

Which channels to process. Default is ("Left", "Right").

TYPE: tuple or list of str or str DEFAULT: ('Left', 'Right')

as_df

Whether to return a dataframe or not. Default is True. If True, returns a MultiIndex Dataframe with ("Recording", "Channel") as the index.

TYPE: bool DEFAULT: True

return_time_series

Whether to return the time series of the metric. Default is False. Cannot return time series if as_df is True.

TYPE: bool DEFAULT: False

parallel

Whether to run the channels in parallel. Default is True. If False, will run each channel sequentially.

TYPE: bool DEFAULT: True

metric_settings

Settings for metric analysis. Default is None.

TYPE: MetricSettings DEFAULT: None

func_args

Any settings given here will override those in the other options. Can pass any args or *kwargs to the underlying acoustic_toolbox method.

TYPE: dict DEFAULT: {}

RETURNS DESCRIPTION
dict or DataFrame

Dictionary of results if as_df is False, otherwise a pandas DataFrame.

See Also

binaural.mosqito_metric_2ch : Method for running metrics on 2 channels. binaural.mosqito_metric_1ch : Method for running metrics on 1 channel.

Source code in soundscapy/audio/binaural.py
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
def mosqito_metric(
    self,
    metric: str,
    statistics: tuple | list = (
        5,
        10,
        50,
        90,
        95,
        "avg",
        "max",
        "min",
        "kurt",
        "skew",
    ),
    label: str | None = None,
    channel: int | tuple | list | str = ("Left", "Right"),
    as_df: bool = True,
    return_time_series: bool = False,
    parallel: bool = True,
    metric_settings: MetricSettings | None = None,
    func_args: dict = {},
) -> dict | pd.DataFrame:
    """
    Run a metric from the mosqito library.

    Parameters
    ----------
    metric : {"loudness_zwtv", "sharpness_din_from_loudness", "sharpness_din_perseg", "sharpness_tv", "roughness_dw"}
        Metric to run from mosqito library.
    statistics : tuple or list, optional
        List of level statistics to calculate (e.g. L_5, L_90, etc.).
        Default is (5, 10, 50, 90, 95, "avg", "max", "min", "kurt", "skew").
    label : str, optional
        Label to use for the metric. If None, will pull from default label for that metric.
    channel : tuple or list of str or str, optional
        Which channels to process. Default is ("Left", "Right").
    as_df : bool, optional
        Whether to return a dataframe or not. Default is True.
        If True, returns a MultiIndex Dataframe with ("Recording", "Channel") as the index.
    return_time_series : bool, optional
        Whether to return the time series of the metric. Default is False.
        Cannot return time series if as_df is True.
    parallel : bool, optional
        Whether to run the channels in parallel. Default is True.
        If False, will run each channel sequentially.
    metric_settings : MetricSettings, optional
        Settings for metric analysis. Default is None.
    func_args : dict, optional
        Any settings given here will override those in the other options.
        Can pass any *args or **kwargs to the underlying acoustic_toolbox method.

    Returns
    -------
    dict or pd.DataFrame
        Dictionary of results if as_df is False, otherwise a pandas DataFrame.

    See Also
    --------
    binaural.mosqito_metric_2ch : Method for running metrics on 2 channels.
    binaural.mosqito_metric_1ch : Method for running metrics on 1 channel.

    """
    logger.info(f"Running mosqito metric: {metric}")
    if metric_settings:
        logger.debug("Using provided analysis settings")
        if not metric_settings.run:
            logger.info(f"Metric {metric} is disabled in analysis settings")
            return None

        channel = metric_settings.channel
        statistics = metric_settings.statistics
        label = metric_settings.label
        parallel = metric_settings.parallel
        func_args = metric_settings.func_args

    channel = ("Left", "Right") if channel is None else channel
    s = self._get_channel(channel)

    if s.channels == 1:
        logger.debug("Processing single channel")
        return mosqito_metric_1ch(
            s,
            metric,
            statistics,
            label,
            as_df=as_df,
            return_time_series=return_time_series,
            **func_args,
        )
    logger.debug("Processing both channels")
    return mosqito_metric_2ch(
        s,
        metric,
        statistics,
        label,
        channel,
        as_df=as_df,
        return_time_series=return_time_series,
        parallel=parallel,
        func_args=func_args,
    )

process_all_metrics

process_all_metrics(analysis_settings=AnalysisSettings.default(), parallel=True)

Process all metrics specified in the analysis settings.

This method runs all enabled metrics from the provided AnalysisSettings object and compiles the results into a single DataFrame.

PARAMETER DESCRIPTION
analysis_settings

Configuration object specifying which metrics to run and their parameters.

TYPE: AnalysisSettings DEFAULT: default()

parallel

Whether to run calculations in parallel where possible. Default is True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
DataFrame

A MultiIndex DataFrame containing the results of all processed metrics. The index includes "Recording" and "Channel" levels.

Notes

The parallel option primarily affects the MoSQITo metrics. Other metrics may not benefit from parallelization.

TODO: Provide default settings to analysis_settings to make it optional.

Examples:

>>> # xdoctest: +SKIP
>>> signal = Binaural.from_wav("audio.wav")
>>> settings = AnalysisSettings.from_yaml("settings.yaml")
>>> results = signal.process_all_metrics(settings)
Source code in soundscapy/audio/binaural.py
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def process_all_metrics(
    self,
    analysis_settings: AnalysisSettings = AnalysisSettings.default(),
    parallel: bool = True,
) -> pd.DataFrame:
    """
    Process all metrics specified in the analysis settings.

    This method runs all enabled metrics from the provided AnalysisSettings object
    and compiles the results into a single DataFrame.

    Parameters
    ----------
    analysis_settings : AnalysisSettings
        Configuration object specifying which metrics to run and their parameters.
    parallel : bool, optional
        Whether to run calculations in parallel where possible. Default is True.

    Returns
    -------
    pd.DataFrame
        A MultiIndex DataFrame containing the results of all processed metrics.
        The index includes "Recording" and "Channel" levels.

    Notes
    -----
    The parallel option primarily affects the MoSQITo metrics. Other metrics may not benefit from parallelization.

    TODO: Provide default settings to analysis_settings to make it optional.

    Examples
    --------
    >>> # xdoctest: +SKIP
    >>> signal = Binaural.from_wav("audio.wav")
    >>> settings = AnalysisSettings.from_yaml("settings.yaml")
    >>> results = signal.process_all_metrics(settings)

    """
    logger.info(f"Processing all metrics for {self.recording}")
    logger.debug(f"Parallel processing: {parallel}")
    return process_all_metrics(self, analysis_settings, parallel)

pyacoustics_metric

pyacoustics_metric(metric, statistics=(5, 10, 50, 90, 95, 'avg', 'max', 'min', 'kurt', 'skew'), label=None, channel=('Left', 'Right'), as_df=True, return_time_series=False, metric_settings=None, func_args=None)

Run a metric from the pyacoustics library (deprecated).

This method has been deprecated. Use acoustics_metric instead. All parameters are passed directly to acoustics_metric.

PARAMETER DESCRIPTION
metric

The metric to run.

TYPE: (LZeq, Leq, LAeq, LCeq, SEL) DEFAULT: "LZeq"

statistics

List of level statistics to calculate (e.g. L_5, L_90, etc.). Default is (5, 10, 50, 90, 95, "avg", "max", "min", "kurt", "skew").

TYPE: tuple or list DEFAULT: (5, 10, 50, 90, 95, 'avg', 'max', 'min', 'kurt', 'skew')

label

Label to use for the metric. If None, will pull from default label for that metric.

TYPE: str DEFAULT: None

channel

Which channels to process. Default is ("Left", "Right").

TYPE: tuple, list, or str DEFAULT: ('Left', 'Right')

as_df

Whether to return a dataframe or not. Default is True. If True, returns a MultiIndex Dataframe with ("Recording", "Channel") as the index.

TYPE: bool DEFAULT: True

return_time_series

Whether to return the time series of the metric. Default is False. Cannot return time series if as_df is True.

TYPE: bool DEFAULT: False

metric_settings

Settings for metric analysis. Default is None.

TYPE: MetricSettings DEFAULT: None

func_args

Any settings given here will override those in the other options. Can pass any args or *kwargs to the underlying acoustic_toolbox method.

TYPE: dict DEFAULT: None

RETURNS DESCRIPTION
dict or DataFrame

Results of the metric calculation.

See Also

Binaural.acoustics_metric

Source code in soundscapy/audio/binaural.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def pyacoustics_metric(
    self,
    metric: Literal["LZeq", "Leq", "LAeq", "LCeq", "SEL"],
    statistics: tuple | list = (
        5,
        10,
        50,
        90,
        95,
        "avg",
        "max",
        "min",
        "kurt",
        "skew",
    ),
    label: str | None = None,
    channel: str | int | list | tuple = ("Left", "Right"),
    as_df: bool = True,  # noqa: FBT001, FBT002
    return_time_series: bool = False,  # noqa: FBT001, FBT002
    metric_settings: MetricSettings | None = None,
    func_args: dict | None = None,
) -> dict | pd.DataFrame | None:
    """
    Run a metric from the pyacoustics library (deprecated).

    This method has been deprecated. Use `acoustics_metric` instead.
    All parameters are passed directly to `acoustics_metric`.

    Parameters
    ----------
    metric : {"LZeq", "Leq", "LAeq", "LCeq", "SEL"}
        The metric to run.
    statistics : tuple or list, optional
        List of level statistics to calculate (e.g. L_5, L_90, etc.).
        Default is (5, 10, 50, 90, 95, "avg", "max", "min", "kurt", "skew").
    label : str, optional
        Label to use for the metric.
        If None, will pull from default label for that metric.
    channel : tuple, list, or str, optional
        Which channels to process. Default is ("Left", "Right").
    as_df : bool, optional
        Whether to return a dataframe or not. Default is True.
        If True, returns a MultiIndex Dataframe with
        ("Recording", "Channel") as the index.
    return_time_series : bool, optional
        Whether to return the time series of the metric. Default is False.
        Cannot return time series if as_df is True.
    metric_settings : MetricSettings, optional
        Settings for metric analysis. Default is None.
    func_args : dict, optional
        Any settings given here will override those in the other options.
        Can pass any *args or **kwargs to the underlying acoustic_toolbox method.

    Returns
    -------
    dict or pd.DataFrame
        Results of the metric calculation.

    See Also
    --------
    Binaural.acoustics_metric

    """
    if func_args is None:
        func_args = {}
    warnings.warn(
        "pyacoustics has been deprecated. Use acoustics_metric instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    return self.acoustics_metric(
        metric,
        statistics,
        label,
        channel,
        as_df=as_df,
        return_time_series=return_time_series,
        metric_settings=metric_settings,
        func_args=func_args,
    )

CentredParams

CentredParams(mean, sigma, skew)

Represents the centered parameters of a distribution.

PARAMETER DESCRIPTION
mean

The mean of the distribution.

TYPE: float

sigma

The standard deviation of the distribution.

TYPE: float

skew

The skewness of the distribution.

TYPE: float

ATTRIBUTE DESCRIPTION
mean

The mean of the distribution.

TYPE: float

sigma

The standard deviation of the distribution.

TYPE: float

skew

The skewness of the distribution.

TYPE: float

METHOD DESCRIPTION
from_dp

Converts DirectParams object to CentredParams object.

Initialize CentredParams instance.

Source code in soundscapy/spi/msn.py
161
162
163
164
165
def __init__(self, mean: np.ndarray, sigma: np.ndarray, skew: np.ndarray) -> None:
    """Initialize CentredParams instance."""
    self.mean = mean
    self.sigma = sigma
    self.skew = skew

__repr__

__repr__()

Return a string representation of the CentredParams object.

Source code in soundscapy/spi/msn.py
167
168
169
def __repr__(self) -> str:
    """Return a string representation of the CentredParams object."""
    return f"CentredParams(mean={self.mean}, sigma={self.sigma}, skew={self.skew})"

__str__

__str__()

Return a user-friendly string representation of the CentredParams object.

Source code in soundscapy/spi/msn.py
171
172
173
174
175
176
177
178
def __str__(self) -> str:
    """Return a user-friendly string representation of the CentredParams object."""
    return (
        f"Centred Parameters:"
        f"\nmean:  {self.mean.round(3)}"
        f"\nsigma: {self.sigma.round(3)}"
        f"\nskew:  {self.skew.round(3)}"
    )

from_dp classmethod

from_dp(dp)

Convert a DirectParams object to a CentredParams object.

PARAMETER DESCRIPTION
dp

The DirectParams object to convert.

TYPE: DirectParams

RETURNS DESCRIPTION
CentredParams

A new CentredParams object with the converted parameters.

Source code in soundscapy/spi/msn.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@classmethod
def from_dp(cls, dp: DirectParams) -> "CentredParams":
    """
    Convert a DirectParams object to a CentredParams object.

    Parameters
    ----------
    dp : DirectParams
        The DirectParams object to convert.

    Returns
    -------
    CentredParams
        A new CentredParams object with the converted parameters.

    """
    cp = dp2cp(dp)
    return cls(cp.mean, cp.sigma, cp.skew)

ConfigManager

ConfigManager(config_path=None)

Manage configuration settings for audio analysis.

PARAMETER DESCRIPTION
default_config_path

Path to the default configuration file.

TYPE: str | Path | None

METHOD DESCRIPTION
generate_minimal_config

Generate a minimal configuration containing only changes from the default.

load_config

Load a configuration file or use the default configuration.

merge_configs

Merge the current config with override values and update the current_config.

save_config

Save the current configuration to a file.

Source code in soundscapy/audio/analysis_settings.py
316
317
318
def __init__(self, config_path: str | Path | None = None) -> None:  # noqa: D107
    self.config_path = _ensure_path(config_path) if config_path else None
    self.current_config: AnalysisSettings | None = None

generate_minimal_config

generate_minimal_config()

Generate a minimal configuration containing only changes from the default.

RETURNS DESCRIPTION
dict

A dictionary containing the minimal configuration.

RAISES DESCRIPTION
ValueError

If no current configuration is loaded.

Source code in soundscapy/audio/analysis_settings.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def generate_minimal_config(self) -> dict:
    """
    Generate a minimal configuration containing only changes from the default.

    Returns
    -------
    dict
        A dictionary containing the minimal configuration.

    Raises
    ------
    ValueError
        If no current configuration is loaded.

    """
    if not self.current_config:
        msg = "No current configuration loaded."
        raise ValueError(msg)
    default_config = AnalysisSettings.default()
    current_dict = self.current_config.model_dump()
    default_dict = default_config.model_dump()
    return self._get_diff(current_dict, default_dict)

load_config

load_config(config_path=None)

Load a configuration file or use the default configuration.

PARAMETER DESCRIPTION
config_path

Path to the configuration file. If None, uses the default configuration.

TYPE: str | Path | None DEFAULT: None

RETURNS DESCRIPTION
AnalysisSettings

The loaded configuration.

Source code in soundscapy/audio/analysis_settings.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
def load_config(self, config_path: str | Path | None = None) -> AnalysisSettings:
    """
    Load a configuration file or use the default configuration.

    Parameters
    ----------
    config_path : str | Path | None, optional
        Path to the configuration file. If None, uses the default configuration.

    Returns
    -------
    AnalysisSettings
        The loaded configuration.

    """
    if config_path:
        logger.info(f"Loading configuration from {config_path}")
        self.current_config = AnalysisSettings.from_yaml(config_path)
    elif self.config_path:
        logger.info(f"Loading configuration from {self.config_path}")
        self.current_config = AnalysisSettings.from_yaml(self.config_path)
    else:
        logger.info("Loading default configuration")
        self.current_config = AnalysisSettings.default()
    return self.current_config

merge_configs

merge_configs(override_config)

Merge the current config with override values and update the current_config.

PARAMETER DESCRIPTION
override_config

Dictionary containing override configuration values.

TYPE: dict

RETURNS DESCRIPTION
AnalysisSettings

The merged configuration.

RAISES DESCRIPTION
ValueError

If no base configuration is loaded.

Source code in soundscapy/audio/analysis_settings.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
def merge_configs(self, override_config: dict) -> AnalysisSettings:
    """
    Merge the current config with override values and update the current_config.

    Parameters
    ----------
    override_config : dict
        Dictionary containing override configuration values.

    Returns
    -------
    AnalysisSettings
        The merged configuration.

    Raises
    ------
    ValueError
        If no base configuration is loaded.

    """
    if not self.current_config:
        logger.error("No base configuration loaded")
        msg = "No base configuration loaded."
        raise ValueError(msg)
    logger.info("Merging configurations")
    merged_dict = self.current_config.model_dump()
    self._deep_update(merged_dict, override_config)
    merged_config = AnalysisSettings(**merged_dict)
    self.current_config = merged_config  # Update the current_config
    return merged_config

save_config

save_config(filepath)

Save the current configuration to a file.

PARAMETER DESCRIPTION
filepath

Path to save the configuration file.

TYPE: str | Path

RAISES DESCRIPTION
ValueError

If no current configuration is loaded.

Source code in soundscapy/audio/analysis_settings.py
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def save_config(self, filepath: str | Path) -> None:
    """
    Save the current configuration to a file.

    Parameters
    ----------
    filepath : str | Path
        Path to save the configuration file.

    Raises
    ------
    ValueError
        If no current configuration is loaded.

    """
    if self.current_config:
        logger.info(f"Saving configuration to {filepath}")
        self.current_config.to_yaml(filepath)
    else:
        logger.error("No current configuration to save")
        msg = "No current configuration to save."
        raise ValueError(msg)

DirectParams

DirectParams(xi, omega, alpha)

Represents a set of direct parameters for a statistical model.

Direct parameters are the parameters that are directly used in the model. They are the parameters that are used to define the distribution of the data. In the case of a skew normal distribution, the direct parameters are the xi, omega, and alpha values.

PARAMETER DESCRIPTION
xi

The location of the distribution in 2D space, represented as a 2x1 array with the x and y coordinates.

TYPE: ndarray

omega

The covariance matrix of the distribution, represented as a 2x2 array. The covariance matrix represents the measure of the relationship between different variables. It provides information about how changes in one variable are associated with changes in other variables.

TYPE: ndarray

alpha

The shape parameters for the x and y dimensions, controlling the shape (skewness) of the distribution. It is represented as a 2x1 array.

TYPE: ndarray

Initialize DirectParams instance.

METHOD DESCRIPTION
__repr__

Return a string representation of the DirectParams object.

__str__

Return a user-friendly string representation of the DirectParams object.

from_cp

Convert a CentredParams object to a DirectParams object.

validate

Validate the direct parameters.

Source code in soundscapy/spi/msn.py
48
49
50
51
52
53
def __init__(self, xi: np.ndarray, omega: np.ndarray, alpha: np.ndarray) -> None:
    """Initialize DirectParams instance."""
    self.xi = xi
    self.omega = omega
    self.alpha = alpha
    self.validate()

__repr__

__repr__()

Return a string representation of the DirectParams object.

Source code in soundscapy/spi/msn.py
55
56
57
def __repr__(self) -> str:
    """Return a string representation of the DirectParams object."""
    return f"DirectParams(xi={self.xi}, omega={self.omega}, alpha={self.alpha})"

__str__

__str__()

Return a user-friendly string representation of the DirectParams object.

Source code in soundscapy/spi/msn.py
59
60
61
62
63
64
65
66
def __str__(self) -> str:
    """Return a user-friendly string representation of the DirectParams object."""
    return (
        f"Direct Parameters:"
        f"\nxi:    {self.xi.round(3)}"
        f"\nomega: {self.omega.round(3)}"
        f"\nalpha: {self.alpha.round(3)}"
    )

from_cp classmethod

from_cp(cp)

Convert a CentredParams object to a DirectParams object.

PARAMETER DESCRIPTION
cp

The CentredParams object to convert.

TYPE: CentredParams

RETURNS DESCRIPTION
DirectParams

A new DirectParams object with the converted parameters.

Source code in soundscapy/spi/msn.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@classmethod
def from_cp(cls, cp: "CentredParams") -> "DirectParams":
    """
    Convert a CentredParams object to a DirectParams object.

    Parameters
    ----------
    cp : CentredParams
        The CentredParams object to convert.

    Returns
    -------
    DirectParams
        A new DirectParams object with the converted parameters.

    """
    warnings.warn(
        "Converting from Centred Parameters to Direct Parameters "
        "is not guaranteed.",
        UserWarning,
        stacklevel=2,
    )  # TODO(MitchellAcoustics): Add a more specific warning message  # noqa: TD003
    dp = cp2dp(cp)
    return cls(dp.xi, dp.omega, dp.alpha)

validate

validate()

Validate the direct parameters.

In a skew normal distribution, the covariance matrix, often denoted as Ω (Omega), represents the measure of the relationship between different variables. It provides information about how changes in one variable are associated with changes in other variables. The covariance matrix must be positive definite and symmetric.

RAISES DESCRIPTION
ValueError

If the direct parameters are not valid.

RETURNS DESCRIPTION
None
Source code in soundscapy/spi/msn.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def validate(self) -> None:
    """
    Validate the direct parameters.

    In a skew normal distribution, the covariance matrix, often denoted as
    Ω (Omega), represents the measure of the relationship between different
    variables. It provides information about how changes in one variable are
    associated with changes in other variables. The covariance matrix must
    be positive definite and symmetric.

    Raises
    ------
    ValueError
        If the direct parameters are not valid.

    Returns
    -------
    None

    """
    if not self._omega_is_pos_def():
        msg = "Omega must be positive definite"
        raise ValueError(msg)
    if not self._omega_is_symmetric():
        msg = "Omega must be symmetric"
        raise ValueError(msg)

ISOPlot

ISOPlot(data=None, x='ISOPleasant', y='ISOEventful', title='Soundscape Density Plot', hue=None, palette='colorblind', figure=None, axes=None)

A class for creating circumplex plots using different backends.

This class provides methods for creating scatter plots and density plots based on the circumplex model of soundscape perception.

Examples:

>>> from soundscapy import isd, surveys
>>> df = isd.load()
>>> df = surveys.add_iso_coords(df)
>>> ct = isd.select_location_ids(df, ["CamdenTown", "RegentsParkJapan"])
>>> cp = (ISOPlot(ct, hue="LocationID")
...         .create_subplots()
...         .add_scatter()
...         .add_density()
...         .style())
>>> cp.show() # xdoctest: +SKIP

Initialize a ISOPlot instance.

PARAMETER DESCRIPTION
data

The data to be plotted, by default None

TYPE: DataFrame | None DEFAULT: None

x

Column name or data for x-axis, by default "ISOPleasant"

TYPE: str | ndarray | Series | None DEFAULT: 'ISOPleasant'

y

Column name or data for y-axis, by default "ISOEventful"

TYPE: str | ndarray | Series | None DEFAULT: 'ISOEventful'

title

Title of the plot, by default "Soundscape Density Plot"

TYPE: str | None DEFAULT: 'Soundscape Density Plot'

hue

Column name for color encoding, by default None

TYPE: str | None DEFAULT: None

palette

Color palette to use, by default "colorblind"

TYPE: SeabornPaletteType | None DEFAULT: 'colorblind'

figure

Existing figure to plot on, by default None

TYPE: Figure | SubFigure | None DEFAULT: None

axes

Existing axes to plot on, by default None

TYPE: Axes | ndarray | None DEFAULT: None

Examples:

Create a plot with default parameters:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame(
...    rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...    columns=['ISOPleasant', 'ISOEventful']
... )
>>> plot = ISOPlot()
>>> isinstance(plot, ISOPlot)
True

Create a plot with a DataFrame:

>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> plot = ISOPlot(data=data, hue='Group')
>>> plot.hue
'Group'

Create a plot directly with arrays:

>>> x, y = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], 100).T
>>> plot = ISOPlot(x=x, y=y)
>>> isinstance(plot, ISOPlot)
True
METHOD DESCRIPTION
add_annotation

Add an annotation to the plot.

add_density

Add a density layer to specific subplot(s).

add_layer

Add a visualization layer, optionally targeting specific subplot(s).

add_scatter

Add a scatter layer to specific subplot(s).

add_simple_density

Add a simple density layer to specific subplot(s).

add_spi

Add a SPI layer to specific subplot(s).

close

Close the figure.

create_subplots

Create subplots for the circumplex plot.

get_axes

Get the axes object.

get_figure

Get the figure object.

get_single_axes

Get a specific axes object.

savefig

Save the figure.

show

Show the figure.

style

Apply styling to the plot.

yield_axes_objects

Generate a sequence of axes objects to iterate over.

ATTRIBUTE DESCRIPTION
hue

Get the hue column name.

TYPE: str | None

title

Get the plot title.

TYPE: str | None

x

Get the x-axis column name.

TYPE: str

y

Get the y-axis column name.

TYPE: str

Source code in soundscapy/plotting/iso_plot.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def __init__(
    self,
    data: pd.DataFrame | None = None,
    x: str | np.ndarray | pd.Series | None = "ISOPleasant",
    y: str | np.ndarray | pd.Series | None = "ISOEventful",
    title: str | None = "Soundscape Density Plot",
    hue: str | None = None,
    palette: SeabornPaletteType | None = "colorblind",
    figure: Figure | None = None,  # Removed SubFigure type, don't think we need it
    axes: Axes | np.ndarray | None = None,
) -> None:
    """
    Initialize a ISOPlot instance.

    Parameters
    ----------
    data : pd.DataFrame | None, optional
        The data to be plotted, by default None
    x : str | np.ndarray | pd.Series | None, optional
        Column name or data for x-axis, by default "ISOPleasant"
    y : str | np.ndarray | pd.Series | None, optional
        Column name or data for y-axis, by default "ISOEventful"
    title : str | None, optional
        Title of the plot, by default "Soundscape Density Plot"
    hue : str | None, optional
        Column name for color encoding, by default None
    palette : SeabornPaletteType | None, optional
        Color palette to use, by default "colorblind"
    figure : Figure | SubFigure | None, optional
        Existing figure to plot on, by default None
    axes : Axes | np.ndarray | None, optional
        Existing axes to plot on, by default None

    Examples
    --------
    Create a plot with default parameters:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame(
    ...    rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...    columns=['ISOPleasant', 'ISOEventful']
    ... )
    >>> plot = ISOPlot()
    >>> isinstance(plot, ISOPlot)
    True

    Create a plot with a DataFrame:

    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> plot = ISOPlot(data=data, hue='Group')
    >>> plot.hue
    'Group'


    Create a plot directly with arrays:

    >>> x, y = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], 100).T
    >>> plot = ISOPlot(x=x, y=y)
    >>> isinstance(plot, ISOPlot)
    True

    """
    warnings.warn(
        "`ISOPlot` is currently under development and should be considered "
        "experimental. `ISOPlot` implements an experimental API for creating "
        "layered soundscape circumplex plots. Use with caution.",
        ExperimentalWarning,
        stacklevel=2,
    )

    # Process and validate input data and coordinates
    data, x, y = self._check_data_x_y(data, x, y)
    self._check_data_hue(data, hue)

    # Initialize the main plot context
    self.main_context = PlotContext(
        data=data,
        x=x if isinstance(x, str) else DEFAULT_XCOL,
        y=y if isinstance(y, str) else DEFAULT_YCOL,
        hue=hue,
        title=title,
    )

    # Store additional plot attributes
    self.figure = figure
    self.axes = axes
    self.palette = palette

    # Initialize subplot management
    self.subplot_contexts: list[PlotContext] = []
    self.subplots_params = SubplotsParams()

    # Initialize parameter managers
    self._scatter_params = ScatterParams(
        data=data,
        x=self.main_context.x,
        y=self.main_context.y,
        hue=hue,
        palette=self.palette,
    )

    self._density_params = DensityParams(
        data=data,
        x=self.main_context.x,
        y=self.main_context.y,
        hue=hue,
        palette=self.palette,
    )

    self._simple_density_params = SimpleDensityParams(
        data=data,
        x=self.main_context.x,
        y=self.main_context.y,
        hue=hue,
    )

    self._spi_scatter_params = NotImplementedError
    self._spi_density_params = NotImplementedError
    self._spi_simple_density_params = SPISimpleDensityParams(
        x=self.main_context.x,
        y=self.main_context.y,
    )

    self._style_params = StyleParams()

    # SPI-related attributes
    self._spi_data = None

hue property

hue

Get the hue column name.

title property

title

Get the plot title.

x property

x

Get the x-axis column name.

y property

y

Get the y-axis column name.

add_annotation

add_annotation(text, xy, xytext, arrowprops=None)

Add an annotation to the plot.

PARAMETER DESCRIPTION
text

The text to display in the annotation.

TYPE: str

xy

The point to annotate.

TYPE: tuple[float, float]

xytext

The point at which to place the text.

TYPE: tuple[float, float]

arrowprops

Properties for the arrow connecting the annotation text to the point.

TYPE: dict[str, Any] | None DEFAULT: None

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Source code in soundscapy/plotting/iso_plot.py
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
def add_annotation(
    self,
    text: str,
    xy: tuple[float, float],
    xytext: tuple[float, float],
    arrowprops: dict[str, Any] | None = None,
) -> ISOPlot:
    """
    Add an annotation to the plot.

    Parameters
    ----------
    text : str
        The text to display in the annotation.
    xy : tuple[float, float]
        The point to annotate.
    xytext : tuple[float, float]
        The point at which to place the text.
    arrowprops : dict[str, Any] | None, optional
        Properties for the arrow connecting the annotation text to the point.

    Returns
    -------
    ISOPlot
        The current plot instance for chaining

    """
    msg = "AnnotationLayer is not yet implemented. "
    raise NotImplementedError(msg)
    # TODO(MitchellAcoustics): Implement AnnotationLayer  # noqa: TD003
    return self.add_layer(
        "AnnotationLayer",
        text=text,
        xy=xy,
        xytext=xytext,
        arrowprops=arrowprops,
    )

add_density

add_density(on_axis=None, data=None, *, include_outline=False, **params)

Add a density layer to specific subplot(s).

PARAMETER DESCRIPTION
on_axis

Target specific axis/axes

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

data

Custom data for this specific density plot

TYPE: DataFrame DEFAULT: None

include_outline

Whether to include an outline around the density plot, by default False

TYPE: bool DEFAULT: False

**params

Parameters for the density plot

TYPE: dict DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a density layer to all subplots:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame({
...     'ISOPleasant': rng.normal(0.2, 0.25, 50),
...     'ISOEventful': rng.normal(0.15, 0.4, 50),
... })
>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_density()
...     .style()
... )
>>> plot.show() # xdoctest: +SKIP
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> plot.close()  # Clean up

Add a density layer with custom settings:

>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_density(levels=5, alpha=0.7)
...     .style()
... )
>>> plot.show() # xdoctest: +SKIP
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> plot.close()  # Clean up
Source code in soundscapy/plotting/iso_plot.py
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
def add_density(
    self,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    data: pd.DataFrame | None = None,
    *,
    include_outline: bool = False,
    **params: Any,
) -> ISOPlot:
    """
    Add a density layer to specific subplot(s).

    Parameters
    ----------
    on_axis : int | tuple[int, int] | list[int] | None, optional
        Target specific axis/axes
    data : pd.DataFrame, optional
        Custom data for this specific density plot
    include_outline : bool, optional
        Whether to include an outline around the density plot, by default False
    **params : dict
        Parameters for the density plot

    Returns
    -------
    ISOPlot
        The current plot instance for chaining

    Examples
    --------
    Add a density layer to all subplots:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame({
    ...     'ISOPleasant': rng.normal(0.2, 0.25, 50),
    ...     'ISOEventful': rng.normal(0.15, 0.4, 50),
    ... })
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_density()
    ...     .style()
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 1
    True
    >>> plot.close()  # Clean up

    Add a density layer with custom settings:

    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_density(levels=5, alpha=0.7)
    ...     .style()
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 1
    True
    >>> plot.close()  # Clean up

    """
    # Merge default density parameters with provided ones
    density_params = self._density_params.copy()
    density_params.drop("data")
    density_params.update(**params)

    return self.add_layer(
        DensityLayer,
        data=data,
        on_axis=on_axis,
        include_outline=include_outline,
        **density_params.as_dict(drop=["data"]),
    )

add_layer

add_layer(layer_class, data=None, *, on_axis=None, **params)

Add a visualization layer, optionally targeting specific subplot(s).

PARAMETER DESCRIPTION
layer_class

The type of layer to add

TYPE: Layer subclass

on_axis

Target specific axis/axes: - int: Index of subplot (flattened) - tuple: (row, col) coordinates - list: Multiple indices to apply the layer to - None: Apply to all subplots (default)

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

data

Custom data for this specific layer, overriding context data

TYPE: DataFrame DEFAULT: None

**params

Parameters for the layer

TYPE: dict DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a scatter layer to all subplots:

>>> import pandas as pd
>>> import numpy as np
>>> from soundscapy.plotting.layers import ScatterLayer
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> # Will create 2x2 subplots all with the same data
>>> plot = (ISOPlot(data=data)
...         .create_subplots(nrows=2, ncols=2)
...         .add_layer(ScatterLayer)
...         .style())
>>> plot.show() # xdoctest: +SKIP
>>> all(len(ctx.layers) == 1 for ctx in plot.subplot_contexts)
    True
>>> plot.close()  # Clean up

Add a layer to a specific subplot:

>>> plot = (ISOPlot(data=data)
...         .create_subplots(nrows=2, ncols=2)
...         .add_layer(ScatterLayer, on_axis=0)
...         .style())
>>> plot.show() # xdoctest: +SKIP
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> all(len(ctx.layers) == 0 for ctx in plot.subplot_contexts[1:])
True
>>> plot.close()

Add a layer to multiple subplots:

>>> plot = (ISOPlot(data=data)
...            .create_subplots(nrows=2, ncols=2)
...            .add_layer(ScatterLayer, on_axis=[0, 2])
...            .style())
>>> plot.show() # xdoctest: +SKIP
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> len(plot.subplot_contexts[2].layers) == 1
True
>>> len(plot.subplot_contexts[1].layers) == 0
True
>>> plot.close()

Add a layer with custom data to a specific subplot:

>>> custom_data = pd.DataFrame({
...     'ISOPleasant': rng.normal(0.2, 0.1, 50),
...     'ISOEventful': rng.normal(0.15, 0.2, 50),
... })
>>> plot = (ISOPlot(data=data)
...        .create_subplots(nrows=2, ncols=2)
...        .add_layer(ScatterLayer) # Add to all subplots
...        # Add a layer with custom data to the first subplot
...        .add_layer(ScatterLayer, data=data.iloc[:50], on_axis=0, color='red')
...        # Add a layer with custom data to the second subplot
...        .add_layer(ScatterLayer, data=custom_data, on_axis=1)
...        .style())
>>> plot.show() # xdoctest: +SKIP
>>> plot.close()
Source code in soundscapy/plotting/iso_plot.py
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
def add_layer(
    self,
    layer_class: type[Layer],
    data: pd.DataFrame | None = None,
    *,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    **params: Any,
) -> ISOPlot:
    """
    Add a visualization layer, optionally targeting specific subplot(s).

    Parameters
    ----------
    layer_class : Layer subclass
        The type of layer to add
    on_axis : int | tuple[int, int] | list[int] | None, optional
        Target specific axis/axes:
        - int: Index of subplot (flattened)
        - tuple: (row, col) coordinates
        - list: Multiple indices to apply the layer to
        - None: Apply to all subplots (default)
    data : pd.DataFrame, optional
        Custom data for this specific layer, overriding context data
    **params : dict
        Parameters for the layer

    Returns
    -------
    ISOPlot
        The current plot instance for chaining

    Examples
    --------
    Add a scatter layer to all subplots:

    >>> import pandas as pd
    >>> import numpy as np
    >>> from soundscapy.plotting.layers import ScatterLayer
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> # Will create 2x2 subplots all with the same data
    >>> plot = (ISOPlot(data=data)
    ...         .create_subplots(nrows=2, ncols=2)
    ...         .add_layer(ScatterLayer)
    ...         .style())
    >>> plot.show() # xdoctest: +SKIP
    >>> all(len(ctx.layers) == 1 for ctx in plot.subplot_contexts)
        True
    >>> plot.close()  # Clean up

    Add a layer to a specific subplot:

    >>> plot = (ISOPlot(data=data)
    ...         .create_subplots(nrows=2, ncols=2)
    ...         .add_layer(ScatterLayer, on_axis=0)
    ...         .style())
    >>> plot.show() # xdoctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 1
    True
    >>> all(len(ctx.layers) == 0 for ctx in plot.subplot_contexts[1:])
    True
    >>> plot.close()

    Add a layer to multiple subplots:

    >>> plot = (ISOPlot(data=data)
    ...            .create_subplots(nrows=2, ncols=2)
    ...            .add_layer(ScatterLayer, on_axis=[0, 2])
    ...            .style())
    >>> plot.show() # xdoctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 1
    True
    >>> len(plot.subplot_contexts[2].layers) == 1
    True
    >>> len(plot.subplot_contexts[1].layers) == 0
    True
    >>> plot.close()

    Add a layer with custom data to a specific subplot:
    >>> custom_data = pd.DataFrame({
    ...     'ISOPleasant': rng.normal(0.2, 0.1, 50),
    ...     'ISOEventful': rng.normal(0.15, 0.2, 50),
    ... })
    >>> plot = (ISOPlot(data=data)
    ...        .create_subplots(nrows=2, ncols=2)
    ...        .add_layer(ScatterLayer) # Add to all subplots
    ...        # Add a layer with custom data to the first subplot
    ...        .add_layer(ScatterLayer, data=data.iloc[:50], on_axis=0, color='red')
    ...        # Add a layer with custom data to the second subplot
    ...        .add_layer(ScatterLayer, data=custom_data, on_axis=1)
    ...        .style())
    >>> plot.show() # xdoctest: +SKIP
    >>> plot.close()

    """
    # TODO(MitchellAcoustics): Need to handle legend/label creation   # noqa: TD003
    #                          for new data added to a specific subplot
    # Create the layer instance
    layer = layer_class(custom_data=data, **params)

    # Check if we have axes to render on
    self._check_for_axes()

    # If no subplots created yet, add to main context
    if not self.subplot_contexts:
        if self.main_context.ax is None:
            # Get the single axis and assign it to main context
            if isinstance(self.axes, Axes):
                self.main_context.ax = self.axes
            elif isinstance(self.axes, np.ndarray) and self.axes.size > 0:
                self.main_context.ax = self.axes.flatten()[0]

        # Add layer to main context
        self.main_context.layers.append(layer)
        # Render the layer immediately
        layer.render(self.main_context)
        return self

    # Handle various axis targeting options
    target_contexts = self._resolve_target_contexts(on_axis)
    logger.debug(f"N target contexts: {len(target_contexts)}")

    # Add the layer to each target context and render it
    for i, context in enumerate(target_contexts):
        if data is not None and i >= self.subplots_params.n_subplots_by > 0:
            # If custom data is provided, use it for the specific subplot
            break
        context.layers.append(layer)
        layer.render(context)

    return self

add_scatter

add_scatter(data=None, *, on_axis=None, **params)

Add a scatter layer to specific subplot(s).

PARAMETER DESCRIPTION
on_axis

Target specific axis/axes

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

data

Custom data for this specific scatter plot

TYPE: DataFrame DEFAULT: None

**params

Parameters for the scatter plot

TYPE: dict DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a scatter layer to all subplots:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> plot = (ISOPlot(data=data)
...           .create_subplots(nrows=2, ncols=1)
...           .add_scatter(s=50, alpha=0.7, hue='Group')
...           .style())
>>> plot.show() # xdoctest: +SKIP
>>> all(len(ctx.layers) == 1 for ctx in plot.subplot_contexts)
True
>>> plot.close()  # Clean up

Add a scatter layer with custom data to a specific subplot:

>>> custom_data = pd.DataFrame({
...     'ISOPleasant': rng.normal(0.2, 0.1, 50),
...     'ISOEventful': rng.normal(0.15, 0.2, 50),
... })
>>> plot = (ISOPlot(data=data)
...            .create_subplots(nrows=2, ncols=1)
...            .add_scatter(hue='Group')
...            .add_scatter(on_axis=0, data=custom_data, color='red')
...            .style())
>>> plot.show() # xdoctest: +SKIP
>>> plot.subplot_contexts[0].layers[1].custom_data is custom_data
True
>>> plot.close()  # Clean up
Source code in soundscapy/plotting/iso_plot.py
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
def add_scatter(
    self,
    data: pd.DataFrame | None = None,
    *,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    **params: Any,
) -> ISOPlot:
    """
    Add a scatter layer to specific subplot(s).

    Parameters
    ----------
    on_axis : int | tuple[int, int] | list[int] | None, optional
        Target specific axis/axes
    data : pd.DataFrame, optional
        Custom data for this specific scatter plot
    **params : dict
        Parameters for the scatter plot

    Returns
    -------
    ISOPlot
        The current plot instance for chaining

    Examples
    --------
    Add a scatter layer to all subplots:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> plot = (ISOPlot(data=data)
    ...           .create_subplots(nrows=2, ncols=1)
    ...           .add_scatter(s=50, alpha=0.7, hue='Group')
    ...           .style())
    >>> plot.show() # xdoctest: +SKIP
    >>> all(len(ctx.layers) == 1 for ctx in plot.subplot_contexts)
    True
    >>> plot.close()  # Clean up

    Add a scatter layer with custom data to a specific subplot:

    >>> custom_data = pd.DataFrame({
    ...     'ISOPleasant': rng.normal(0.2, 0.1, 50),
    ...     'ISOEventful': rng.normal(0.15, 0.2, 50),
    ... })
    >>> plot = (ISOPlot(data=data)
    ...            .create_subplots(nrows=2, ncols=1)
    ...            .add_scatter(hue='Group')
    ...            .add_scatter(on_axis=0, data=custom_data, color='red')
    ...            .style())
    >>> plot.show() # xdoctest: +SKIP
    >>> plot.subplot_contexts[0].layers[1].custom_data is custom_data
    True
    >>> plot.close()  # Clean up

    """
    # Merge default scatter parameters with provided ones
    # Remove data from scatter_params to avoid conflict
    scatter_params = self._scatter_params.copy()
    scatter_params.drop("data")
    scatter_params.update(**params)

    return self.add_layer(
        ScatterLayer,
        data=data,
        on_axis=on_axis,
        **scatter_params.as_dict(drop=["data"]),
    )

add_simple_density

add_simple_density(on_axis=None, data=None, *, include_outline=True, **params)

Add a simple density layer to specific subplot(s).

PARAMETER DESCRIPTION
on_axis

Target specific axis/axes

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

data

Custom data for this specific density plot

TYPE: DataFrame DEFAULT: None

thresh

Threshold for density contours, by default 0.5

TYPE: float

levels

Contour levels, by default 2

TYPE: int | Iterable[float]

alpha

Transparency level, by default 0.5

TYPE: float

include_outline

Whether to include an outline around the density plot, by default True

TYPE: bool DEFAULT: True

**params

Additional parameters for the density plot

TYPE: dict DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a simple density layer:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame({
...     'ISOPleasant': rng.normal(0.2, 0.25, 30),
...     'ISOEventful': rng.normal(0.15, 0.4, 30),
... })
>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_scatter()
...     .add_simple_density()
...     .style()
... )
>>> plot.show() # xdoctest: +SKIP
>>> len(plot.subplot_contexts[0].layers) == 2
True
>>> plot.close()  # Clean up

Add a simple density with splitting by group:

>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> plot = (
...     ISOPlot(data=data, hue='Group')
...     .create_subplots()
...     .add_scatter()
...     .add_simple_density()
...     .style()
... )
>>> plot.show() # xdoctest: +SKIP
>>> len(plot.subplot_contexts[0].layers) == 2
True
>>> plot.close()
...
Source code in soundscapy/plotting/iso_plot.py
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
def add_simple_density(
    self,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    data: pd.DataFrame | None = None,
    *,
    include_outline: bool = True,
    **params: Any,
) -> ISOPlot:
    """
    Add a simple density layer to specific subplot(s).

    Parameters
    ----------
    on_axis : int | tuple[int, int] | list[int] | None, optional
        Target specific axis/axes
    data : pd.DataFrame, optional
        Custom data for this specific density plot
    thresh : float, optional
        Threshold for density contours, by default 0.5
    levels : int | Iterable[float], optional
        Contour levels, by default 2
    alpha : float, optional
        Transparency level, by default 0.5
    include_outline : bool, optional
        Whether to include an outline around the density plot, by default True
    **params : dict
        Additional parameters for the density plot

    Returns
    -------
    ISOPlot
        The current plot instance for chaining

    Examples
    --------
    Add a simple density layer:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame({
    ...     'ISOPleasant': rng.normal(0.2, 0.25, 30),
    ...     'ISOEventful': rng.normal(0.15, 0.4, 30),
    ... })
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_scatter()
    ...     .add_simple_density()
    ...     .style()
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 2
    True
    >>> plot.close()  # Clean up

    Add a simple density with splitting by group:
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> plot = (
    ...     ISOPlot(data=data, hue='Group')
    ...     .create_subplots()
    ...     .add_scatter()
    ...     .add_simple_density()
    ...     .style()
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 2
    True
    >>> plot.close()
    ...

    """
    # Merge default simple density parameters with provided ones
    simple_density_params = self._simple_density_params.copy()
    simple_density_params.drop("data")
    simple_density_params.update(**params)

    return self.add_layer(
        SimpleDensityLayer,
        on_axis=on_axis,
        data=data,
        include_outline=include_outline,
        **simple_density_params.as_dict(drop=["data"]),
    )

add_spi

add_spi(on_axis=None, spi_target_data=None, msn_params=None, *, layer_class=SPISimpleLayer, **params)

Add a SPI layer to specific subplot(s).

PARAMETER DESCRIPTION
on_axis

Target specific axis/axes

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

spi_target_data

Custom data for this specific SPI plot

TYPE: DataFrame | ndarray | None DEFAULT: None

msn_params

Parameters for the SPI plot

TYPE: DirectParams | CentredParams | None DEFAULT: None

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a SPI layer to all subplots:

>>> import pandas as pd
>>> import numpy as np
>>> from soundscapy.spi import DirectParams
>>> rng = np.random.default_rng(42)
>>>    # Create a DataFrame with random data
>>> data = pd.DataFrame(
...    rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...    columns=['ISOPleasant', 'ISOEventful']
... )
>>>    # Define MSN parameters for the SPI target
>>> msn_params = DirectParams(
...     xi=np.array([0.5, 0.7]),
...     omega=np.array([[0.1, 0.05], [0.05, 0.1]]),
...     alpha=np.array([0, -5]),
...     )
>>>    # Create the plot with only an SPI layer
>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_scatter()
...     .add_spi(msn_params=msn_params)
...     .style()
... )
>>> plot.show() # xdoctest: +SKIP
>>> len(plot.subplot_contexts[0].layers) == 2
True
>>> plot.close()  # Clean up

Add an SPI layer over top of 'real' data:

>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_scatter()
...     .add_density()
...     .add_spi(msn_params=msn_params, show_score="on axis")
...     .style()
... )
>>> plot.show() # xdoctest: +SKIP
>>> len(plot.subplot_contexts[0].layers) == 3
True

Add a SPI layer from spi data:

>>> # Create a custom distribution
>>> from soundscapy.spi import MultiSkewNorm
>>> import soundscapy as sspy
>>> spi_msn = MultiSkewNorm.from_params(msn_params)
>>> # Generate random samples
>>> spi_msn.sample(1000)
>>> data = sspy.add_iso_coords(sspy.isd.load())
>>> data = sspy.isd.select_location_ids(
...     data,
...     ['CamdenTown', 'PancrasLock', 'RussellSq', 'RegentsParkJapan']
... )
>>> mp3 = (
...     ISOPlot(
...         data=data,
...         title="Soundscape Density Plots with corrected ISO coordinates",
...         hue="SessionID",
...     )
...     .create_subplots(
...         subplot_by="LocationID",
...         figsize=(4, 4),
...         auto_allocate_axes=True,
...     )
...     .add_scatter()
...     .add_simple_density(fill=False)
...     .add_spi(spi_target_data=spi_msn.sample_data, show_score="under title")
...     .style()
... )
>>> mp3.show() # xdoctest: +SKIP
>>> plot.close()  # Clean up

BUG: This last doctest doesn't show the spi score under the title

Source code in soundscapy/plotting/iso_plot.py
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
def add_spi(
    self,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    spi_target_data: pd.DataFrame | np.ndarray | None = None,
    msn_params: DirectParams | CentredParams | None = None,
    *,
    layer_class: type[Layer] = SPISimpleLayer,
    **params: Any,
) -> ISOPlot:
    """
    Add a SPI layer to specific subplot(s).

    Parameters
    ----------
    on_axis : int | tuple[int, int] | list[int] | None, optional
        Target specific axis/axes
    spi_target_data : pd.DataFrame | np.ndarray | None, optional
        Custom data for this specific SPI plot
    msn_params : DirectParams | CentredParams | None, optional
        Parameters for the SPI plot

    Returns
    -------
    ISOPlot
        The current plot instance for chaining

    Examples
    --------
    Add a SPI layer to all subplots:

    >>> import pandas as pd
    >>> import numpy as np
    >>> from soundscapy.spi import DirectParams
    >>> rng = np.random.default_rng(42)
    >>>    # Create a DataFrame with random data
    >>> data = pd.DataFrame(
    ...    rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...    columns=['ISOPleasant', 'ISOEventful']
    ... )
    >>>    # Define MSN parameters for the SPI target
    >>> msn_params = DirectParams(
    ...     xi=np.array([0.5, 0.7]),
    ...     omega=np.array([[0.1, 0.05], [0.05, 0.1]]),
    ...     alpha=np.array([0, -5]),
    ...     )
    >>>    # Create the plot with only an SPI layer
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_scatter()
    ...     .add_spi(msn_params=msn_params)
    ...     .style()
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 2
    True
    >>> plot.close()  # Clean up

    Add an SPI layer over top of 'real' data:
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_scatter()
    ...     .add_density()
    ...     .add_spi(msn_params=msn_params, show_score="on axis")
    ...     .style()
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 3
    True

    Add a SPI layer from spi data:
    >>> # Create a custom distribution
    >>> from soundscapy.spi import MultiSkewNorm
    >>> import soundscapy as sspy
    >>> spi_msn = MultiSkewNorm.from_params(msn_params)
    >>> # Generate random samples
    >>> spi_msn.sample(1000)
    >>> data = sspy.add_iso_coords(sspy.isd.load())
    >>> data = sspy.isd.select_location_ids(
    ...     data,
    ...     ['CamdenTown', 'PancrasLock', 'RussellSq', 'RegentsParkJapan']
    ... )

    >>> mp3 = (
    ...     ISOPlot(
    ...         data=data,
    ...         title="Soundscape Density Plots with corrected ISO coordinates",
    ...         hue="SessionID",
    ...     )
    ...     .create_subplots(
    ...         subplot_by="LocationID",
    ...         figsize=(4, 4),
    ...         auto_allocate_axes=True,
    ...     )
    ...     .add_scatter()
    ...     .add_simple_density(fill=False)
    ...     .add_spi(spi_target_data=spi_msn.sample_data, show_score="under title")
    ...     .style()
    ... )
    >>> mp3.show() # xdoctest: +SKIP
    >>> plot.close()  # Clean up

    # BUG: This last doctest doesn't show the spi score under the title

    """
    if layer_class == SPISimpleLayer:
        spi_simple_params = self._spi_simple_density_params.copy()
        spi_simple_params.drop("data")
        spi_simple_params.update(**params)

        return self.add_layer(
            layer_class,
            on_axis=on_axis,
            msn_params=msn_params,
            spi_target_data=spi_target_data,
            **spi_simple_params.as_dict(drop=["data"]),
        )
    if layer_class in (SPIDensityLayer, SPIScatterLayer):
        msg = (
            "Only the simple density layer type is currently supported for "
            "SPI plots. Please use SPISimpleLayer"
        )
        raise NotImplementedError(msg)

    msg = "Invalid layer class provided. Expected SPISimpleLayer. "
    raise ValueError(msg)

close

close(fig=None)

Close the figure.

This method is a wrapper around plt.close() to close the figure.

Source code in soundscapy/plotting/iso_plot.py
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def close(self, fig: int | str | Figure | None = None) -> None:
    """
    Close the figure.

    This method is a wrapper around plt.close() to close the figure.

    """
    if fig is None:
        fig = self.figure
        if fig is None:
            msg = (
                "No figure object provided. "
                "Please create a figure using create_subplots() first."
            )
            raise ValueError(msg)
    plt.close(fig)

create_subplots

create_subplots(nrows=1, ncols=1, figsize=(5, 5), subplot_by=None, subplot_datas=None, subplot_titles=None, *, adjust_figsize=True, auto_allocate_axes=False, **kwargs)

Create subplots for the circumplex plot.

PARAMETER DESCRIPTION
nrows

Number of rows in the subplot grid, by default 1

TYPE: int DEFAULT: 1

ncols

Number of columns in the subplot grid, by default 1

TYPE: int DEFAULT: 1

figsize

Size of the figure (width, height), by default (5, 5)

TYPE: tuple[int, int] DEFAULT: (5, 5)

subplot_by

Column name to create subplots by unique values, by default None

TYPE: str | None DEFAULT: None

subplot_datas

List of dataframes for each subplot, by default None

TYPE: list[DataFrame] | None DEFAULT: None

subplot_titles

List of titles for each subplot, by default None

TYPE: list[str] | None DEFAULT: None

adjust_figsize

Whether to adjust the figure size based on nrows/ncols, by default True

TYPE: bool DEFAULT: True

auto_allocate_axes

Whether to automatically determine nrows/ncols based on data, by default False

TYPE: bool DEFAULT: False

**kwargs

Additional parameters for plt.subplots

DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Create a basic subplot grid:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> plot = ISOPlot(data=data).create_subplots(nrows=2, ncols=2)
>>> len(plot.subplot_contexts) == 4
True
>>> plot.close()  # Clean up

Create subplots by a column in the data:

>>> plot = (ISOPlot(data=data)
...         .create_subplots(nrows=1, ncols=2, subplot_by='Group'))
>>> len(plot.subplot_contexts) == 2
True
>>> plot.close()  # Clean up

Create subplots with auto-allocation of axes:

>>> plot = (ISOPlot(data=data)
...        .create_subplots(subplot_by='Group', auto_allocate_axes=True))
>>> len(plot.subplot_contexts) == 2
True
>>> plot.close()  # Clean up
Source code in soundscapy/plotting/iso_plot.py
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
def create_subplots(
    self,
    nrows: int = 1,
    ncols: int = 1,
    figsize: tuple[int, int] = (5, 5),
    subplot_by: str | None = None,
    subplot_datas: list[pd.DataFrame] | None = None,
    subplot_titles: list[str] | None = None,
    *,
    adjust_figsize: bool = True,
    auto_allocate_axes: bool = False,
    **kwargs,
) -> ISOPlot:
    """
    Create subplots for the circumplex plot.

    Parameters
    ----------
    nrows : int, optional
        Number of rows in the subplot grid, by default 1
    ncols : int, optional
        Number of columns in the subplot grid, by default 1
    figsize : tuple[int, int], optional
        Size of the figure (width, height), by default (5, 5)
    subplot_by : str | None, optional
        Column name to create subplots by unique values, by default None
    subplot_datas : list[pd.DataFrame] | None, optional
        List of dataframes for each subplot, by default None
    subplot_titles : list[str] | None, optional
        List of titles for each subplot, by default None
    adjust_figsize : bool, optional
        Whether to adjust the figure size based on nrows/ncols, by default True
    auto_allocate_axes : bool, optional
        Whether to automatically determine nrows/ncols based on data,
        by default False
    **kwargs :
        Additional parameters for plt.subplots

    Returns
    -------
    ISOPlot
        The current plot instance for chaining

    Examples
    --------
    Create a basic subplot grid:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> plot = ISOPlot(data=data).create_subplots(nrows=2, ncols=2)
    >>> len(plot.subplot_contexts) == 4
    True
    >>> plot.close()  # Clean up

    Create subplots by a column in the data:

    >>> plot = (ISOPlot(data=data)
    ...         .create_subplots(nrows=1, ncols=2, subplot_by='Group'))
    >>> len(plot.subplot_contexts) == 2
    True
    >>> plot.close()  # Clean up

    Create subplots with auto-allocation of axes:

    >>> plot = (ISOPlot(data=data)
    ...        .create_subplots(subplot_by='Group', auto_allocate_axes=True))
    >>> len(plot.subplot_contexts) == 2
    True
    >>> plot.close()  # Clean up

    """
    # Set up subplot params
    self.subplots_params.update(
        nrows=nrows,
        ncols=ncols,
        figsize=figsize,
        subplot_by=subplot_by,
        adjust_figsize=adjust_figsize,
        auto_allocate_axes=auto_allocate_axes,
        **kwargs,
    )
    # Create a list of dataframes and titles for each subplot
    # based on the unique values in the specified column
    if self.subplots_params.subplot_by:
        logger.debug(
            f"Creating subplots by unique values in {self.subplots_params.subplot_by}."
        )
        subplot_datas, subplot_titles, n_subplots_by = self._setup_subplot_by(
            self.subplots_params.subplot_by, subplot_datas, subplot_titles
        )
    else:
        n_subplots_by = -1

    if subplot_titles and self.subplots_params.auto_allocate_axes:
        # Attempt to allocate axes based on the number of subplots
        self.subplots_params.nrows, self.subplots_params.ncols = (
            self._allocate_subplot_axes(subplot_titles)
        )

    if adjust_figsize:
        self.subplots_params.figsize = (
            self.subplots_params.ncols * self.subplots_params.figsize[0],
            self.subplots_params.nrows * self.subplots_params.figsize[1],
        )

    logger.debug(f"Subplot parameters: {self.subplots_params}")

    # Create the figure and axes
    self.figure, self.axes = plt.subplots(
        **self.subplots_params.as_plt_subplots_args()
    )

    # If subplot_datas or subplot_titles are provided, validate them
    if subplot_datas is not None or subplot_titles is not None:
        self._validate_subplots_datas(subplot_datas, subplot_titles)

    # Create PlotContext objects for each subplot
    self.subplot_contexts = []

    for i, ax in enumerate(self.yield_axes_objects()):
        if i >= self._naxes:
            break
        if subplot_by and i >= n_subplots_by:
            logger.debug(f"Created {i + 1} subplots for {subplot_by}.")
            break
        # Get data and title for this subplot if available
        data = (
            subplot_datas[i] if subplot_datas and i < len(subplot_datas) else None
        )
        title = (
            subplot_titles[i]
            if subplot_titles and i < len(subplot_titles)
            else None
        )

        context = self.main_context.create_child(data=data, title=title, ax=ax)
        self.subplot_contexts.append(context)

    return self

get_axes

get_axes()

Get the axes object.

RETURNS DESCRIPTION
Axes | np.ndarray: The axes object to be used for plotting.
RAISES DESCRIPTION
ValueError: If the axes object does not exist.

TypeError: If the axes object is not a valid Axes or ndarray of Axes.

Source code in soundscapy/plotting/iso_plot.py
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
def get_axes(self) -> Axes | np.ndarray:
    """
    Get the axes object.

    Returns
    -------
        Axes | np.ndarray: The axes object to be used for plotting.

    Raises
    ------
        ValueError: If the axes object does not exist.
        TypeError: If the axes object is not a valid Axes or ndarray of Axes.

    """
    self._check_for_axes()
    if isinstance(self.axes, Axes | np.ndarray):
        return self.axes
    msg = "Invalid axes object. Please provide a valid Axes or ndarray of Axes."
    raise TypeError(msg)

get_figure

get_figure()

Get the figure object.

RETURNS DESCRIPTION
Figure | SubFigure: The figure object to be used for plotting.
RAISES DESCRIPTION
ValueError: If the figure object does not exist.

TypeError: If the figure object is not a valid Figure or SubFigure.

Source code in soundscapy/plotting/iso_plot.py
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def get_figure(self) -> Figure | SubFigure:
    """
    Get the figure object.

    Returns
    -------
        Figure | SubFigure: The figure object to be used for plotting.

    Raises
    ------
        ValueError: If the figure object does not exist.
        TypeError: If the figure object is not a valid Figure or SubFigure.

    """
    if self.figure is None:
        msg = (
            "No figure object provided. "
            "Please create a figure using create_subplots() first."
        )
        raise ValueError(msg)
    if isinstance(self.figure, Figure | SubFigure):
        return self.figure
    msg = "Invalid figure object. Please provide a valid Figure or SubFigure."
    raise TypeError(msg)

get_single_axes

get_single_axes(ax_idx=None)

Get a specific axes object.

PARAMETER DESCRIPTION
ax_idx

The index of the axes to get. If None, returns the first axes. Can be an integer for flattened access or a tuple of (row, col).

TYPE: int | tuple[int, int] | None DEFAULT: None

RETURNS DESCRIPTION
Axes

The requested matplotlib Axes object

RAISES DESCRIPTION
ValueError

If the axes object does not exist or the index is invalid.

TypeError

If the axes object is not a valid Axes or ndarray of Axes.

Source code in soundscapy/plotting/iso_plot.py
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
def get_single_axes(self, ax_idx: int | tuple[int, int] | None = None) -> Axes:
    """
    Get a specific axes object.

    Parameters
    ----------
    ax_idx : int | tuple[int, int] | None, optional
        The index of the axes to get. If None, returns the first axes.
        Can be an integer for flattened access or a tuple of (row, col).

    Returns
    -------
    Axes
        The requested matplotlib Axes object

    Raises
    ------
    ValueError
        If the axes object does not exist or the index is invalid.
    TypeError
        If the axes object is not a valid Axes or ndarray of Axes.

    """
    self._check_for_axes()

    def validate_tuple_axes_index(
        nrows: int, ncols: int, naxes: int, ax_idx: tuple[int, int]
    ) -> None:
        """
        Validate the tuple axes index.

        This checks the ax_idx types and compares the implied number of axes
        with the actual number of axes in the figure.
        """
        if (
            len(ax_idx) != 2  # noqa: PLR2004
            or not isinstance(ax_idx[0], int)
            or not isinstance(ax_idx[1], int)
            or ax_idx[0] < 0
            or ax_idx[1] < 0
        ):
            msg = (
                "Invalid axes index provided. "
                "Expected a tuple of 2 positive integers."
            )
            raise ValueError(msg)

        if ax_idx[0] >= (nrows - 1) or ax_idx[1] >= (ncols - 1):
            msg = (
                "Invalid axes index provided."
                f" The figure contains {nrows} rows and {ncols} columns. "
                f"ax_idx implied {ax_idx[0] + 1} rows and {ax_idx[1] + 1} columns."
            )
            raise ValueError(msg)

        idx_implied_n_axes = (ax_idx[0] + 1) * (ax_idx[1] + 1)
        if naxes < idx_implied_n_axes:
            msg = (
                "Invalid axes index provided."
                f" The figure contains {naxes} axes. "
                f"ax_idx implied {idx_implied_n_axes} axes."
            )
            raise ValueError(msg)

    def validate_int_axes_index(naxes: int, ax_idx: int) -> None:
        """
        Validate the integer axes index.

        This checks the ax_idx type and compares the implied number of axes
        with the actual number of axes in the figure.
        """
        if not isinstance(ax_idx, int) or ax_idx < 0:
            msg = "Invalid axes index provided. Expected a positive integer."
            raise ValueError(msg)

        if (ax_idx + 1) > naxes:
            msg = (
                "Invalid axes index provided."
                f" The figure contains {naxes} axes. "
                f"ax_idx implied {ax_idx + 1} axes."
            )
            raise ValueError(msg)

    if isinstance(self.axes, np.ndarray) and ax_idx is not None:
        if isinstance(ax_idx, tuple):
            validate_tuple_axes_index(self._nrows, self._ncols, self._naxes, ax_idx)
            return self.axes[ax_idx[0], ax_idx[1]]

        validate_int_axes_index(self._naxes, ax_idx)
        return self.axes.flatten()[ax_idx]

    if isinstance(self.axes, Axes) and (ax_idx == 0 or ax_idx is None):
        return self.axes

    msg = "Invalid axes index provided."
    raise ValueError(msg)

savefig

savefig(*args, **kwargs)

Save the figure.

This method is a wrapper around plt.savefig() to save the figure.

Source code in soundscapy/plotting/iso_plot.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
def savefig(self, *args: Any, **kwargs: Any) -> None:
    """
    Save the figure.

    This method is a wrapper around plt.savefig() to save the figure.

    """
    if self.figure is None:
        msg = (
            "No figure object provided. "
            "Please create a figure using create_subplots() first."
        )
        raise ValueError(msg)
    self.figure.savefig(*args, **kwargs)

show

show()

Show the figure.

This method is a wrapper around plt.show() to display the figure.

Source code in soundscapy/plotting/iso_plot.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
def show(self) -> None:
    """
    Show the figure.

    This method is a wrapper around plt.show() to display the figure.

    """
    if self.figure is None:
        msg = (
            "No figure object provided. "
            "Please create a figure using create_subplots() first."
        )
        raise ValueError(msg)
    if self._has_subplots:
        plt.tight_layout()
    plt.show()

style

style(**kwargs)

Apply styling to the plot.

PARAMETER DESCRIPTION
**kwargs

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Apply styling with default parameters:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> # Create simple data for styling example
>>> data = pd.DataFrame(
...     np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...             rng.integers(1, 3, 100)],
...     columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> # Create plot with default styling
>>> plot = (
...    ISOPlot(data=data)
...       .create_subplots()
...       .add_scatter()
...       .style()
... )
>>> plot.show() # xdoctest: +SKIP
>>> plot.get_figure() is not None
True
>>> plot.close()  # Clean up

Apply styling with custom parameters:

>>> plot = (
...         ISOPlot(data=data)
...         .create_subplots()
...         .add_scatter()
...         .style(xlim=(-2, 2), ylim=(-2, 2), primary_lines=False)
... )
>>> plot.show() # xdoctest: +SKIP
>>> plot.get_figure() is not None
True
>>> plot.close()  # Clean up

Demonstrate the fluent interface (method chaining):

>>> # Create plot with method chaining
>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots(nrows=1, ncols=1)
...     .add_scatter(alpha=0.7)
...     .add_density(levels=5)
...     .style(title_fontsize=14)
... )
>>> plot.show() # xdoctest: +SKIP
>>> # Verify results
>>> isinstance(plot, ISOPlot)
True
>>> plot.close()  # Clean up
Source code in soundscapy/plotting/iso_plot.py
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
def style(
    self,
    **kwargs: Any,
) -> ISOPlot:
    """
    Apply styling to the plot.

    Parameters
    ----------
    **kwargs: Styling parameters to override defaults

    Returns
    -------
    ISOPlot
        The current plot instance for chaining

    Examples
    --------
    Apply styling with default parameters:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> # Create simple data for styling example
    >>> data = pd.DataFrame(
    ...     np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...             rng.integers(1, 3, 100)],
    ...     columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> # Create plot with default styling
    >>> plot = (
    ...    ISOPlot(data=data)
    ...       .create_subplots()
    ...       .add_scatter()
    ...       .style()
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> plot.get_figure() is not None
    True
    >>> plot.close()  # Clean up

    Apply styling with custom parameters:

    >>> plot = (
    ...         ISOPlot(data=data)
    ...         .create_subplots()
    ...         .add_scatter()
    ...         .style(xlim=(-2, 2), ylim=(-2, 2), primary_lines=False)
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> plot.get_figure() is not None
    True
    >>> plot.close()  # Clean up

    Demonstrate the fluent interface (method chaining):

    >>> # Create plot with method chaining
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots(nrows=1, ncols=1)
    ...     .add_scatter(alpha=0.7)
    ...     .add_density(levels=5)
    ...     .style(title_fontsize=14)
    ... )
    >>> plot.show() # xdoctest: +SKIP
    >>> # Verify results
    >>> isinstance(plot, ISOPlot)
    True
    >>> plot.close()  # Clean up

    """
    self._style_params.update(**kwargs)
    self._check_for_axes()

    self._set_style()
    self._circumplex_grid()
    self._set_title()
    self._set_axes_titles()
    self._primary_labels()
    if self._style_params.get("primary_lines"):
        self._primary_lines()
    if self._style_params.get("diagonal_lines"):
        self._diagonal_lines_and_labels()

    if self._style_params.get("legend_loc") is not False:
        self._move_legend()

    return self

yield_axes_objects

yield_axes_objects()

Generate a sequence of axes objects to iterate over.

This method is a helper to iterate over all axes in the figure, whether the figure contains a single Axes object or an array of Axes objects.

YIELDS DESCRIPTION
Axes

Individual matplotlib Axes objects from the current figure.

Source code in soundscapy/plotting/iso_plot.py
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
def yield_axes_objects(self) -> Generator[Axes, None, None]:
    """
    Generate a sequence of axes objects to iterate over.

    This method is a helper to iterate over all axes in the figure,
    whether the figure contains a single Axes object or an array of Axes objects.

    Yields
    ------
    Axes
        Individual matplotlib Axes objects from the current figure.

    """
    if isinstance(self.axes, np.ndarray):
        yield from self.axes.flatten()
    elif isinstance(self.axes, Axes):
        yield self.axes

MultiSkewNorm

MultiSkewNorm()

A class representing a multi-dimensional skewed normal distribution.

ATTRIBUTE DESCRIPTION
selm_model

The fitted SELM model.

cp

The centred parameters of the fitted model.

TYPE: CentredParams

dp

The direct parameters of the fitted model.

TYPE: DirectParams

sample_data

The generated sample data from the fitted model.

TYPE: ndarray | None

data

The input data used for fitting the model.

TYPE: DataFrame | None

METHOD DESCRIPTION
summary

Prints a summary of the fitted model.

fit

Fits the model to the provided data.

define_dp

Defines the direct parameters of the model.

sample

Generates a sample from the fitted model.

sspy_plot

Plots the joint distribution of the generated sample.

ks2ds

Computes the two-sample Kolmogorov-Smirnov statistic.

spi

Computes the similarity percentage index.

Initialize the MultiSkewNorm object.

Source code in soundscapy/spi/msn.py
236
237
238
239
240
241
242
def __init__(self) -> None:
    """Initialize the MultiSkewNorm object."""
    self.selm_model = None
    self.cp = None
    self.dp = None
    self.sample_data = None
    self.data: pd.DataFrame | None = None

__repr__

__repr__()

Return a string representation of the MultiSkewNorm object.

Source code in soundscapy/spi/msn.py
244
245
246
247
248
def __repr__(self) -> str:
    """Return a string representation of the MultiSkewNorm object."""
    if self.cp is None and self.dp is None and self.selm_model is None:
        return "MultiSkewNorm() (unfitted)"
    return f"MultiSkewNorm(dp={self.dp})"

define_dp

define_dp(xi, omega, alpha)

Initiate a distribution from the direct parameters.

PARAMETER DESCRIPTION
xi

The xi values of the direct parameters.

TYPE: ndarray

omega

The omega values of the direct parameters.

TYPE: ndarray

alpha

The alpha values of the direct parameters.

TYPE: ndarray

RETURNS DESCRIPTION
self
Source code in soundscapy/spi/msn.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def define_dp(
    self, xi: np.ndarray, omega: np.ndarray, alpha: np.ndarray
) -> "MultiSkewNorm":
    """
    Initiate a distribution from the direct parameters.

    Parameters
    ----------
    xi : np.ndarray
        The xi values of the direct parameters.
    omega : np.ndarray
        The omega values of the direct parameters.
    alpha : np.ndarray
        The alpha values of the direct parameters.

    Returns
    -------
    self

    """
    self.dp = DirectParams(xi, omega, alpha)
    self.cp = CentredParams.from_dp(self.dp)
    return self

fit

fit(data=None, x=None, y=None)

Fit the multi-dimensional skewed normal model to the provided data.

PARAMETER DESCRIPTION
data

The input data as a pandas DataFrame or numpy array.

TYPE: DataFrame or ndarray DEFAULT: None

x

The x-values of the input data as a numpy array or pandas Series.

TYPE: ndarray or Series DEFAULT: None

y

The y-values of the input data as a numpy array or pandas Series.

TYPE: ndarray or Series DEFAULT: None

RAISES DESCRIPTION
ValueError

If neither data nor both x and y are provided.

Source code in soundscapy/spi/msn.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def fit(
    self,
    data: pd.DataFrame | np.ndarray | None = None,
    x: np.ndarray | pd.Series | None = None,
    y: np.ndarray | pd.Series | None = None,
) -> None:
    """
    Fit the multi-dimensional skewed normal model to the provided data.

    Parameters
    ----------
    data : pd.DataFrame or np.ndarray, optional
        The input data as a pandas DataFrame or numpy array.
    x : np.ndarray or pd.Series, optional
        The x-values of the input data as a numpy array or pandas Series.
    y : np.ndarray or pd.Series, optional
        The y-values of the input data as a numpy array or pandas Series.

    Raises
    ------
    ValueError
        If neither `data` nor both `x` and `y` are provided.

    """
    if data is None and (x is None or y is None):
        # Either data or x and y must be provided
        msg = "Either data or x and y must be provided"
        raise ValueError(msg)

    if data is not None:
        # If data is provided, convert it to a pandas DataFrame
        if isinstance(data, pd.DataFrame):
            # If data is already a DataFrame, no need to convert
            data.columns = ["x", "y"]

        elif isinstance(data, np.ndarray):
            # If data is a numpy array, convert it to a DataFrame
            if data.ndim == 2:  # noqa: PLR2004
                # If data is 2D, assume it's two variables
                data = pd.DataFrame(data, columns=["x", "y"])
            else:
                msg = "Data must be a 2D numpy array or DataFrame"
                raise ValueError(msg)
        else:
            # If data is neither a DataFrame nor a numpy array, raise an error
            msg = "Data must be a pandas DataFrame or 2D numpy array."
            raise ValueError(msg)

    elif x is not None and y is not None:
        # If x and y are provided, convert them to a pandas DataFrame
        data = pd.DataFrame({"x": x, "y": y})

    else:
        # This should never happen
        msg = "Either data or x and y must be provided"
        raise ValueError(msg)

    # Fit the model
    m = rsn.selm("x", "y", data)

    # Extract the parameters
    cp = rsn.extract_cp(m)
    dp = rsn.extract_dp(m)

    self.cp = CentredParams(*cp)
    self.dp = DirectParams(*dp)
    self.data = data
    self.selm_model = m

from_params classmethod

from_params(params=None, *, xi=None, omega=None, alpha=None, mean=None, sigma=None, skew=None)

Create a MultiSkewNorm instance from direct parameters.

PARAMETER DESCRIPTION
params

The direct parameters to initialize the model.

TYPE: DirectParams DEFAULT: None

RETURNS DESCRIPTION
MultiSkewNorm

A new instance of MultiSkewNorm initialized with the provided parameters.

Source code in soundscapy/spi/msn.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
@classmethod
def from_params(
    cls,
    params: DirectParams | CentredParams | None = None,
    *,
    xi: np.ndarray | None = None,
    omega: np.ndarray | None = None,
    alpha: np.ndarray | None = None,
    mean: np.ndarray | None = None,
    sigma: np.ndarray | None = None,
    skew: np.ndarray | None = None,
) -> "MultiSkewNorm":
    """
    Create a MultiSkewNorm instance from direct parameters.

    Parameters
    ----------
    params : DirectParams
        The direct parameters to initialize the model.

    Returns
    -------
    MultiSkewNorm
        A new instance of MultiSkewNorm initialized with the provided parameters.

    """
    instance = cls()

    if params is None:
        if (xi is None or omega is None or alpha is None) and (
            mean is None or sigma is None or skew is None
        ):
            msg = "Either params object or xi, omega, and alpha must be provided."
            raise ValueError(msg)
        if xi is not None and omega is not None and alpha is not None:
            # If xi, omega, and alpha are provided, create DirectParams
            instance.dp = DirectParams(xi, omega, alpha)
        elif mean is not None and sigma is not None and skew is not None:
            # If mean, sigma, and skew are provided, create CentredParams
            cp = CentredParams(mean, sigma, skew)
            dp = DirectParams.from_cp(cp)
            instance.dp = dp
            instance.cp = cp
        return instance
    if isinstance(params, DirectParams):
        # If params is a DirectParams object, set it directly
        instance.dp = params
        instance.cp = CentredParams.from_dp(params)
        return instance
    if isinstance(params, CentredParams):
        # If params is a CentredParams object, convert it to DirectParams
        instance.cp = params
        dp = DirectParams.from_cp(params)
        instance.dp = dp
        return instance
    # If params is neither DirectParams nor CentredParams, raise an error
    msg = (
        "Either params or xi, omega, and alpha must be provided."
        "Or mean, sigma, and skew must be provided."
    )
    raise ValueError(msg)

ks2d2s

ks2d2s(test)

Compute the two-sample, two-dimensional Kolmogorov-Smirnov statistic.

PARAMETER DESCRIPTION
test

The test data.

TYPE: DataFrame or ndarray

RETURNS DESCRIPTION
tuple

The KS2D statistic and p-value.

Source code in soundscapy/spi/msn.py
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
def ks2d2s(self, test: pd.DataFrame | np.ndarray) -> tuple[float, float]:
    """
    Compute the two-sample, two-dimensional Kolmogorov-Smirnov statistic.

    Parameters
    ----------
    test : pd.DataFrame or np.ndarray
        The test data.

    Returns
    -------
    tuple
        The KS2D statistic and p-value.

    """
    # Ensure sample_data exists, generate if needed and possible
    if self.sample_data is None:
        logger.info("Sample data not found, generating default sample (n=1000).")
        self.sample(n=1000, return_sample=False)  # Generate sample if missing
        if self.sample_data is None:  # Check again in case sample failed
            msg = (
                "Could not generate sample data. "
                "Ensure model is defined (fit or define_dp)."
            )
            raise ValueError(msg)

    # Perform the 2-sample KS test using ks2d2s
    # Note: ks2d2s expects data1, data2
    return ks2d(self.sample_data, test)

sample

sample(n=1000, *, return_sample=False)

Generate a sample from the fitted model.

PARAMETER DESCRIPTION
n

The number of samples to generate, by default 1000.

TYPE: int DEFAULT: 1000

return_sample

Whether to return the generated sample as an np.ndarray, by default False.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
None or ndarray

The generated sample if return_sample is True, otherwise None.

RAISES DESCRIPTION
ValueError

If the model is not fitted (i.e., selm_model is None) and direct parameters (dp) are also not defined.

Source code in soundscapy/spi/msn.py
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
def sample(
    self, n: int = 1000, *, return_sample: bool = False
) -> None | np.ndarray:
    """
    Generate a sample from the fitted model.

    Parameters
    ----------
    n : int, optional
        The number of samples to generate, by default 1000.
    return_sample : bool, optional
        Whether to return the generated sample as an np.ndarray, by default False.

    Returns
    -------
    None or np.ndarray
        The generated sample if `return_sample` is True, otherwise None.

    Raises
    ------
    ValueError
        If the model is not fitted (i.e., `selm_model` is None) and direct
        parameters (`dp`) are also not defined.

    """
    if self.selm_model is not None:
        sample = rsn.sample_msn(selm_model=self.selm_model, n=n)
    elif self.dp is not None:
        sample = rsn.sample_msn(
            xi=self.dp.xi, omega=self.dp.omega, alpha=self.dp.alpha, n=n
        )
    else:
        msg = "Either selm_model or xi, omega, and alpha must be provided."
        raise ValueError(msg)

    self.sample_data = sample

    if return_sample:
        return sample
    return None

sample_mtsn

sample_mtsn(n=1000, a=-1, b=1, *, return_sample=False)

Generate a sample from the multi-dimensional truncated skew-normal distribution.

Uses rejection sampling to ensure that the samples are within the bounds [a, b] for both dimensions.

PARAMETER DESCRIPTION
n

The number of samples to generate, by default 1000.

TYPE: int DEFAULT: 1000

a

Lower truncation bound for both dimensions, by default -1.

TYPE: float DEFAULT: -1

b

Upper truncation bound for both dimensions, by default 1.

TYPE: float DEFAULT: 1

return_sample

Whether to return the generated sample as an np.ndarray, by default False.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
None or ndarray

The generated sample if return_sample is True, otherwise None.

Source code in soundscapy/spi/msn.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
def sample_mtsn(
    self, n: int = 1000, a: float = -1, b: float = 1, *, return_sample: bool = False
) -> None | np.ndarray:
    """
    Generate a sample from the multi-dimensional truncated skew-normal distribution.

    Uses rejection sampling to ensure that the samples are within the bounds [a, b]
    for both dimensions.

    Parameters
    ----------
    n : int, optional
        The number of samples to generate, by default 1000.
    a : float, optional
        Lower truncation bound for both dimensions, by default -1.
    b : float, optional
        Upper truncation bound for both dimensions, by default 1.
    return_sample : bool, optional
        Whether to return the generated sample as an np.ndarray, by default False.

    Returns
    -------
    None or np.ndarray
        The generated sample if `return_sample` is True, otherwise None.

    """
    if self.selm_model is not None:
        sample = rsn.sample_mtsn(
            selm_model=self.selm_model,
            n=n,
            a=a,
            b=b,
        )
    elif self.dp is not None:
        sample = rsn.sample_mtsn(
            xi=self.dp.xi,
            omega=self.dp.omega,
            alpha=self.dp.alpha,
            n=n,
            a=a,
            b=b,
        )
    else:
        msg = "Either selm_model or xi, omega, and alpha must be provided."
        raise ValueError(msg)

    # Store the sample data
    self.sample_data = sample

    if return_sample:
        return sample
    return None

spi_score

spi_score(test)

Compute the Soundscape Perception Index (SPI).

Calculates the SPI for the test data against the target distribution represented by this MultiSkewNorm instance.

PARAMETER DESCRIPTION
test

The test data.

TYPE: DataFrame or ndarray

RETURNS DESCRIPTION
int

The Soundscape Perception Index (SPI), ranging from 0 to 100.

Source code in soundscapy/spi/msn.py
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
def spi_score(self, test: pd.DataFrame | np.ndarray) -> int:
    """
    Compute the Soundscape Perception Index (SPI).

    Calculates the SPI for the test data against the target distribution
    represented by this MultiSkewNorm instance.

    Parameters
    ----------
    test : pd.DataFrame or np.ndarray
        The test data.

    Returns
    -------
    int
        The Soundscape Perception Index (SPI), ranging from 0 to 100.

    """
    # Ensure sample_data exists, generate if needed and possible
    if self.sample_data is None:
        logger.info("Sample data not found, generating default sample (n=1000).")
        self.sample(n=1000, return_sample=False)  # Generate sample if missing
        if self.sample_data is None:  # Check again in case sample failed
            msg = (
                "Could not generate sample data. "
                "Ensure model is defined (fit or define_dp)."
            )
            raise ValueError(msg)
    return spi_score(self.sample_data, test)

sspy_plot

sspy_plot(color='blue', title=None, n=1000)

Plot the joint distribution of the generated sample using soundscapy.

PARAMETER DESCRIPTION
color

Color for the density plot, by default "blue".

TYPE: str DEFAULT: 'blue'

title

Title for the plot, by default None.

TYPE: str DEFAULT: None

n

Number of samples to generate if sample_data is None, by default 1000.

TYPE: int DEFAULT: 1000

Source code in soundscapy/spi/msn.py
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
def sspy_plot(
    self, color: str = "blue", title: str | None = None, n: int = 1000
) -> None:
    """
    Plot the joint distribution of the generated sample using soundscapy.

    Parameters
    ----------
    color : str, optional
        Color for the density plot, by default "blue".
    title : str, optional
        Title for the plot, by default None.
    n : int, optional
        Number of samples to generate if `sample_data` is None, by default 1000.

    """
    if self.sample_data is None:
        self.sample(n=n)

    data = pd.DataFrame(self.sample_data, columns=["ISOPleasant", "ISOEventful"])
    plot_title = title if title is not None else "Soundscapy Density Plot"
    scatter(data, color=color, title=plot_title)

summary

summary()

Provide a summary of the fitted MultiSkewNorm model.

RETURNS DESCRIPTION
str or None

A string summarizing the model parameters and data, or a message indicating the model is not fitted. Returns None if fitted but summary logic is not fully implemented yet.

Source code in soundscapy/spi/msn.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def summary(self) -> str | None:
    """
    Provide a summary of the fitted MultiSkewNorm model.

    Returns
    -------
    str or None
        A string summarizing the model parameters and data, or a message
        indicating the model is not fitted. Returns None if fitted but
        summary logic is not fully implemented yet.

    """
    if self.cp is None and self.dp is None and self.selm_model is None:
        return "MultiSkewNorm is not fitted."
    if self.data is not None:
        print(f"Fitted from data. n = {len(self.data)}")  # noqa: T201
    else:
        print("Fitted from direct parameters.")  # noqa: T201
    print(self.dp)  # noqa: T201
    print("\n")  # noqa: T201
    print(self.cp)  # noqa: RET503, T201

add_iso_coords

add_iso_coords(data, val_range=(1, 5), names=('ISOPleasant', 'ISOEventful'), angles=EQUAL_ANGLES, *, overwrite=False)

Calculate and add ISO coordinates as new columns in the DataFrame.

PARAMETER DESCRIPTION
data

Input DataFrame containing PAQ data

TYPE: DataFrame

val_range

(min, max) range of original PAQ responses, by default (1, 5)

TYPE: Tuple[int, int] DEFAULT: (1, 5)

names

Names for new coordinate columns, by default ("ISOPleasant", "ISOEventful")

TYPE: Tuple[str, str] DEFAULT: ('ISOPleasant', 'ISOEventful')

angles

Angles for each PAQ in degrees, by default EQUAL_ANGLES

TYPE: Tuple[int, ...] DEFAULT: EQUAL_ANGLES

overwrite

Whether to overwrite existing ISO coordinate columns, by default False

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
DataFrame

DataFrame with new ISO coordinate columns added

RAISES DESCRIPTION
Warning

If ISO coordinate columns already exist and overwrite is False

Examples:

>>> import pandas as pd
>>> df = pd.DataFrame({
...     'PAQ1': [4, 2], 'PAQ2': [3, 5], 'PAQ3': [2, 4], 'PAQ4': [1, 3],
...     'PAQ5': [5, 1], 'PAQ6': [3, 2], 'PAQ7': [4, 3], 'PAQ8': [2, 5]
... })
>>> df_with_iso = add_iso_coords(df)
>>> df_with_iso[['ISOPleasant', 'ISOEventful']].round(2)
   ISOPleasant  ISOEventful
0        -0.03        -0.28
1         0.47         0.18
Source code in soundscapy/surveys/processing.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
def add_iso_coords(
    data: pd.DataFrame,
    val_range: tuple[int, int] = (1, 5),
    names: tuple[str, str] = ("ISOPleasant", "ISOEventful"),
    angles: tuple[int, ...] = EQUAL_ANGLES,
    *,
    overwrite: bool = False,
) -> pd.DataFrame:
    """
    Calculate and add ISO coordinates as new columns in the DataFrame.

    Parameters
    ----------
    data : pd.DataFrame
        Input DataFrame containing PAQ data
    val_range : Tuple[int, int], optional
        (min, max) range of original PAQ responses, by default (1, 5)
    names : Tuple[str, str], optional
        Names for new coordinate columns, by default ("ISOPleasant", "ISOEventful")
    angles : Tuple[int, ...], optional
        Angles for each PAQ in degrees, by default EQUAL_ANGLES
    *
    overwrite : bool, optional
        Whether to overwrite existing ISO coordinate columns, by default False

    Returns
    -------
    pd.DataFrame
        DataFrame with new ISO coordinate columns added

    Raises
    ------
    Warning
        If ISO coordinate columns already exist and overwrite is False

    Examples
    --------
    >>> import pandas as pd
    >>> df = pd.DataFrame({
    ...     'PAQ1': [4, 2], 'PAQ2': [3, 5], 'PAQ3': [2, 4], 'PAQ4': [1, 3],
    ...     'PAQ5': [5, 1], 'PAQ6': [3, 2], 'PAQ7': [4, 3], 'PAQ8': [2, 5]
    ... })
    >>> df_with_iso = add_iso_coords(df)
    >>> df_with_iso[['ISOPleasant', 'ISOEventful']].round(2)
       ISOPleasant  ISOEventful
    0        -0.03        -0.28
    1         0.47         0.18

    """
    for name in names:
        if name in data.columns:
            if overwrite:
                data = data.drop(name, axis=1)
            else:
                msg = (
                    f"{name} already in dataframe. Use `overwrite=True` to replace it."
                )
                raise Warning(msg)

    iso_pleasant, iso_eventful = calculate_iso_coords(
        data, val_range=val_range, angles=angles
    )
    data = data.assign(**{names[0]: iso_pleasant, names[1]: iso_eventful})

    logger.info(f"Added ISO coordinates to DataFrame with column names: {names}")
    return data

add_results

add_results(results_df, metric_results)

Add results to MultiIndex dataframe.

PARAMETER DESCRIPTION
results_df

MultiIndex dataframe to add results to.

TYPE: DataFrame

metric_results

MultiIndex dataframe of results to add.

TYPE: DataFrame

RETURNS DESCRIPTION
DataFrame

Index includes "Recording" and "Channel" with a column for each index.

RAISES DESCRIPTION
ValueError

If the input DataFrames are not in the expected format.

Source code in soundscapy/audio/metrics.py
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
def add_results(results_df: pd.DataFrame, metric_results: pd.DataFrame):
    """
    Add results to MultiIndex dataframe.

    Parameters
    ----------
    results_df : pd.DataFrame
        MultiIndex dataframe to add results to.
    metric_results : pd.DataFrame
        MultiIndex dataframe of results to add.

    Returns
    -------
    pd.DataFrame
        Index includes "Recording" and "Channel" with a column for each index.

    Raises
    ------
    ValueError
        If the input DataFrames are not in the expected format.

    """
    logger.info("Adding results to MultiIndex DataFrame")
    try:
        # TODO: Add check for whether all of the recordings have rows in the dataframe
        # If not, add new rows first

        if not set(metric_results.columns).issubset(set(results_df.columns)):
            # Check if results_df already has the columns in results
            results_df = results_df.join(metric_results)
        else:
            results_df.update(metric_results, errors="ignore")
        logger.debug("Results added successfully")
        return results_df
    except Exception as e:
        logger.error(f"Error adding results to DataFrame: {e!s}")
        raise ValueError("Invalid input DataFrame format") from e

cp2dp

cp2dp(cp, family='SN')

Convert centred parameters to direct parameters.

PARAMETER DESCRIPTION
cp

The centred parameters object.

TYPE: CentredParams

family

The distribution family, by default "SN" (Skew Normal).

TYPE: str DEFAULT: 'SN'

RETURNS DESCRIPTION
DirectParams

The corresponding direct parameters object.

Source code in soundscapy/spi/msn.py
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
def cp2dp(
    cp: CentredParams, family: Literal["SN", "ESN", "ST", "SC"] = "SN"
) -> DirectParams:
    """
    Convert centred parameters to direct parameters.

    Parameters
    ----------
    cp : CentredParams
        The centred parameters object.
    family : str, optional
        The distribution family, by default "SN" (Skew Normal).

    Returns
    -------
    DirectParams
        The corresponding direct parameters object.

    """
    dp_r = rsn.cp2dp(cp.mean, cp.sigma, cp.skew, family=family)

    return DirectParams(*dp_r)

create_iso_subplots

create_iso_subplots(data, x='ISOPleasant', y='ISOEventful', subplot_by=None, title='Soundscapy Plot', plot_layers=('scatter', 'density'), *, subplot_size=(4, 4), subplot_titles='by_group', subplot_title_prefix='Plot', nrows=None, ncols=None, **kwargs)

Create a set of subplots displaying data visualizations for soundscape analysis.

This function generates a collection of subplots, where each subplot corresponds to a subset of the input data. The subplots can display scatter plots, density plots, or simplified density plots, and can be organized by specific grouping criteria. Users can specify titles, overall size, row and column layout, and layering of plot types.

PARAMETER DESCRIPTION
data

Input data to be visualized. Can be a single data frame or a list of data frames for use in multiple subplots.

TYPE: pandas.DataFrame or list of pandas.DataFrame

x

The name of the column in the data to be used for the x-axis. Default is "ISOPleasant".

TYPE: str DEFAULT: 'ISOPleasant'

y

The name of the column in the data to be used for the y-axis. Default is "ISOEventful".

TYPE: str DEFAULT: 'ISOEventful'

subplot_by

The column name by which to group data into subplots. If None, data is not grouped and plotted in a single set of axes. Default is None.

TYPE: str or None DEFAULT: None

title

The overarching title of the figure. If None, no overall title is added. Default is "Soundscapy Plot".

TYPE: str or None DEFAULT: 'Soundscapy Plot'

plot_layers

such Literals, optional Type(s) of plot layers to include in each subplot. Can be a single type or a sequence of types. Default is ("scatter", "density").

TYPE: Literal["scatter", "density", "simple_density"] or Sequence of DEFAULT: ('scatter', 'density')

subplot_size

Size of each subplot in inches as (width, height). Default is (4, 4).

TYPE: tuple of int DEFAULT: (4, 4)

subplot_titles

optional Determines how subplot titles are assigned. Options are "by_group" (titles derived from group names), "numbered" (titles as indices), or a list of custom titles. If None, no titles are added. Default is "by_group".

TYPE: Literal["by_group", "numbered"], list of str, or None, DEFAULT: 'by_group'

subplot_title_prefix

Prefix for subplot titles if "numbered" is selected as subplot_titles. Default is "Plot".

TYPE: str DEFAULT: 'Plot'

nrows

Number of rows for the subplot grid. If None, automatically calculated based on the number of subplots. Default is None.

TYPE: int or None DEFAULT: None

ncols

Number of columns for the subplot grid. If None, automatically calculated based on the number of subplots. Default is None.

TYPE: int or None DEFAULT: None

**kwargs

Additional keyword arguments to pass to matplotlib's plt.subplots or for customizing the figure and subplots.

DEFAULT: {}

RETURNS DESCRIPTION
tuple

A tuple containing: - fig : matplotlib.figure.Figure The created matplotlib figure object containing the subplots. - np.ndarray An array of matplotlib.axes.Axes objects corresponding to the subplots.

Examples:

Basic subplots with default settings:

>>> import soundscapy as sspy
>>> import matplotlib.pyplot as plt
>>> import pandas as pd
>>> data = sspy.isd.load()
>>> data = sspy.add_iso_coords(data)
>>> four_locs = sspy.isd.select_location_ids(data,
...     ['CamdenTown', 'PancrasLock', 'RegentsParkJapan', 'RegentsParkFields']
... )
>>> fig, axes = sspy.create_iso_subplots(four_locs, subplot_by="LocationID")
>>> plt.show() # xdoctest: +SKIP

Create subplots by specifying a list of data

>>> data1 = pd.DataFrame({'ISOPleasant': np.random.uniform(-1, 1, 50),
...                       'ISOEventful': np.random.uniform(-1, 1, 50)})
>>> data2 = pd.DataFrame({'ISOPleasant': np.random.uniform(-1, 1, 50),
...                       'ISOEventful': np.random.uniform(-1, 1, 50)})
>>> fig, axes = create_iso_subplots(
...     [data1, data2], plot_layers="scatter", nrows=1, ncols=2
... )
>>> plt.show() # xdoctest: +SKIP
>>> assert len(axes) == 2
>>> plt.close('all')
Source code in soundscapy/plotting/plot_functions.py
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def create_iso_subplots(
    data: pd.DataFrame | list[pd.DataFrame],
    x: str = "ISOPleasant",
    y: str = "ISOEventful",
    subplot_by: str | None = None,
    title: str | None = "Soundscapy Plot",
    plot_layers: Literal["scatter", "density", "simple_density"]
    | Sequence[Literal["scatter", "simple_density", "density"]] = (
        "scatter",
        "density",
    ),
    *,
    subplot_size: tuple[int, int] = (4, 4),
    subplot_titles: Literal["by_group", "numbered"] | list[str] | None = "by_group",
    subplot_title_prefix: str = "Plot",  # Only used if subplot_titles = 'numbered'
    nrows: int | None = None,
    ncols: int | None = None,
    **kwargs,
) -> tuple[Figure, np.ndarray]:
    """
    Create a set of subplots displaying data visualizations for soundscape analysis.

    This function generates a collection of subplots, where each subplot corresponds
    to a subset of the input data. The subplots can display scatter plots, density
    plots, or simplified density plots, and can be organized by specific grouping
    criteria. Users can specify titles, overall size, row and column layout, and
    layering of plot types.

    Parameters
    ----------
    data : pandas.DataFrame or list of pandas.DataFrame
        Input data to be visualized. Can be a single data frame or a list of data
        frames for use in multiple subplots.
    x : str, optional
        The name of the column in the data to be used for the x-axis. Default is
        "ISOPleasant".
    y : str, optional
        The name of the column in the data to be used for the y-axis. Default is
        "ISOEventful".
    subplot_by : str or None, optional
        The column name by which to group data into subplots. If None, data is not
        grouped and plotted in a single set of axes. Default is None.
    title : str or None, optional
        The overarching title of the figure. If None, no overall title is added.
        Default is "Soundscapy Plot".
    plot_layers : Literal["scatter", "density", "simple_density"] or Sequence of
        such Literals, optional
        Type(s) of plot layers to include in each subplot. Can be a single type
        or a sequence of types. Default is ("scatter", "density").
    subplot_size : tuple of int, optional
        Size of each subplot in inches as (width, height). Default is (4, 4).
    subplot_titles : Literal["by_group", "numbered"], list of str, or None,
        optional
        Determines how subplot titles are assigned. Options are "by_group" (titles
        derived from group names), "numbered" (titles as indices), or a list of
        custom titles. If None, no titles are added. Default is "by_group".
    subplot_title_prefix : str, optional
        Prefix for subplot titles if "numbered" is selected as `subplot_titles`.
        Default is "Plot".
    nrows : int or None, optional
        Number of rows for the subplot grid. If None, automatically calculated
        based on the number of subplots. Default is None.
    ncols : int or None, optional
        Number of columns for the subplot grid. If None, automatically calculated
        based on the number of subplots. Default is None.
    **kwargs
        Additional keyword arguments to pass to matplotlib's `plt.subplots` or for
        customizing the figure and subplots.

    Returns
    -------
    tuple
        A tuple containing:
        - fig : matplotlib.figure.Figure
            The created matplotlib figure object containing the subplots.
        - np.ndarray
            An array of matplotlib.axes.Axes objects corresponding to the subplots.

    Examples
    --------
    Basic subplots with default settings:
    >>> import soundscapy as sspy
    >>> import matplotlib.pyplot as plt
    >>> import pandas as pd
    >>> data = sspy.isd.load()
    >>> data = sspy.add_iso_coords(data)
    >>> four_locs = sspy.isd.select_location_ids(data,
    ...     ['CamdenTown', 'PancrasLock', 'RegentsParkJapan', 'RegentsParkFields']
    ... )
    >>> fig, axes = sspy.create_iso_subplots(four_locs, subplot_by="LocationID")
    >>> plt.show() # xdoctest: +SKIP

    Create subplots by specifying a list of data
    >>> data1 = pd.DataFrame({'ISOPleasant': np.random.uniform(-1, 1, 50),
    ...                       'ISOEventful': np.random.uniform(-1, 1, 50)})
    >>> data2 = pd.DataFrame({'ISOPleasant': np.random.uniform(-1, 1, 50),
    ...                       'ISOEventful': np.random.uniform(-1, 1, 50)})
    >>> fig, axes = create_iso_subplots(
    ...     [data1, data2], plot_layers="scatter", nrows=1, ncols=2
    ... )
    >>> plt.show() # xdoctest: +SKIP
    >>> assert len(axes) == 2
    >>> plt.close('all')

    """
    # Process input data and prepare for subplot creation
    data_list, subplot_titles_list, n_subplots = _prepare_subplot_data(
        data=data, x=y, y=y, subplot_by=subplot_by, subplot_titles=subplot_titles
    )

    # Calculate subplot layout
    nrows, ncols, n_subplots = allocate_subplot_axes(nrows, ncols, n_subplots)

    # Set up figure and subplots
    if title:
        vert_adjust = 1.2
    else:
        vert_adjust = 1.0
    figsize = kwargs.pop(
        "figsize", (ncols * subplot_size[0], nrows * (vert_adjust * subplot_size[1]))
    )

    subplots_params = SubplotsParams()
    subplots_params.update(
        nrows=nrows,
        ncols=ncols,
        figsize=figsize,
        subplot_by=subplot_by,
        extra="ignore",
        **kwargs,
    )

    fig, axes = plt.subplots(**subplots_params.as_plt_subplots_args())

    # Create each subplot
    _create_subplots(
        data_list,
        axes,
        n_subplots,
        subplot_titles_list,
        x,
        y,
        plot_layers,
        subplot_title_prefix,
        **kwargs,
    )

    # Add overall title and adjust layout
    if title:
        fig.suptitle(title, fontsize=DEFAULT_STYLE_PARAMS["title_fontsize"])

    fig.tight_layout()

    return fig, axes

density

density(data, title='Soundscape Density Plot', ax=None, *, x='ISOPleasant', y='ISOEventful', hue=None, incl_scatter=True, density_type='full', palette='colorblind', scatter_kws=None, legend='auto', prim_labels=None, **kwargs)

Plot a density plot of ISOCoordinates.

Creates a kernel density estimate visualization of data distribution on a circumplex grid with the custom Soundscapy styling for soundscape circumplex visualisations. Can optionally include a scatter plot of the underlying data points.

PARAMETER DESCRIPTION
data

Input data structure containing coordinate data, typically with ISOPleasant and ISOEventful columns.

TYPE: DataFrame

title

Title to add to circumplex plot, by default "Soundscape Density Plot"

TYPE: str | None DEFAULT: 'Soundscape Density Plot'

ax

Pre-existing axes object to use for the plot, by default None If None call matplotlib.pyplot.subplots with figsize internally.

TYPE: Axes DEFAULT: None

x

Column name for x variable, by default "ISOPleasant"

TYPE: str DEFAULT: 'ISOPleasant'

y

Column name for y variable, by default "ISOEventful"

TYPE: str DEFAULT: 'ISOEventful'

hue

Grouping variable that will produce density contours with different colors. Can be either categorical or numeric, although color mapping will behave differently in latter case, by default None

TYPE: str | ndarray | Series | None DEFAULT: None

incl_scatter

Whether to include a scatter plot of the data points, by default True

TYPE: bool DEFAULT: True

density_type

Type of density plot to draw. "full" uses default parameters, "simple" uses a lower number of levels (2), higher threshold (0.5), and lower alpha (0.5) for a cleaner visualization, by default "full"

TYPE: (full, simple) DEFAULT: "full"

palette

Method for choosing the colors to use when mapping the hue semantic. String values are passed to seaborn.color_palette(). List or dict values imply categorical mapping, while a colormap object implies numeric mapping, by default "colorblind"

TYPE: SeabornPaletteType | None DEFAULT: 'colorblind'

scatter_kws

Keyword arguments to pass to seaborn.scatterplot if incl_scatter is True, by default {"s": 25, "linewidth": 0}

TYPE: dict | None DEFAULT: None

incl_outline

Whether to include an outline for the density contours, by default False

TYPE: bool

legend

How to draw the legend. If "brief", numeric hue variables will be represented with a sample of evenly spaced values. If "full", every group will get an entry in the legend. If "auto", choose between brief or full representation based on number of levels. If False, no legend data is added and no legend is drawn, by default "auto"

TYPE: (auto, brief, full, False) DEFAULT: "auto"

prim_labels

Deprecated. Use xlabel and ylabel parameters instead.

TYPE: bool | None DEFAULT: None

**kwargs

Additional styling parameters:

  • alpha : float, optional Proportional opacity of the density fill, by default 0.8
  • fill : bool, optional If True, fill in the area between bivariate contours, by default True
  • levels : int | Iterable[float], optional Number of contour levels or values to draw contours at, by default 10
  • thresh : float, optional Lowest iso-proportional level at which to draw a contour line, by default 0.05
  • bw_adjust : float, optional Factor that multiplicatively scales the bandwidth, by default 1.2
  • xlabel, ylabel : str | Literal[False], optional Custom axis labels. By default, "\(P_{ISO}\)" and "\(E_{ISO}\)" with math rendering. If None is passed, the column names (x and y) will be used as labels. If a string is provided, it will be used as the label. If False is passed, axis labels will be hidden.
  • xlim, ylim : tuple[float, float], optional Limits for x and y axes, by default (-1, 1) for both
  • legend_loc : MplLegendLocType, optional Location of legend, by default "best"
  • diagonal_lines : bool, optional Whether to include diagonal dimension labels (e.g. calm, etc.), by default False
  • figsize : tuple[int, int], optional Size of the figure to return if ax is None, by default (5, 5)
  • prim_ax_fontdict : dict, optional Font dictionary for axis labels with these defaults:

    { "family": "sans-serif", "fontstyle": "normal", "fontsize": "large", "fontweight": "medium", "parse_math": True, "c": "black", "alpha": 1, } - fontsize, fontweight, fontstyle, family, c, alpha, parse_math: Direct parameters for font styling in axis labels

Also accepts additional keyword arguments for matplotlib's contour and contourf functions.

TYPE: dict DEFAULT: {}

RETURNS DESCRIPTION
Axes object containing the plot.
Notes

This function will raise a warning if the dataset has fewer than RECOMMENDED_MIN_SAMPLES (30) data points, as density plots are not reliable with small sample sizes.

Examples:

Basic density plot with default settings:

>>> import soundscapy as sspy
>>> import matplotlib.pyplot as plt
>>> data = sspy.isd.load()
>>> data = sspy.add_iso_coords(data)
>>> ax = sspy.density(data)
>>> plt.show() # xdoctest: +SKIP

Simple density plot with fewer contour levels:

>>> ax = sspy.density(data, density_type="simple")
>>> plt.show() # xdoctest: +SKIP

Density plot with custom styling:

>>> sub_data = sspy.isd.select_location_ids(
...    data, ['CamdenTown', 'PancrasLock', 'RegentsParkJapan', 'RegentsParkFields'])
>>> ax = sspy.density(
...     sub_data,
...     hue="SessionID",
...     incl_scatter=True,
...     legend_loc="upper right",
...     fill = False,
...     density_type = "simple",
... )
>>> plt.show() # xdoctest: +SKIP

Add density to existing plots:

>>> fig, axes = plt.subplots(1, 2, figsize=(12, 6))
>>> axes[0] = sspy.density(
...     sspy.isd.select_location_ids(data, ['CamdenTown', 'PancrasLock']),
...     ax=axes[0], title="CamdenTown and PancrasLock", hue="LocationID",
...     density_type="simple"
... )
>>> axes[1] = sspy.density(
...     sspy.isd.select_location_ids(data, ['RegentsParkJapan']),
...     ax=axes[1], title="RegentsParkJapan"
... )
>>> plt.tight_layout()
>>> plt.show() # xdoctest: +SKIP
>>> plt.close('all')
Source code in soundscapy/plotting/plot_functions.py
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
def density(
    data: pd.DataFrame,
    title: str | None = "Soundscape Density Plot",
    ax: Axes | None = None,
    *,
    x: str | None = "ISOPleasant",
    y: str | None = "ISOEventful",
    hue: str | None = None,
    incl_scatter: bool = True,
    density_type: str = "full",
    palette: SeabornPaletteType | None = "colorblind",
    scatter_kws: dict | None = None,
    legend: Literal["auto", "brief", "full", False] = "auto",
    prim_labels: bool | None = None,  # Alias for primary_labels, deprecated
    **kwargs,
) -> Axes:
    """
    Plot a density plot of ISOCoordinates.

    Creates a kernel density estimate visualization of data distribution on a
    circumplex grid with the custom Soundscapy styling for soundscape circumplex
    visualisations. Can optionally include a scatter plot of the underlying data points.

    Parameters
    ----------
    data : pd.DataFrame
        Input data structure containing coordinate data, typically with ISOPleasant
        and ISOEventful columns.
    title : str | None, optional
        Title to add to circumplex plot, by default "Soundscape Density Plot"
    ax : matplotlib.axes.Axes, optional
        Pre-existing axes object to use for the plot, by default None
        If `None` call `matplotlib.pyplot.subplots` with `figsize` internally.
    x : str, optional
        Column name for x variable, by default "ISOPleasant"
    y : str, optional
        Column name for y variable, by default "ISOEventful"
    hue : str | np.ndarray | pd.Series | None, optional
        Grouping variable that will produce density contours with different colors.
        Can be either categorical or numeric, although color mapping will behave
        differently in latter case, by default None
    incl_scatter : bool, optional
        Whether to include a scatter plot of the data points, by default True
    density_type : {"full", "simple"}, optional
        Type of density plot to draw. "full" uses default parameters, "simple"
        uses a lower number of levels (2), higher threshold (0.5), and lower alpha (0.5)
        for a cleaner visualization, by default "full"
    palette : SeabornPaletteType | None, optional
        Method for choosing the colors to use when mapping the hue semantic.
        String values are passed to seaborn.color_palette().
        List or dict values imply categorical mapping, while a colormap object
        implies numeric mapping, by default "colorblind"
    scatter_kws : dict | None, optional
        Keyword arguments to pass to `seaborn.scatterplot` if incl_scatter is True,
        by default {"s": 25, "linewidth": 0}
    incl_outline : bool, optional
        Whether to include an outline for the density contours, by default False
    legend : {"auto", "brief", "full", False}, optional
        How to draw the legend. If "brief", numeric hue variables will be
        represented with a sample of evenly spaced values. If "full", every group will
        get an entry in the legend. If "auto", choose between brief or full
        representation based on number of levels.
        If False, no legend data is added and no legend is drawn, by default "auto"
    prim_labels : bool | None, optional
        Deprecated. Use xlabel and ylabel parameters instead.

    **kwargs : dict, optional
        Additional styling parameters:

        - alpha : float, optional
            Proportional opacity of the density fill, by default 0.8
        - fill : bool, optional
            If True, fill in the area between bivariate contours, by default True
        - levels : int | Iterable[float], optional
            Number of contour levels or values to draw contours at, by default 10
        - thresh : float, optional
            Lowest iso-proportional level at which to draw a contour line,
            by default 0.05
        - bw_adjust : float, optional
            Factor that multiplicatively scales the bandwidth, by default 1.2
        - xlabel, ylabel : str | Literal[False], optional
            Custom axis labels. By default, "$P_{ISO}$" and "$E_{ISO}$" with math
            rendering.
            If None is passed, the column names (x and y) will be used as labels.
            If a string is provided, it will be used as the label.
            If False is passed, axis labels will be hidden.
        - xlim, ylim : tuple[float, float], optional
            Limits for x and y axes, by default (-1, 1) for both
        - legend_loc : MplLegendLocType, optional
            Location of legend, by default "best"
        - diagonal_lines : bool, optional
            Whether to include diagonal dimension labels (e.g. calm, etc.),
            by default False
        - figsize : tuple[int, int], optional
            Size of the figure to return if `ax` is None, by default (5, 5)
        - prim_ax_fontdict : dict, optional
            Font dictionary for axis labels with these defaults:

            {
                "family": "sans-serif",
                "fontstyle": "normal",
                "fontsize": "large",
                "fontweight": "medium",
                "parse_math": True,
                "c": "black",
                "alpha": 1,
            }
        - fontsize, fontweight, fontstyle, family, c, alpha, parse_math:
            Direct parameters for font styling in axis labels

        Also accepts additional keyword arguments for matplotlib's contour and contourf
        functions.

    Returns
    -------
        Axes object containing the plot.

    Notes
    -----
    This function will raise a warning if the dataset has fewer than
    RECOMMENDED_MIN_SAMPLES (30) data points, as density plots are not reliable
    with small sample sizes.

    Examples
    --------
    Basic density plot with default settings:

    >>> import soundscapy as sspy
    >>> import matplotlib.pyplot as plt
    >>> data = sspy.isd.load()
    >>> data = sspy.add_iso_coords(data)
    >>> ax = sspy.density(data)
    >>> plt.show() # xdoctest: +SKIP

    Simple density plot with fewer contour levels:

    >>> ax = sspy.density(data, density_type="simple")
    >>> plt.show() # xdoctest: +SKIP

    Density plot with custom styling:

    >>> sub_data = sspy.isd.select_location_ids(
    ...    data, ['CamdenTown', 'PancrasLock', 'RegentsParkJapan', 'RegentsParkFields'])
    >>> ax = sspy.density(
    ...     sub_data,
    ...     hue="SessionID",
    ...     incl_scatter=True,
    ...     legend_loc="upper right",
    ...     fill = False,
    ...     density_type = "simple",
    ... )
    >>> plt.show() # xdoctest: +SKIP

    Add density to existing plots:

    >>> fig, axes = plt.subplots(1, 2, figsize=(12, 6))
    >>> axes[0] = sspy.density(
    ...     sspy.isd.select_location_ids(data, ['CamdenTown', 'PancrasLock']),
    ...     ax=axes[0], title="CamdenTown and PancrasLock", hue="LocationID",
    ...     density_type="simple"
    ... )
    >>> axes[1] = sspy.density(
    ...     sspy.isd.select_location_ids(data, ['RegentsParkJapan']),
    ...     ax=axes[1], title="RegentsParkJapan"
    ... )
    >>> plt.tight_layout()
    >>> plt.show() # xdoctest: +SKIP
    >>> plt.close('all')

    """
    style_args, subplots_args, kwargs = _setup_style_and_subplots_args_from_kwargs(
        x=x, y=y, prim_labels=prim_labels, kwargs=kwargs
    )

    # Set up density parameters
    density_args = _setup_density_params(
        data=data,
        x=x,
        y=y,
        hue=hue,
        density_type=density_type,
        palette=palette,
        legend=legend,
        **kwargs,
    )

    # Check if dataset is large enough for density plots
    _valid_density(data)

    if ax is None:
        _, ax = plt.subplots(1, 1, figsize=subplots_args.get("figsize"))

    # Removes the palette if no hue is specified
    if density_args.get("hue") is None:
        density_args.update(palette=None)

    # Set up scatter parameters if needed
    scatter_args = ScatterParams()
    scatter_args.update(
        data=data,
        x=x,
        y=y,
        palette=palette,
        hue=density_args.get("hue"),
        color=density_args.get("color"),
        **(scatter_kws or {}),
    )

    scatter_args.crosscheck_palette_hue()
    density_args.crosscheck_palette_hue()

    if incl_scatter:
        d = sns.scatterplot(ax=ax, **scatter_args.as_seaborn_kwargs())

    if density_type == "simple":
        d = sns.kdeplot(ax=ax, **density_args.as_seaborn_kwargs())
        d = sns.kdeplot(ax=ax, **density_args.to_outline().as_seaborn_kwargs())

    elif density_type == "full":
        d = sns.kdeplot(ax=ax, **density_args.as_seaborn_kwargs())
    else:
        raise ValueError

    _set_style()
    _circumplex_grid(
        ax=ax,
        xlim=style_args.get("xlim"),
        ylim=style_args.get("ylim"),
        xlabel=style_args.get("xlabel"),
        ylabel=style_args.get("ylabel"),
        diagonal_lines=style_args.get("diagonal_lines"),
        prim_ax_fontdict=style_args.get("prim_ax_fontdict"),
    )
    if title is not None:
        _set_circum_title(
            ax=ax,
            title=title,
            xlabel=style_args.get("xlabel"),
            ylabel=style_args.get("ylabel"),
        )
    if legend is not None and hue is not None:
        _move_legend(ax=ax, new_loc=style_args.get("legend_loc"))

    return d

disable_logging

disable_logging()

Disable all Soundscapy logging.

Examples:

>>> from soundscapy import disable_logging
>>> disable_logging()
>>> # No more logging messages will be shown
Source code in soundscapy/sspylogging.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def disable_logging() -> None:
    """
    Disable all Soundscapy logging.

    Examples
    --------
    >>> from soundscapy import disable_logging
    >>> disable_logging()
    >>> # No more logging messages will be shown

    """
    # First remove all handlers to ensure no output
    logger.remove()
    # Then disable the soundscapy namespace
    logger.disable("soundscapy")
    # Add a handler with an impossibly high level to ensure nothing is logged
    logger.add(sys.stderr, level=100)  # Level 100 is higher than any standard level

dp2cp

dp2cp(dp, family='SN')

Convert direct parameters to centred parameters.

PARAMETER DESCRIPTION
dp

The direct parameters object.

TYPE: DirectParams

family

The distribution family, by default "SN" (Skew Normal).

TYPE: str DEFAULT: 'SN'

RETURNS DESCRIPTION
CentredParams

The corresponding centred parameters object.

Source code in soundscapy/spi/msn.py
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
def dp2cp(
    dp: DirectParams, family: Literal["SN", "ESN", "ST", "SC"] = "SN"
) -> CentredParams:
    """
    Convert direct parameters to centred parameters.

    Parameters
    ----------
    dp : DirectParams
        The direct parameters object.
    family : str, optional
        The distribution family, by default "SN" (Skew Normal).

    Returns
    -------
    CentredParams
        The corresponding centred parameters object.

    """
    cp_r = rsn.dp2cp(dp.xi, dp.omega, dp.alpha, family=family)

    return CentredParams(*cp_r)

enable_debug

enable_debug()

Quickly enable DEBUG level logging to console.

This is a convenience function for debugging during interactive sessions.

Examples:

>>> from soundscapy import enable_debug
>>> enable_debug()
>>> # Now all debug messages will be shown
Source code in soundscapy/sspylogging.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def enable_debug() -> None:
    """
    Quickly enable DEBUG level logging to console.

    This is a convenience function for debugging during interactive sessions.

    Examples
    --------
    >>> from soundscapy import enable_debug
    >>> enable_debug()
    >>> # Now all debug messages will be shown

    """
    setup_logging(level="DEBUG", format_level="detailed")
    logger.info("Debug logging enabled")

get_logger

get_logger()

Get the Soundscapy logger instance.

Returns the loguru logger configured for Soundscapy. This is mainly for advanced users who want to configure logging themselves.

RETURNS DESCRIPTION
logger

The loguru logger instance

TYPE: logger

Examples:

>>> from soundscapy import get_logger
>>> logger = get_logger()
>>> logger.debug("Custom debug message")
Source code in soundscapy/sspylogging.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def get_logger() -> loguru.Logger:
    """
    Get the Soundscapy logger instance.

    Returns the loguru logger configured for Soundscapy. This is mainly for
    advanced users who want to configure logging themselves.

    Returns
    -------
    logger : loguru.logger
        The loguru logger instance

    Examples
    --------
    >>> from soundscapy import get_logger
    >>> logger = get_logger()
    >>> logger.debug("Custom debug message")

    """
    return logger

jointplot

jointplot(data, *, x=DEFAULT_XCOL, y=DEFAULT_YCOL, title='Soundscape Joint Plot', hue=None, incl_scatter=True, density_type='full', palette='colorblind', color=DEFAULT_COLOR, figsize=DEFAULT_FIGSIZE, scatter_kws=None, incl_outline=False, alpha=DEFAULT_SEABORN_PARAMS['alpha'], fill=True, levels=10, thresh=0.05, bw_adjust=DEFAULT_BW_ADJUST, legend='auto', prim_labels=None, joint_kws=None, marginal_kws=None, marginal_kind='kde', **kwargs)

Create a jointplot with a central distribution and marginal plots.

Creates a visualization with a main plot (density or scatter) in the center and marginal distribution plots along the x and y axes. The main plot uses the custom Soundscapy styling for soundscape circumplex visualisations, and the marginals show the individual distributions of each variable.

PARAMETER DESCRIPTION
data

Input data structure containing coordinate data, typically with ISOPleasant and ISOEventful columns.

TYPE: DataFrame

x

Column name for x variable, by default "ISOPleasant"

TYPE: str DEFAULT: DEFAULT_XCOL

y

Column name for y variable, by default "ISOEventful"

TYPE: str DEFAULT: DEFAULT_YCOL

title

Title to add to the jointplot, by default "Soundscape Joint Plot"

TYPE: str | None DEFAULT: 'Soundscape Joint Plot'

hue

Grouping variable that will produce plots with different colors. Can be either categorical or numeric, although color mapping will behave differently in latter case, by default None

TYPE: str | ndarray | Series | None DEFAULT: None

incl_scatter

Whether to include a scatter plot of the data points in the joint plot, by default True

TYPE: bool DEFAULT: True

density_type

Type of density plot to draw. "full" uses default parameters, "simple" uses a lower number of levels (2), higher threshold (0.5), and lower alpha (0.5) for a cleaner visualization, by default "full"

TYPE: (full, simple) DEFAULT: "full"

palette

Method for choosing the colors to use when mapping the hue semantic. String values are passed to seaborn.color_palette(). List or dict values imply categorical mapping, while a colormap object implies numeric mapping, by default "colorblind"

TYPE: SeabornPaletteType | None DEFAULT: 'colorblind'

color

Color to use for the plot elements when not using hue mapping, by default "#0173B2" (first color from colorblind palette)

TYPE: ColorType | None DEFAULT: DEFAULT_COLOR

figsize

Size of the figure to create (determines height, width is proportional), by default (5, 5)

TYPE: tuple[int, int] DEFAULT: DEFAULT_FIGSIZE

scatter_kws

Additional keyword arguments to pass to scatter plot if incl_scatter is True, by default None

TYPE: dict[str, Any] | None DEFAULT: None

incl_outline

Whether to include an outline for the density contours, by default False

TYPE: bool DEFAULT: False

alpha

Opacity level for the density fill, by default 0.8

TYPE: float DEFAULT: DEFAULT_SEABORN_PARAMS['alpha']

fill

Whether to fill the density contours, by default True

TYPE: bool DEFAULT: True

levels

Number of contour levels or specific levels to draw. A vector argument must have increasing values in [0, 1], by default 10

TYPE: int | Iterable[float] DEFAULT: 10

thresh

Lowest iso-proportion level at which to draw contours, by default 0.05

TYPE: float DEFAULT: 0.05

bw_adjust

Factor that multiplicatively scales the bandwidth. Increasing will make the density estimate smoother, by default 1.2

TYPE: float DEFAULT: DEFAULT_BW_ADJUST

legend

How to draw the legend for hue mapping, by default "auto"

TYPE: (auto, brief, full, False) DEFAULT: "auto"

prim_labels

Deprecated. Use xlabel and ylabel parameters instead.

TYPE: bool | None DEFAULT: None

joint_kws

Additional keyword arguments to pass to the joint plot, by default None

TYPE: dict[str, Any] | None DEFAULT: None

marginal_kws

Additional keyword arguments to pass to the marginal plots, by default {"fill": True, "common_norm": False}

TYPE: dict[str, Any] | None DEFAULT: None

marginal_kind

Type of plot to draw in the marginal axes, either "kde" for kernel density estimation or "hist" for histogram, by default "kde"

TYPE: (kde, hist) DEFAULT: "kde"

**kwargs

Additional styling parameters:

  • xlabel, ylabel : str | Literal[False], optional Custom axis labels. By default "\(P_{ISO}\)" and "\(E_{ISO}\)" with math rendering.

    If None is passed, the column names (x and y) will be used as labels.

    If a string is provided, it will be used as the label.

    If False is passed, axis labels will be hidden. - xlim, ylim : tuple[float, float], optional Limits for x and y axes, by default (-1, 1) for both - legend_loc : MplLegendLocType, optional Location of legend, by default "best" - diagonal_lines : bool, optional Whether to include diagonal dimension labels (e.g. calm, etc.), by default False - prim_ax_fontdict : dict, optional Font dictionary for axis labels with these defaults:

    { "family": "sans-serif", "fontstyle": "normal", "fontsize": "large", "fontweight": "medium", "parse_math": True, "c": "black", "alpha": 1, }

TYPE: dict DEFAULT: {}

RETURNS DESCRIPTION
JointGrid

The seaborn JointGrid object containing the plot

Notes

This function will raise a warning if the dataset has fewer than RECOMMENDED_MIN_SAMPLES (30) data points, as density plots are not reliable with small sample sizes.

Examples:

Basic jointplot with default settings:

>>> import soundscapy as sspy
>>> import matplotlib.pyplot as plt
>>> data = sspy.isd.load()
>>> data = sspy.add_iso_coords(data)
>>> g = sspy.jointplot(data)
>>> plt.show() # xdoctest: +SKIP

Jointplot with histogram marginals:

>>> g = sspy.jointplot(data, marginal_kind="hist")
>>> plt.show() # xdoctest: +SKIP

Jointplot with custom styling and grouping:

>>> g = sspy.jointplot(
...     data,
...     hue="LocationID",
...     incl_scatter=True,
...     density_type="simple",
...     diagonal_lines=True,
...     figsize=(6, 6),
...     title="Grouped Soundscape Analysis"
... )
>>> plt.show() # xdoctest: +SKIP
>>> plt.close('all')
Source code in soundscapy/plotting/plot_functions.py
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
def jointplot(
    data: pd.DataFrame,
    *,
    x: str = DEFAULT_XCOL,
    y: str = DEFAULT_YCOL,
    title: str | None = "Soundscape Joint Plot",
    hue: str | None = None,
    incl_scatter: bool = True,
    density_type: str = "full",
    palette: SeabornPaletteType | None = "colorblind",
    color: ColorType | None = DEFAULT_COLOR,
    figsize: tuple[int, int] = DEFAULT_FIGSIZE,
    scatter_kws: dict[str, Any] | None = None,
    incl_outline: bool = False,
    alpha: float = DEFAULT_SEABORN_PARAMS["alpha"],
    fill: bool = True,
    levels: int | tuple[float, ...] = 10,
    thresh: float = 0.05,
    bw_adjust: float = DEFAULT_BW_ADJUST,
    legend: Literal["auto", "brief", "full", False] = "auto",
    prim_labels: bool | None = None,  # Alias for primary_labels, deprecated
    joint_kws: dict[str, Any] | None = None,
    marginal_kws: dict[str, Any] | None = None,
    marginal_kind: str = "kde",
    **kwargs,
) -> sns.JointGrid:
    """
    Create a jointplot with a central distribution and marginal plots.

    Creates a visualization with a main plot (density or scatter) in the center and
    marginal distribution plots along the x and y axes. The main plot uses the custom
    Soundscapy styling for soundscape circumplex visualisations, and the marginals show
    the individual distributions of each variable.

    Parameters
    ----------
    data : pd.DataFrame
        Input data structure containing coordinate data, typically with ISOPleasant
        and ISOEventful columns.
    x : str, optional
        Column name for x variable, by default "ISOPleasant"
    y : str, optional
        Column name for y variable, by default "ISOEventful"
    title : str | None, optional
        Title to add to the jointplot, by default "Soundscape Joint Plot"
    hue : str | np.ndarray | pd.Series | None, optional
        Grouping variable that will produce plots with different colors.
        Can be either categorical or numeric, although color mapping will behave
        differently in latter case, by default None
    incl_scatter : bool, optional
        Whether to include a scatter plot of the data points in the joint plot,
        by default True
    density_type : {"full", "simple"}, optional
        Type of density plot to draw. "full" uses default parameters, "simple"
        uses a lower number of levels (2), higher threshold (0.5), and lower alpha (0.5)
        for a cleaner visualization, by default "full"
    palette : SeabornPaletteType | None, optional
        Method for choosing the colors to use when mapping the hue semantic.
        String values are passed to seaborn.color_palette().
        List or dict values imply categorical mapping, while a colormap object
        implies numeric mapping, by default "colorblind"
    color : ColorType | None, optional
        Color to use for the plot elements when not using hue mapping,
        by default "#0173B2" (first color from colorblind palette)
    figsize : tuple[int, int], optional
        Size of the figure to create (determines height, width is proportional),
        by default (5, 5)
    scatter_kws : dict[str, Any] | None, optional
        Additional keyword arguments to pass to scatter plot if incl_scatter is True,
        by default None
    incl_outline : bool, optional
        Whether to include an outline for the density contours, by default False
    alpha : float, optional
        Opacity level for the density fill, by default 0.8
    fill : bool, optional
        Whether to fill the density contours, by default True
    levels : int | Iterable[float], optional
        Number of contour levels or specific levels to draw. A vector argument
        must have increasing values in [0, 1], by default 10
    thresh : float, optional
        Lowest iso-proportion level at which to draw contours, by default 0.05
    bw_adjust : float, optional
        Factor that multiplicatively scales the bandwidth. Increasing will make
        the density estimate smoother, by default 1.2
    legend : {"auto", "brief", "full", False}, optional
        How to draw the legend for hue mapping, by default "auto"
    prim_labels : bool | None, optional
        Deprecated. Use xlabel and ylabel parameters instead.
    joint_kws : dict[str, Any] | None, optional
        Additional keyword arguments to pass to the joint plot, by default None
    marginal_kws : dict[str, Any] | None, optional
        Additional keyword arguments to pass to the marginal plots,
        by default {"fill": True, "common_norm": False}
    marginal_kind : {"kde", "hist"}, optional
        Type of plot to draw in the marginal axes, either "kde" for kernel
        density estimation or "hist" for histogram, by default "kde"

    **kwargs : dict, optional
        Additional styling parameters:

        - xlabel, ylabel : str | Literal[False], optional
            Custom axis labels. By default "$P_{ISO}$" and "$E_{ISO}$" with
            math rendering.

            If None is passed, the column names (x and y) will be used as labels.

            If a string is provided, it will be used as the label.

            If False is passed, axis labels will be hidden.
        - xlim, ylim : tuple[float, float], optional
            Limits for x and y axes, by default (-1, 1) for both
        - legend_loc : MplLegendLocType, optional
            Location of legend, by default "best"
        - diagonal_lines : bool, optional
            Whether to include diagonal dimension labels (e.g. calm, etc.),
            by default False
        - prim_ax_fontdict : dict, optional
            Font dictionary for axis labels with these defaults:

            {
                "family": "sans-serif",
                "fontstyle": "normal",
                "fontsize": "large",
                "fontweight": "medium",
                "parse_math": True,
                "c": "black",
                "alpha": 1,
            }

    Returns
    -------
    sns.JointGrid
        The seaborn JointGrid object containing the plot

    Notes
    -----
    This function will raise a warning if the dataset has fewer than
    RECOMMENDED_MIN_SAMPLES (30) data points, as density plots are not reliable
    with small sample sizes.

    Examples
    --------
    Basic jointplot with default settings:

    >>> import soundscapy as sspy
    >>> import matplotlib.pyplot as plt
    >>> data = sspy.isd.load()
    >>> data = sspy.add_iso_coords(data)
    >>> g = sspy.jointplot(data)
    >>> plt.show() # xdoctest: +SKIP

    Jointplot with histogram marginals:

    >>> g = sspy.jointplot(data, marginal_kind="hist")
    >>> plt.show() # xdoctest: +SKIP

    Jointplot with custom styling and grouping:

    >>> g = sspy.jointplot(
    ...     data,
    ...     hue="LocationID",
    ...     incl_scatter=True,
    ...     density_type="simple",
    ...     diagonal_lines=True,
    ...     figsize=(6, 6),
    ...     title="Grouped Soundscape Analysis"
    ... )
    >>> plt.show() # xdoctest: +SKIP
    >>> plt.close('all')

    """
    # Check if dataset is large enough for density plots
    _valid_density(data)

    style_args, subplots_args, kwargs = _setup_style_and_subplots_args_from_kwargs(
        x=x, y=y, prim_labels=prim_labels, kwargs=kwargs
    )

    # Initialize default dicts if None
    scatter_args = ScatterParams()
    scatter_args.update(**scatter_kws) if scatter_kws is not None else None

    joint_kws = {} if joint_kws is None else joint_kws
    marginal_kws = (
        {"fill": True, "common_norm": False} if marginal_kws is None else marginal_kws
    )

    if density_type == "simple":
        thresh = DEFAULT_SIMPLE_DENSITY_PARAMS["thresh"]
        levels = DEFAULT_SIMPLE_DENSITY_PARAMS["levels"]
        alpha = DEFAULT_SIMPLE_DENSITY_PARAMS["alpha"]
        incl_outline = True

    # Handle hue and color
    if hue is None:
        # Removes the palette if no hue is specified
        palette = None
        color = sns.color_palette("colorblind", 1)[0] if color is None else color

    # Create the joint grid
    g = sns.JointGrid(
        data=data,
        x=x,
        y=y,
        hue=hue,
        palette=palette,
        # height=figsize[0],  # Use figsize for height
        xlim=style_args.xlim,
        ylim=style_args.ylim,
    )

    # Add the density plot to the joint plot area
    density(
        data,
        x=x,
        y=y,
        incl_scatter=incl_scatter,
        density_type=density_type,
        title=None,  # We'll set the title separately
        ax=g.ax_joint,
        hue=hue,
        palette=palette,
        color=color,
        scatter_kws=scatter_kws,
        incl_outline=incl_outline,
        legend_loc=style_args.legend_loc,
        alpha=alpha,
        legend=legend,
        fill=fill,
        levels=levels,
        thresh=thresh,
        bw_adjust=bw_adjust,
        diagonal_lines=style_args.diagonal_lines,
        xlim=style_args.xlim,
        ylim=style_args.ylim,
        **joint_kws,
    )

    # Add the marginal plots
    if marginal_kind == "hist":
        sns.histplot(
            data=data,
            x=x,
            hue=hue,
            palette=palette,
            ax=g.ax_marg_x,
            binrange=style_args.xlim,
            legend=False,
            **marginal_kws,
        )
        sns.histplot(
            data=data,
            y=y,
            hue=hue,
            palette=palette,
            ax=g.ax_marg_y,
            binrange=style_args.ylim,
            legend=False,
            **marginal_kws,
        )
    elif marginal_kind == "kde":
        sns.kdeplot(
            data=data,
            x=x,
            hue=hue,
            palette=palette,
            ax=g.ax_marg_x,
            bw_adjust=bw_adjust,
            legend=False,
            **marginal_kws,
        )
        sns.kdeplot(
            data=data,
            y=y,
            hue=hue,
            palette=palette,
            ax=g.ax_marg_y,
            bw_adjust=bw_adjust,
            legend=False,
            **marginal_kws,
        )

    # Set title
    if title is not None:
        g.ax_marg_x.set_title(title, pad=6.0)

    _set_style()
    _circumplex_grid(
        ax=g.ax_joint,
        xlim=style_args.get("xlim"),
        ylim=style_args.get("ylim"),
        xlabel=style_args.get("xlabel"),
        ylabel=style_args.get("ylabel"),
        diagonal_lines=style_args.get("diagonal_lines"),
        prim_ax_fontdict=style_args.get("prim_ax_fontdict"),
    )

    if legend is not None and hue is not None:
        _move_legend(ax=g.ax_joint, new_loc=style_args.get("legend_loc"))

    return g

paq_likert

paq_likert(data, title='Stacked Likert Plot', paq_cols=PAQ_IDS, *, legend=True, ax=None, plot_percentage=False, bar_labels=True, **kwargs)

Create a Likert scale plot for PAQ (Perceived Affective Quality) data.

PARAMETER DESCRIPTION
data

DataFrame containing PAQ values.

TYPE: DataFrame

paq_cols

List of column names containing PAQ data, by default PAQ_IDS.

TYPE: list[str] DEFAULT: PAQ_IDS

title

Plot title, by default "Stacked Likert Plot".

TYPE: str DEFAULT: 'Stacked Likert Plot'

legend

Whether to show the legend, by default True.

TYPE: bool DEFAULT: True

ax

Matplotlib axes to plot on, by default None.

TYPE: Axes DEFAULT: None

plot_percentage

Whether to show percentages instead of absolute values, by default False.

TYPE: bool DEFAULT: False

bar_labels

Whether to show bar labels, by default True.

TYPE: bool DEFAULT: True

**kwargs

Additional keyword arguments passed to plot_likert.plot_likert.

DEFAULT: {}

RETURNS DESCRIPTION
None

This function does not return anything, it plots directly to the given axes.

Examples:

>>> import soundscapy as sspy
>>> data = sspy.isd.load(['CamdenTown'])
>>> paq_likert(data, "Camden Town Likert data")
>>> plt.show() # xdoctest: +SKIP
Source code in soundscapy/plotting/likert.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def paq_likert(
    data: pd.DataFrame,
    title: str = "Stacked Likert Plot",
    paq_cols: list[str] = PAQ_IDS,
    *,
    legend: bool = True,
    ax: Axes | None = None,
    plot_percentage: bool = False,
    bar_labels: bool = True,
    **kwargs,
) -> None:
    """
    Create a Likert scale plot for PAQ (Perceived Affective Quality) data.

    Parameters
    ----------
    data : pd.DataFrame
        DataFrame containing PAQ values.
    paq_cols : list[str], optional
        List of column names containing PAQ data, by default PAQ_IDS.
    title : str, optional
        Plot title, by default "Stacked Likert Plot".
    legend : bool, optional
        Whether to show the legend, by default True.
    ax : Axes, optional
        Matplotlib axes to plot on, by default None.
    plot_percentage : bool, optional
        Whether to show percentages instead of absolute values, by default False.
    bar_labels : bool, optional
        Whether to show bar labels, by default True.
    **kwargs
        Additional keyword arguments passed to plot_likert.plot_likert.

    Returns
    -------
    None
        This function does not return anything, it plots directly to the given axes.

    Examples
    --------
    >>> import soundscapy as sspy
    >>> data = sspy.isd.load(['CamdenTown'])
    >>> paq_likert(data, "Camden Town Likert data")
    >>> plt.show() # xdoctest: +SKIP

    """
    warnings.warn(
        "This is an experimental function. It may change in the future. ",
        ExperimentalWarning,
        stacklevel=2,
    )

    new_data = data[paq_cols].copy()
    new_data = new_data.apply(likert_categorical_from_data, axis=0)  # type: ignore

    if ax is None:
        _, ax = plt.subplots(figsize=(8, 6))

    plot_likert.plot_likert(
        new_data,
        LIKERT_SCALES.paq,
        plot_percentage=plot_percentage,
        ax=ax,
        legend=legend,
        bar_labels=bar_labels,  # show the bar labels
        title=title,
        **kwargs,
    )

paq_radar_plot

paq_radar_plot(data, ax=None, index=None, angles=EQUAL_ANGLES, *, figsize=(8, 8), palette='colorblind', alpha=0.25, linewidth=1.5, linestyle='solid', ylim=(1, 5), title=None, label_pad=15, legend_loc='upper right', legend_bbox_to_anchor=(0.1, 0.1))

Generate a radar/spider plot of PAQ values.

This function creates a radar plot showing PAQ (Perceived Affective Quality) values from a dataframe. The radar plot displays values for all 8 PAQ dimensions arranged in a circular layout.

PARAMETER DESCRIPTION
data

DataFrame containing PAQ values. Must contain columns matching PAQ_LABELS or they will be filtered out.

TYPE: DataFrame

ax

Existing polar subplot axes to plot to. If None, new axes will be created.

TYPE: Axes DEFAULT: None

index

Column(s) to set as index for the data. Useful for labeling in the legend.

TYPE: str DEFAULT: None

figsize

Figure size (width, height) in inches, by default (8, 8). Only used when creating new axes.

TYPE: Tuple[float, float] DEFAULT: (8, 8)

colors

Colors for the plot lines and fills. Can be: - List of color names/values for each data row - Dictionary mapping index values to colors - Single color name/value to use for all data rows - A matplotlib colormap to generate colors from If None, a default colormap will be used.

TYPE: Optional[Union[List[str], Dict[str, str], str, Colormap]]

alpha

Transparency for the filled areas, by default 0.25

TYPE: float DEFAULT: 0.25

linewidth

Width of the plot lines, by default 1.5

TYPE: float DEFAULT: 1.5

linestyle

Style of the plot lines, by default "solid"

TYPE: str DEFAULT: 'solid'

ylim

Y-axis limits (min, max), by default (1, 5) for standard Likert scale

TYPE: Tuple[int, int] DEFAULT: (1, 5)

title

Plot title, by default None

TYPE: str DEFAULT: None

text_padding

Padding for category labels, by default auto-generated

TYPE: Dict[str, int]

legend_loc

Legend location, by default "upper right"

TYPE: str DEFAULT: 'upper right'

legend_bbox_to_anchor

Legend bbox_to_anchor parameter, by default (0.1, 0.1)

TYPE: Tuple[float, float] DEFAULT: (0.1, 0.1)

RETURNS DESCRIPTION
Axes

Matplotlib Axes with radar plot

Examples:

>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> from soundscapy.plotting.likert import paq_radar_plot
>>>
>>> # Sample data with PAQ values for two locations
>>> data = pd.DataFrame({
...     "Location": ["Park", "Street"],
...     "pleasant": [4.2, 2.1],
...     "vibrant": [3.5, 4.2],
...     "eventful": [2.8, 4.5],
...     "chaotic": [1.5, 3.9],
...     "annoying": [1.2, 3.7],
...     "monotonous": [2.5, 1.8],
...     "uneventful": [3.1, 1.9],
...     "calm": [4.3, 1.4]
... })
>>>
>>> # Create radar plot with the "Location" column as index
>>> ax = paq_radar_plot(data, index="Location", title="PAQ Comparison")
>>> plt.show() # xdoctest: +SKIP
Source code in soundscapy/plotting/likert.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def paq_radar_plot(
    data: pd.DataFrame,
    ax: Axes | None = None,
    index: str | None = None,
    angles: list[float] | tuple[float, ...] = EQUAL_ANGLES,
    *,
    figsize: tuple[float, float] = (8, 8),
    palette: str | Sequence[str] | None = "colorblind",
    alpha: float = 0.25,
    linewidth: float = 1.5,
    linestyle: str = "solid",
    ylim: tuple[int, int] = (1, 5),
    title: str | None = None,
    label_pad: float | None = 15,
    legend_loc: str = "upper right",
    legend_bbox_to_anchor: tuple[float, float] | None = (0.1, 0.1),
) -> Axes:
    """
    Generate a radar/spider plot of PAQ values.

    This function creates a radar plot showing PAQ (Perceived Affective Quality)
    values from a dataframe. The radar plot displays values for all 8 PAQ dimensions
    arranged in a circular layout.

    Parameters
    ----------
    data : pd.DataFrame
        DataFrame containing PAQ values. Must contain columns matching PAQ_LABELS
        or they will be filtered out.
    ax : matplotlib.pyplot.Axes, optional
        Existing polar subplot axes to plot to. If None, new axes will be created.
    index : str, optional
        Column(s) to set as index for the data. Useful for labeling in the legend.
    figsize : Tuple[float, float], optional
        Figure size (width, height) in inches, by default (8, 8).
        Only used when creating new axes.
    colors : Optional[Union[List[str], Dict[str, str], str, Colormap]], optional
        Colors for the plot lines and fills. Can be:
        - List of color names/values for each data row
        - Dictionary mapping index values to colors
        - Single color name/value to use for all data rows
        - A matplotlib colormap to generate colors from
        If None, a default colormap will be used.
    alpha : float, optional
        Transparency for the filled areas, by default 0.25
    linewidth : float, optional
        Width of the plot lines, by default 1.5
    linestyle : str, optional
        Style of the plot lines, by default "solid"
    ylim : Tuple[int, int], optional
        Y-axis limits (min, max), by default (1, 5) for standard Likert scale
    title : str, optional
        Plot title, by default None
    text_padding : Dict[str, int], optional
        Padding for category labels, by default auto-generated
    legend_loc : str, optional
        Legend location, by default "upper right"
    legend_bbox_to_anchor : Tuple[float, float], optional
        Legend bbox_to_anchor parameter, by default (0.1, 0.1)

    Returns
    -------
    plt.Axes
        Matplotlib Axes with radar plot

    Examples
    --------
    >>> import pandas as pd
    >>> import matplotlib.pyplot as plt
    >>> from soundscapy.plotting.likert import paq_radar_plot
    >>>
    >>> # Sample data with PAQ values for two locations
    >>> data = pd.DataFrame({
    ...     "Location": ["Park", "Street"],
    ...     "pleasant": [4.2, 2.1],
    ...     "vibrant": [3.5, 4.2],
    ...     "eventful": [2.8, 4.5],
    ...     "chaotic": [1.5, 3.9],
    ...     "annoying": [1.2, 3.7],
    ...     "monotonous": [2.5, 1.8],
    ...     "uneventful": [3.1, 1.9],
    ...     "calm": [4.3, 1.4]
    ... })
    >>>
    >>> # Create radar plot with the "Location" column as index
    >>> ax = paq_radar_plot(data, index="Location", title="PAQ Comparison")
    >>> plt.show() # xdoctest: +SKIP

    """
    # Input validation
    if not isinstance(data, pd.DataFrame):
        msg = "The 'data' parameter must be a pandas DataFrame"
        raise TypeError(msg)

    # Set index if provided
    if index is not None:
        data = data.set_index(index)

    # Filter to only include columns that match PAQ_LABELS
    # This handles cases where the data might have extra columns
    data = rename_paqs(data, paq_aliases=PAQ_LABELS)
    data = return_paqs(data, incl_ids=False)

    # Create axes if needed
    if ax is None:
        fig = plt.figure(figsize=figsize)
        ax = fig.add_subplot(111, polar=True)

    # ---------- Part 1: Create background
    # Calculate angles for each axis
    rad_angles = np.deg2rad(angles)

    # Draw one axis per variable + add labels
    plt.xticks(rad_angles, PAQ_LABELS)
    ax.tick_params(axis="x", pad=label_pad)

    # Draw y-labels
    ax.set_rlabel_position(0)  # type: ignore[reportAttributeAccessIssues]
    y_ticks = list(range(ylim[0], ylim[1] + 1))
    plt.yticks(y_ticks, [str(y) for y in y_ticks], color="grey", size=8)
    plt.ylim(*ylim)

    # Add title if provided
    if title:
        ax.set_title(title, pad=2.5 * label_pad if label_pad else 20, fontsize=16)

    # -------- Part 2: Add plots

    # Need to add the first value to the end of the data to close the loop
    ext_angles = [*list(rad_angles), rad_angles[0]]
    # Plot each row of data
    with sns.color_palette(palette) as plot_colors:
        for i, (idx, row) in enumerate(data.iterrows()):
            if i == 4:  # noqa: PLR2004
                warnings.warn(
                    "More than 4 sets of data may not be visually clear.", stacklevel=2
                )

            # Extract values and duplicate the first value at the end to close the loop
            values = row.to_numpy().flatten().tolist()
            values += values[:1]

            # Get current color
            color = plot_colors[i]

            # Plot values
            ax.plot(
                ext_angles,
                values,
                linewidth=linewidth,
                linestyle=linestyle,
                color=color,
                label=idx,
            )
            ax.fill(ext_angles, values, color=color, alpha=alpha)

    # Add legend
    if legend_bbox_to_anchor:
        ax.legend(loc=legend_loc, bbox_to_anchor=legend_bbox_to_anchor)
    else:
        ax.legend(loc=legend_loc)

    plt.tight_layout()

    return ax

parallel_process

parallel_process(wav_files, results_df, levels, analysis_settings, max_workers=None, resample=None, *, parallel_mosqito=True)

Process multiple binaural files in parallel.

PARAMETER DESCRIPTION
resample

TYPE: int | None DEFAULT: None

wav_files

List of WAV files to process.

TYPE: List[Path]

results_df

Initial results DataFrame to update.

TYPE: DataFrame

levels

Dictionary with calibration levels for each file.

TYPE: Dict

analysis_settings

Analysis settings object.

TYPE: AnalysisSettings

max_workers

Maximum number of worker processes. If None, it will default to the number of processors on the machine.

TYPE: int DEFAULT: None

parallel_mosqito

Whether to process MoSQITo metrics in parallel within each file. Defaults to True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
DataFrame

Updated results DataFrame with analysis results for all files.

Source code in soundscapy/audio/parallel_processing.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def parallel_process(
    wav_files: list[Path],
    results_df: pd.DataFrame,
    levels: dict,
    analysis_settings: AnalysisSettings,
    max_workers: int | None = None,
    resample: int | None = None,
    *,
    parallel_mosqito: bool = True,
) -> pd.DataFrame:
    """
    Process multiple binaural files in parallel.

    Parameters
    ----------
    resample
    wav_files : List[Path]
        List of WAV files to process.
    results_df : pd.DataFrame
        Initial results DataFrame to update.
    levels : Dict
        Dictionary with calibration levels for each file.
    analysis_settings : AnalysisSettings
        Analysis settings object.
    max_workers : int, optional
        Maximum number of worker processes. If None, it will default to the number of processors on the machine.
    parallel_mosqito : bool, optional
        Whether to process MoSQITo metrics in parallel within each file. Defaults to True.

    Returns
    -------
    pd.DataFrame
        Updated results DataFrame with analysis results for all files.

    """
    logger.info(f"Starting parallel processing of {len(wav_files)} files")

    # Add a handler that uses tqdm.write for output
    tqdm_handler_id = logger.add(tqdm_write_sink, format="{message}")

    with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
        futures = []
        for wav_file in wav_files:
            future = executor.submit(
                load_analyse_binaural,
                wav_file,
                levels,
                analysis_settings,
                resample,
                parallel_mosqito=parallel_mosqito,
            )
            futures.append(future)

        with tqdm(total=len(futures), desc="Processing files") as pbar:
            for future in concurrent.futures.as_completed(futures):
                try:
                    result = future.result()
                    results_df = add_results(results_df, result)
                except Exception as e:
                    logger.error(f"Error processing file: {e!s}")
                finally:
                    pbar.update(1)

    # Remove the tqdm-compatible handler
    logger.remove(tqdm_handler_id)

    logger.info("Parallel processing completed")
    return results_df

prep_multiindex_df

prep_multiindex_df(dictionary, label='Leq', incl_metric=True)

Prepare a MultiIndex dataframe from a dictionary of results.

PARAMETER DESCRIPTION
dictionary

Dict of results with recording name as key, channels {"Left", "Right"} as second key, and Leq metric as value.

TYPE: dict

label

Name of metric included, by default "Leq".

TYPE: str DEFAULT: 'Leq'

incl_metric

Whether to include the metric value in the resulting dataframe, by default True. If False, will only set up the DataFrame with the proper MultiIndex.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
DataFrame

Index includes "Recording" and "Channel" with a column for each index if incl_metric.

RAISES DESCRIPTION
ValueError

If the input dictionary is not in the expected format.

Source code in soundscapy/audio/metrics.py
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
def prep_multiindex_df(dictionary: dict, label: str = "Leq", incl_metric: bool = True):
    """
    Prepare a MultiIndex dataframe from a dictionary of results.

    Parameters
    ----------
    dictionary : dict
        Dict of results with recording name as key, channels {"Left", "Right"} as second key,
        and Leq metric as value.
    label : str, optional
        Name of metric included, by default "Leq".
    incl_metric : bool, optional
        Whether to include the metric value in the resulting dataframe, by default True.
        If False, will only set up the DataFrame with the proper MultiIndex.

    Returns
    -------
    pd.DataFrame
        Index includes "Recording" and "Channel" with a column for each index if `incl_metric`.

    Raises
    ------
    ValueError
        If the input dictionary is not in the expected format.

    """
    logger.info("Preparing MultiIndex DataFrame")
    try:
        new_dict = {}
        for outerKey, innerDict in dictionary.items():
            for innerKey, values in innerDict.items():
                new_dict[(outerKey, innerKey)] = values
        idx = pd.MultiIndex.from_tuples(new_dict.keys())
        df = pd.DataFrame(new_dict.values(), index=idx, columns=[label])
        df.index.names = ["Recording", "Channel"]
        if not incl_metric:
            df = df.drop(columns=[label])
        logger.debug("MultiIndex DataFrame prepared successfully")
        return df
    except Exception as e:
        logger.error(f"Error preparing MultiIndex DataFrame: {e!s}")
        raise ValueError("Invalid input dictionary format") from e

process_all_metrics

process_all_metrics(b, analysis_settings, parallel=True)

Process all metrics specified in the analysis settings for a binaural signal.

This function runs through all enabled metrics in the provided analysis settings, computes them for the given binaural signal, and compiles the results into a single DataFrame.

PARAMETER DESCRIPTION
b

Binaural signal object to process.

TYPE: Binaural

analysis_settings

Configuration object specifying which metrics to run and their parameters.

TYPE: AnalysisSettings

parallel

If True, run applicable calculations in parallel. Defaults to True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
DataFrame

A MultiIndex DataFrame containing results from all processed metrics. The index includes "Recording" and "Channel" levels.

RAISES DESCRIPTION
ValueError

If there's an error processing any of the metrics.

Notes

The parallel option primarily affects the MoSQITo metrics. Other metrics may not benefit from parallelization.

Examples:

>>> # xdoctest: +SKIP
>>> from soundscapy.audio import Binaural
>>> from soundscapy import AnalysisSettings
>>> signal = Binaural.from_wav("audio.wav", resample=480000)
>>> settings = AnalysisSettings.from_yaml("settings.yaml")
>>> results = process_all_metrics(signal,settings)
Source code in soundscapy/audio/metrics.py
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
def process_all_metrics(
    b, analysis_settings: AnalysisSettings, parallel: bool = True
) -> pd.DataFrame:
    """
    Process all metrics specified in the analysis settings for a binaural signal.

    This function runs through all enabled metrics in the provided analysis settings,
    computes them for the given binaural signal, and compiles the results into a single DataFrame.

    Parameters
    ----------
    b : Binaural
        Binaural signal object to process.
    analysis_settings : AnalysisSettings
        Configuration object specifying which metrics to run and their parameters.
    parallel : bool, optional
        If True, run applicable calculations in parallel. Defaults to True.

    Returns
    -------
    pd.DataFrame
        A MultiIndex DataFrame containing results from all processed metrics.
        The index includes "Recording" and "Channel" levels.

    Raises
    ------
    ValueError
        If there's an error processing any of the metrics.

    Notes
    -----
    The parallel option primarily affects the MoSQITo metrics. Other metrics may not
    benefit from parallelization.

    Examples
    --------
    >>> # xdoctest: +SKIP
    >>> from soundscapy.audio import Binaural
    >>> from soundscapy import AnalysisSettings
    >>> signal = Binaural.from_wav("audio.wav", resample=480000)
    >>> settings = AnalysisSettings.from_yaml("settings.yaml")
    >>> results = process_all_metrics(signal,settings)

    """
    logger.info(f"Processing all metrics for {b.recording}")
    logger.debug(f"Parallel processing: {parallel}")

    idx = pd.MultiIndex.from_tuples(((b.recording, "Left"), (b.recording, "Right")))
    results_df = pd.DataFrame(index=idx)
    results_df.index.names = ["Recording", "Channel"]

    try:
        for (
            library,
            metrics_settings,
        ) in analysis_settings.get_enabled_metrics().items():
            for metric in metrics_settings.keys():
                logger.debug(f"Processing {library} metric: {metric}")
                if library == "AcousticToolbox":
                    results_df = pd.concat(
                        (
                            results_df,
                            b.acoustics_metric(
                                metric, metric_settings=metrics_settings[metric]
                            ),
                        ),
                        axis=1,
                    )
                elif library == "MoSQITo":
                    results_df = pd.concat(
                        (
                            results_df,
                            b.mosqito_metric(
                                metric,
                                parallel=parallel,
                                metric_settings=metrics_settings[metric],
                            ),
                        ),
                        axis=1,
                    )
                elif library == "scikit-maad" or library == "scikit_maad":
                    results_df = pd.concat(
                        (
                            results_df,
                            b.maad_metric(
                                metric, metric_settings=metrics_settings[metric]
                            ),
                        ),
                        axis=1,
                    )
        logger.info("All metrics processed successfully")
        return results_df
    except Exception as e:
        logger.error(f"Error processing metrics: {e!s}")
        raise ValueError("Error processing metrics") from e

rename_paqs

rename_paqs(df, paq_aliases=None)

Rename the PAQ columns in a DataFrame to standard PAQ IDs.

PARAMETER DESCRIPTION
df

Input DataFrame containing PAQ data.

TYPE: DataFrame

paq_aliases

Specify which PAQs are to be renamed. If None, will check if the column names are in pre-defined options. If a tuple, the order must match PAQ_IDS. If a dict, keys are current names and values are desired PAQ IDs.

TYPE: Union[Tuple, Dict] DEFAULT: None

RETURNS DESCRIPTION
DataFrame

DataFrame with renamed PAQ columns.

RAISES DESCRIPTION
ValueError

If paq_aliases is not a tuple, list, or dictionary.

Examples:

>>> import pandas as pd
>>> df = pd.DataFrame({
...     'pleasant': [4, 3],
...     'vibrant': [2, 5],
...     'other_col': [1, 2]
... })
>>> rename_paqs(df)
   PAQ1  PAQ2  other_col
0     4     2          1
1     3     5          2
>>> df_custom = pd.DataFrame({
...     'pl': [4, 3],
...     'vb': [2, 5],
... })
>>> rename_paqs(df_custom, paq_aliases={'pl': 'PAQ1', 'vb': 'PAQ2'})
   PAQ1  PAQ2
0     4     2
1     3     5
Source code in soundscapy/surveys/survey_utils.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def rename_paqs(
    df: pd.DataFrame, paq_aliases: list | tuple | dict | None = None
) -> pd.DataFrame:
    """
    Rename the PAQ columns in a DataFrame to standard PAQ IDs.

    Parameters
    ----------
    df : pd.DataFrame
        Input DataFrame containing PAQ data.
    paq_aliases : Union[Tuple, Dict], optional
        Specify which PAQs are to be renamed. If None, will check if the column names
        are in pre-defined options. If a tuple, the order must match PAQ_IDS.
        If a dict, keys are current names and values are desired PAQ IDs.

    Returns
    -------
    pd.DataFrame
        DataFrame with renamed PAQ columns.

    Raises
    ------
    ValueError
        If paq_aliases is not a tuple, list, or dictionary.

    Examples
    --------
    >>> import pandas as pd
    >>> df = pd.DataFrame({
    ...     'pleasant': [4, 3],
    ...     'vibrant': [2, 5],
    ...     'other_col': [1, 2]
    ... })
    >>> rename_paqs(df)
       PAQ1  PAQ2  other_col
    0     4     2          1
    1     3     5          2
    >>> df_custom = pd.DataFrame({
    ...     'pl': [4, 3],
    ...     'vb': [2, 5],
    ... })
    >>> rename_paqs(df_custom, paq_aliases={'pl': 'PAQ1', 'vb': 'PAQ2'})
       PAQ1  PAQ2
    0     4     2
    1     3     5

    """
    if paq_aliases is None:
        if any(paq_id in df.columns for paq_id in PAQ_IDS):
            logger.info("PAQs already correctly named.")
            return df
        if any(paq_name in df.columns for paq_name in PAQ_LABELS):
            paq_aliases = PAQ_LABELS

    if isinstance(paq_aliases, list | tuple):
        rename_dict = dict(zip(paq_aliases, PAQ_IDS, strict=False))
    elif isinstance(paq_aliases, dict):
        rename_dict = paq_aliases
    else:
        msg = "paq_aliases must be a tuple, list, or dictionary."
        raise TypeError(msg)

    logger.debug(f"Renaming PAQs with the following mapping: {rename_dict}")
    return df.rename(columns=rename_dict)

scatter

scatter(data, title='Soundscape Scatter Plot', ax=None, *, x='ISOPleasant', y='ISOEventful', hue=None, palette='colorblind', legend='auto', prim_labels=None, **kwargs)

Plot ISOcoordinates as scatter points on a soundscape circumplex grid.

Creates a scatter plot of data on a standardized circumplex grid with the custom Soundscapy styling for soundscape circumplex visualisations.

PARAMETER DESCRIPTION
data

Input data structure containing coordinate data, typically with ISOPleasant and ISOEventful columns.

TYPE: DataFrame

x

Column name for x variable, by default "ISOPleasant"

TYPE: str DEFAULT: 'ISOPleasant'

y

Column name for y variable, by default "ISOEventful"

TYPE: str DEFAULT: 'ISOEventful'

title

Title to add to circumplex plot, by default "Soundscape Scatter Plot"

TYPE: str | None DEFAULT: 'Soundscape Scatter Plot'

ax

Pre-existing matplotlib axes for the plot, by default None If None call matplotlib.pyplot.subplots with figsize internally.

TYPE: Axes DEFAULT: None

hue

Grouping variable that will produce points with different colors.

Can be either categorical or numeric, although color mapping will behave differently in latter case, by default None

TYPE: str | ndarray | Series | None DEFAULT: None

palette

Method for choosing the colors to use when mapping the hue semantic. String values are passed to seaborn.color_palette(). List or dict values imply categorical mapping, while a colormap object implies numeric mapping, by default "colorblind"

TYPE: SeabornPaletteType DEFAULT: 'colorblind'

color

Color to use for the plot elements when not using hue mapping, by default "#0173B2" (first color from colorblind palette)

TYPE: ColorType | None

figsize

Size of the figure to return if ax is None, by default (5, 5)

TYPE: tuple[int, int]

s

Size of scatter points, by default 20

TYPE: float

legend

How to draw the legend. If "brief", numeric hue and size variables will be represented with a sample of evenly spaced values. If "full", every group will get an entry in the legend. If "auto", choose between brief or full representation based on number of levels.

If False, no legend data is added and no legend is drawn, by default "auto"

TYPE: (auto, brief, full, False) DEFAULT: "auto"

prim_labels

Deprecated. Use xlabel and ylabel parameters instead.

TYPE: bool | None DEFAULT: None

PARAMETER DESCRIPTION
xlabel

Custom axis labels. By default "\(P_{ISO}\)" and "\(E_{ISO}\)" with math rendering.

If None is passed, the column names (x and y) will be used as labels.

If a string is provided, it will be used as the label.

If False is passed, axis labels will be hidden.

ylabel

Custom axis labels. By default "\(P_{ISO}\)" and "\(E_{ISO}\)" with math rendering.

If None is passed, the column names (x and y) will be used as labels.

If a string is provided, it will be used as the label.

If False is passed, axis labels will be hidden.

xlim

Limits for x and y axes, by default (-1, 1) for both

TYPE: tuple[float, float]

ylim

Limits for x and y axes, by default (-1, 1) for both

TYPE: tuple[float, float]

legend_loc

Location of legend, by default "best"

TYPE: MplLegendLocType

diagonal_lines

Whether to include diagonal dimension labels (e.g. calm, etc.), by default False

TYPE: bool

prim_ax_fontdict

Font dictionary for axis labels with these defaults:

{ "family": "sans-serif", "fontstyle": "normal", "fontsize": "large", "fontweight": "medium", "parse_math": True, "c": "black", "alpha": 1, }

TYPE: dict

fontsize

Direct parameters for font styling in axis labels

fontweight

Direct parameters for font styling in axis labels

fontstyle

Direct parameters for font styling in axis labels

family

Direct parameters for font styling in axis labels

c

Direct parameters for font styling in axis labels

alpha

Direct parameters for font styling in axis labels

parse_math

Direct parameters for font styling in axis labels

RETURNS DESCRIPTION
Axes object containing the plot.
Notes

This function applies special styling appropriate for circumplex plots including gridlines, axis labels, and proportional axes.

Examples:

Basic scatter plot with default settings:

>>> import soundscapy as sspy
>>> import matplotlib.pyplot as plt
>>> data = sspy.isd.load()
>>> data = sspy.add_iso_coords(data)
>>> ax = sspy.scatter(data)
>>> plt.show() # xdoctest: +SKIP

Scatter plot with grouping by location:

>>> ax = sspy.scatter(data, hue="LocationID", diagonal_lines=True, legend=False)
>>> plt.show() # xdoctest: +SKIP
>>> plt.close('all')
Source code in soundscapy/plotting/plot_functions.py
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
def scatter(
    data: pd.DataFrame,
    title: str | None = "Soundscape Scatter Plot",
    ax: Axes | None = None,
    *,
    x: str | None = "ISOPleasant",
    y: str | None = "ISOEventful",
    hue: str | None = None,
    palette: SeabornPaletteType | None = "colorblind",
    legend: Literal["auto", "brief", "full", False] = "auto",
    prim_labels: bool | None = None,  # Alias for primary_labels, deprecated
    **kwargs,
) -> Axes:
    """
    Plot ISOcoordinates as scatter points on a soundscape circumplex grid.

    Creates a scatter plot of data on a standardized circumplex grid with the custom
    Soundscapy styling for soundscape circumplex visualisations.

    Parameters
    ----------
    data
        Input data structure containing coordinate data, typically with ISOPleasant
        and ISOEventful columns.
    x : str, optional
        Column name for x variable, by default "ISOPleasant"
    y : str, optional
        Column name for y variable, by default "ISOEventful"
    title : str | None, optional
        Title to add to circumplex plot, by default "Soundscape Scatter Plot"
    ax : matplotlib.axes.Axes, optional
        Pre-existing matplotlib axes for the plot, by default None
        If `None` call `matplotlib.pyplot.subplots` with `figsize` internally.
    hue : str | np.ndarray | pd.Series | None, optional
        Grouping variable that will produce points with different colors.

        Can be either categorical or numeric,
        although color mapping will behave differently in latter case, by default None
    palette : SeabornPaletteType, optional
        Method for choosing the colors to use when mapping the hue semantic.
        String values are passed to seaborn.color_palette().
        List or dict values imply categorical mapping, while a colormap object
        implies numeric mapping, by default "colorblind"
    color : ColorType | None, optional
        Color to use for the plot elements when not using hue mapping,
        by default "#0173B2" (first color from colorblind palette)
    figsize : tuple[int, int], optional
        Size of the figure to return if `ax` is None, by default (5, 5)
    s : float, optional
        Size of scatter points, by default 20
    legend : {"auto", "brief", "full", False}, optional
        How to draw the legend. If "brief", numeric hue and size variables will be
        represented with a sample of evenly spaced values. If "full", every group will
        get an entry in the legend. If "auto", choose between brief or full
        representation based on number of levels.

        If False, no legend data is added and no legend is drawn, by default "auto"
    prim_labels : bool | None, optional
        Deprecated. Use xlabel and ylabel parameters instead.

    Other Parameters
    ----------------
    xlabel, ylabel
        Custom axis labels. By default "$P_{ISO}$" and "$E_{ISO}$"
        with math rendering.

        If None is passed, the column names (x and y) will be used as labels.

        If a string is provided, it will be used as the label.

        If False is passed, axis labels will be hidden.
    xlim, ylim : tuple[float, float], optional
        Limits for x and y axes, by default (-1, 1) for both
    legend_loc : MplLegendLocType, optional
        Location of legend, by default "best"
    diagonal_lines : bool, optional
        Whether to include diagonal dimension labels (e.g. calm, etc.),
        by default False
    prim_ax_fontdict : dict, optional
        Font dictionary for axis labels with these defaults:

        {
            "family": "sans-serif",
            "fontstyle": "normal",
            "fontsize": "large",
            "fontweight": "medium",
            "parse_math": True,
            "c": "black",
            "alpha": 1,
        }
    fontsize, fontweight, fontstyle, family, c, alpha, parse_math:
        Direct parameters for font styling in axis labels

    Returns
    -------
        Axes object containing the plot.

    Notes
    -----
    This function applies special styling appropriate for circumplex plots including
    gridlines, axis labels, and proportional axes.

    Examples
    --------
    Basic scatter plot with default settings:

    >>> import soundscapy as sspy
    >>> import matplotlib.pyplot as plt
    >>> data = sspy.isd.load()
    >>> data = sspy.add_iso_coords(data)
    >>> ax = sspy.scatter(data)
    >>> plt.show() # xdoctest: +SKIP

    Scatter plot with grouping by location:

    >>> ax = sspy.scatter(data, hue="LocationID", diagonal_lines=True, legend=False)
    >>> plt.show() # xdoctest: +SKIP
    >>> plt.close('all')

    """
    style_args, subplots_args, kwargs = _setup_style_and_subplots_args_from_kwargs(
        x=x, y=y, prim_labels=prim_labels, kwargs=kwargs
    )

    scatter_args = ScatterParams()
    scatter_args.update(
        data=data,
        x=x,
        y=y,
        palette=palette,
        hue=hue,
        legend=legend,
        extra="allow",
        ignore_null=False,
        **kwargs,
    )  # pass all the rest to scatter

    # Removes the palette if no hue is specified
    scatter_args.crosscheck_palette_hue()

    if ax is None:
        _, ax = plt.subplots(1, 1, figsize=subplots_args.figsize)

    p = sns.scatterplot(ax=ax, **scatter_args.as_dict())

    _set_style()
    _circumplex_grid(
        ax=ax,
        **style_args.get_multiple(
            ["xlim", "ylim", "xlabel", "ylabel", "diagonal_lines", "prim_ax_fontdict"]
        ),
    )
    if title is not None:
        _set_circum_title(
            ax=ax,
            title=title,
            xlabel=style_args.get("xlabel"),
            ylabel=style_args.get("ylabel"),
        )
    if legend is not None and hue is not None and style_args.legend_loc is not False:
        _move_legend(ax=ax, new_loc=style_args.get("legend_loc"))
    return p

setup_logging

setup_logging(level='INFO', log_file=None, format_level='basic')

Set up logging for Soundscapy with sensible defaults.

PARAMETER DESCRIPTION
level

Logging level for console output. Options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"

TYPE: str DEFAULT: "INFO"

log_file

Path to a log file. If provided, all messages (including DEBUG) will be logged to this file.

TYPE: str or Path DEFAULT: None

format_level

Format complexity level. Options: - "basic": Simple format with timestamp, level, and message - "detailed": Adds module, function and line information - "developer": Adds exception details and diagnostics

TYPE: str DEFAULT: "basic"

Examples:

>>> from soundscapy import setup_logging
>>> # Basic usage - show INFO level and above in console
>>> setup_logging()
>>>
>>> # Enable DEBUG level and log to file
>>> setup_logging(level="DEBUG", log_file="soundscapy.log")
>>>
>>> # Use detailed format for debugging
>>> setup_logging(level="DEBUG", format_level="detailed")
Source code in soundscapy/sspylogging.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def setup_logging(
    level: str = "INFO",
    log_file: str | Path | None = None,
    format_level: str = "basic",
) -> None:
    """
    Set up logging for Soundscapy with sensible defaults.

    Parameters
    ----------
    level : str, default="INFO"
        Logging level for console output.
        Options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
    log_file : str or Path, optional
        Path to a log file.
        If provided, all messages (including DEBUG) will be logged to this file.
    format_level : str, default="basic"
        Format complexity level. Options:
        - "basic": Simple format with timestamp, level, and message
        - "detailed": Adds module, function and line information
        - "developer": Adds exception details and diagnostics

    Examples
    --------
    >>> from soundscapy import setup_logging
    >>> # Basic usage - show INFO level and above in console
    >>> setup_logging()
    >>>
    >>> # Enable DEBUG level and log to file
    >>> setup_logging(level="DEBUG", log_file="soundscapy.log")
    >>>
    >>> # Use detailed format for debugging
    >>> setup_logging(level="DEBUG", format_level="detailed")

    """
    # Enable soundscapy logging (disabled by default in __init__.py)
    logger.enable("soundscapy")

    # Remove default handlers
    logger.remove()

    # Format configurations
    formats = {
        "basic": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
        "detailed": (
            "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | "
            "{name}:{function}:{line} | {message}"
        ),
        "developer": (
            "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | "
            "{name}:{function}:{line} | {message}\n{exception}"
        ),
    }

    # Use the appropriate format
    if format_level not in formats:
        logger.warning(f"Unknown format_level '{format_level}'. Using 'basic' instead.")
        format_level = "basic"

    log_format = formats[format_level]

    # Configure console handler
    logger.add(
        sys.stderr,
        format=log_format,
        level=level,
        colorize=True,
        enqueue=True,
    )

    # Add file handler if specified
    if log_file:
        logger.add(
            log_file,
            format=log_format,
            level="DEBUG",  # Always log everything to file
            rotation="1 MB",
            compression="zip",
            enqueue=True,
        )

    logger.debug(f"Soundscapy logging configured - console:{level}, file:{log_file}")

show_submodules: true