Skip to content

SPI

soundscapy.spi

Soundscape Perception Indices (SPI) calculation module.

This module provides functions and classes for calculating SPI, based on the R implementation. Requires optional dependencies.

MODULE DESCRIPTION
ks2d
msn

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

Multi-skew normal model

soundscapy.spi.msn

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

Provides classes and functions for defining, fitting, sampling, and analyzing MSN distributions, often used in soundscape analysis for modeling ISOPleasant and ISOEventful ratings.

CLASS DESCRIPTION
DirectParams

Container for direct parameters (xi, omega, alpha) of a skew-normal distribution.

CentredParams

Container for centred parameters (mean, sigma, skew) of a skew-normal distribution.

MultiSkewNorm

High-level interface for fitting, sampling, and scoring a 2-D skew-normal model.

FUNCTION DESCRIPTION
dp2cp

Convert a :class:DirectParams object to a :class:CentredParams object via R.

cp2dp

Convert a :class:CentredParams object to a :class:DirectParams object via R.

spi_score

Soundscape Perception Index: int((1 - KS_statistic) * 100).

DirectParams

DirectParams(xi: ndarray, omega: ndarray, alpha: ndarray)

Represents a set of direct parameters for a statistical model.

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

PARAMETER DESCRIPTION
xi

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

TYPE: ndarray

omega

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

TYPE: ndarray

alpha

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

TYPE: ndarray

Initialize DirectParams instance.

METHOD DESCRIPTION
__repr__

Return a string representation of the DirectParams object.

__str__

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

validate

Validate the direct parameters.

from_cp

Convert a CentredParams object to a DirectParams object.

Source code in src/soundscapy/spi/msn.py
def __init__(self, xi: np.ndarray, omega: np.ndarray, alpha: np.ndarray) -> None:
    """Initialize DirectParams instance."""
    self.xi = xi
    self.omega = omega
    self.alpha = alpha
    self.validate()
__repr__
__repr__() -> str

Return a string representation of the DirectParams object.

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

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

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

Validate the direct parameters.

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

RAISES DESCRIPTION
ValueError

If the direct parameters are not valid.

RETURNS DESCRIPTION
None
Source code in src/soundscapy/spi/msn.py
def validate(self) -> None:
    """
    Validate the direct parameters.

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

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

    Returns
    -------
    :

    """
    if not self._omega_is_pos_def():
        msg = "Omega must be positive definite"
        raise ValueError(msg)
    if not self._omega_is_symmetric():
        msg = "Omega must be symmetric"
        raise ValueError(msg)
from_cp classmethod
from_cp(cp: CentredParams) -> DirectParams

Convert a CentredParams object to a DirectParams object.

PARAMETER DESCRIPTION
cp

The CentredParams object to convert.

TYPE: CentredParams

RETURNS DESCRIPTION
DirectParams

A new DirectParams object with the converted parameters.

Source code in src/soundscapy/spi/msn.py
@classmethod
def from_cp(cls, cp: "CentredParams") -> "DirectParams":
    """
    Convert a CentredParams object to a DirectParams object.

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

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

    """
    warnings.warn(
        "Converting from Centred Parameters to Direct Parameters "
        "is not guaranteed to produce a unique result. "
        "Prefer constructing from Direct Parameters (xi, omega, alpha) "
        "directly when possible.",
        UserWarning,
        stacklevel=2,
    )
    dp = cp2dp(cp)
    return cls(dp.xi, dp.omega, dp.alpha)

CentredParams

CentredParams(mean: ndarray, sigma: ndarray, skew: ndarray)

Represents the centered parameters of a distribution.

PARAMETER DESCRIPTION
mean

The mean of the distribution.

TYPE: ndarray

sigma

The standard deviation of the distribution.

TYPE: ndarray

skew

The skewness of the distribution.

TYPE: ndarray

ATTRIBUTE DESCRIPTION
mean

The mean of the distribution.

sigma

The standard deviation of the distribution.

skew

The skewness of the distribution.

METHOD DESCRIPTION
from_dp

Converts DirectParams object to CentredParams object.

Initialize CentredParams instance.

Source code in src/soundscapy/spi/msn.py
def __init__(self, mean: np.ndarray, sigma: np.ndarray, skew: np.ndarray) -> None:
    """Initialize CentredParams instance."""
    self.mean = mean
    self.sigma = sigma
    self.skew = skew
__repr__
__repr__() -> str

Return a string representation of the CentredParams object.

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

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

Source code in src/soundscapy/spi/msn.py
def __str__(self) -> str:
    """Return a user-friendly string representation of the CentredParams object."""
    return (
        f"Centred Parameters:"
        f"\nmean:  {self.mean.round(3)}"
        f"\nsigma: {self.sigma.round(3)}"
        f"\nskew:  {self.skew.round(3)}"
    )
from_dp classmethod
from_dp(dp: DirectParams) -> CentredParams

Convert a DirectParams object to a CentredParams object.

PARAMETER DESCRIPTION
dp

The DirectParams object to convert.

TYPE: DirectParams

RETURNS DESCRIPTION
CentredParams

A new CentredParams object with the converted parameters.

