Source code for neurokit2.complexity.complexity

import numpy as np
import pandas as pd

from .complexity_hjorth import complexity_hjorth
from .entropy_attention import entropy_attention
from .entropy_bubble import entropy_bubble
from .entropy_multiscale import entropy_multiscale
from .entropy_permutation import entropy_permutation
from .entropy_svd import entropy_svd
from .fractal_dfa import fractal_dfa
from .fractal_linelength import fractal_linelength


[docs] def complexity(signal, which="makowski2022", delay=1, dimension=2, tolerance="sd", **kwargs): """**Complexity and Chaos Analysis** Measuring the complexity of a signal refers to the quantification of various aspects related to concepts such as **chaos**, **entropy**, **unpredictability**, and **fractal dimension**. .. tip:: We recommend checking our open-access `review <https://onlinelibrary.wiley.com/doi/10.1111/ejn.15800>`_ for an introduction to **fractal physiology** and its application in neuroscience. There are many indices that have been developed and used to assess the complexity of signals, and all of them come with different specificities and limitations. While they should be used in an informed manner, it is also convenient to have a single function that can compute multiple indices at once. The ``nk.complexity()`` function can be used to compute a useful subset of complexity metrics and features. While this is great for exploratory analyses, we recommend running each function separately, to gain more control over the parameters and information that you get. .. warning:: The indices included in this function will be subjected to change in future versions, depending on what the literature suggests. We recommend using this function only for quick exploratory analyses, but then replacing it by the calls to the individual functions. Check-out our `open-access study <https://onlinelibrary.wiley.com/doi/10.1111/ejn.15800>`_ explaining the selection of indices. The categorization by "computation time" is based on `our study <https://www.mdpi.com/1099-4300/24/8/1036>`_ results: .. figure:: https://raw.githubusercontent.com/DominiqueMakowski/ComplexityStructure/main/figures/time1-1.png :alt: Complexity Benchmark (Makowski). :target: https://www.mdpi.com/1099-4300/24/8/1036 Parameters ---------- signal : Union[list, np.array, pd.Series] The signal (i.e., a time series) in the form of a vector of values. which : list What metrics to compute. Can be "makowski2022". delay : int Time delay (often denoted *Tau* :math:`\\tau`, sometimes referred to as *lag*) in samples. See :func:`complexity_delay` to estimate the optimal value for this parameter. dimension : int Embedding Dimension (*m*, sometimes referred to as *d* or *order*). See :func:`complexity_dimension` to estimate the optimal value for this parameter. tolerance : float Tolerance (often denoted as *r*), distance to consider two data points as similar. If ``"sd"`` (default), will be set to :math:`0.2 * SD_{signal}`. See :func:`complexity_tolerance` to estimate the optimal value for this parameter. Returns -------- df : pd.DataFrame A dataframe with one row containing the results for each metric as columns. info : dict A dictionary containing additional information. See Also -------- complexity_delay, complexity_dimension, complexity_tolerance Examples ---------- * **Example 1**: Compute fast and medium-fast complexity metrics .. ipython:: python import neurokit2 as nk # Simulate a signal of 3 seconds signal = nk.signal_simulate(duration=3, frequency=[5, 10]) # Compute selection of complexity metrics (Makowski et al., 2022) df, info = nk.complexity(signal, which = "makowski2022") df * **Example 2**: Compute complexity over time .. ipython:: python import numpy as np import pandas as pd import neurokit2 as nk # Create dynamically varying noise amount_noise = nk.signal_simulate(duration=2, frequency=0.9) amount_noise = nk.rescale(amount_noise, [0, 0.5]) noise = np.random.uniform(0, 2, len(amount_noise)) * amount_noise # Add to simple signal signal = noise + nk.signal_simulate(duration=2, frequency=5) @savefig p_complexity1.png scale=100% nk.signal_plot(signal, sampling_rate = 1000) @suppress plt.close() .. ipython:: python # Create function-wrappers that only return the index value pfd = lambda x: nk.fractal_petrosian(x)[0] kfd = lambda x: nk.fractal_katz(x)[0] sfd = lambda x: nk.fractal_sevcik(x)[0] svden = lambda x: nk.entropy_svd(x)[0] fisher = lambda x: -1 * nk.fisher_information(x)[0] # FI is anticorrelated with complexity # Use them in a rolling window rolling_kfd = pd.Series(signal).rolling(500, min_periods = 300, center=True).apply(kfd) rolling_pfd = pd.Series(signal).rolling(500, min_periods = 300, center=True).apply(pfd) rolling_sfd = pd.Series(signal).rolling(500, min_periods = 300, center=True).apply(sfd) rolling_svden = pd.Series(signal).rolling(500, min_periods = 300, center=True).apply(svden) rolling_fisher = pd.Series(signal).rolling(500, min_periods = 300, center=True).apply(fisher) @savefig p_complexity2.png scale=100% nk.signal_plot([signal, rolling_kfd.values, rolling_pfd.values, rolling_sfd.values, rolling_svden.values, rolling_fisher], labels = ["Signal", "Petrosian Fractal Dimension", "Katz Fractal Dimension", "Sevcik Fractal Dimension", "SVD Entropy", "Fisher Information"], sampling_rate = 1000, standardize = True) @suppress plt.close() References ---------- * Lau, Z. J., Pham, T., Chen, S. H. A., & Makowski, D. (2022). Brain entropy, fractal dimensions and predictability: A review of complexity measures for EEG in healthy and neuropsychiatric populations. European Journal of Neuroscience, 1-23. * Makowski, D., Te, A. S., Pham, T., Lau, Z. J., & Chen, S. H. (2022). The Structure of Chaos: An Empirical Comparison of Fractal Physiology Complexity Indices Using NeuroKit2. Entropy, 24 (8), 1036. """ # Sanity checks if isinstance(signal, (np.ndarray, pd.DataFrame)) and signal.ndim > 1: raise ValueError( "Multidimensional inputs (e.g., matrices or multichannel data) are not supported yet." ) # Initialize df = {} info = {} # Fast ====================================================================================== if which in ["makowski2022", "makowski"]: df["LL"], info["LL"] = fractal_linelength(signal) df["Hjorth"], info["Hjorth"] = complexity_hjorth(signal) df["AttEn"], info["AttEn"] = entropy_attention(signal) df["SVDEn"], info["SVDEn"] = entropy_svd(signal, delay=delay, dimension=dimension) df["BubbEn"], info["BubbEn"] = entropy_bubble( signal, delay=delay, dimension=dimension, **kwargs ) df["CWPEn"], info["CWPEn"] = entropy_permutation( signal, delay=delay, dimension=dimension, corrected=True, weighted=True, conditional=True, **kwargs ) df["MSPEn"], info["MSPEn"] = entropy_multiscale( signal, dimension=dimension, method="MSPEn", **kwargs ) mfdfa, _ = fractal_dfa(signal, multifractal=True, **kwargs) for k in mfdfa.columns: df["MFDFA_" + k] = mfdfa[k].values[0] # Prepare output df = pd.DataFrame.from_dict(df, orient="index").T # Convert to dataframe df = df.reindex(sorted(df.columns), axis=1) # Reorder alphabetically return df, info