Source code for neurokit2.complexity.entropy_multiscale

```# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from ..misc import copyfunction
from .complexity_lempelziv import complexity_lempelziv
from .entropy_approximate import entropy_approximate
from .entropy_cosinesimilarity import entropy_cosinesimilarity
from .entropy_increment import entropy_increment
from .entropy_permutation import entropy_permutation
from .entropy_sample import entropy_sample
from .entropy_slope import entropy_slope
from .entropy_symbolicdynamic import entropy_symbolicdynamic
from .optim_complexity_tolerance import complexity_tolerance
from .utils_complexity_coarsegraining import _get_scales, complexity_coarsegraining
from .utils_entropy import _phi, _phi_divide

[docs]
def entropy_multiscale(
signal,
scale="default",
dimension=3,
tolerance="sd",
method="MSEn",
show=False,
**kwargs,
):
"""**Multiscale entropy (MSEn) and its Composite (CMSEn), Refined (RCMSEn) or fuzzy versions**

One of the limitation of :func:`SampEn <entropy_sample>` is that it characterizes
complexity strictly on the time scale defined by the sampling procedure (via the ``delay``
argument). To address this, Costa et al. (2002) proposed the multiscale entropy (MSEn),
which computes sample entropies at multiple scales.

The conventional MSEn algorithm consists of two steps:

1. A :func:`coarse-graining <complexity_coarsegraining>` procedure is used to represent the
signal at different time scales.
2. :func:`Sample entropy <entropy_sample>` (or other function) is used to quantify the
regularity of a coarse-grained time series at each time scale factor.

However, in the traditional coarse-graining procedure, the larger the scale factor is, the
shorter the coarse-grained time series is. As such, the variance of the entropy of the
coarse-grained series estimated by SampEn increases as the time scale factor increases, making
it problematic for shorter signals.

* **CMSEn**: In order to reduce the variance of estimated entropy values at large scales, Wu et
al. (2013) introduced the **Composite Multiscale Entropy** algorithm, which computes
multiple coarse-grained series for each scale factor (via the **time-shift** method for
:func:`coarse-graining <complexity_coarsegraining>`).
* **RCMSEn**: Wu et al. (2014) further **Refined** their CMSEn by averaging not the entropy
values of each subcoarsed vector, but its components at a lower level.
* **MMSEn**: Wu et al. (2013) also introduced the **Modified Multiscale Entropy**
algorithm, which is based on rolling-average :func:`coarse-graining <complexity_coarsegraining>`.
* **IMSEn**: Liu et al. (2012) introduced an adaptive-resampling procedure to resample the
coarse-grained series. We implement a generalization of this via interpolation that can be
referred to as **Interpolated Multiscale Entropy**.

.. warning::

Interpolated Multiscale variants don't work as expected. Help is needed to fix this
procedure.

Their :func:`Fuzzy <entropy_fuzzy>` version can be obtained by setting ``fuzzy=True``.

This function can be called either via ``entropy_multiscale()`` or ``complexity_mse()``.
Moreover, variants can be directly accessed via ``complexity_cmse()``, `complexity_rcmse()``,
``complexity_fuzzymse()``, ``complexity_fuzzycmse()`` and ``complexity_fuzzyrcmse()``.

Parameters
----------
signal : Union[list, np.array, pd.Series]
The signal (i.e., a time series) in the form of a vector of values.
or dataframe.
scale : str or int or list
A list of scale factors used for coarse graining the time series. If 'default', will use
``range(len(signal) / (dimension + 10))`` (see discussion
`here <https://github.com/neuropsychology/NeuroKit/issues/75#issuecomment-583884426>`_).
If 'max', will use all scales until half the length of the signal. If an integer, will
create a range until the specified int. See :func:`complexity_coarsegraining` for details.
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.
method : str
What version of multiscale entropy to compute. Can be one of ``"MSEn"``, ``"CMSEn"``,
``"RCMSEn"``, ``"MMSEn"``, ``"IMSEn"``, ``"MSApEn"``, ``"MSPEn"``, ``"CMSPEn"``,
``"MMSPEn"``, ``"IMSPEn"``, ``"MSWPEn"``, ``"CMSWPEn"``, ``"MMSWPEn"``, ``"IMSWPEn"``
(case sensitive).
show : bool
Show the entropy values for each scale factor.
**kwargs
Optional arguments.