Source code in src/soundscapy/spi/msn.py
@classmethod
def from_dp(cls, dp: DirectParams) -> "CentredParams":
    """
    Convert a DirectParams object to a CentredParams object.

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

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

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

MultiSkewNorm

MultiSkewNorm()

A class representing a multi-dimensional skewed normal distribution.

ATTRIBUTE DESCRIPTION
cp

The centred parameters of the fitted model.

dp

The direct parameters of the fitted model.

sample_data

The generated sample data from the fitted model.

data

The input data used for fitting the model.

TYPE: DataFrame | None

METHOD DESCRIPTION
summary

Prints a summary of the fitted model.

fit

Fits the model to the provided data.

define_dp

Defines the direct parameters of the model.

sample

Generates an unrestricted sample from the fitted model.

sample_mtsn

Generates a truncated sample (rejection sampling within [a, b]).

sspy_plot

Plots the joint distribution of the generated sample.

ks2d2s

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

spi_score

Computes the Soundscape Perception Index (SPI).

Initialize the MultiSkewNorm object.

Source code in src/soundscapy/spi/msn.py
def __init__(self) -> None:
    """Initialize the MultiSkewNorm object."""
    warnings.warn(
        "The SPI analysis module is experimental. Use with caution.",
        UserWarning,
        stacklevel=2,
    )
    self.cp = None
    self.dp = None
    self.sample_data = None
    self.data: pd.DataFrame | None = None
__repr__
__repr__() -> str

Return a string representation of the MultiSkewNorm object.

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

Provide a summary of the fitted MultiSkewNorm model.

RETURNS DESCRIPTION
str

indicating the model is not fitted.

Source code in src/soundscapy/spi/msn.py
def summary(self) -> str:
    """
    Provide a summary of the fitted MultiSkewNorm model.

    Returns
    -------
    :
        indicating the model is not fitted.

    """
    if self.cp is None and self.dp is None:
        return "MultiSkewNorm is not fitted."
    lines = []
    if self.data is not None:
        lines.append(f"Fitted from data. n = {len(self.data)}")
    else:
        lines.append("Fitted from direct parameters.")
    lines.append(str(self.dp))
    lines.append(str(self.cp))
    return "\n".join(lines)
fit
fit(
    data: DataFrame | ndarray | None = None,
    x: ndarray | Series | None = None,
    y: ndarray | Series | None = None,
) -> None

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

PARAMETER DESCRIPTION
data

The input data as a pandas DataFrame or numpy array.

TYPE: DataFrame | ndarray | None DEFAULT: None

x

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

TYPE: ndarray | Series | None DEFAULT: None

y

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

TYPE: ndarray | Series | None DEFAULT: None

RAISES DESCRIPTION
ValueError

If neither data nor both x and y are provided.

Source code in src/soundscapy/spi/msn.py
def fit(
    self,
    data: pd.DataFrame | np.ndarray | None = None,
    x: np.ndarray | pd.Series | None = None,
    y: np.ndarray | pd.Series | None = None,
) -> None:
    """
    Fit the multi-dimensional skewed normal model to the provided data.

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

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

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

    if data is not None:
        # If data is provided, convert it to a pandas DataFrame
        if isinstance(data, pd.DataFrame):
            # Rename columns to "x"/"y" on a copy so we don't mutate the
            # caller's DataFrame.
            data = data.copy()
            data.columns = ["x", "y"]

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

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

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

    # Fit the model, extract parameters immediately, then discard the R object.
    # Storing rpy2 objects (RS4) beyond the function boundary creates a
    # persistent reference into R's heap that can outlive the session.
    m = sspyr.selm("x", "y", data)
    cp = sspyr.extract_cp(m)
    dp = sspyr.extract_dp(m)

    self.cp = CentredParams(*cp)
    self.dp = DirectParams(*dp)
    self.data = data
define_dp
define_dp(
    xi: ndarray, omega: ndarray, alpha: ndarray
) -> MultiSkewNorm

Initiate a distribution from the direct parameters.

PARAMETER DESCRIPTION
xi

The xi values of the direct parameters.

TYPE: ndarray

omega

The omega values of the direct parameters.

TYPE: ndarray

alpha

The alpha values of the direct parameters.

TYPE: ndarray

RETURNS DESCRIPTION
MultiSkewNorm
Source code in src/soundscapy/spi/msn.py
def define_dp(
    self, xi: np.ndarray, omega: np.ndarray, alpha: np.ndarray
) -> "MultiSkewNorm":
    """
    Initiate a distribution from the direct parameters.

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

    Returns
    -------
    :

    """
    self.dp = DirectParams(xi, omega, alpha)
    self.cp = CentredParams.from_dp(self.dp)
    return self
from_params classmethod
from_params(
    params: DirectParams | CentredParams | None = None,
    *,
    xi: ndarray | None = None,
    omega: ndarray | None = None,
    alpha: ndarray | None = None,
    mean: ndarray | None = None,
    sigma: ndarray | None = None,
    skew: ndarray | None = None,
) -> MultiSkewNorm

Create a MultiSkewNorm instance from direct parameters.

PARAMETER DESCRIPTION
params

The direct parameters to initialize the model.

TYPE: DirectParams | CentredParams | None DEFAULT: None

RETURNS DESCRIPTION
MultiSkewNorm

A new instance of MultiSkewNorm initialized with the provided parameters.

