Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add membership function options for fuzzy entropy #1051

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions neurokit2/complexity/entropy_fuzzy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .entropy_sample import entropy_sample


def entropy_fuzzy(signal, delay=1, dimension=2, tolerance="sd", approximate=False, **kwargs):
def entropy_fuzzy(signal, delay=1, dimension=2, tolerance="sd", approximate=False, func_name="exp", **kwargs):
"""**Fuzzy Entropy (FuzzyEn)**

Fuzzy entropy (FuzzyEn) of a signal stems from the combination between information theory and
Expand All @@ -24,12 +24,25 @@ def entropy_fuzzy(signal, delay=1, dimension=2, tolerance="sd", approximate=Fals
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
tolerance : scalar and two-element vector
Tolerance (often denoted as *rn*), refers to a threshold or threshold and power respectively.
distance to consider two data points as similar.See
:func:`complexity_tolerance` to estimate the optimal value for this parameter.
approximate : bool
If ``True``, will compute the fuzzy approximate entropy (FuzzyApEn).
func_name: string
The name of membership functions. Choose in
exp : exponential
gauss : gaussian
cgauss : constgaussian
bell : bell
z : z
trapez : trapezoidal
tri : triangular
sig : sigmoid



**kwargs
Other arguments.

Expand Down Expand Up @@ -78,6 +91,7 @@ def entropy_fuzzy(signal, delay=1, dimension=2, tolerance="sd", approximate=Fals
dimension=dimension,
tolerance=tolerance,
fuzzy=True,
func_name=func_name,
**kwargs,
)
else:
Expand Down
17 changes: 13 additions & 4 deletions neurokit2/complexity/entropy_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .utils_entropy import _phi, _phi_divide


def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs):
def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", func_name="exp", **kwargs):
"""**Sample Entropy (SampEn)**

Compute the sample entropy (SampEn) of a signal. SampEn is a modification
Expand All @@ -30,6 +30,16 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs):
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.
func_name: string
The name of membership functions. Choose in
exp : exponential
gauss : gaussian
cgauss : constgaussian
bell : bell
z : z
trapez : trapezoidal
tri : triangular
sig : sigmoid
**kwargs : optional
Other arguments.

Expand Down Expand Up @@ -64,9 +74,7 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs):
"""
# 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."
)
raise ValueError("Multidimensional inputs (e.g., matrices or multichannel data) are not supported yet.")

# Store parameters
info = {
Expand All @@ -87,6 +95,7 @@ def entropy_sample(signal, delay=1, dimension=2, tolerance="sd", **kwargs):
dimension=dimension,
tolerance=info["Tolerance"],
approximate=False,
func_name=func_name,
**kwargs
)

Expand Down
175 changes: 78 additions & 97 deletions neurokit2/complexity/optim_complexity_tolerance.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
from .utils_entropy import _entropy_apen


def complexity_tolerance(
signal, method="maxApEn", r_range=None, delay=None, dimension=None, show=False
):
def complexity_tolerance(signal, method="maxApEn", r_range=None, delay=None, dimension=None, show=False):
"""**Automated selection of tolerance (r)**

