# Source code for neurokit2.complexity.entropy_permutation

import numpy as np
import pandas as pd

from .entropy_shannon import entropy_shannon
from .utils_complexity_ordinalpatterns import complexity_ordinalpatterns

[docs]
def entropy_permutation(
signal, delay=1, dimension=3, corrected=True, weighted=False, conditional=False, **kwargs
):
"""**Permutation Entropy (PEn), its Weighted (WPEn) and Conditional (CPEn) forms**

Permutation Entropy (PEn) is a robust measure of the complexity of a dynamic system by
capturing the order relations between values of a time series and extracting a probability
distribution of the ordinal patterns (see Henry and Judge, 2019). Using ordinal descriptors
increases robustness to large artifacts occurring with low frequencies. PEn is applicable
for regular, chaotic, noisy, or real-world time series and has been employed in the context of
EEG, ECG, and stock market time series.

Mathematically, it corresponds to the :func:Shannon entropy <entropy_shannon> after the
signal has been made :func:discrete <complexity_symbolize> by analyzing the permutations in
the time-embedded space.

However, the main shortcoming of traditional PEn is that no information besides the order
structure is retained when extracting the ordinal patterns, which leads to several possible
issues (Fadlallah et al., 2013). The **Weighted PEn** was developed to address these
limitations by incorporating significant information (regarding the amplitude) from the
original time series into the ordinal patterns.

The **Conditional Permutation Entropy (CPEn)** was originally defined by Bandt & Pompe as
*Sorting Entropy*, but recently gained in popularity as conditional through the work of
Unakafov et al. (2014). It describes the average diversity of the ordinal patterns succeeding a
given ordinal pattern (dimension+1 vs. dimension).

This function can be called either via entropy_permutation() or complexity_pe().
Moreover, variants can be directly accessed via complexity_wpe() and complexity_mspe().

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.
corrected : bool
If True, divide by log2(factorial(m)) to normalize the entropy between 0 and 1. Otherwise,
return the permutation entropy in bit.
weighted : bool
If True, compute the weighted permutation entropy (WPE).
**kwargs
Optional arguments, such as a function to compute Entropy (:func:nk.entropy_shannon
(default), :func:nk.entropy_tsallis or :func:nk.entropy_reyni).

Returns
-------
PEn : float
Permutation Entropy
info : dict
A dictionary containing additional information regarding the parameters used.

--------
complexity_ordinalpatterns, entropy_shannon, entropy_multiscale

Examples
----------
.. ipython:: python

signal = nk.signal_simulate(duration=2, sampling_rate=100, frequency=[5, 6], noise=0.5)

# Permutation Entropy (uncorrected)
pen, info = nk.entropy_permutation(signal, corrected=False)
pen

# Weighted Permutation Entropy (WPEn)
wpen, info = nk.entropy_permutation(signal, weighted=True)
wpen

# Conditional Permutation Entropy (CPEn)
cpen, info = nk.entropy_permutation(signal, conditional=True)
cpen

# Conditional Weighted Permutation Entropy (CWPEn)
cwpen, info = nk.entropy_permutation(signal, weighted=True, conditional=True)
cwpen

# Conditional Renyi Permutation Entropy (CRPEn)
crpen, info = nk.entropy_permutation(signal, conditional=True, algorithm=nk.entropy_renyi, alpha=2)
crpen

References
----------
* Henry, M., & Judge, G. (2019). Permutation entropy and information recovery in nonlinear
dynamic economic time series. Econometrics, 7(1), 10.
* Fadlallah, B., Chen, B., Keil, A., & Principe, J. (2013). Weighted-permutation entropy: A
complexity measure for time series incorporating amplitude information. Physical Review E, 87
(2), 022911.
* Zanin, M., Zunino, L., Rosso, O. A., & Papo, D. (2012). Permutation entropy and its main
biomedical and econophysics applications: a review. Entropy, 14(8), 1553-1577.
* Bandt, C., & Pompe, B. (2002). Permutation entropy: a natural complexity measure for time
series. Physical review letters, 88(17), 174102.
* Unakafov, A. M., & Keller, K. (2014). Conditional entropy of ordinal patterns. Physica D:
Nonlinear Phenomena, 269, 94-102.

"""
# 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."
)

info = {"Corrected": corrected, "Weighted": weighted, "Dimension": dimension, "Delay": delay}

pen = _entropy_permutation(
signal,
dimension=dimension,
delay=delay,
corrected=corrected,
weighted=weighted,
**kwargs,
)

if conditional is True:
# Compute PEn at m+1
pen_m1 = _entropy_permutation(
signal,
dimension=dimension + 1,
delay=delay,
corrected=corrected,
weighted=weighted,
**kwargs,
)
# Get difference
pen = pen_m1 - pen

if corrected:
pen = pen / np.log2(np.math.factorial(dimension + 1))
else:
if corrected:
pen = pen / np.log2(np.math.factorial(dimension))

return pen, info

# =============================================================================
# Permutation Entropy
# =============================================================================
def _entropy_permutation(
signal,
dimension=3,
delay=1,
corrected=True,
weighted=False,
algorithm=entropy_shannon,
sorting="quicksort",
**kwargs
):
patterns, info = complexity_ordinalpatterns(
signal,
dimension=dimension,
delay=delay,
algorithm=sorting,
)

# Weighted permutation entropy ----------------------------------------------
if weighted is True:
info["Weights"] = np.var(info["Embedded"], axis=1)

# Weighted frequencies of all permutations
freq = np.array(
[
info["Weights"][np.all(info["Permutations"] == patterns[i], axis=1)].sum()
for i in range(len(patterns))
]
)
# Normalize
freq = freq / info["Weights"].sum()
else:
freq = info["Frequencies"]

# Compute entropy algorithm ------------------------------------------------
pe, _ = algorithm(freq=freq, **kwargs)

return pe