#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the
# Scikit-NeuroMSI Project (https://github.com/renatoparedes/scikit-neuromsi).
# Copyright (c) 2021-2025, Renato Paredes; Cabral, Juan
# License: BSD 3-Clause
# Full Text:
# https://github.com/renatoparedes/scikit-neuromsi/blob/main/LICENSE.txt
# =============================================================================
# DOCS
# =============================================================================
"""Implementation of bias analysis in multisensory integration.
The NDResultBiasAcc class provides an accessor, NDResultBiasAcc, for analyzing
biases in the context of multisensory integration. It offers methods to
calculate biases and mean biases based on specified influence and changing
parameters.
"""
# =============================================================================
# IMPORTS
# =============================================================================
import methodtools
import numpy as np
import pandas as pd
from ..utils import AccessorABC
# =============================================================================
# BIAS ACC
# =============================================================================
[docs]
class NDResultCollectionBiasAcc(AccessorABC):
"""Accessor for calculating biases in an NDResultCollection.
Bias analysis in multisensory integration refers to the examination and
identification of potential biases that may influence the way different
sensory modalities are combined or integrated in the human brain.
This accessor provides methods for calculating biases based on
various parameters.
Parameters
----------
ndcollection : NDResultCollection
The NDResultCollection for which to calculate biases.
tqdm_cls : tqdm.tqdm, optional
The tqdm class to use. Defaults to None.
"""
_default_kind = "bias"
def __init__(self, ndcollection, tqdm_cls):
self._nd_collection = ndcollection
self._tqdm_cls = tqdm_cls
def _bias_as_frame(
self, disp_mtx, influence_parameter, changing_parameter, bias_arr
):
"""Create a DataFrame from the calculated biases.
Parameters
----------
disp_mtx : pandas.DataFrame
Disparity matrix containing the changing and influence parameters.
influence_parameter : str
Parameter being influenced by the cross-modal bias.
changing_parameter : str
Parameter changing across iterations.
bias_arr : numpy.ndarray
Array of calculated biases.
Returns
-------
pandas.DataFrame
A DataFrame representing biases with columns representing changing
and influence parameters.
"""
# The new index is obtained by subtracting the relative value of the
# changing parameter from the influence parameter.
dispm_diff = (
disp_mtx[changing_parameter] - disp_mtx[influence_parameter]
)
# Now we need to know how many repetitions there are in our biases
cpd_len = len(disp_mtx)
cpd_unique_len = len(np.unique(disp_mtx[changing_parameter]))
repeat = cpd_len // cpd_unique_len
pos = cpd_len // repeat
# Create the DataFrame
bias_df = pd.DataFrame(bias_arr.reshape(pos, repeat))
# The index is repeated, so we only want one each 'repeat'.
bias_df.index = pd.Index(dispm_diff.values[::repeat], name="Disparity")
# Create a multi-level column
cnames = ["Changing parameter", "Influence parameter", "Iteration"]
cvalues = [
(changing_parameter, influence_parameter, it)
for it in bias_df.columns
]
bias_df.columns = pd.MultiIndex.from_tuples(cvalues, names=cnames)
return bias_df
[docs]
@methodtools.lru_cache(maxsize=None)
def bias(
self,
influence_parameter,
*,
changing_parameter=None,
dim=None,
mode=None,
quiet=False,
):
"""Calculate biases for a specified influence parameter.
This method calculates biases in the context of multisensory
integration analysis. Biases represent the deviation of a specific
parameter's influence on the integration process across different
iterations. The analysis considers the relationship between the
changing parameter and the influence parameter in each iteration over
a given mode.
Parameters
----------
influence_parameter : str
The parameter being influenced by the cross-modal bias.
changing_parameter : str or None, optional
The parameter changing across iterations. If None,
automatically selected.
dim : str or None, optional
The dimension for calculating biases. If None, defaults to 'time'.
mode : str or None, optional
The mode for which to calculate biases. If None, defaults to the
mode with maximum variance.
quiet : bool, optional
If True, suppress tqdm progress bar. Defaults to False.
Returns
-------
pandas.DataFrame
A DataFrame containing biases with columns representing changing
and influence parameters.
The columns represent the changing and influence parameters, and
each row corresponds to a specific disparity in the relationship
between these parameters across iterations.
Raises
------
ValueError
If the specified parameters are invalid.
"""
ndresults = self._nd_collection
tqdm_cls = self._tqdm_cls
mode = ndresults.coerce_mode(mode)
changing_parameter = ndresults.coerce_parameter(changing_parameter)
dim = ndresults.coerce_dimension(dim)
# unchanged_parameters = ~nd_collection.changing_parameters()
# if not unchanged_parameters[influence_parameter]:
# raise ValueError(
# f"influence_parameter {influence_parameter!r} are not fixed"
# )
disp_mtx = ndresults.disparity_matrix()[
[changing_parameter, influence_parameter]
]
influence_value = disp_mtx[influence_parameter][0]
# tqdm progress bar
if quiet is False and tqdm_cls is not None:
ndresults = tqdm_cls(iterable=ndresults, desc="Calculating biases")
bias_arr = np.zeros(len(ndresults))
for idx, res in enumerate(ndresults):
ref_value = res.run_parameters[changing_parameter]
# here we extract all the values of the mode we are interested in
modes_values = res.get_modes(include=mode)
# we determine the values of the mode in the dimension
# that interests us
max_dim_index = modes_values.index.get_level_values(dim).max()
max_dim_values = modes_values.xs(
max_dim_index, level=dim
).values.T[0]
current_dim_position = max_dim_values.argmax()
bias = np.abs(current_dim_position - influence_value) / np.abs(
influence_value - ref_value
)
bias_arr[idx] = bias
# convert biases array to a DataFrame
bias_df = self._bias_as_frame(
disp_mtx, influence_parameter, changing_parameter, bias_arr
)
return bias_df
[docs]
def bias_mean(
self,
influence_parameter,
*,
changing_parameter=None,
dim=None,
mode=None,
quiet=False,
):
"""Calculate the mean biases in multisensory integration analysis.
This method calculates the mean biases in the context of multisensory
integration analysis. Mean biases represent the average deviation of a
specific parameter's influence on the integration process across
different iterations. The analysis considers the relationship between
the changing parameter and the influence parameter in each iteration.
Parameters
----------
influence_parameter : str
The parameter being influenced by the cross-modal biases.
changing_parameter : str or None, optional
The parameter that changes across iterations. If None, the function
automatically selects it.
dim : str or None, optional
The dimension for calculating mean biases. If None, it defaults to
'time'.
mode : str or None, optional
The mode for which to calculate mean biases. If None, it defaults
to the mode with maximum variance.
quiet : bool, optional
If True, suppresses the tqdm progress bar. Defaults to False.
Returns
-------
pandas.DataFrame
A DataFrame containing the mean biases in multisensory integration
analysis. The columns represent the changing and influence
parameters, and each row corresponds to a specific disparity in
the relationship between these parameters across iterations.
Raises
------
ValueError
If the specified parameters are invalid or the influence parameter
is not fixed across iterations.
"""
bias = self.bias(
influence_parameter=influence_parameter,
changing_parameter=changing_parameter,
dim=dim,
mode=mode,
quiet=quiet,
)
mbias_df = bias.mean(axis=1).to_frame()
cnames = bias.columns.names[:-1] + ["Bias"]
cvalues = bias.columns[0][:-1] + ("mean",)
mbias_df.columns = pd.MultiIndex.from_tuples([cvalues], names=cnames)
return mbias_df