Skip to content

soundscapy

soundscapy

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.

db

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.

plotting

Soundscapy Plotting Module.

processing

Soundscape survey data processing module.

r_wrapper

Module for wrapping R functionality with rpy2.

satp

Soundscape Attributes Translation (SATP) calculation module.

spi

Soundscape Perception Indices (SPI) calculation module.

sspylogging

Logging configuration for Soundscapy.

surveys

Soundscapy Surveys Package.

CLASS DESCRIPTION
ISOPlot

A class for creating circumplex plots using different backends.

FUNCTION DESCRIPTION
create_iso_subplots

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

density

Plot a density plot of ISOCoordinates.

jointplot

Create a jointplot with a central distribution and marginal plots.

scatter

Plot ISOcoordinates as scatter points on a soundscape circumplex grid.

paq_likert

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

paq_radar_plot

Generate a radar/spider plot of PAQ values.

stacked_likert

Create a stacked Likert scale plot for a single column of survey data.

disable_logging

Disable all Soundscapy logging.

enable_debug

Quickly enable DEBUG level logging to console.

get_logger

Get the Soundscapy logger instance.

setup_logging

Set up logging for Soundscapy with sensible defaults.

add_iso_coords

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

ipsatize

Participant-level ipsatization for circumplex analysis.

rename_paqs

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

ISOPlot