Source code in src/soundscapy/spi/msn.py
@classmethod
def from_params(
    cls,
    params: DirectParams | CentredParams | None = None,
    *,
    xi: np.ndarray | None = None,
    omega: np.ndarray | None = None,
    alpha: np.ndarray | None = None,
    mean: np.ndarray | None = None,
    sigma: np.ndarray | None = None,
    skew: np.ndarray | None = None,
) -> "MultiSkewNorm":
    """
    Create a MultiSkewNorm instance from direct parameters.

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

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

    """
    instance = cls()

    if params is None:
        if (xi is None or omega is None or alpha is None) and (
            mean is None or sigma is None or skew is None
        ):
            msg = "Either params object or xi, omega, and alpha must be provided."
            raise ValueError(msg)
        if xi is not None and omega is not None and alpha is not None:
            # xi/omega/alpha provided — create DirectParams and derive CP
            instance.dp = DirectParams(xi, omega, alpha)
            instance.cp = CentredParams.from_dp(instance.dp)
        elif mean is not None and sigma is not None and skew is not None:
            # If mean, sigma, and skew are provided, create CentredParams
            cp = CentredParams(mean, sigma, skew)
            dp = DirectParams.from_cp(cp)
            instance.dp = dp
            instance.cp = cp
        return instance
    if isinstance(params, DirectParams):
        # If params is a DirectParams object, set it directly
        instance.dp = params
        instance.cp = CentredParams.from_dp(params)
        return instance
    if isinstance(params, CentredParams):
        # If params is a CentredParams object, convert it to DirectParams
        instance.cp = params
        dp = DirectParams.from_cp(params)
        instance.dp = dp
        return instance
    # If params is neither DirectParams nor CentredParams, raise an error
    msg = (
        "Either params or xi, omega, and alpha must be provided."
        "Or mean, sigma, and skew must be provided."
    )
    raise ValueError(msg)
sample
sample(
    n: int = 1000, *, return_sample: bool = False
) -> None | np.ndarray

Generate a sample from the fitted model.

PARAMETER DESCRIPTION
n

The number of samples to generate, by default 1000.

TYPE: int DEFAULT: 1000

return_sample

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

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
None | ndarray

The generated sample if return_sample is True, otherwise None.

RAISES DESCRIPTION
ValueError

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

Source code in src/soundscapy/spi/msn.py
def sample(
    self, n: int = 1000, *, return_sample: bool = False
) -> None | np.ndarray:
    """
    Generate a sample from the fitted model.

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

    Returns
    -------
    :
        The generated sample if `return_sample` is True, otherwise None.

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

    """
    if self.dp is not None:
        sample = sspyr.sample_msn(
            xi=self.dp.xi, omega=self.dp.omega, alpha=self.dp.alpha, n=n
        )
    else:
        msg = "Model is not fitted. Call fit() or define_dp() first."
        raise ValueError(msg)

    self.sample_data = sample

    if return_sample:
        return sample
    return None
sample_mtsn
sample_mtsn(
    n: int = 1000,
    a: float = -1,
    b: float = 1,
    *,
    return_sample: bool = False,
) -> None | np.ndarray

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

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

PARAMETER DESCRIPTION
n

The number of samples to generate, by default 1000.

TYPE: int DEFAULT: 1000

a

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

TYPE: float DEFAULT: -1

b

Upper truncation bound for both dimensions, by default 1.

TYPE: float DEFAULT: 1

return_sample

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

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
None | ndarray

The generated sample if return_sample is True, otherwise None.

Source code in src/soundscapy/spi/msn.py
def sample_mtsn(
    self, n: int = 1000, a: float = -1, b: float = 1, *, return_sample: bool = False
) -> None | np.ndarray:
    """
    Generate a sample from the multi-dimensional truncated skew-normal distribution.

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

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

    Returns
    -------
    :
        The generated sample if `return_sample` is True, otherwise None.

    """
    if self.dp is not None:
        sample = sspyr.sample_mtsn(
            xi=self.dp.xi,
            omega=self.dp.omega,
            alpha=self.dp.alpha,
            n=n,
            a=a,
            b=b,
        )
    else:
        msg = "Model is not fitted. Call fit() or define_dp() first."
        raise ValueError(msg)

    # Store the sample data
    self.sample_data = sample

    if return_sample:
        return sample
    return None
sspy_plot
sspy_plot(
    color: str = "blue",
    title: str | None = None,
    n: int = 1000,
) -> None

Plot the joint distribution of the generated sample using soundscapy.

PARAMETER DESCRIPTION
color

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

TYPE: str DEFAULT: 'blue'

title

Title for the plot, by default None.

TYPE: str | None DEFAULT: None

n

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

TYPE: int DEFAULT: 1000

Source code in src/soundscapy/spi/msn.py
def sspy_plot(
    self, color: str = "blue", title: str | None = None, n: int = 1000
) -> None:
    """
    Plot the joint distribution of the generated sample using soundscapy.

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

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

    data = pd.DataFrame(self.sample_data, columns=["ISOPleasant", "ISOEventful"])
    plot_title = title if title is not None else "Soundscapy Density Plot"
    scatter(data, color=color, title=plot_title)
ks2d2s
ks2d2s(test: DataFrame | ndarray) -> tuple[float, float]

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

PARAMETER DESCRIPTION
test

The test data.

TYPE: DataFrame | ndarray

RETURNS DESCRIPTION
tuple[float, float]

The KS2D statistic and p-value.

Source code in src/soundscapy/spi/msn.py
def ks2d2s(self, test: pd.DataFrame | np.ndarray) -> tuple[float, float]:
    """
    Compute the two-sample, two-dimensional Kolmogorov-Smirnov statistic.

    Parameters
    ----------
    test
        The test data.

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

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

    # Perform the 2-sample KS test using ks2d2s
    # Note: ks2d2s expects data1, data2
    return ks2d(self.sample_data, test)
spi_score
spi_score(test: DataFrame | ndarray) -> int

Compute the Soundscape Perception Index (SPI).

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

PARAMETER DESCRIPTION
test

The test data.

TYPE: DataFrame | ndarray

RETURNS DESCRIPTION
int

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