Returns
----------
float
The point-estimate of multiscale entropy (MSEn) of the single time series corresponding to
the area under the MSEn values curve, which is essentially the sum of sample entropy values
over the range of scale factors.
dict
A dictionary containing additional information regarding the parameters used
to compute multiscale entropy. The entropy values corresponding to each ``"Scale"``
factor are stored under the ``"Value"`` key.

--------
complexity_coarsegraining, entropy_sample, entropy_fuzzy, entropy_permutation

Examples
----------
**MSEn** (basic coarse-graining)

.. ipython:: python

import neurokit2 as nk

signal = nk.signal_simulate(duration=2, frequency=[5, 12, 40])

@savefig p_entropy_multiscale1.png scale=100%
msen, info = nk.entropy_multiscale(signal, show=True)
@suppress
plt.close()

**CMSEn** (time-shifted coarse-graining)

.. ipython:: python

@savefig p_entropy_multiscale2.png scale=100%
cmsen, info = nk.entropy_multiscale(signal, method="CMSEn", show=True)
@suppress
plt.close()

**RCMSEn** (refined composite MSEn)

.. ipython:: python

@savefig p_entropy_multiscale3.png scale=100%
rcmsen, info = nk.entropy_multiscale(signal, method="RCMSEn", show=True)
@suppress
plt.close()

**MMSEn** (rolling-window coarse-graining)

.. ipython:: python

@savefig p_entropy_multiscale4.png scale=100%
mmsen, info = nk.entropy_multiscale(signal, method="MMSEn", show=True)
@suppress
plt.close()

**IMSEn** (interpolated coarse-graining)

.. ipython:: python

@savefig p_entropy_multiscale5.png scale=100%
imsen, info = nk.entropy_multiscale(signal, method="IMSEn", show=True)
@suppress
plt.close()

**MSApEn** (based on ApEn instead of SampEn)

.. ipython:: python

@savefig p_entropy_multiscale6.png scale=100%
msapen, info = nk.entropy_multiscale(signal, method="MSApEn", show=True)
@suppress
plt.close()

**MSPEn** (based on PEn), **CMSPEn**, **MMSPEn** and **IMSPEn**

.. ipython:: python

@savefig p_entropy_multiscale7.png scale=100%
mspen, info = nk.entropy_multiscale(signal, method="MSPEn", show=True)
@suppress
plt.close()

.. ipython:: python

cmspen, info = nk.entropy_multiscale(signal, method="CMSPEn")
cmspen
mmspen, info = nk.entropy_multiscale(signal, method="MMSPEn")
mmspen
imspen, info = nk.entropy_multiscale(signal, method="IMSPEn")
imspen

**MSWPEn** (based on WPEn), **CMSWPEn**, **MMSWPEn** and **IMSWPEn**

.. ipython:: python

mswpen, info = nk.entropy_multiscale(signal, method="MSWPEn")
cmswpen, info = nk.entropy_multiscale(signal, method="CMSWPEn")
mmswpen, info = nk.entropy_multiscale(signal, method="MMSWPEn")
imswpen, info = nk.entropy_multiscale(signal, method="IMSWPEn")

**FuzzyMSEn**, **FuzzyCMSEn** and **FuzzyRCMSEn**

.. ipython:: python

@savefig p_entropy_multiscale8.png scale=100%
fuzzymsen, info = nk.entropy_multiscale(signal, method="MSEn", fuzzy=True, show=True)
@suppress
plt.close()