Estimate and select the optimal tolerance (*r*) parameter used by other entropy and other
Expand Down Expand Up @@ -210,97 +208,87 @@ def complexity_tolerance(
54(5), 723-732.

"""
if not isinstance(method, str):
return method, {"Method": "None"}

# Method
method = method.lower()
if method in ["traditional", "sd", "std", "default"]:
r = 0.2 * np.std(signal, ddof=1)
info = {"Method": "20% SD"}

elif method in ["adjusted_sd", "nolds"] and (
isinstance(dimension, (int, float)) or dimension is None
):
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)
)
info = {"Method": "Adjusted 20% SD"}

elif method in ["chon", "chon2009"] and (
isinstance(dimension, (int, float)) or dimension is None
):
if dimension is None:
raise ValueError("'dimension' cannot be empty for the 'chon2009' method.")
sd1 = np.std(np.diff(signal), ddof=1) # short-term variability
sd2 = np.std(signal, ddof=1) # long-term variability of the signal

# Here are the 3 formulas from Chon (2009):
# For m=2: r =(−0.036 + 0.26 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=3: r =(−0.08 + 0.46 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=4: r =(−0.12 + 0.62 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=5: r =(−0.16 + 0.78 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=6: r =(−0.19 + 0.91 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=7: r =(−0.2 + 1 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
if dimension <= 2 and dimension <= 7:
x = [-0.036, -0.08, -0.12, -0.16, -0.19, -0.2][dimension - 2]
y = [0.26, 0.46, 0.62, 0.78, 0.91, 1][dimension - 2]
else:
# We need to extrapolate the 2 first numbers, x and y
# np.polyfit(np.log([2,3,4, 5, 6, 7]), [-0.036, -0.08, -0.12, -0.16, -0.19, -0.2], 1)
# np.polyfit([2,3,4, 5, 6, 7], [0.26, 0.46, 0.62, 0.78, 0.91, 1], 1)
x = -0.034 * dimension + 0.022
y = 0.14885714 * dimension - 0.00180952

r = (x + y * np.sqrt(sd1 / sd2)) / (len(signal) / 1000) ** 1 / 4
info = {"Method": "Chon (2009)"}

elif method in ["neurokit", "makowski"] and (
isinstance(dimension, (int, float)) or dimension is None
):
# Method described in
# https://github.com/DominiqueMakowski/ComplexityTolerance
if dimension is None:
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))
)
if isinstance(method, str):

# Method for str.
method = method.lower()
if method in ["traditional", "sd", "std", "default"]:
r = 0.2 * np.std(signal, ddof=1)
info = {"Method": "20% SD"}

elif method in ["adjusted_sd", "nolds"] and (isinstance(dimension, (int, float)) or dimension is None):
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)
info = {"Method": "Adjusted 20% SD"}

elif method in ["chon", "chon2009"] and (isinstance(dimension, (int, float)) or dimension is None):
if dimension is None:
raise ValueError("'dimension' cannot be empty for the 'chon2009' method.")
sd1 = np.std(np.diff(signal), ddof=1) # short-term variability
sd2 = np.std(signal, ddof=1) # long-term variability of the signal

# Here are the 3 formulas from Chon (2009):
# For m=2: r =(−0.036 + 0.26 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=3: r =(−0.08 + 0.46 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=4: r =(−0.12 + 0.62 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=5: r =(−0.16 + 0.78 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=6: r =(−0.19 + 0.91 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
# For m=7: r =(−0.2 + 1 * sqrt(sd1/sd2)) / (len(signal) / 1000)**1/4
if dimension <= 2 and dimension <= 7:
x = [-0.036, -0.08, -0.12, -0.16, -0.19, -0.2][dimension - 2]
y = [0.26, 0.46, 0.62, 0.78, 0.91, 1][dimension - 2]
else:
# We need to extrapolate the 2 first numbers, x and y
# np.polyfit(np.log([2,3,4, 5, 6, 7]), [-0.036, -0.08, -0.12, -0.16, -0.19, -0.2], 1)
# np.polyfit([2,3,4, 5, 6, 7], [0.26, 0.46, 0.62, 0.78, 0.91, 1], 1)
x = -0.034 * dimension + 0.022
y = 0.14885714 * dimension - 0.00180952

r = (x + y * np.sqrt(sd1 / sd2)) / (len(signal) / 1000) ** 1 / 4
info = {"Method": "Chon (2009)"}

elif method in ["neurokit", "makowski"] and (isinstance(dimension, (int, float)) or dimension is None):
# Method described in
# https://github.com/DominiqueMakowski/ComplexityTolerance
if dimension is None:
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))
)

info = {"Method": "Makowski"}
info = {"Method": "Makowski"}

elif method in ["maxapen", "optimize"]:
r, info = _optimize_tolerance_maxapen(
signal, r_range=r_range, delay=delay, dimension=dimension
)
info.update({"Method": "Max ApEn"})
elif method in ["maxapen", "optimize"]:
r, info = _optimize_tolerance_maxapen(signal, r_range=r_range, delay=delay, dimension=dimension)
info.update({"Method": "Max ApEn"})

elif method in ["recurrence", "rqa"]:
r, info = _optimize_tolerance_recurrence(
signal, r_range=r_range, delay=delay, dimension=dimension
)
info.update({"Method": "1% Recurrence Rate"})
elif method in ["recurrence", "rqa"]:
r, info = _optimize_tolerance_recurrence(signal, r_range=r_range, delay=delay, dimension=dimension)
info.update({"Method": "1% Recurrence Rate"})

elif method in ["neighbours", "neighbors", "nn"]:
r, info = _optimize_tolerance_neighbours(
signal, r_range=r_range, delay=delay, dimension=dimension
)
info.update({"Method": "2% Neighbours"})
elif method in ["neighbours", "neighbors", "nn"]:
r, info = _optimize_tolerance_neighbours(signal, r_range=r_range, delay=delay, dimension=dimension)
info.update({"Method": "2% Neighbours"})

elif method in ["bin", "bins", "singh", "singh2016"]:
r, info = _optimize_tolerance_bin(signal, delay=delay, dimension=dimension)
info.update({"Method": "bin"})
elif method in ["bin", "bins", "singh", "singh2016"]:
r, info = _optimize_tolerance_bin(signal, delay=delay, dimension=dimension)
info.update({"Method": "bin"})

else:
raise ValueError("NeuroKit error: complexity_tolerance(): 'method' not recognized.")

elif np.isscalar(method):
r = [method * np.std(signal, ddof=1)]
info = {"fuzzy_entropy"}

elif isinstance(method, (list, np.ndarray)) and len(method) == 2:
r = [method[0] * np.std(signal, ddof=1), method[1]]
info = {"fuzzy_entropy"}

else:
raise ValueError(
"NeuroKit error: complexity_tolerance(): 'method' not recognized."
)
raise ValueError("Invalid type for method")

if show is True:
_optimize_tolerance_plot(r, info, method=method, signal=signal)
Expand All @@ -315,9 +303,7 @@ 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)
Expand All @@ -343,9 +329,7 @@ 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
Expand Down Expand Up @@ -383,10 +367,7 @@ def _optimize_tolerance_neighbours(signal, r_range=None, delay=None, dimension=N
kdtree = sklearn.neighbors.KDTree(embedded, metric="chebyshev")
counts = np.array(
[
np.mean(
kdtree.query_radius(embedded, r, count_only=True).astype(np.float64)
/ embedded.shape[0]
)
np.mean(kdtree.query_radius(embedded, r, count_only=True).astype(np.float64) / embedded.shape[0])
for r in r_range
]
)
Expand Down
Loading