From 00f73cc2bdb8d4604725ff63f4c9427bfd92fcae Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Tue, 29 Aug 2023 18:09:07 +0100 Subject: [PATCH 01/11] adjust delay --- neurokit2/complexity/entropy_multiscale.py | 40 +++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/neurokit2/complexity/entropy_multiscale.py b/neurokit2/complexity/entropy_multiscale.py index d469691670..177fc41700 100644 --- a/neurokit2/complexity/entropy_multiscale.py +++ b/neurokit2/complexity/entropy_multiscale.py @@ -249,10 +249,12 @@ def entropy_multiscale( if "delay" in kwargs: kwargs.pop("delay") - # Parameters selection + # Default parameters algorithm = entropy_sample refined = False coarsegraining = "nonoverlapping" + + # Parameters adjustement for variants if method in ["MSEn", "SampEn"]: pass # The default arguments are good elif method in ["MSApEn", "ApEn", "MSPEn", "PEn", "MSWPEn", "WPEn"]: @@ -326,13 +328,9 @@ def entropy_multiscale( info["Value"] = np.array( [ _entropy_multiscale( - coarse=complexity_coarsegraining( - signal, - scale=scale, - method=coarsegraining, - show=False, - **kwargs, - ), + signal, + scale=scale, + coarsegraining=coarsegraining, algorithm=algorithm, dimension=dimension, tolerance=info["Tolerance"], @@ -378,13 +376,31 @@ def _entropy_multiscale_plot(mse, info): # ============================================================================= # Methods # ============================================================================= -def _entropy_multiscale(coarse, algorithm, dimension, tolerance, refined=False, **kwargs): +def _entropy_multiscale( + signal, + scale, + coarsegraining, + algorithm, + dimension, + tolerance, + refined=False, + **kwargs, +): """Wrapper function that works both on 1D and 2D coarse-grained (for composite)""" + + # Get coarse-grained signal + coarse = complexity_coarsegraining(signal, scale=scale, method=coarsegraining) + + # Get delay + delay = 1 # If non-overlapping + if coarsegraining in ["rolling", "interpolate"]: + delay = scale + # For 1D coarse-graining if coarse.ndim == 1: return algorithm( coarse, - delay=1, + delay=delay, dimension=dimension, tolerance=tolerance, **kwargs, @@ -398,7 +414,7 @@ def _entropy_multiscale(coarse, algorithm, dimension, tolerance, refined=False, [ algorithm( coarse[i], - delay=1, + delay=delay, dimension=dimension, tolerance=tolerance, **kwargs, @@ -412,7 +428,7 @@ def _entropy_multiscale(coarse, algorithm, dimension, tolerance, refined=False, [ _phi( coarse[i], - delay=1, + delay=delay, dimension=dimension, tolerance=tolerance, approximate=False, From 1570a100c1bed9b92e498b8151887a69a437726b Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Wed, 30 Aug 2023 16:06:45 +0100 Subject: [PATCH 02/11] change coarsegraining --- .../utils_complexity_coarsegraining.py | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/neurokit2/complexity/utils_complexity_coarsegraining.py b/neurokit2/complexity/utils_complexity_coarsegraining.py index 83362bc15c..5c414c7cdb 100644 --- a/neurokit2/complexity/utils_complexity_coarsegraining.py +++ b/neurokit2/complexity/utils_complexity_coarsegraining.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- import matplotlib.pyplot as plt import numpy as np -import scipy.ndimage.filters from ..signal import signal_interpolate +from .utils_complexity_embedding import complexity_embedding -def complexity_coarsegraining(signal, scale=2, method="nonoverlapping", show=False, **kwargs): +def complexity_coarsegraining( + signal, scale=2, method="nonoverlapping", show=False, **kwargs +): """**Coarse-graining of a signal** The goal of coarse-graining is to represent the signal at a different "scale". The @@ -184,11 +186,16 @@ def complexity_coarsegraining(signal, scale=2, method="nonoverlapping", show=Fal # Relying on scipy is a fast alternative to: # pd.Series(signal).rolling(window=scale).mean().values[scale-1::] # https://stackoverflow.com/questions/13728392/moving-average-or-running-mean - coarse = scipy.ndimage.filters.uniform_filter1d(signal, size=scale, mode="nearest") - coarse = coarse[scale - 1 : :] + # coarse = scipy.ndimage.filters.uniform_filter1d( + # signal, size=scale, mode="nearest" + # ) + # coarse = coarse[scale - 1 : :] + coarse = complexity_embedding(signal, dimension=scale, delay=1).mean(axis=1) elif method == "timeshift": - coarse = np.transpose(np.reshape(signal[: scale * (n // scale)], (n // scale, scale))) + coarse = np.transpose( + np.reshape(signal[: scale * (n // scale)], (n // scale, scale)) + ) else: raise ValueError("Unknown `method`: {}".format(method)) @@ -204,8 +211,15 @@ def complexity_coarsegraining(signal, scale=2, method="nonoverlapping", show=Fal def _complexity_show(signal, coarse, method="nonoverlapping"): plt.plot(signal, linewidth=1.5) if method == "nonoverlapping": - plt.plot(np.linspace(0, len(signal), len(coarse)), coarse, color="red", linewidth=0.75) - plt.scatter(np.linspace(0, len(signal), len(coarse)), coarse, color="red", linewidth=0.5) + plt.plot( + np.linspace(0, len(signal), len(coarse)), + coarse, + color="red", + linewidth=0.75, + ) + plt.scatter( + np.linspace(0, len(signal), len(coarse)), coarse, color="red", linewidth=0.5 + ) elif method == "timeshift": for i in range(len(coarse)): plt.plot( @@ -215,7 +229,9 @@ def _complexity_show(signal, coarse, method="nonoverlapping"): linewidth=0.75, ) else: - plt.plot(np.linspace(0, len(signal), len(coarse)), coarse, color="red", linewidth=1) + plt.plot( + np.linspace(0, len(signal), len(coarse)), coarse, color="red", linewidth=1 + ) plt.title(f'Coarse-graining using method "{method}"') From a12ce582cb0c92c962d47e3ae8c4ef2605731341 Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Thu, 31 Aug 2023 09:25:30 +0100 Subject: [PATCH 03/11] entropy_sample: return phi in info dict --- neurokit2/complexity/entropy_multiscale.py | 15 ++++++++------- neurokit2/complexity/entropy_sample.py | 6 +++--- .../complexity/utils_complexity_coarsegraining.py | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/neurokit2/complexity/entropy_multiscale.py b/neurokit2/complexity/entropy_multiscale.py index 177fc41700..2d59d09e40 100644 --- a/neurokit2/complexity/entropy_multiscale.py +++ b/neurokit2/complexity/entropy_multiscale.py @@ -391,13 +391,14 @@ def _entropy_multiscale( # Get coarse-grained signal coarse = complexity_coarsegraining(signal, scale=scale, method=coarsegraining) - # Get delay - delay = 1 # If non-overlapping - if coarsegraining in ["rolling", "interpolate"]: - delay = scale - # For 1D coarse-graining if coarse.ndim == 1: + # Get delay + delay = 1 # If non-overlapping + if coarsegraining in ["rolling", "interpolate"]: + delay = scale + + # Compute entropy return algorithm( coarse, delay=delay, @@ -414,7 +415,7 @@ def _entropy_multiscale( [ algorithm( coarse[i], - delay=delay, + delay=1, dimension=dimension, tolerance=tolerance, **kwargs, @@ -428,7 +429,7 @@ def _entropy_multiscale( [ _phi( coarse[i], - delay=delay, + delay=1, dimension=dimension, tolerance=tolerance, approximate=False, diff --git a/neurokit2/complexity/entropy_sample.py b/neurokit2/complexity/entropy_sample.py index 3aacca7052..3c0e87ea6b 100644 --- a/neurokit2/complexity/entropy_sample.py +++ b/neurokit2/complexity/entropy_sample.py @@ -81,13 +81,13 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs): } # Compute phi - phi = _phi( + info["phi"], _ = _phi( signal, delay=delay, dimension=dimension, tolerance=info["Tolerance"], approximate=False, **kwargs - )[0] + ) - return _phi_divide(phi), info + return _phi_divide(info["phi"]), info diff --git a/neurokit2/complexity/utils_complexity_coarsegraining.py b/neurokit2/complexity/utils_complexity_coarsegraining.py index 5c414c7cdb..49303c2ecd 100644 --- a/neurokit2/complexity/utils_complexity_coarsegraining.py +++ b/neurokit2/complexity/utils_complexity_coarsegraining.py @@ -201,14 +201,14 @@ def complexity_coarsegraining( raise ValueError("Unknown `method`: {}".format(method)) if show is True: - _complexity_show(signal[0:n], coarse, method=method) + _complexity_coarsegraining_show(signal[0:n], coarse, method=method) return coarse # ============================================================================= # Utils # ============================================================================= -def _complexity_show(signal, coarse, method="nonoverlapping"): +def _complexity_coarsegraining_show(signal, coarse, method="nonoverlapping"): plt.plot(signal, linewidth=1.5) if method == "nonoverlapping": plt.plot( From 820d0d93111b8e3e6ba33aef2b933735b1aed666 Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Thu, 31 Aug 2023 09:38:00 +0100 Subject: [PATCH 04/11] return more info in entropy_sample --- neurokit2/complexity/entropy_sample.py | 3 ++- neurokit2/complexity/utils.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/neurokit2/complexity/entropy_sample.py b/neurokit2/complexity/entropy_sample.py index 3c0e87ea6b..61694e9e2c 100644 --- a/neurokit2/complexity/entropy_sample.py +++ b/neurokit2/complexity/entropy_sample.py @@ -81,7 +81,7 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs): } # Compute phi - info["phi"], _ = _phi( + info["phi"], details = _phi( signal, delay=delay, dimension=dimension, @@ -89,5 +89,6 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs): approximate=False, **kwargs ) + info.update(details) # This should be removed in the future to avoid huge returns return _phi_divide(info["phi"]), info diff --git a/neurokit2/complexity/utils.py b/neurokit2/complexity/utils.py index f3801bfb09..eb8242dc9a 100644 --- a/neurokit2/complexity/utils.py +++ b/neurokit2/complexity/utils.py @@ -117,9 +117,8 @@ def _get_count( valid_metrics = sklearn.neighbors.KDTree.valid_metrics + ["range"] if distance not in valid_metrics: raise ValueError( - "The given metric (%s) is not valid." - "The valid metric names are: %s" - % (distance, valid_metrics) + f"The given metric ({distance}) is not valid." + f" Valid metric names are: {valid_metrics}" ) if fuzzy is True: @@ -152,7 +151,10 @@ def distrange(x, y): # Count for each row count = np.array( - [np.sum(distrange(embedded, embedded[i]) < tolerance) for i in range(len(embedded))] + [ + np.sum(distrange(embedded, embedded[i]) < tolerance) + for i in range(len(embedded)) + ] ) else: # chebyshev and other sklearn methods @@ -160,5 +162,7 @@ def distrange(x, y): # has a `workers` argument to use multiple cores? Benchmark or opinion required! if kdtree is None: kdtree = sklearn.neighbors.KDTree(embedded, metric=distance) - count = kdtree.query_radius(embedded, tolerance, count_only=True).astype(np.float64) + count = kdtree.query_radius(embedded, tolerance, count_only=True).astype( + np.float64 + ) return embedded, count, kdtree From 487407d03ffdd4320371ca84eb921e7bbcb2f48a Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Mon, 4 Sep 2023 21:28:43 +0100 Subject: [PATCH 05/11] Update utils_complexity_coarsegraining.py --- neurokit2/complexity/utils_complexity_coarsegraining.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/neurokit2/complexity/utils_complexity_coarsegraining.py b/neurokit2/complexity/utils_complexity_coarsegraining.py index 49303c2ecd..8e585dbfbe 100644 --- a/neurokit2/complexity/utils_complexity_coarsegraining.py +++ b/neurokit2/complexity/utils_complexity_coarsegraining.py @@ -183,13 +183,7 @@ def complexity_coarsegraining( ) elif method == "rolling": - # Relying on scipy is a fast alternative to: - # pd.Series(signal).rolling(window=scale).mean().values[scale-1::] - # https://stackoverflow.com/questions/13728392/moving-average-or-running-mean - # coarse = scipy.ndimage.filters.uniform_filter1d( - # signal, size=scale, mode="nearest" - # ) - # coarse = coarse[scale - 1 : :] + # See https://github.com/neuropsychology/NeuroKit/pull/892 coarse = complexity_embedding(signal, dimension=scale, delay=1).mean(axis=1) elif method == "timeshift": From 6fbef91b7d3a6fc7db24f34bcda8b62a764f5983 Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Fri, 15 Sep 2023 14:48:41 +0100 Subject: [PATCH 06/11] rename .utils to .utils_entropy --- neurokit2/complexity/entropy_approximate.py | 7 +++-- neurokit2/complexity/entropy_multiscale.py | 4 +-- neurokit2/complexity/entropy_sample.py | 5 ++-- .../complexity/optim_complexity_tolerance.py | 29 ++++++++++++------- .../complexity/{utils.py => utils_entropy.py} | 4 +++ 5 files changed, 30 insertions(+), 19 deletions(-) rename neurokit2/complexity/{utils.py => utils_entropy.py} (94%) diff --git a/neurokit2/complexity/entropy_approximate.py b/neurokit2/complexity/entropy_approximate.py index cf6af72848..c8ef2dcefb 100644 --- a/neurokit2/complexity/entropy_approximate.py +++ b/neurokit2/complexity/entropy_approximate.py @@ -3,10 +3,12 @@ import pandas as pd from .optim_complexity_tolerance import _entropy_apen, complexity_tolerance -from .utils import _get_count +from .utils_entropy import _get_count -def entropy_approximate(signal, delay=1, dimension=2, tolerance="sd", corrected=False, **kwargs): +def entropy_approximate( + signal, delay=1, dimension=2, tolerance="sd", corrected=False, **kwargs +): """**Approximate entropy (ApEn) and its corrected version (cApEn)** Approximate entropy is a technique used to quantify the amount of regularity and the @@ -110,7 +112,6 @@ def entropy_approximate(signal, delay=1, dimension=2, tolerance="sd", corrected= def _entropy_capen(signal, delay, dimension, tolerance, **kwargs): - __, count1, _ = _get_count( signal, delay=delay, diff --git a/neurokit2/complexity/entropy_multiscale.py b/neurokit2/complexity/entropy_multiscale.py index 2d59d09e40..419258181a 100644 --- a/neurokit2/complexity/entropy_multiscale.py +++ b/neurokit2/complexity/entropy_multiscale.py @@ -13,8 +13,8 @@ from .entropy_slope import entropy_slope from .entropy_symbolicdynamic import entropy_symbolicdynamic from .optim_complexity_tolerance import complexity_tolerance -from .utils import _phi, _phi_divide from .utils_complexity_coarsegraining import _get_scales, complexity_coarsegraining +from .utils_entropy import _phi, _phi_divide def entropy_multiscale( @@ -397,7 +397,7 @@ def _entropy_multiscale( delay = 1 # If non-overlapping if coarsegraining in ["rolling", "interpolate"]: delay = scale - + # Compute entropy return algorithm( coarse, diff --git a/neurokit2/complexity/entropy_sample.py b/neurokit2/complexity/entropy_sample.py index 61694e9e2c..bbe64a3249 100644 --- a/neurokit2/complexity/entropy_sample.py +++ b/neurokit2/complexity/entropy_sample.py @@ -3,7 +3,7 @@ import pandas as pd from .optim_complexity_tolerance import complexity_tolerance -from .utils import _phi, _phi_divide +from .utils_entropy import _phi, _phi_divide def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs): @@ -81,7 +81,7 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs): } # Compute phi - info["phi"], details = _phi( + info["phi"], _ = _phi( signal, delay=delay, dimension=dimension, @@ -89,6 +89,5 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs): approximate=False, **kwargs ) - info.update(details) # This should be removed in the future to avoid huge returns return _phi_divide(info["phi"]), info diff --git a/neurokit2/complexity/optim_complexity_tolerance.py b/neurokit2/complexity/optim_complexity_tolerance.py index aa10d4607d..27917c5d2f 100644 --- a/neurokit2/complexity/optim_complexity_tolerance.py +++ b/neurokit2/complexity/optim_complexity_tolerance.py @@ -6,8 +6,8 @@ import sklearn.neighbors from ..stats import density -from .utils import _phi from .utils_complexity_embedding import complexity_embedding +from .utils_entropy import _phi def complexity_tolerance( @@ -224,7 +224,11 @@ def complexity_tolerance( ): if dimension is None: raise ValueError("'dimension' cannot be empty for the 'nolds' method.") - r = 0.11604738531196232 * np.std(signal, ddof=1) * (0.5627 * np.log(dimension) + 1.3334) + r = ( + 0.11604738531196232 + * np.std(signal, ddof=1) + * (0.5627 * np.log(dimension) + 1.3334) + ) info = {"Method": "Adjusted 20% SD"} elif method in ["chon", "chon2009"] and ( @@ -264,7 +268,9 @@ def complexity_tolerance( raise ValueError("'dimension' cannot be empty for the 'makowski' method.") n = len(signal) r = np.std(signal, ddof=1) * ( - 0.2811 * (dimension - 1) + 0.0049 * np.log(n) - 0.02 * ((dimension - 1) * np.log(n)) + 0.2811 * (dimension - 1) + + 0.0049 * np.log(n) + - 0.02 * ((dimension - 1) * np.log(n)) ) info = {"Method": "Makowski"} @@ -292,7 +298,9 @@ def complexity_tolerance( info.update({"Method": "bin"}) else: - raise ValueError("NeuroKit error: complexity_tolerance(): 'method' not recognized.") + raise ValueError( + "NeuroKit error: complexity_tolerance(): 'method' not recognized." + ) if show is True: _optimize_tolerance_plot(r, info, method=method, signal=signal) @@ -305,10 +313,11 @@ def complexity_tolerance( def _optimize_tolerance_recurrence(signal, r_range=None, delay=None, dimension=None): - # Optimize missing parameters if delay is None or dimension is None: - raise ValueError("If method='recurrence', both delay and dimension must be specified.") + raise ValueError( + "If method='recurrence', both delay and dimension must be specified." + ) # Compute distance matrix emb = complexity_embedding(signal, delay=delay, dimension=dimension) @@ -332,10 +341,11 @@ def _optimize_tolerance_recurrence(signal, r_range=None, delay=None, dimension=N def _optimize_tolerance_maxapen(signal, r_range=None, delay=None, dimension=None): - # Optimize missing parameters if delay is None or dimension is None: - raise ValueError("If method='maxApEn', both delay and dimension must be specified.") + raise ValueError( + "If method='maxApEn', both delay and dimension must be specified." + ) if r_range is None: r_range = 40 @@ -359,7 +369,6 @@ def _optimize_tolerance_maxapen(signal, r_range=None, delay=None, dimension=None def _entropy_apen(signal, delay, dimension, tolerance, **kwargs): - phi, info = _phi( signal, delay=delay, @@ -400,7 +409,6 @@ def _optimize_tolerance_neighbours(signal, r_range=None, delay=None, dimension=N def _optimize_tolerance_bin(signal, delay=None, dimension=None): - # Optimize missing parameters if delay is None or dimension is None: raise ValueError("If method='bin', both delay and dimension must be specified.") @@ -424,7 +432,6 @@ def _optimize_tolerance_bin(signal, delay=None, dimension=None): # Plotting # ============================================================================= def _optimize_tolerance_plot(r, info, ax=None, method="maxApEn", signal=None): - if ax is None: fig, ax = plt.subplots() else: diff --git a/neurokit2/complexity/utils.py b/neurokit2/complexity/utils_entropy.py similarity index 94% rename from neurokit2/complexity/utils.py rename to neurokit2/complexity/utils_entropy.py index eb8242dc9a..60d9fc54bf 100644 --- a/neurokit2/complexity/utils.py +++ b/neurokit2/complexity/utils_entropy.py @@ -54,8 +54,12 @@ def _phi( phi[0] = np.mean(np.log(count1 / embedded1.shape[0])) phi[1] = np.mean(np.log(count2 / embedded2.shape[0])) else: + # TODO: What is the correct normalization? + # See https://github.com/neuropsychology/NeuroKit/pull/892#issuecomment-1720992976 phi[0] = np.mean((count1 - 1) / (embedded1.shape[0] - 1)) phi[1] = np.mean((count2 - 1) / (embedded2.shape[0] - 1)) + # phi[0] = np.mean((count1 - 1) / (len(signal) - dimension)) + # phi[1] = np.mean((count2 - 1) / (len(signal) - dimension + 1)) return phi, { "embedded1": embedded1, From 41b923ac8df7d5a39500cccf0d7c8bbca9ce0e93 Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Sat, 16 Sep 2023 16:46:04 +0100 Subject: [PATCH 07/11] move entropy_approximate internals and add entropy_quadratic --- neurokit2/complexity/__init__.py | 6 +- neurokit2/complexity/entropy_approximate.py | 2 +- neurokit2/complexity/entropy_quadratic.py | 71 +++++++++++++++++++ neurokit2/complexity/entropy_sample.py | 2 +- .../complexity/optim_complexity_tolerance.py | 15 +--- neurokit2/complexity/utils_entropy.py | 21 ++++-- 6 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 neurokit2/complexity/entropy_quadratic.py diff --git a/neurokit2/complexity/__init__.py b/neurokit2/complexity/__init__.py index 88e1e555aa..c0383bf52f 100644 --- a/neurokit2/complexity/__init__.py +++ b/neurokit2/complexity/__init__.py @@ -30,6 +30,7 @@ from .entropy_permutation import entropy_permutation from .entropy_phase import entropy_phase from .entropy_power import entropy_power +from .entropy_quadratic import entropy_quadratic from .entropy_range import entropy_range from .entropy_rate import entropy_rate from .entropy_renyi import entropy_renyi @@ -96,7 +97,9 @@ complexity_rcmse = functools.partial(entropy_multiscale, method="RCMSEn") complexity_fuzzymse = functools.partial(entropy_multiscale, fuzzy=True) complexity_fuzzycmse = functools.partial(entropy_multiscale, method="CMSEn", fuzzy=True) -complexity_fuzzyrcmse = functools.partial(entropy_multiscale, method="RCMSEn", fuzzy=True) +complexity_fuzzyrcmse = functools.partial( + entropy_multiscale, method="RCMSEn", fuzzy=True +) complexity_dfa = fractal_dfa @@ -175,6 +178,7 @@ "entropy_symbolicdynamic", "entropy_cumulativeresidual", "entropy_approximate", + "entropy_quadratic", "entropy_bubble", "entropy_coalition", "entropy_sample", diff --git a/neurokit2/complexity/entropy_approximate.py b/neurokit2/complexity/entropy_approximate.py index c8ef2dcefb..900b73373f 100644 --- a/neurokit2/complexity/entropy_approximate.py +++ b/neurokit2/complexity/entropy_approximate.py @@ -97,7 +97,7 @@ def entropy_approximate( # Compute index if corrected is False: - # ApEn is implemented in 'optim_complexity_tolerance()' to avoid circular imports + # ApEn is implemented in 'utils_entropy.py' to avoid circular imports # as one of the method for optimizing tolerance relies on ApEn out, _ = _entropy_apen(signal, delay, dimension, info["Tolerance"], **kwargs) else: diff --git a/neurokit2/complexity/entropy_quadratic.py b/neurokit2/complexity/entropy_quadratic.py new file mode 100644 index 0000000000..cb052a6440 --- /dev/null +++ b/neurokit2/complexity/entropy_quadratic.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +import numpy as np +import pandas as pd + +from .entropy_sample import entropy_sample + + +def entropy_quadratic(signal, delay=1, dimension=2, tolerance="sd", **kwargs): + """**Quadratic Sample Entropy (QSE)** + + Compute the quadratic sample entropy (QSE) of a signal. It is essentially a correction of SampEn introduced by Lake (2005) defined as: + + .. math:: + + QSE = SampEn + ln(2 * tolerannce) + + QSE has been described as a more stable measure of entropy than SampEn (Gylling, 2017). + + Parameters + ---------- + signal : Union[list, np.array, pd.Series] + The signal (i.e., a time series) in the form of a vector of values. + 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. + **kwargs : optional + Other arguments. + + See Also + -------- + entropy_sample + + Returns + ---------- + qse : float + The uadratic sample entropy of the single time series. + info : dict + A dictionary containing additional information regarding the parameters used + to compute sample entropy. + + Examples + ---------- + .. ipython:: python + + import neurokit2 as nk + + signal = nk.signal_simulate(duration=2, frequency=5) + + qsa, parameters = nk.entropy_quadratic(signal, delay=1, dimension=2) + qsa + + References + ---------- + * Huselius Gylling, K. (2017). Quadratic sample entropy as a measure of burstiness: A study in + how well Rényi entropy rate and quadratic sample entropy can capture the presence of spikes in + time-series data. + * Lake, D. E. (2005). Renyi entropy measures of heart rate Gaussianity. IEEE Transactions on + Biomedical Engineering, 53(1), 21-27. + + """ + sampen, info = entropy_sample( + signal, delay=delay, dimension=dimension, tolerance=tolerance, **kwargs + ) + return sampen + np.log(2 * info["Tolerance"]), info diff --git a/neurokit2/complexity/entropy_sample.py b/neurokit2/complexity/entropy_sample.py index bbe64a3249..debbf84367 100644 --- a/neurokit2/complexity/entropy_sample.py +++ b/neurokit2/complexity/entropy_sample.py @@ -35,7 +35,7 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs): See Also -------- - entropy_shannon, entropy_approximate, entropy_fuzzy + entropy_shannon, entropy_approximate, entropy_fuzzy, entropy_quadratic Returns ---------- diff --git a/neurokit2/complexity/optim_complexity_tolerance.py b/neurokit2/complexity/optim_complexity_tolerance.py index 27917c5d2f..af8bb06504 100644 --- a/neurokit2/complexity/optim_complexity_tolerance.py +++ b/neurokit2/complexity/optim_complexity_tolerance.py @@ -7,7 +7,7 @@ from ..stats import density from .utils_complexity_embedding import complexity_embedding -from .utils_entropy import _phi +from .utils_entropy import _entropy_apen def complexity_tolerance( @@ -368,19 +368,6 @@ def _optimize_tolerance_maxapen(signal, r_range=None, delay=None, dimension=None return r_range[np.argmax(apens)], {"Values": r_range, "Scores": np.array(apens)} -def _entropy_apen(signal, delay, dimension, tolerance, **kwargs): - phi, info = _phi( - signal, - delay=delay, - dimension=dimension, - tolerance=tolerance, - approximate=True, - **kwargs, - ) - - return np.abs(np.subtract(phi[0], phi[1])), info - - def _optimize_tolerance_neighbours(signal, r_range=None, delay=None, dimension=None): if delay is None: delay = 1 diff --git a/neurokit2/complexity/utils_entropy.py b/neurokit2/complexity/utils_entropy.py index 60d9fc54bf..3af3e9dce2 100644 --- a/neurokit2/complexity/utils_entropy.py +++ b/neurokit2/complexity/utils_entropy.py @@ -6,6 +6,23 @@ from .utils_complexity_embedding import complexity_embedding + +# ============================================================================= +# ApEn +# ============================================================================= +def _entropy_apen(signal, delay, dimension, tolerance, **kwargs): + phi, info = _phi( + signal, + delay=delay, + dimension=dimension, + tolerance=tolerance, + approximate=True, + **kwargs, + ) + + return np.abs(np.subtract(phi[0], phi[1])), info + + # ============================================================================= # Phi # ============================================================================= @@ -54,12 +71,8 @@ def _phi( phi[0] = np.mean(np.log(count1 / embedded1.shape[0])) phi[1] = np.mean(np.log(count2 / embedded2.shape[0])) else: - # TODO: What is the correct normalization? - # See https://github.com/neuropsychology/NeuroKit/pull/892#issuecomment-1720992976 phi[0] = np.mean((count1 - 1) / (embedded1.shape[0] - 1)) phi[1] = np.mean((count2 - 1) / (embedded2.shape[0] - 1)) - # phi[0] = np.mean((count1 - 1) / (len(signal) - dimension)) - # phi[1] = np.mean((count2 - 1) / (len(signal) - dimension + 1)) return phi, { "embedded1": embedded1, From ec67af6c69dd354328ff1b3ef79102702ee6336b Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Sat, 16 Sep 2023 17:54:48 +0100 Subject: [PATCH 08/11] add to docs --- docs/functions/complexity.rst | 22 +++++++++++++--------- neurokit2/complexity/entropy_quadratic.py | 9 +++++++-- neurokit2/complexity/entropy_svd.py | 1 + 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/functions/complexity.rst b/docs/functions/complexity.rst index 755811a922..6bb2f9588b 100644 --- a/docs/functions/complexity.rst +++ b/docs/functions/complexity.rst @@ -103,6 +103,18 @@ Entropy """"""""""""""""""" .. autofunction:: neurokit2.complexity.entropy_renyi +*entropy_approximate()* +"""""""""""""""""""""""" +.. autofunction:: neurokit2.complexity.entropy_approximate + +*entropy_sample()* +"""""""""""""""""""" +.. autofunction:: neurokit2.complexity.entropy_sample + +*entropy_quadratic()* +"""""""""""""""""""" +.. autofunction:: neurokit2.complexity.entropy_quadratic + *entropy_cumulativeresidual()* """"""""""""""""""""""""""""""""" .. autofunction:: neurokit2.complexity.entropy_cumulativeresidual @@ -155,14 +167,6 @@ Entropy """"""""""""""""""""""" .. autofunction:: neurokit2.complexity.entropy_ofentropy -*entropy_approximate()* -"""""""""""""""""""""""" -.. autofunction:: neurokit2.complexity.entropy_approximate - -*entropy_sample()* -"""""""""""""""""""" -.. autofunction:: neurokit2.complexity.entropy_sample - *entropy_permutation()* """""""""""""""""""""""" .. autofunction:: neurokit2.complexity.entropy_permutation @@ -274,4 +278,4 @@ Joint/Multivariate .. .. automodule:: neurokit2.complexity .. :members: -.. :exclude-members: complexity, complexity_delay, complexity_dimension, complexity_tolerance, complexity_k, fractal_katz, fractal_petrosian, fractal_sevcik, fractal_nld, fractal_psdslope, fractal_higuchi, fractal_correlation, entropy_shannon, entropy_maximum, entropy_differential, entropy_tsallis, entropy_renyi, entropy_cumulativeresidual, entropy_svd, entropy_spectral, entropy_phase, entropy_grid, entropy_attention, entropy_increment, entropy_slope, entropy_symbolicdynamic, entropy_dispersion, entropy_ofentropy, entropy_approximate, entropy_sample, entropy_permutation, entropy_bubble, entropy_range, entropy_fuzzy, entropy_multiscale, entropy_hierarchical, fisher_information, complexity_hjorth, complexity_lempelziv, complexity_relativeroughness, fractal_hurst, complexity_lyapunov, complexity_rqa, fractal_mandelbrot, complexity_simulate, complexity_attractor, complexity_symbolize, complexity_coarsegraining, complexity_ordinalpatterns, recurrence_matrix, entropy_shannon_joint, entropy_rate \ No newline at end of file +.. :exclude-members: complexity, complexity_delay, complexity_dimension, complexity_tolerance, complexity_k, fractal_katz, fractal_petrosian, fractal_sevcik, fractal_nld, fractal_psdslope, fractal_higuchi, fractal_correlation, entropy_shannon, entropy_maximum, entropy_differential, entropy_tsallis, entropy_renyi, entropy_cumulativeresidual, entropy_svd, entropy_spectral, entropy_phase, entropy_grid, entropy_attention, entropy_increment, entropy_slope, entropy_symbolicdynamic, entropy_dispersion, entropy_ofentropy, entropy_approximate, entropy_sample, entropy_permutation, entropy_bubble, entropy_range, entropy_fuzzy, entropy_multiscale, entropy_hierarchical, fisher_information, complexity_hjorth, complexity_lempelziv, complexity_relativeroughness, fractal_hurst, complexity_lyapunov, complexity_rqa, fractal_mandelbrot, complexity_simulate, complexity_attractor, complexity_symbolize, complexity_coarsegraining, complexity_ordinalpatterns, recurrence_matrix, entropy_shannon_joint, entropy_rate, entropy_quadratic \ No newline at end of file diff --git a/neurokit2/complexity/entropy_quadratic.py b/neurokit2/complexity/entropy_quadratic.py index cb052a6440..4139db8e43 100644 --- a/neurokit2/complexity/entropy_quadratic.py +++ b/neurokit2/complexity/entropy_quadratic.py @@ -8,7 +8,8 @@ def entropy_quadratic(signal, delay=1, dimension=2, tolerance="sd", **kwargs): """**Quadratic Sample Entropy (QSE)** - Compute the quadratic sample entropy (QSE) of a signal. It is essentially a correction of SampEn introduced by Lake (2005) defined as: + Compute the quadratic sample entropy (QSE) of a signal. It is essentially a correction of + SampEn introduced by Lake (2005) defined as: .. math:: @@ -66,6 +67,10 @@ def entropy_quadratic(signal, delay=1, dimension=2, tolerance="sd", **kwargs): """ sampen, info = entropy_sample( - signal, delay=delay, dimension=dimension, tolerance=tolerance, **kwargs + signal, + delay=delay, + dimension=dimension, + tolerance=tolerance, + **kwargs, ) return sampen + np.log(2 * info["Tolerance"]), info diff --git a/neurokit2/complexity/entropy_svd.py b/neurokit2/complexity/entropy_svd.py index 27098cae76..8c8cddcf71 100644 --- a/neurokit2/complexity/entropy_svd.py +++ b/neurokit2/complexity/entropy_svd.py @@ -70,4 +70,5 @@ def entropy_svd(signal, delay=1, dimension=2, show=False): W = np.linalg.svd(embedded, compute_uv=False) # Compute SVD W /= np.sum(W) # Normalize singular values + plt.plot(W) return -1 * sum(W * np.log2(W)), {"Dimension": dimension, "Delay": delay} From 19d5487e3bcb772a1c9e02f45927d9f0f88000e2 Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Sat, 16 Sep 2023 18:28:39 +0100 Subject: [PATCH 09/11] Update entropy_svd.py --- neurokit2/complexity/entropy_svd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neurokit2/complexity/entropy_svd.py b/neurokit2/complexity/entropy_svd.py index 8c8cddcf71..27098cae76 100644 --- a/neurokit2/complexity/entropy_svd.py +++ b/neurokit2/complexity/entropy_svd.py @@ -70,5 +70,4 @@ def entropy_svd(signal, delay=1, dimension=2, show=False): W = np.linalg.svd(embedded, compute_uv=False) # Compute SVD W /= np.sum(W) # Normalize singular values - plt.plot(W) return -1 * sum(W * np.log2(W)), {"Dimension": dimension, "Delay": delay} From 067f1cc80f514969cca516dfc9d5e9c2a5fa42c3 Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Thu, 21 Sep 2023 17:05:04 +0100 Subject: [PATCH 10/11] fix scikit-learn #910 --- neurokit2/complexity/utils_entropy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neurokit2/complexity/utils_entropy.py b/neurokit2/complexity/utils_entropy.py index 3af3e9dce2..dee79b3b2b 100644 --- a/neurokit2/complexity/utils_entropy.py +++ b/neurokit2/complexity/utils_entropy.py @@ -128,7 +128,7 @@ def _get_count( # ------------------- # Sanity checks sklearn_version = version.parse(sklearn.__version__) - if sklearn_version >= version.parse("1.3.0"): + if sklearn_version == version.parse("1.3.0"): valid_metrics = sklearn.neighbors.KDTree.valid_metrics() + ["range"] else: valid_metrics = sklearn.neighbors.KDTree.valid_metrics + ["range"] From a7382fd55615eb4deaded7015e00761c9faada1d Mon Sep 17 00:00:00 2001 From: Dominique Makowski Date: Thu, 21 Sep 2023 17:07:15 +0100 Subject: [PATCH 11/11] Update entropy_quadratic.py --- neurokit2/complexity/entropy_quadratic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neurokit2/complexity/entropy_quadratic.py b/neurokit2/complexity/entropy_quadratic.py index 4139db8e43..de859bcb85 100644 --- a/neurokit2/complexity/entropy_quadratic.py +++ b/neurokit2/complexity/entropy_quadratic.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import numpy as np -import pandas as pd from .entropy_sample import entropy_sample