.. ipython:: python

fuzzycmsen, info = nk.entropy_multiscale(signal, method="CMSEn", fuzzy=True)
fuzzycmsen

fuzzyrcmsen, info = nk.entropy_multiscale(signal, method="RCMSEn", fuzzy=True)
fuzzycmsen

References
-----------
* Costa, M., Goldberger, A. L., & Peng, C. K. (2002). Multiscale entropy analysis of complex
physiologic time series. Physical review letters, 89(6), 068102.
* Costa, M., Goldberger, A. L., & Peng, C. K. (2005). Multiscale entropy analysis of biological
signals. Physical review E, 71(2), 021906.
* Wu, S. D., Wu, C. W., Lee, K. Y., & Lin, S. G. (2013). Modified multiscale entropy for
short-term time series analysis. Physica A: Statistical Mechanics and its Applications, 392
(23), 5865-5873.
* Wu, S. D., Wu, C. W., Lin, S. G., Wang, C. C., & Lee, K. Y. (2013). Time series analysis
using composite multiscale entropy. Entropy, 15(3), 1069-1084.
* Wu, S. D., Wu, C. W., Lin, S. G., Lee, K. Y., & Peng, C. K. (2014). Analysis of complex time
series using refined composite multiscale entropy. Physics Letters A, 378(20), 1369-1374.
* Gow, B. J., Peng, C. K., Wayne, P. M., & Ahn, A. C. (2015). Multiscale entropy analysis of
center-of-pressure dynamics in human postural control: methodological considerations. Entropy,
17(12), 7926-7947.
* Norris, P. R., Anderson, S. M., Jenkins, J. M., Williams, A. E., & Morris Jr, J. A. (2008).
Heart rate multiscale entropy at three hours predicts hospital mortality in 3,154 trauma
patients. Shock, 30(1), 17-22.
* Liu, Q., Wei, Q., Fan, S. Z., Lu, C. W., Lin, T. Y., Abbod, M. F., & Shieh, J. S. (2012).
Adaptive computation of multiscale entropy and its application in EEG signals for monitoring
depth of anesthesia during surgery. Entropy, 14(6), 978-992.