ISOPlot(
    data: DataFrame | None = None,
    x: str | ndarray | Series | None = "ISOPleasant",
    y: str | ndarray | Series | None = "ISOEventful",
    title: str | None = "Soundscape Density Plot",
    hue: str | None = None,
    palette: SeabornPaletteType | None = "colorblind",
    figure: Figure | None = None,
    axes: Axes | ndarray | None = 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()

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 | 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
create_subplots

Create subplots for the circumplex plot.

show

Show the figure.

close

Close the figure.

savefig

Save the figure.

get_figure

Get the figure object.

get_axes

Get the axes object.

get_single_axes

Get a specific axes object.

yield_axes_objects

Generate a sequence of axes objects to iterate over.

add_layer

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

add_scatter

Add a scatter layer to specific subplot(s).

add_spi

Add a SPI layer to specific subplot(s).

add_density

Add a density layer to specific subplot(s).

add_simple_density

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

add_annotation

Add an annotation to the plot.

style

Apply styling to the plot.

ATTRIBUTE DESCRIPTION
x

Get the x-axis column name.

TYPE: str

y

Get the y-axis column name.

TYPE: str

hue

Get the hue column name.

TYPE: str | None

title

Get the plot title.

TYPE: str | None

Source code in src/soundscapy/plotting/iso_plot.py
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
        The data to be plotted, by default None
    x
        Column name or data for x-axis, by default "ISOPleasant"
    y
        Column name or data for y-axis, by default "ISOEventful"
    title
        Title of the plot, by default "Soundscape Density Plot"
    hue
        Column name for color encoding, by default None
    palette
        Color palette to use, by default "colorblind"
    figure
        Existing figure to plot on, by default None
    axes
        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

x property

x: str

Get the x-axis column name.

y property

y: str

Get the y-axis column name.

hue property

hue: str | None

Get the hue column name.

title property

title: str | None

Get the plot title.

create_subplots

create_subplots(
    nrows: int = 1,
    ncols: int = 1,
    figsize: tuple[int, int] = (5, 5),
    subplot_by: str | None = None,
    subplot_datas: list[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.

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 src/soundscapy/plotting/iso_plot.py
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
        Number of rows in the subplot grid, by default 1
    ncols
        Number of columns in the subplot grid, by default 1
    figsize
        Size of the figure (width, height), by default (5, 5)
    subplot_by
        Column name to create subplots by unique values, by default None
    subplot_datas
        List of dataframes for each subplot, by default None
    subplot_titles
        List of titles for each subplot, by default None
    adjust_figsize
        Whether to adjust the figure size based on nrows/ncols, by default True
    auto_allocate_axes
        Whether to automatically determine nrows/ncols based on data,
        by default False
    **kwargs :
        Additional parameters for plt.subplots

    Returns
    -------
    :
        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(
            "Creating subplots by unique values "
            f"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

show

show() -> None

Show the figure.

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

Source code in src/soundscapy/plotting/iso_plot.py
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()

close

close(fig: int | str | Figure | None = None) -> None

Close the figure.

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

Source code in src/soundscapy/plotting/iso_plot.py
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)

savefig

savefig(*args: Any, **kwargs: Any) -> None

Save the figure.

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

Source code in src/soundscapy/plotting/iso_plot.py
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)

get_figure

get_figure() -> Figure | SubFigure

Get the figure object.

RETURNS DESCRIPTION
Figure | SubFigure

The matplotlib Figure or SubFigure object associated with this plot.

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 src/soundscapy/plotting/iso_plot.py
def get_figure(self) -> Figure | SubFigure:
    """
    Get the figure object.

    Returns
    -------
    :
        The matplotlib Figure or SubFigure object associated with this plot.


    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_axes

get_axes() -> Axes | np.ndarray

Get the axes object.

RETURNS DESCRIPTION
Axes | ndarray

The matplotlib Axes object or array of Axes associated with this plot.

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 src/soundscapy/plotting/iso_plot.py
def get_axes(self) -> Axes | np.ndarray:
    """
    Get the axes object.

    Returns
    -------
    :
        The matplotlib Axes object or array of Axes associated with this plot.

    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_single_axes

get_single_axes(
    ax_idx: int | tuple[int, int] | None = None,
) -> Axes

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 src/soundscapy/plotting/iso_plot.py
def get_single_axes(self, ax_idx: int | tuple[int, int] | None = None) -> Axes:
    """
    Get a specific axes object.

    Parameters
    ----------
    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).

    Returns
    -------
    :
        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)

yield_axes_objects

yield_axes_objects() -> 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 DESCRIPTION
Axes

Individual matplotlib Axes objects from the current figure.

Source code in src/soundscapy/plotting/iso_plot.py
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
    ------
    :
        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

add_layer

add_layer(
    layer_class: type[Layer],
    data: 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).

PARAMETER DESCRIPTION
layer_class

The type of layer to add

TYPE: type[Layer]

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 | None DEFAULT: None

**params

Parameters for the layer

TYPE: Any 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()
>>> 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()
>>> 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()
>>> 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()
>>> plot.close()
Source code in src/soundscapy/plotting/iso_plot.py
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
        The type of layer to add
    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)
    data
        Custom data for this specific layer, overriding context data
    **params
        Parameters for the layer

    Returns
    -------
    :
        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() # doctest: +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() # doctest: +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() # doctest: +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() # doctest: +SKIP
    >>> plot.close()

    """
    # TODO(MitchellAcoustics): Need to handle legend/label creation
    #                          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: DataFrame | None = None,
    *,
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    **params: Any,
) -> ISOPlot

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 | None DEFAULT: None

**params

Parameters for the scatter plot

TYPE: Any 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()
>>> 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()
>>> plot.subplot_contexts[0].layers[1].custom_data is custom_data
True
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
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
        Target specific axis/axes
    data
        Custom data for this specific scatter plot
    **params
        Parameters for the scatter plot

    Returns
    -------
    :
        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() # doctest: +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() # doctest: +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_spi

add_spi(
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    spi_target_data: DataFrame | 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).

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()
>>> 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()
>>> 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()
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
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
        Target specific axis/axes
    spi_target_data
        Custom data for this specific SPI plot
    msn_params
        Parameters for the SPI plot

    Returns
    -------
    :
        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() # doctest: +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() # doctest: +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() # doctest: +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)

add_density

add_density(
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    data: DataFrame | None = None,
    *,
    include_outline: bool = False,
    **params: Any,
) -> ISOPlot

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 | None 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: Any 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()
>>> 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()
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
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
        Target specific axis/axes
    data
        Custom data for this specific density plot
    include_outline
        Whether to include an outline around the density plot, by default False
    **params
        Parameters for the density plot

    Returns
    -------
    :
        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() # doctest: +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() # doctest: +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_simple_density

add_simple_density(
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    data: DataFrame | None = None,
    *,
    include_outline: bool = True,
    **params: Any,
) -> ISOPlot

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 | None DEFAULT: None

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. Useful options include thresh (default 0.5), levels (default 2), and alpha (default 0.5).

TYPE: Any 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()
>>> 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()
>>> len(plot.subplot_contexts[0].layers) == 2
True
>>> plot.close()
...
Source code in src/soundscapy/plotting/iso_plot.py
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
        Target specific axis/axes
    data
        Custom data for this specific density plot
    include_outline
        Whether to include an outline around the density plot, by default True
    **params
        Additional parameters for the density plot. Useful options include
        `thresh` (default `0.5`), `levels` (default `2`), and `alpha`
        (default `0.5`).

    Returns
    -------
    :
        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() # doctest: +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() # doctest: +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_annotation

add_annotation(
    text: str,
    xy: tuple[float, float],
    xytext: tuple[float, float],
    arrowprops: dict[str, Any] | None = None,
) -> ISOPlot

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 src/soundscapy/plotting/iso_plot.py
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
        The text to display in the annotation.
    xy
        The point to annotate.
    xytext
        The point at which to place the text.
    arrowprops
        Properties for the arrow connecting the annotation text to the point.

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

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

style

style(**kwargs: Any) -> ISOPlot

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()
>>> 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()
>>> 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()
>>> # Verify results
>>> isinstance(plot, ISOPlot)
True
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
def style(
    self,
    **kwargs: Any,
) -> ISOPlot:
    """
    Apply styling to the plot.

    Parameters
    ----------
    **kwargs

    Returns
    -------
    :
        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() # doctest: +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() # doctest: +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() # doctest: +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

create_iso_subplots

create_iso_subplots(
    data: DataFrame | list[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",
    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.

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: DataFrame | list[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 | None DEFAULT: None

title

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

TYPE: str | None DEFAULT: 'Soundscapy Plot'

plot_layers

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'] | Sequence[Literal['scatter', 'simple_density', 'density']] DEFAULT: ('scatter', 'density')

subplot_size

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

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

subplot_titles

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[str] | 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 | 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 | 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[Figure, ndarray]

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()

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()
>>> assert len(axes) == 2
>>> plt.close('all')
Source code in src/soundscapy/plotting/plot_functions.py
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
        Input data to be visualized. Can be a single data frame or a list of data
        frames for use in multiple subplots.
    x
        The name of the column in the data to be used for the x-axis. Default is
        "ISOPleasant".
    y
        The name of the column in the data to be used for the y-axis. Default is
        "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.
    title
        The overarching title of the figure. If None, no overall title is added.
        Default is "Soundscapy Plot".
    plot_layers
        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
        Size of each subplot in inches as (width, height). Default is (4, 4).
    subplot_titles
        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
        Prefix for subplot titles if "numbered" is selected as `subplot_titles`.
        Default is "Plot".
    nrows
        Number of rows for the subplot grid. If None, automatically calculated
        based on the number of subplots. Default is None.
    ncols
        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
    -------
    :
        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() # doctest: +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() # doctest: +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
    vert_adjust = 1.2 if title else 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: 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,
    **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.

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 | None DEFAULT: None

x

Column name for x variable, by default "ISOPleasant"

TYPE: str | None DEFAULT: 'ISOPleasant'

y

Column name for y variable, by default "ISOEventful"

TYPE: str | None 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 | 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: str 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

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: Literal['auto', 'brief', 'full', False] DEFAULT: 'auto'

prim_labels

Deprecated. Use xlabel and ylabel parameters instead.

TYPE: bool | None DEFAULT: None

**kwargs

Additional styling parameters. Common options include incl_outline, density controls such as alpha, fill, levels, thresh, and bw_adjust, axis label and limit controls such as xlabel, ylabel, xlim, ylim, legend placement via legend_loc, diagonal_lines, figsize for newly created figures, font settings such as prim_ax_fontdict, fontsize, fontweight, fontstyle, family, c, alpha, and parse_math, plus any extra keyword arguments accepted by matplotlib's contour and contourf.

DEFAULT: {}

RETURNS DESCRIPTION
Axes

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()

Simple density plot with fewer contour levels:

>>> ax = sspy.density(data, density_type="simple")
>>> plt.show()

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()

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()
>>> plt.close('all')
Source code in src/soundscapy/plotting/plot_functions.py
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
        Input data structure containing coordinate data, typically with ISOPleasant
        and ISOEventful columns.
    title
        Title to add to circumplex plot, by 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.
    x
        Column name for x variable, by default "ISOPleasant"
    y
        Column name for y variable, by 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
    incl_scatter
        Whether to include a scatter plot of the data points, by 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"
    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"
    scatter_kws
        Keyword arguments to pass to `seaborn.scatterplot` if incl_scatter is True,
        by default {"s": 25, "linewidth": 0}
    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"
    prim_labels
        Deprecated. Use xlabel and ylabel parameters instead.

    **kwargs
        Additional styling parameters. Common options include `incl_outline`,
        density controls such as `alpha`, `fill`, `levels`, `thresh`, and
        `bw_adjust`, axis label and limit controls such as `xlabel`, `ylabel`,
        `xlim`, `ylim`, legend placement via `legend_loc`, `diagonal_lines`,
        `figsize` for newly created figures, font settings such as
        `prim_ax_fontdict`, `fontsize`, `fontweight`, `fontstyle`, `family`, `c`,
        `alpha`, and `parse_math`, plus any extra keyword arguments accepted by
        matplotlib's `contour` and `contourf`.

    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() # doctest: +SKIP

    Simple density plot with fewer contour levels:

    >>> ax = sspy.density(data, density_type="simple")
    >>> plt.show() # doctest: +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() # doctest: +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() # doctest: +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

jointplot

jointplot(
    data: 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,
    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,
    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.

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 | 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: str 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

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 | tuple[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: Literal['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: str DEFAULT: 'kde'

**kwargs

Additional styling parameters. Common options include color, figsize, axis label and limit controls such as xlabel, ylabel, xlim, ylim, legend placement via legend_loc, diagonal_lines, and font settings such as prim_ax_fontdict, fontsize, fontweight, fontstyle, family, c, alpha, and parse_math.

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()

Jointplot with histogram marginals:

>>> g = sspy.jointplot(data, marginal_kind="hist")
>>> plt.show()

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()
>>> plt.close('all')
Source code in src/soundscapy/plotting/plot_functions.py
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,
    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
        Input data structure containing coordinate data, typically with ISOPleasant
        and ISOEventful columns.
    x
        Column name for x variable, by default "ISOPleasant"
    y
        Column name for y variable, by default "ISOEventful"
    title
        Title to add to the jointplot, by 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
    incl_scatter
        Whether to include a scatter plot of the data points in the joint plot,
        by 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"
    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"
    scatter_kws
        Additional keyword arguments to pass to scatter plot if incl_scatter is True,
        by default None
    incl_outline
        Whether to include an outline for the density contours, by default False
    alpha
        Opacity level for the density fill, by default 0.8
    fill
        Whether to fill the density contours, by 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
    thresh
        Lowest iso-proportion level at which to draw contours, by default 0.05
    bw_adjust
        Factor that multiplicatively scales the bandwidth. Increasing will make
        the density estimate smoother, by default 1.2
    legend
        How to draw the legend for hue mapping, by default "auto"
    prim_labels
        Deprecated. Use xlabel and ylabel parameters instead.
    joint_kws
        Additional keyword arguments to pass to the joint plot, by default None
    marginal_kws
        Additional keyword arguments to pass to the marginal plots,
        by default {"fill": True, "common_norm": False}
    marginal_kind
        Type of plot to draw in the marginal axes, either "kde" for kernel
        density estimation or "hist" for histogram, by default "kde"

    **kwargs
        Additional styling parameters. Common options include `color`,
        `figsize`, axis label and limit controls such as `xlabel`, `ylabel`,
        `xlim`, `ylim`, legend placement via `legend_loc`, `diagonal_lines`, and
        font settings such as `prim_ax_fontdict`, `fontsize`, `fontweight`,
        `fontstyle`, `family`, `c`, `alpha`, and `parse_math`.

    Returns
    -------
    :
        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() # doctest: +SKIP

    Jointplot with histogram marginals:

    >>> g = sspy.jointplot(data, marginal_kind="hist")
    >>> plt.show() # doctest: +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() # doctest: +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,
        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

scatter

scatter(
    data: 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,
    **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.

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 | None DEFAULT: 'ISOPleasant'

y

Column name for y variable, by default "ISOEventful"

TYPE: str | None 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 | None 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 | 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 | None DEFAULT: 'colorblind'

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: Literal['auto', 'brief', 'full', False] DEFAULT: 'auto'

prim_labels

Deprecated. Use xlabel and ylabel parameters instead.

TYPE: bool | None DEFAULT: None

**kwargs

Additional style arguments. Common options include color (default "#0173B2" when hue mapping is unused), figsize (default (5, 5) when creating a new figure), s (default 20 for point size), axis label and limit controls such as xlabel, ylabel, xlim, ylim, legend placement via legend_loc, diagonal_lines, and font settings such as prim_ax_fontdict, fontsize, fontweight, fontstyle, family, c, alpha, and parse_math.

DEFAULT: {}

RETURNS DESCRIPTION
Axes

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()

Scatter plot with grouping by location:

>>> ax = sspy.scatter(data, hue="LocationID", diagonal_lines=True, legend=False)
>>> plt.show()
>>> plt.close('all')
Source code in src/soundscapy/plotting/plot_functions.py
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
        Column name for x variable, by default "ISOPleasant"
    y
        Column name for y variable, by default "ISOEventful"
    title
        Title to add to circumplex plot, by default "Soundscape Scatter Plot"
    ax
        Pre-existing matplotlib axes for the plot, by default None
        If `None` call `matplotlib.pyplot.subplots` with `figsize` internally.
    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
    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"
    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"
    prim_labels
        Deprecated. Use xlabel and ylabel parameters instead.

    **kwargs
        Additional style arguments. Common options include `color` (default
        `"#0173B2"` when hue mapping is unused), `figsize` (default `(5, 5)` when
        creating a new figure), `s` (default `20` for point size), axis label and
        limit controls such as `xlabel`, `ylabel`, `xlim`, `ylim`, legend placement
        via `legend_loc`, `diagonal_lines`, and font settings such as
        `prim_ax_fontdict`, `fontsize`, `fontweight`, `fontstyle`, `family`, `c`,
        `alpha`, and `parse_math`.

    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() # doctest: +SKIP

    Scatter plot with grouping by location:

    >>> ax = sspy.scatter(data, hue="LocationID", diagonal_lines=True, legend=False)
    >>> plt.show() # doctest: +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

paq_likert

paq_likert(
    data: 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.

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 | None 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()
Source code in src/soundscapy/plotting/likert.py
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
        DataFrame containing PAQ values.
    paq_cols
        List of column names containing PAQ data, by default PAQ_IDS.
    title
        Plot title, by default "Stacked Likert Plot".
    legend
        Whether to show the legend, by default True.
    ax
        Matplotlib axes to plot on, by default None.
    plot_percentage
        Whether to show percentages instead of absolute values, by default False.
    bar_labels
        Whether to show bar labels, by default True.
    **kwargs
        Additional keyword arguments passed to plot_likert.plot_likert.

    Returns
    -------
    :
        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() # doctest: +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)

    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: 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.

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 | None DEFAULT: None

index

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

TYPE: str | None 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)

palette

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: str | Sequence[str] | None DEFAULT: 'colorblind'

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 | None DEFAULT: None

label_pad

Padding for category labels, by default 15

TYPE: float | None DEFAULT: 15

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] | None 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()
Source code in src/soundscapy/plotting/likert.py
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
        DataFrame containing PAQ values. Must contain columns matching PAQ_LABELS
        or they will be filtered out.
    ax
        Existing polar subplot axes to plot to. If None, new axes will be created.
    index
        Column(s) to set as index for the data. Useful for labeling in the legend.
    figsize
        Figure size (width, height) in inches, by default (8, 8).
        Only used when creating new axes.
    palette
        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
        Transparency for the filled areas, by default 0.25
    linewidth
        Width of the plot lines, by default 1.5
    linestyle
        Style of the plot lines, by default "solid"
    ylim
        Y-axis limits (min, max), by default (1, 5) for standard Likert scale
    title
        Plot title, by default None
    label_pad
        Padding for category labels, by default 15
    legend_loc
        Legend location, by default "upper right"
    legend_bbox_to_anchor
        Legend bbox_to_anchor parameter, by default (0.1, 0.1)

    Returns
    -------
    :
        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() # doctest: +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

stacked_likert

stacked_likert(
    data: DataFrame,
    column: str = "appropriate",
    title: str = "Stacked Likert Plot",
    *,
    legend: bool = True,
    ax: Axes | None = None,
    plot_percentage: bool = False,
    bar_labels: bool = True,
    **kwargs,
) -> None

Create a stacked Likert scale plot for a single column of survey data.

This function creates a horizontal stacked bar chart showing the distribution of responses across Likert scale categories for a specified column. The data is automatically cleaned by removing NaN values and converted to categorical format for plotting.

PARAMETER DESCRIPTION
data

DataFrame containing survey response data.

TYPE: DataFrame

column

Name of the column to plot, by default "appropriate".

TYPE: str DEFAULT: 'appropriate'

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. If None, new axes will be created, by default None.

TYPE: Axes | None 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.

Warnings

This is an experimental function that applies brute force data cleaning. Use with caution as it may change in future versions.

Examples:

>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> from soundscapy.plotting.likert import stacked_likert
>>>
>>> # Sample survey data
>>> data = pd.DataFrame({
...     "appropriate": [1, 2, 3, 4, 5, 3, 4, 2, 5, 1]
... })
>>>
>>> # Create stacked Likert plot
>>> stacked_likert(data, column="appropriate", title="Appropriateness Ratings")
>>> plt.show()
Source code in src/soundscapy/plotting/likert.py
def stacked_likert(
    data: pd.DataFrame,
    column: str = "appropriate",
    title: str = "Stacked Likert Plot",
    *,
    legend: bool = True,
    ax: Axes | None = None,
    plot_percentage: bool = False,
    bar_labels: bool = True,
    **kwargs,
) -> None:
    """
    Create a stacked Likert scale plot for a single column of survey data.

    This function creates a horizontal stacked bar chart showing the distribution
    of responses across Likert scale categories for a specified column. The data
    is automatically cleaned by removing NaN values and converted to categorical
    format for plotting.

    Parameters
    ----------
    data
        DataFrame containing survey response data.
    column
        Name of the column to plot, by default "appropriate".
    title
        Plot title, by default "Stacked Likert Plot".
    legend
        Whether to show the legend, by default True.
    ax
        Matplotlib axes to plot on. If None, new axes will be created,
        by default None.
    plot_percentage
        Whether to show percentages instead of absolute values, by default False.
    bar_labels
        Whether to show bar labels, by default True.
    **kwargs
        Additional keyword arguments passed to plot_likert.plot_likert.

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

    Warnings
    --------
    This is an experimental function that applies brute force data cleaning.
    Use with caution as it may change in future versions.

    Examples
    --------
    >>> import pandas as pd
    >>> import matplotlib.pyplot as plt
    >>> from soundscapy.plotting.likert import stacked_likert
    >>>
    >>> # Sample survey data
    >>> data = pd.DataFrame({
    ...     "appropriate": [1, 2, 3, 4, 5, 3, 4, 2, 5, 1]
    ... })
    >>>
    >>> # Create stacked Likert plot
    >>> stacked_likert(data, column="appropriate", title="Appropriateness Ratings")
    >>> plt.show() # doctest: +SKIP

    """
    warnings.warn(
        "This is an experimental function. It may change in the future. "
        "Currently, this functio applies brute data cleaning, use with caution. ",
        ExperimentalWarning,
        stacklevel=2,
    )

    # Extract and clean the specified column
    new_data = data[column].copy()
    new_data = new_data.dropna()

    # Convert to categorical format for Likert plotting
    new_data = likert_categorical_from_data(new_data)

    # Create new axes if none provided
    if ax is None:
        _, ax = plt.subplots(figsize=(8, 6))

    # Create the stacked Likert plot
    plot_likert.plot_likert(
        pd.Series(new_data),
        match_col_to_likert_scale(column),
        plot_percentage=plot_percentage,
        ax=ax,
        legend=legend,
        bar_labels=bar_labels,
        title=title,
        **kwargs,
    )

disable_logging

disable_logging() -> None

Disable all Soundscapy logging.

Examples:

>>> from soundscapy import disable_logging
>>> disable_logging()
>>> # No more logging messages will be shown
Source code in src/soundscapy/sspylogging.py
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

enable_debug

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
Source code in src/soundscapy/sspylogging.py
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() -> 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 DESCRIPTION
Logger

The loguru logger instance

Examples:

>>> from soundscapy import get_logger
>>> logger = get_logger()
>>> logger.debug("Custom debug message")
Source code in src/soundscapy/sspylogging.py
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
    -------
    :
        The loguru logger instance

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

    """
    return logger

setup_logging

setup_logging(
    level: str = "INFO",
    log_file: str | Path | None = None,
    format_level: str = "basic",
) -> None

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 | Path | None 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 src/soundscapy/sspylogging.py
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
        Logging level for console output.

        Options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
    log_file
        Path to a log file.
        If provided, all messages (including DEBUG) will be logged to this file.
    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

    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}")