Source code in src/soundscapy/spi/msn.py
def spi_score(self, test: pd.DataFrame | np.ndarray) -> int:
    """
    Compute the Soundscape Perception Index (SPI).

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

    Parameters
    ----------
    test
        The test data.

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

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

spi_score

spi_score(
    target: DataFrame | ndarray, test: DataFrame | ndarray
) -> int

Compute the Soundscape Perception Index (SPI).

Calculates the SPI for the test data against the target distribution represented by the sample data.

PARAMETER DESCRIPTION
target

The sample data representing the target distribution.

TYPE: DataFrame | ndarray

test

The test data.

TYPE: DataFrame | ndarray

RETURNS DESCRIPTION
int

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

Source code in src/soundscapy/spi/msn.py
def spi_score(
    target: pd.DataFrame | np.ndarray, test: pd.DataFrame | np.ndarray
) -> int:
    """
    Compute the Soundscape Perception Index (SPI).

    Calculates the SPI for the test data against the target distribution
    represented by the sample data.

    Parameters
    ----------
    target
        The sample data representing the target distribution.
    test
        The test data.

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

    """
    return int((1 - ks2d(target, test)[0]) * 100)

ks2d

ks2d(
    target: DataFrame | ndarray, test: DataFrame | ndarray
) -> tuple[float, float]

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

PARAMETER DESCRIPTION
target

The sample data representing the target distribution.

TYPE: DataFrame | ndarray

test

The test data.

TYPE: DataFrame | ndarray

RETURNS DESCRIPTION
tuple[float, float]

The KS2D statistic and p-value.

Source code in src/soundscapy/spi/msn.py
def ks2d(
    target: pd.DataFrame | np.ndarray, test: pd.DataFrame | np.ndarray
) -> tuple[float, float]:
    """
    Compute the two-sample, two-dimensional Kolmogorov-Smirnov statistic.

    Parameters
    ----------
    target
        The sample data representing the target distribution.
    test
        The test data.

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

    """
    # Ensure target is a numpy array
    if isinstance(target, pd.DataFrame):
        if target.shape[1] != 2:  # noqa: PLR2004
            msg = "Test data must have two columns."
            raise ValueError(msg)
        target_np = target.to_numpy()
    elif isinstance(target, np.ndarray):
        target_np = target
    else:
        msg = "target must be a pandas DataFrame or numpy array."
        raise TypeError(msg)

    # Ensure test_data is a numpy array
    if isinstance(test, pd.DataFrame):
        if test.shape[1] != 2:  # noqa: PLR2004
            msg = "Test data must have two columns."
            raise ValueError(msg)
        test_np = test.to_numpy()
    elif isinstance(test, np.ndarray):
        test_np = test
    else:
        msg = "test_data must be a pandas DataFrame or numpy array."
        raise TypeError(msg)

    # Perform the 2-sample KS test using ks2d2s
    # Note: ks2d2s expects data1, data2
    ks_statistic, p_value = ks2d2s(target_np, test_np)

    return ks_statistic, p_value

cp2dp

cp2dp(
    cp: CentredParams,
    family: Literal["SN", "ESN", "ST", "SC"] = "SN",
) -> DirectParams

Convert centred parameters to direct parameters.

PARAMETER DESCRIPTION
cp

The centred parameters object.

TYPE: CentredParams

family

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

TYPE: Literal['SN', 'ESN', 'ST', 'SC'] DEFAULT: 'SN'

RETURNS DESCRIPTION
DirectParams

The corresponding direct parameters object.

Source code in src/soundscapy/spi/msn.py
def cp2dp(
    cp: CentredParams, family: Literal["SN", "ESN", "ST", "SC"] = "SN"
) -> DirectParams:
    """
    Convert centred parameters to direct parameters.

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

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

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

    return DirectParams(*dp_r)

dp2cp

dp2cp(
    dp: DirectParams,
    family: Literal["SN", "ESN", "ST", "SC"] = "SN",
) -> CentredParams

Convert direct parameters to centred parameters.

PARAMETER DESCRIPTION
dp

The direct parameters object.

TYPE: DirectParams

family

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

TYPE: Literal['SN', 'ESN', 'ST', 'SC'] DEFAULT: 'SN'

RETURNS DESCRIPTION
CentredParams

The corresponding centred parameters object.