"""
# 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."
)
# Prevent multiple arguments error in case 'delay' is passed in kwargs
if "delay" in kwargs:
kwargs.pop("delay")

# Default parameters
algorithm = entropy_sample
refined = False
coarsegraining = "nonoverlapping"

if method in ["MSEn", "SampEn"]:
pass  # The default arguments are good
elif method in ["MSApEn", "ApEn", "MSPEn", "PEn", "MSWPEn", "WPEn"]:
if method in ["MSApEn", "ApEn"]:
algorithm = entropy_approximate
if method in ["MSPEn", "PEn"]:
algorithm = entropy_permutation
if method in ["MSWPEn", "WPEn"]:
algorithm = copyfunction(entropy_permutation, weighted=True)
elif method in ["MMSEn", "MMSPEn", "MMSWPEn"]:
coarsegraining = "rolling"
if method in ["MMSPEn"]:
algorithm = entropy_permutation
if method in ["MMSWPEn"]:
algorithm = copyfunction(entropy_permutation, weighted=True)
elif method in ["IMSEn", "IMSPEn", "IMSWPEn"]:
coarsegraining = "interpolate"
if method in ["IMSPEn"]:
algorithm = entropy_permutation
if method in ["IMSWPEn"]:
algorithm = copyfunction(entropy_permutation, weighted=True)
elif method in ["CMSEn", "RCMSEn", "CMSPEn", "CMSWPEn"]:
coarsegraining = "timeshift"
if method in ["CMSPEn"]:
algorithm = entropy_permutation
if method in ["CMSWPEn"]:
algorithm = copyfunction(entropy_permutation, weighted=True)
if method in ["RCMSEn"]:
refined = True
elif method in ["MSCoSiEn", "CoSiEn"]:
algorithm = entropy_cosinesimilarity
elif method in ["MSIncrEn", "IncrEn"]:
algorithm = entropy_increment
elif method in ["MSSlopEn", "SlopEn"]:
algorithm = entropy_slope
elif method in ["MSLZC", "LZC"]:
algorithm = complexity_lempelziv
elif method in ["MSPLZC", "PLZC"]:
algorithm = copyfunction(complexity_lempelziv, permutation=True)
elif method in ["MSSyDyEn", "SyDyEn", "MMSyDyEn"]:
algorithm = entropy_symbolicdynamic
if method in ["MMSyDyEn"]:
coarsegraining = "rolling"
else:
raise ValueError(
"Method '{method}' is not supported. Please use "
"'MSEn', 'CMSEn', 'RCMSEn', 'MMSEn', 'IMSPEn',"
"'MSPEn', 'CMSPEn', 'MMSPEn', 'IMSPEn',"
"'MSWPEn', 'CMSWPEn', 'MMSWPEn', 'IMSWPEn',"
"'MSCoSiEn', 'MSIncrEn', 'MSSlopEn', 'MSSyDyEn'"
"'MSLZC', 'MSPLZC'"
" or 'MSApEn' (case sensitive)."
)

# Store parameters
info = {
"Method": method,
"Algorithm": algorithm.__name__,
"Coarsegraining": coarsegraining,
"Dimension": dimension,
"Scale": _get_scales(signal, scale=scale, dimension=dimension),
"Tolerance": complexity_tolerance(
signal,
method=tolerance,
dimension=dimension,
show=False,
)[0],
}

# Compute entropy for each coarsegrained segment
info["Value"] = np.array(
[
_entropy_multiscale(
signal,
scale=scale,
coarsegraining=coarsegraining,
algorithm=algorithm,
dimension=dimension,
tolerance=info["Tolerance"],
refined=refined,
**kwargs,
)
for scale in info["Scale"]
]
)

# Remove inf, nan and 0
mse = info["Value"][np.isfinite(info["Value"])]

# The MSE index is quantified as the area under the curve (AUC),
# which is like the sum normalized by the number of values. It's similar to the mean.
mse = np.trapz(mse) / len(mse)

# Plot overlay
if show is True:
_entropy_multiscale_plot(mse, info)

return mse, info

# =============================================================================
# Internal
# =============================================================================
def _entropy_multiscale_plot(mse, info):
fig = plt.figure(constrained_layout=False)
fig.suptitle("Entropy values across scale factors")
plt.title(f"(Total {info['Method']} = {np.round(mse, 3)})")
plt.ylabel("Entropy values")
plt.xlabel("Scale")
plt.plot(
info["Scale"][np.isfinite(info["Value"])],
info["Value"][np.isfinite(info["Value"])],
color="#FF9800",
)

return fig

# =============================================================================
# Methods
# =============================================================================
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)

# 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,
dimension=dimension,
tolerance=tolerance,
**kwargs,
)[0]

# 2D coarse-graining (time-shifted, used in composite)
else:
# CMSE
if refined is False:
return _validmean(
[
algorithm(
coarse[i],
delay=1,
dimension=dimension,
tolerance=tolerance,
**kwargs,
)[0]
for i in range(len(coarse))
]
)
# RCMSE
else:
phis = np.array(
[
_phi(
coarse[i],
delay=1,
dimension=dimension,
tolerance=tolerance,
approximate=False,
)[0]
for i in range(len(coarse))
]
)
# Average all phi of the same dimension, then divide, then log
return _phi_divide([_validmean(phis[:, 0]), _validmean(phis[:, 1])])

def _validmean(x):
"""Mean that is robust to NaN and Inf."""
x = np.array(x)[np.isfinite(x)]
if len(x) == 0:
return np.nan
else:
return np.mean(x)
```