add_iso_coords

add_iso_coords(
    data: 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.

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 src/soundscapy/surveys/processing.py
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
        Input DataFrame containing PAQ data
    val_range
        (min, max) range of original PAQ responses, by default (1, 5)
    names
        Names for new coordinate columns, by default ("ISOPleasant", "ISOEventful")
    angles
        Angles for each PAQ in degrees, by default EQUAL_ANGLES
    overwrite
        Whether to overwrite existing ISO coordinate columns, by default False

    Returns
    -------
    :
        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

ipsatize

ipsatize(
    data: DataFrame,
    method: Literal[
        "grand_mean", "column_wise", "row_wise"
    ] = "grand_mean",
    participant_col: str = "participant",
    scales: list[str] | None = None,
) -> pd.DataFrame

Participant-level ipsatization for circumplex analysis.

Removes systematic response biases before computing a correlation matrix. The choice of method depends on the study design and the type of bias being corrected.

PARAMETER DESCRIPTION
data

DataFrame containing PAQ scale columns and (for participant-level methods) a grouping column.

TYPE: DataFrame

method

Centering strategy:

"grand_mean" (default) — one scalar per participant: the mean across all PAQ values and all observations for that participant. Removes overall response-level differences between participants. Matches the published SATP analysis (Aletta et al., 2024) and the original R implementation.

"column_wise" — eight scalars per participant: the per-scale mean across that participant's observations. Removes scale-specific response biases. This is the behaviour of the legacy :func:person_center function.

"row_wise" — one scalar per observation: the mean across all PAQ scales within that observation. Removes the general impression of each individual soundscape stimulus. Equivalent to circumplex.ipsatize().

TYPE: Literal['grand_mean', 'column_wise', 'row_wise'] DEFAULT: 'grand_mean'

participant_col

Column used to group observations by participant. Required for "grand_mean" and "column_wise"; ignored for "row_wise".

TYPE: str DEFAULT: 'participant'

scales

PAQ column names to centre. Defaults to :data:PAQ_IDS when None.

TYPE: list[str] | None DEFAULT: None

RETURNS DESCRIPTION
DataFrame

DataFrame containing only the scale columns with centred values. The participant_col grouping column is excluded from the result.

RAISES DESCRIPTION
KeyError

If participant_col is not present in data when method is "grand_mean" or "column_wise".

Examples:

>>> import pandas as pd
>>> data = pd.DataFrame({
...     'PAQ1': [50., 60., 40., 30.], 'PAQ2': [50., 60., 40., 30.],
...     'PAQ3': [50., 60., 40., 30.], 'PAQ4': [50., 60., 40., 30.],
...     'PAQ5': [50., 60., 40., 30.], 'PAQ6': [50., 60., 40., 30.],
...     'PAQ7': [50., 60., 40., 30.], 'PAQ8': [50., 60., 40., 30.],
...     'participant': ['A', 'A', 'B', 'B'],
... })
>>> result = ipsatize(data, method="grand_mean")
>>> result['PAQ1'].tolist()
[-5.0, 5.0, 5.0, -5.0]
Source code in src/soundscapy/surveys/processing.py
def ipsatize(
    data: pd.DataFrame,
    method: Literal["grand_mean", "column_wise", "row_wise"] = "grand_mean",
    participant_col: str = "participant",
    scales: list[str] | None = None,
) -> pd.DataFrame:
    """
    Participant-level ipsatization for circumplex analysis.

    Removes systematic response biases before computing a correlation matrix.
    The choice of method depends on the study design and the type of bias
    being corrected.

    Parameters
    ----------
    data
        DataFrame containing PAQ scale columns and (for participant-level
        methods) a grouping column.
    method
        Centering strategy:

        ``"grand_mean"`` *(default)* — one scalar per participant: the mean
        across *all* PAQ values and *all* observations for that participant.
        Removes overall response-level differences between participants.
        **Matches the published SATP analysis (Aletta et al., 2024) and the
        original R implementation.**

        ``"column_wise"`` — eight scalars per participant: the per-scale mean
        across that participant's observations.  Removes scale-specific
        response biases.  This is the behaviour of the legacy
        :func:`person_center` function.

        ``"row_wise"`` — one scalar per observation: the mean across all PAQ
        scales within that observation.  Removes the general impression of
        each individual soundscape stimulus.  Equivalent to
        ``circumplex.ipsatize()``.
    participant_col
        Column used to group observations by participant.  Required for
        ``"grand_mean"`` and ``"column_wise"``; ignored for ``"row_wise"``.
    scales
        PAQ column names to centre.  Defaults to :data:`PAQ_IDS` when
        ``None``.

    Returns
    -------
    :
        DataFrame containing only the scale columns with centred values.
        The ``participant_col`` grouping column is excluded from the result.

    Raises
    ------
    KeyError
        If ``participant_col`` is not present in ``data`` when
        ``method`` is ``"grand_mean"`` or ``"column_wise"``.

    Examples
    --------
    >>> import pandas as pd
    >>> data = pd.DataFrame({
    ...     'PAQ1': [50., 60., 40., 30.], 'PAQ2': [50., 60., 40., 30.],
    ...     'PAQ3': [50., 60., 40., 30.], 'PAQ4': [50., 60., 40., 30.],
    ...     'PAQ5': [50., 60., 40., 30.], 'PAQ6': [50., 60., 40., 30.],
    ...     'PAQ7': [50., 60., 40., 30.], 'PAQ8': [50., 60., 40., 30.],
    ...     'participant': ['A', 'A', 'B', 'B'],
    ... })
    >>> result = ipsatize(data, method="grand_mean")
    >>> result['PAQ1'].tolist()
    [-5.0, 5.0, 5.0, -5.0]

    """
    _scales = scales if scales is not None else PAQ_IDS

    if method == "column_wise":
        means = data.groupby(participant_col)[_scales].transform("mean")
        return data[_scales] - means

    if method == "grand_mean":
        # Compute a single scalar per participant: mean across all PAQ values
        # and all observations for that participant.  Use nanmean so that
        # participants with partial NaN data still get a valid grand mean
        # computed from their non-NaN values; NaN rows are then removed by
        # downstream listwise deletion rather than silently expanding data loss
        # to the whole participant.
        grand_means = data.groupby(participant_col)[_scales].apply(
            lambda df: float(np.nanmean(df.values))
        )
        grand_mean_per_row = data[participant_col].map(grand_means)
        return data[_scales].subtract(grand_mean_per_row, axis=0)

    if method == "row_wise":
        row_means = data[_scales].mean(axis=1)
        return data[_scales].sub(row_means, axis=0)

    msg = f"method must be 'grand_mean', 'column_wise', or 'row_wise'; got {method!r}"
    raise ValueError(msg)

rename_paqs

rename_paqs(
    df: DataFrame,
    paq_aliases: list | tuple | dict | None = None,
) -> pd.DataFrame

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: list | tuple | dict | None 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 src/soundscapy/surveys/survey_utils.py
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
        Input DataFrame containing PAQ data.
    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.

    Returns
    -------
    :
        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)