Source code in src/soundscapy/spi/msn.py
def dp2cp(
    dp: DirectParams, family: Literal["SN", "ESN", "ST", "SC"] = "SN"
) -> CentredParams:
    """
    Convert direct parameters to centred parameters.

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

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

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

    return CentredParams(*cp_r)

KS2D utilities

soundscapy.spi.ks2d

FUNCTION DESCRIPTION
CountQuads

Compute probabilities by counting points in quadrants.

FuncQuads

Compute probabilities by integrating a density function in quadrants.

Qks

Compute the Kolmogorov-Smirnov probability function Q(lambda).

ks2d2s

Perform the 2-dimensional, 2-sample Kolmogorov-Smirnov test.

ks2d1s

Perform the 2-dimensional, 1-sample Kolmogorov-Smirnov test.

CountQuads

CountQuads(
    Arr2D: ndarray, point: ndarray
) -> tuple[float, float, float, float]

Compute probabilities by counting points in quadrants.

Computes the probabilities of finding points in each of the 4 quadrants defined by a vertical and horizontal line crossing the given point. The probabilities are determined by counting the proportion of points from Arr2D that fall into each quadrant.

PARAMETER DESCRIPTION
Arr2D

Array of 2D points (shape N x 2) to be counted.

TYPE: ndarray

point

A 1D array or list with 2 elements representing the center (x, y) of the 4 quadrants.

TYPE: ndarray

RETURNS DESCRIPTION
tuple[float, float, float, float]

A tuple containing four floats (fpp, fnp, fpn, fnn), representing the normalized fractions (probabilities) of points in each quadrant:

  • fpp: Fraction in the positive-x, positive-y quadrant.
  • fnp: Fraction in the negative-x, positive-y quadrant.
  • fpn: Fraction in the positive-x, negative-y quadrant.
  • fnn: Fraction in the negative-x, negative-y quadrant.
RAISES DESCRIPTION
TypeError

If point or Arr2D are not list-like or numpy arrays, or if point does not have 2 elements, or if Arr2D is not 2D.

Source code in src/soundscapy/spi/ks2d.py
def CountQuads(
    Arr2D: np.ndarray, point: np.ndarray
) -> tuple[float, float, float, float]:
    """
    Compute probabilities by counting points in quadrants.

    Computes the probabilities of finding points in each of the 4 quadrants
    defined by a vertical and horizontal line crossing the given `point`.
    The probabilities are determined by counting the proportion of points
    from `Arr2D` that fall into each quadrant.

    Parameters
    ----------
    Arr2D
        Array of 2D points (shape N x 2) to be counted.
    point
        A 1D array or list with 2 elements representing the center (x, y)
        of the 4 quadrants.

    Returns
    -------
    :
        A tuple containing four floats (fpp, fnp, fpn, fnn), representing the
        normalized fractions (probabilities) of points in each quadrant:

        - fpp: Fraction in the positive-x, positive-y quadrant.
        - fnp: Fraction in the negative-x, positive-y quadrant.
        - fpn: Fraction in the positive-x, negative-y quadrant.
        - fnn: Fraction in the negative-x, negative-y quadrant.

    Raises
    ------
    TypeError
        If `point` or `Arr2D` are not list-like or numpy arrays, or if
        `point` does not have 2 elements, or if `Arr2D` is not 2D.

    """
    if isinstance(point, list):
        point = np.asarray(np.ravel(point))
    elif type(point).__module__ + type(point).__name__ == "numpyndarray":
        point = np.ravel(point.copy())
    else:
        raise TypeError("Input point is neither list nor numpyndarray")
    if len(point) != 2:
        raise TypeError("Input point must have exactly 2 elements")
    if isinstance(Arr2D, list):
        Arr2D = np.asarray(Arr2D)
    elif type(Arr2D).__module__ + type(Arr2D).__name__ == "numpyndarray":
        pass
    else:
        raise TypeError("Input Arr2D is neither list nor numpyndarray")
    if Arr2D.shape[1] > Arr2D.shape[0]:  # Reshape to A[row,column]
        Arr2D = Arr2D.copy().T
    if Arr2D.shape[1] != 2:
        raise TypeError("Input Arr2D is not 2D")
    # The pp of Qpp refer to p for 'positive' and n for 'negative' quadrants.
    # In order. first subscript is x, second is y.
    Qpp = Arr2D[(Arr2D[:, 0] > point[0]) & (Arr2D[:, 1] > point[1]), :]
    Qnp = Arr2D[(Arr2D[:, 0] < point[0]) & (Arr2D[:, 1] > point[1]), :]
    Qpn = Arr2D[(Arr2D[:, 0] > point[0]) & (Arr2D[:, 1] < point[1]), :]
    Qnn = Arr2D[(Arr2D[:, 0] < point[0]) & (Arr2D[:, 1] < point[1]), :]
    # Normalized fractions:
    ff = 1.0 / len(Arr2D)
    fpp = len(Qpp) * ff
    fnp = len(Qnp) * ff
    fpn = len(Qpn) * ff
    fnn = len(Qnn) * ff
    # NOTE:  all the f's are supposed to sum to 1.0. Float representation
    # cause SOMETIMES sum to 1.000000002 or something. I don't know how to
    # test for that reliably, OR what to do about it yet. Keep in mind.
    return fpp, fnp, fpn, fnn

FuncQuads

FuncQuads(func2D, point, xlim, ylim, rounddig=4)

Compute probabilities by integrating a density function in quadrants.

Computes the probabilities of finding points in each of the 4 quadrants defined by a vertical and horizontal line crossing the given point. The probabilities are determined by numerically integrating the 2D density function func2D over each quadrant within the specified limits.

PARAMETER DESCRIPTION
func2D

A 2D density function that accepts two arguments (x, y).

TYPE: callable

point

A 1D array or list with 2 elements representing the center (x, y) of the 4 quadrants.

TYPE: list or ndarray

xlim

A list or array with 2 elements defining the integration limits for x.

TYPE: list or ndarray

ylim

A list or array with 2 elements defining the integration limits for y.

TYPE: list or ndarray

rounddig

Number of decimal digits to round the resulting probabilities to, by default 4.

TYPE: int DEFAULT: 4

RETURNS DESCRIPTION
tuple[float, float, float, float]

A tuple containing four floats (fpp, fnp, fpn, fnn), representing the integrated probabilities in each quadrant, normalized by the total integral:

  • fpp: Probability in the positive-x, positive-y quadrant.
  • fnp: Probability in the negative-x, positive-y quadrant.
  • fpn: Probability in the positive-x, negative-y quadrant.
  • fnn: Probability in the negative-x, negative-y quadrant.
RAISES DESCRIPTION
TypeError

If func2D is not a callable function with 2 arguments, or if point, xlim, or ylim are not list-like or numpy arrays with exactly 2 elements, or if limits in xlim or ylim are equal.

Source code in src/soundscapy/spi/ks2d.py
def FuncQuads(func2D, point, xlim, ylim, rounddig=4):
    """
    Compute probabilities by integrating a density function in quadrants.

    Computes the probabilities of finding points in each of the 4 quadrants
    defined by a vertical and horizontal line crossing the given `point`.
    The probabilities are determined by numerically integrating the 2D density
    function `func2D` over each quadrant within the specified limits.

    Parameters
    ----------
    func2D : callable
        A 2D density function that accepts two arguments (x, y).
    point : list or np.ndarray
        A 1D array or list with 2 elements representing the center (x, y)
        of the 4 quadrants.
    xlim : list or np.ndarray
        A list or array with 2 elements defining the integration limits for x.
    ylim : list or np.ndarray
        A list or array with 2 elements defining the integration limits for y.
    rounddig : int, optional
        Number of decimal digits to round the resulting probabilities to,
        by default 4.

    Returns
    -------
    tuple[float, float, float, float]
        A tuple containing four floats (fpp, fnp, fpn, fnn), representing the
        integrated probabilities in each quadrant, normalized by the total integral:

        - fpp: Probability in the positive-x, positive-y quadrant.
        - fnp: Probability in the negative-x, positive-y quadrant.
        - fpn: Probability in the positive-x, negative-y quadrant.
        - fnn: Probability in the negative-x, negative-y quadrant.

    Raises
    ------
    TypeError
        If `func2D` is not a callable function with 2 arguments, or if
        `point`, `xlim`, or `ylim` are not list-like or numpy arrays with
        exactly 2 elements, or if limits in `xlim` or `ylim` are equal.

    """
    if callable(func2D):
        if len(inspect.getfullargspec(func2D)[0]) != 2:
            raise TypeError("Input func2D is not a function with 2 arguments")
    else:
        raise TypeError("Input func2D is not a function")
    # If xlim, ylim and point are not lists or ndarray, exit.
    if isinstance(point, list):
        point = np.asarray(np.ravel(point))
    elif type(point).__module__ + type(point).__name__ == "numpyndarray":
        point = np.ravel(point.copy())
    else:
        raise TypeError("Input point is not a list or numpyndarray")
    if len(point) != 2:
        raise TypeError("Input point has not exactly 2 elements")
    if isinstance(xlim, list):
        xlim = np.asarray(np.sort(np.ravel(xlim)))
    elif type(xlim).__module__ + type(xlim).__name__ == "numpyndarray":
        xlim = np.sort(np.ravel(xlim.copy()))
    else:
        raise TypeError("Input xlim is not a list or ndarray")
    if len(xlim) != 2:
        raise TypeError("Input xlim has not exactly 2 elements")
    if xlim[0] == xlim[1]:
        raise TypeError("Input xlim[0] should be different to xlim[1]")
    if isinstance(ylim, list):
        ylim = np.asarray(np.sort(np.ravel(ylim)))
    elif type(ylim).__module__ + type(ylim).__name__ == "numpyndarray":
        ylim = np.sort(np.ravel(ylim.copy()))
    else:
        raise TypeError("Input ylim is not a list or ndarray")
    if len(ylim) != 2:
        raise TypeError("Input ylim has not exactly 2 elements")
    if ylim[0] == ylim[1]:
        raise TypeError("Input ylim[0] should be different to ylim[1]")
    # Numerical integration to find the quadrant probabilities.
    totInt = scipy.integrate.dblquad(
        func2D, *xlim, lambda x: np.amin(ylim), lambda x: np.amax(ylim)
    )[0]
    Qpp = scipy.integrate.dblquad(
        func2D, point[0], np.amax(xlim), lambda x: point[1], lambda x: np.amax(ylim)
    )[0]
    Qpn = scipy.integrate.dblquad(
        func2D, point[0], np.amax(xlim), lambda x: np.amin(ylim), lambda x: point[1]
    )[0]
    Qnp = scipy.integrate.dblquad(
        func2D, np.amin(xlim), point[0], lambda x: point[1], lambda x: np.amax(ylim)
    )[0]
    Qnn = scipy.integrate.dblquad(
        func2D, np.amin(xlim), point[0], lambda x: np.amin(ylim), lambda x: point[1]
    )[0]
    fpp = round(Qpp / totInt, rounddig)
    fnp = round(Qnp / totInt, rounddig)
    fpn = round(Qpn / totInt, rounddig)
    fnn = round(Qnn / totInt, rounddig)
    return (fpp, fnp, fpn, fnn)

Qks

Qks(alam, iter=100, prec=1e-17)

Compute the Kolmogorov-Smirnov probability function Q(lambda).

Calculates the significance level for a given KS statistic alam (D). This function is based on the approximation given in Numerical Recipes in C, page 623. It represents the probability that the KS statistic will exceed the observed value alam under the null hypothesis.

PARAMETER DESCRIPTION
alam

The KS statistic D (or a related value, often D * sqrt(N_eff)).

TYPE: float

iter

Maximum number of iterations for the series summation, by default 100.

TYPE: int DEFAULT: 100

prec

Convergence precision. The summation stops if the absolute value of the term to add is less than prec, by default 1e-17.

TYPE: float DEFAULT: 1e-17

RETURNS DESCRIPTION
float

The significance level P(D > observed) associated with alam. Returns 1.0 if the series does not converge within iter iterations or if the result exceeds 1.0. Returns 0.0 if the result is below prec.

RAISES DESCRIPTION
TypeError

If alam is not an integer or float.

Source code in src/soundscapy/spi/ks2d.py
def Qks(alam, iter=100, prec=1e-17):
    """
    Compute the Kolmogorov-Smirnov probability function Q(lambda).

    Calculates the significance level for a given KS statistic `alam` (D).
    This function is based on the approximation given in Numerical Recipes in C,
    page 623. It represents the probability that the KS statistic will exceed
    the observed value `alam` under the null hypothesis.

    Parameters
    ----------
    alam : float
        The KS statistic D (or a related value, often D * sqrt(N_eff)).
    iter : int, optional
        Maximum number of iterations for the series summation, by default 100.
    prec : float, optional
        Convergence precision. The summation stops if the absolute value of
        the term to add is less than `prec`, by default 1e-17.

    Returns
    -------
    float
        The significance level P(D > observed) associated with `alam`.
        Returns 1.0 if the series does not converge within `iter` iterations
        or if the result exceeds 1.0. Returns 0.0 if the result is below `prec`.

    Raises
    ------
    TypeError
        If `alam` is not an integer or float.

    """
    # If j iterations are performed, meaning that toadd
    # is still 2 times larger than the precision.
    if isinstance(alam, int) | isinstance(alam, float):
        pass
    else:
        raise TypeError("Input alam is neither int nor float")
    toadd = [1]
    qks = 0.0
    j = 1
    while (j < iter) & (abs(toadd[-1]) > prec * 2):
        toadd.append(2.0 * (-1.0) ** (j - 1.0) * np.exp(-2.0 * j**2.0 * alam**2.0))
        qks += toadd[-1]
        j += 1
    if (j == iter) | (qks > 1):  # If no convergence after j iter, return 1.0
        return 1.0
    if qks < prec:
        return 0.0
    return qks

ks2d2s

ks2d2s(
    Arr2D1: ndarray, Arr2D2: ndarray
) -> tuple[float, float]

Perform the 2-dimensional, 2-sample Kolmogorov-Smirnov test.

Tests the null hypothesis that two independent 2D samples, Arr2D1 and Arr2D2, are drawn from the same underlying probability distribution. This implementation is based on the methods described by Peacock (1983) and Fasano & Franceschini (1987).

PARAMETER DESCRIPTION
Arr2D1

First 2D sample array (shape N1 x 2).

TYPE: ndarray

Arr2D2

Second 2D sample array (shape N2 x 2).

TYPE: ndarray

RETURNS DESCRIPTION
tuple[float, float]

d : float The 2D KS statistic, representing the maximum difference found between the cumulative distributions in any of the four quadrants, evaluated at all data points. prob : float The significance level (p-value) of the observed statistic d. A small prob indicates that the two samples are significantly different.

RAISES DESCRIPTION
TypeError

If Arr2D1 or Arr2D2 are not numpy arrays or are not 2D.

Source code in src/soundscapy/spi/ks2d.py
def ks2d2s(Arr2D1: np.ndarray, Arr2D2: np.ndarray) -> tuple[float, float]:
    """
    Perform the 2-dimensional, 2-sample Kolmogorov-Smirnov test.

    Tests the null hypothesis that two independent 2D samples, `Arr2D1` and
    `Arr2D2`, are drawn from the same underlying probability distribution.
    This implementation is based on the methods described by Peacock (1983)
    and Fasano & Franceschini (1987).

    Parameters
    ----------
    Arr2D1 : np.ndarray
        First 2D sample array (shape N1 x 2).
    Arr2D2 : np.ndarray
        Second 2D sample array (shape N2 x 2).

    Returns
    -------
    :
        d : float
            The 2D KS statistic, representing the maximum difference found
            between the cumulative distributions in any of the four quadrants,
            evaluated at all data points.
        prob : float
            The significance level (p-value) of the observed statistic `d`.
            A small `prob` indicates that the two samples are significantly
            different.

    Raises
    ------
    TypeError
        If `Arr2D1` or `Arr2D2` are not numpy arrays or are not 2D.

    """
    if not isinstance(Arr2D1, np.ndarray):
        raise TypeError("Input Arr2D1 is not a numpyndarray")
    if Arr2D1.shape[1] > Arr2D1.shape[0]:
        Arr2D1 = Arr2D1.copy().T
    if not isinstance(Arr2D2, np.ndarray):
        raise TypeError("Input Arr2D2 is not a numpyndarray")

    if Arr2D2.shape[1] > Arr2D2.shape[0]:
        Arr2D2 = Arr2D2.copy().T

    if Arr2D1.shape[1] != 2:
        raise TypeError("Input Arr2D1 is not 2D")
    if Arr2D2.shape[1] != 2:
        raise TypeError("Input Arr2D2 is not 2D")

    d1, d2 = 0.0, 0.0
    for point1 in Arr2D1:
        fpp1, fmp1, fpm1, fmm1 = CountQuads(Arr2D1, point1)
        fpp2, fmp2, fpm2, fmm2 = CountQuads(Arr2D2, point1)
        d1 = max(d1, abs(fpp1 - fpp2))
        d1 = max(d1, abs(fpm1 - fpm2))
        d1 = max(d1, abs(fmp1 - fmp2))
        d1 = max(d1, abs(fmm1 - fmm2))
    for point2 in Arr2D2:
        fpp1, fmp1, fpm1, fmm1 = CountQuads(Arr2D1, point2)
        fpp2, fmp2, fpm2, fmm2 = CountQuads(Arr2D2, point2)
        d2 = max(d2, abs(fpp1 - fpp2))
        d2 = max(d2, abs(fpm1 - fpm2))
        d2 = max(d2, abs(fmp1 - fmp2))
        d2 = max(d2, abs(fmm1 - fmm2))
    d = (d1 + d2) / 2.0
    sqen = np.sqrt(len(Arr2D1) * len(Arr2D2) / (len(Arr2D1) + len(Arr2D2)))
    R1 = scipy.stats.pearsonr(Arr2D1[:, 0], Arr2D1[:, 1]).correlation
    R2 = scipy.stats.pearsonr(Arr2D2[:, 0], Arr2D2[:, 1]).correlation
    RR = np.sqrt(1.0 - (R1 * R1 + R2 * R2) / 2.0)
    prob = Qks(d * sqen / (1.0 + RR * (0.25 - 0.75 / sqen)))
    # Small values of prob show that the two samples are significantly
    # different. Prob is the significance level of an observed value of d.
    # NOT the same as the significance level that ou set and compare to D.
    return d, prob

ks2d1s

ks2d1s(Arr2D, func2D, xlim=[], ylim=[])

Perform the 2-dimensional, 1-sample Kolmogorov-Smirnov test.

Tests the null hypothesis that a 2D sample Arr2D is drawn from a given 2D probability density distribution func2D.

PARAMETER DESCRIPTION
Arr2D

The 2D sample array (shape N x 2).

TYPE: ndarray

func2D

The theoretical 2D probability density function func(x, y).

TYPE: callable

xlim

Integration limits for the x-dimension. If empty, defaults are calculated based on the range of Arr2D.

TYPE: list or ndarray DEFAULT: []

ylim

Integration limits for the y-dimension. If empty, defaults are calculated based on the range of Arr2D.

TYPE: list or ndarray DEFAULT: []

RETURNS DESCRIPTION
tuple[float, float]

d : float The 2D KS statistic, representing the maximum difference between the empirical distribution (from Arr2D) and the theoretical distribution (func2D) in any of the four quadrants, evaluated at all data points. prob : float The significance level (p-value) of the observed statistic d. A small prob indicates that the sample is significantly different from the theoretical distribution.

RAISES DESCRIPTION
TypeError

If func2D is not a callable function with 2 arguments, or if Arr2D is not a 2D numpy array.

Source code in src/soundscapy/spi/ks2d.py
def ks2d1s(Arr2D, func2D, xlim=[], ylim=[]):
    """
    Perform the 2-dimensional, 1-sample Kolmogorov-Smirnov test.

    Tests the null hypothesis that a 2D sample `Arr2D` is drawn from a
    given 2D probability density distribution `func2D`.

    Parameters
    ----------
    Arr2D : np.ndarray
        The 2D sample array (shape N x 2).
    func2D : callable
        The theoretical 2D probability density function func(x, y).
    xlim : list or np.ndarray, optional
        Integration limits for the x-dimension. If empty, defaults are
        calculated based on the range of `Arr2D`.
    ylim : list or np.ndarray, optional
        Integration limits for the y-dimension. If empty, defaults are
        calculated based on the range of `Arr2D`.

    Returns
    -------
    tuple[float, float]
        d : float
            The 2D KS statistic, representing the maximum difference between
            the empirical distribution (from `Arr2D`) and the theoretical
            distribution (`func2D`) in any of the four quadrants, evaluated
            at all data points.
        prob : float
            The significance level (p-value) of the observed statistic `d`.
            A small `prob` indicates that the sample is significantly
            different from the theoretical distribution.

    Raises
    ------
    TypeError
        If `func2D` is not a callable function with 2 arguments, or if
        `Arr2D` is not a 2D numpy array.

    """
    if callable(func2D):
        if len(inspect.getfullargspec(func2D)[0]) != 2:
            raise TypeError("Input func2D is not a function with 2 input arguments")
    else:
        raise TypeError("Input func2D is not a function")
    if type(Arr2D).__module__ + type(Arr2D).__name__ == "numpyndarray":
        pass
    else:
        raise TypeError("Input Arr2D is neither list nor numpyndarray")
    print(Arr2D.shape)
    if Arr2D.shape[1] > Arr2D.shape[0]:
        Arr2D = Arr2D.copy().T
    if Arr2D.shape[1] != 2:
        raise TypeError("Input Arr2D is not 2D")
    if xlim == []:
        xlim.append(
            np.amin(Arr2D[:, 0]) - abs(np.amin(Arr2D[:, 0]) - np.amax(Arr2D[:, 0])) / 10
        )
        xlim.append(
            np.amax(Arr2D[:, 0]) - abs(np.amin(Arr2D[:, 0]) - np.amax(Arr2D[:, 0])) / 10
        )
    if ylim == []:
        ylim.append(
            np.amin(Arr2D[:, 1]) - abs(np.amin(Arr2D[:, 1]) - np.amax(Arr2D[:, 1])) / 10
        )

        ylim.append(
            np.amax(Arr2D[:, 1]) - abs(np.amin(Arr2D[:, 1]) - np.amax(Arr2D[:, 1])) / 10
        )
    d = 0
    for point in Arr2D:
        fpp1, fmp1, fpm1, fmm1 = FuncQuads(func2D, point, xlim, ylim)
        fpp2, fmp2, fpm2, fmm2 = CountQuads(Arr2D, point)
        d = max(d, abs(fpp1 - fpp2))
        d = max(d, abs(fpm1 - fpm2))
        d = max(d, abs(fmp1 - fmp2))
        d = max(d, abs(fmm1 - fmm2))
    sqen = np.sqrt(len(Arr2D))
    R1 = scipy.stats.pearsonr(Arr2D[:, 0], Arr2D[:, 1])[0]
    RR = np.sqrt(1.0 - R1**2)
    prob = Qks(d * sqen / (1.0 + RR * (0.25 - 0.75 / sqen)))
    return d, prob