entroscope 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- entroscope-0.1.0/PKG-INFO +83 -0
- entroscope-0.1.0/README.md +64 -0
- entroscope-0.1.0/entroscope/__init__.py +16 -0
- entroscope-0.1.0/entroscope/_core.py +81 -0
- entroscope-0.1.0/entroscope/approximate.py +51 -0
- entroscope-0.1.0/entroscope/differential.py +43 -0
- entroscope-0.1.0/entroscope/multiscale.py +45 -0
- entroscope-0.1.0/entroscope/permutation.py +67 -0
- entroscope-0.1.0/entroscope/sample.py +54 -0
- entroscope-0.1.0/entroscope/shannon.py +53 -0
- entroscope-0.1.0/entroscope/spectral.py +52 -0
- entroscope-0.1.0/entroscope/utils/__init__.py +0 -0
- entroscope-0.1.0/entroscope/utils/normalize.py +13 -0
- entroscope-0.1.0/entroscope/utils/plot.py +88 -0
- entroscope-0.1.0/entroscope/utils/windows.py +19 -0
- entroscope-0.1.0/entroscope.egg-info/PKG-INFO +83 -0
- entroscope-0.1.0/entroscope.egg-info/SOURCES.txt +31 -0
- entroscope-0.1.0/entroscope.egg-info/dependency_links.txt +1 -0
- entroscope-0.1.0/entroscope.egg-info/requires.txt +9 -0
- entroscope-0.1.0/entroscope.egg-info/top_level.txt +1 -0
- entroscope-0.1.0/pyproject.toml +34 -0
- entroscope-0.1.0/setup.cfg +4 -0
- entroscope-0.1.0/tests/test_approximate.py +33 -0
- entroscope-0.1.0/tests/test_consistency.py +491 -0
- entroscope-0.1.0/tests/test_core.py +99 -0
- entroscope-0.1.0/tests/test_differential.py +42 -0
- entroscope-0.1.0/tests/test_examples.py +51 -0
- entroscope-0.1.0/tests/test_multiscale.py +37 -0
- entroscope-0.1.0/tests/test_permutation.py +49 -0
- entroscope-0.1.0/tests/test_plot.py +42 -0
- entroscope-0.1.0/tests/test_sample.py +37 -0
- entroscope-0.1.0/tests/test_shannon.py +44 -0
- entroscope-0.1.0/tests/test_spectral.py +32 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entroscope
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The definitive entropy toolkit for time series data
|
|
5
|
+
Author: entroscope contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/entroscope/entroscope
|
|
8
|
+
Keywords: entropy,time-series,shannon,permutation,spectral
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# entroscope
|
|
21
|
+
|
|
22
|
+
[](https://github.com/entroscope/entroscope/actions/workflows/ci.yml)
|
|
23
|
+
|
|
24
|
+
**The definitive entropy toolkit for time series data.**
|
|
25
|
+
|
|
26
|
+
`pip install entroscope` and get every entropy measure you'd ever need, with one
|
|
27
|
+
consistent interface that works directly on pandas Series and numpy arrays.
|
|
28
|
+
|
|
29
|
+
Born from [NextOnMenu](https://nextonmenu.com), where Shannon entropy of food-trend
|
|
30
|
+
search interest had to be computed by hand. entroscope makes that a one-liner.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install entroscope
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import pandas as pd
|
|
42
|
+
from entroscope import shannon
|
|
43
|
+
|
|
44
|
+
s = pd.Series([10, 20, 15, 80, 90, 85, 88, 92])
|
|
45
|
+
shannon.compute(s) # single entropy value
|
|
46
|
+
shannon.rolling(s, window=20) # rolling entropy over time
|
|
47
|
+
shannon.delta(s, window=20) # rate of change
|
|
48
|
+
shannon.plot(s, window=20) # matplotlib Figure
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Measures
|
|
52
|
+
|
|
53
|
+
shannon · permutation · sample · approximate · spectral · differential · multiscale
|
|
54
|
+
|
|
55
|
+
Every measure shares the same API: `compute`, `rolling`, `delta`, `plot`
|
|
56
|
+
(`normalized` where a theoretical maximum exists). Series in → Series out
|
|
57
|
+
(index preserved); ndarray in → ndarray out.
|
|
58
|
+
|
|
59
|
+
| Method | Returns |
|
|
60
|
+
| ------------- | ------------------------------------ |
|
|
61
|
+
| `compute` | `float` |
|
|
62
|
+
| `rolling` | Series/ndarray, same length |
|
|
63
|
+
| `delta` | Series/ndarray (first difference) |
|
|
64
|
+
| `normalized` | `float` in [0, 1] (where defined) |
|
|
65
|
+
| `plot` | `matplotlib.figure.Figure` |
|
|
66
|
+
|
|
67
|
+
## Real-world example — food-trend analysis (NextOnMenu)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import pandas as pd
|
|
71
|
+
from entroscope import shannon
|
|
72
|
+
|
|
73
|
+
matcha_trends = pd.read_csv("matcha_trends.csv")["interest"]
|
|
74
|
+
shannon.plot(matcha_trends, window=20, title="Matcha — entropy over time")
|
|
75
|
+
# entropy drops before a trend goes mainstream
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A sustained drop in rolling Shannon entropy means search interest is becoming
|
|
79
|
+
concentrated/structured rather than noisy — an early signal of a trend.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# entroscope
|
|
2
|
+
|
|
3
|
+
[](https://github.com/entroscope/entroscope/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
**The definitive entropy toolkit for time series data.**
|
|
6
|
+
|
|
7
|
+
`pip install entroscope` and get every entropy measure you'd ever need, with one
|
|
8
|
+
consistent interface that works directly on pandas Series and numpy arrays.
|
|
9
|
+
|
|
10
|
+
Born from [NextOnMenu](https://nextonmenu.com), where Shannon entropy of food-trend
|
|
11
|
+
search interest had to be computed by hand. entroscope makes that a one-liner.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install entroscope
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import pandas as pd
|
|
23
|
+
from entroscope import shannon
|
|
24
|
+
|
|
25
|
+
s = pd.Series([10, 20, 15, 80, 90, 85, 88, 92])
|
|
26
|
+
shannon.compute(s) # single entropy value
|
|
27
|
+
shannon.rolling(s, window=20) # rolling entropy over time
|
|
28
|
+
shannon.delta(s, window=20) # rate of change
|
|
29
|
+
shannon.plot(s, window=20) # matplotlib Figure
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Measures
|
|
33
|
+
|
|
34
|
+
shannon · permutation · sample · approximate · spectral · differential · multiscale
|
|
35
|
+
|
|
36
|
+
Every measure shares the same API: `compute`, `rolling`, `delta`, `plot`
|
|
37
|
+
(`normalized` where a theoretical maximum exists). Series in → Series out
|
|
38
|
+
(index preserved); ndarray in → ndarray out.
|
|
39
|
+
|
|
40
|
+
| Method | Returns |
|
|
41
|
+
| ------------- | ------------------------------------ |
|
|
42
|
+
| `compute` | `float` |
|
|
43
|
+
| `rolling` | Series/ndarray, same length |
|
|
44
|
+
| `delta` | Series/ndarray (first difference) |
|
|
45
|
+
| `normalized` | `float` in [0, 1] (where defined) |
|
|
46
|
+
| `plot` | `matplotlib.figure.Figure` |
|
|
47
|
+
|
|
48
|
+
## Real-world example — food-trend analysis (NextOnMenu)
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import pandas as pd
|
|
52
|
+
from entroscope import shannon
|
|
53
|
+
|
|
54
|
+
matcha_trends = pd.read_csv("matcha_trends.csv")["interest"]
|
|
55
|
+
shannon.plot(matcha_trends, window=20, title="Matcha — entropy over time")
|
|
56
|
+
# entropy drops before a trend goes mainstream
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A sustained drop in rolling Shannon entropy means search interest is becoming
|
|
60
|
+
concentrated/structured rather than noisy — an early signal of a trend.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""entroscope — the definitive entropy toolkit for time series data."""
|
|
2
|
+
|
|
3
|
+
from . import shannon, permutation, spectral, sample, approximate, differential, multiscale
|
|
4
|
+
from .utils import plot
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = [
|
|
8
|
+
"shannon",
|
|
9
|
+
"permutation",
|
|
10
|
+
"spectral",
|
|
11
|
+
"sample",
|
|
12
|
+
"approximate",
|
|
13
|
+
"differential",
|
|
14
|
+
"multiscale",
|
|
15
|
+
"plot",
|
|
16
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Core engine: input coercion, output wrapping, rolling/delta drivers, plotting.
|
|
2
|
+
|
|
3
|
+
Every measure module delegates its standard methods here so the
|
|
4
|
+
"Series in -> Series out, array in -> array out" contract and the windowing
|
|
5
|
+
logic live in exactly one place.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import matplotlib
|
|
9
|
+
|
|
10
|
+
matplotlib.use("Agg") # headless-safe; never opens a window
|
|
11
|
+
import matplotlib.pyplot as plt # noqa: E402
|
|
12
|
+
import numpy as np # noqa: E402
|
|
13
|
+
import pandas as pd # noqa: E402
|
|
14
|
+
|
|
15
|
+
from .utils.windows import sliding_windows # noqa: E402
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def as_array(x):
|
|
19
|
+
"""Coerce input to a 1-D float ndarray, returning (array, index_or_None)."""
|
|
20
|
+
if isinstance(x, pd.Series):
|
|
21
|
+
index = x.index
|
|
22
|
+
arr = x.to_numpy(dtype=float)
|
|
23
|
+
else:
|
|
24
|
+
index = None
|
|
25
|
+
arr = np.asarray(x, dtype=float)
|
|
26
|
+
if arr.ndim != 1:
|
|
27
|
+
raise ValueError("input must be 1-dimensional")
|
|
28
|
+
if arr.size == 0:
|
|
29
|
+
raise ValueError("input is empty")
|
|
30
|
+
return arr, index
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def wrap(values, index):
|
|
34
|
+
"""Wrap a result array as a Series (if index given) or return the ndarray."""
|
|
35
|
+
values = np.asarray(values, dtype=float)
|
|
36
|
+
if index is not None:
|
|
37
|
+
return pd.Series(values, index=index)
|
|
38
|
+
return values
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def rolling(x, window, kernel, **params):
|
|
42
|
+
"""Apply `kernel` over each full sliding window.
|
|
43
|
+
|
|
44
|
+
Output has the same length as the input; positions before the first full
|
|
45
|
+
window are NaN. Returns a Series (preserving index) if `x` was a Series.
|
|
46
|
+
"""
|
|
47
|
+
arr, index = as_array(x)
|
|
48
|
+
n = len(arr)
|
|
49
|
+
if window <= 0:
|
|
50
|
+
raise ValueError("window must be a positive integer")
|
|
51
|
+
if window > n:
|
|
52
|
+
raise ValueError(f"window ({window}) is larger than series length ({n})")
|
|
53
|
+
out = np.full(n, np.nan)
|
|
54
|
+
for end, w in zip(range(window, n + 1), sliding_windows(arr, window)):
|
|
55
|
+
out[end - 1] = kernel(w, **params)
|
|
56
|
+
return wrap(out, index)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def delta(x, window, kernel, **params):
|
|
60
|
+
"""First difference of the rolling entropy."""
|
|
61
|
+
roll = rolling(x, window, kernel, **params)
|
|
62
|
+
if isinstance(roll, pd.Series):
|
|
63
|
+
return roll.diff()
|
|
64
|
+
out = np.full_like(roll, np.nan)
|
|
65
|
+
out[1:] = np.diff(roll)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def make_plot(x, window, kernel, *, title=None, ylabel="entropy", **params):
|
|
70
|
+
"""Build and return a Figure of rolling entropy. Never calls plt.show()."""
|
|
71
|
+
roll = rolling(x, window, kernel, **params)
|
|
72
|
+
fig, ax = plt.subplots(figsize=(10, 4))
|
|
73
|
+
if isinstance(roll, pd.Series):
|
|
74
|
+
ax.plot(roll.index, roll.to_numpy())
|
|
75
|
+
else:
|
|
76
|
+
ax.plot(range(len(roll)), roll)
|
|
77
|
+
ax.set_title(title or f"Rolling {ylabel} (window={window})")
|
|
78
|
+
ax.set_xlabel("position")
|
|
79
|
+
ax.set_ylabel(ylabel)
|
|
80
|
+
fig.tight_layout()
|
|
81
|
+
return fig
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Approximate entropy (ApEn) — regularity measure, less noise-sensitive."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from . import _core
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _phi(values, m, tol):
|
|
9
|
+
"""Mean of log(fraction of templates within `tol`) over length-m templates."""
|
|
10
|
+
n = len(values)
|
|
11
|
+
templates = np.array([values[i : i + m] for i in range(n - m + 1)])
|
|
12
|
+
counts = np.empty(len(templates))
|
|
13
|
+
for i in range(len(templates)):
|
|
14
|
+
dist = np.max(np.abs(templates - templates[i]), axis=1)
|
|
15
|
+
counts[i] = np.count_nonzero(dist <= tol) / len(templates)
|
|
16
|
+
return float(np.mean(np.log(counts)))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _kernel(values, m=2, r=0.2):
|
|
20
|
+
"""Approximate entropy: phi(m) - phi(m+1)."""
|
|
21
|
+
if r <= 0:
|
|
22
|
+
raise ValueError("r must be positive")
|
|
23
|
+
if m < 1:
|
|
24
|
+
raise ValueError("m must be >= 1")
|
|
25
|
+
values = np.asarray(values, dtype=float)
|
|
26
|
+
n = len(values)
|
|
27
|
+
if n <= m + 1:
|
|
28
|
+
raise ValueError("series too short for given m")
|
|
29
|
+
tol = r * np.std(values)
|
|
30
|
+
if tol == 0:
|
|
31
|
+
return 0.0
|
|
32
|
+
return float(_phi(values, m, tol) - _phi(values, m + 1, tol))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def compute(series, m=2, r=0.2):
|
|
36
|
+
arr, _ = _core.as_array(series)
|
|
37
|
+
return _kernel(arr, m=m, r=r)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def rolling(series, window=50, m=2, r=0.2):
|
|
41
|
+
return _core.rolling(series, window, _kernel, m=m, r=r)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def delta(series, window=50, m=2, r=0.2):
|
|
45
|
+
return _core.delta(series, window, _kernel, m=m, r=r)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def plot(series, window=50, m=2, r=0.2, title=None):
|
|
49
|
+
return _core.make_plot(
|
|
50
|
+
series, window, _kernel, m=m, r=r, title=title, ylabel="approximate entropy"
|
|
51
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Differential entropy — continuous entropy via a fitted distribution."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy import stats
|
|
5
|
+
|
|
6
|
+
from . import _core
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _kernel(values, dist="normal"):
|
|
10
|
+
"""Differential entropy (nats). dist in {'normal', 'kde'}."""
|
|
11
|
+
values = np.asarray(values, dtype=float)
|
|
12
|
+
if dist == "normal":
|
|
13
|
+
var = np.var(values)
|
|
14
|
+
if var == 0:
|
|
15
|
+
return float("-inf") # degenerate: zero-width distribution
|
|
16
|
+
return float(0.5 * np.log(2 * np.pi * np.e * var))
|
|
17
|
+
if dist == "kde":
|
|
18
|
+
if np.std(values) == 0:
|
|
19
|
+
return float("-inf")
|
|
20
|
+
kde = stats.gaussian_kde(values)
|
|
21
|
+
density = kde(values)
|
|
22
|
+
density = density[density > 0]
|
|
23
|
+
return float(-np.mean(np.log(density)))
|
|
24
|
+
raise ValueError(f"unknown dist {dist!r}; expected 'normal' or 'kde'")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compute(series, dist="normal"):
|
|
28
|
+
arr, _ = _core.as_array(series)
|
|
29
|
+
return _kernel(arr, dist=dist)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rolling(series, window=50, dist="kde"):
|
|
33
|
+
return _core.rolling(series, window, _kernel, dist=dist)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def delta(series, window=50, dist="kde"):
|
|
37
|
+
return _core.delta(series, window, _kernel, dist=dist)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def plot(series, window=50, dist="kde", title=None):
|
|
41
|
+
return _core.make_plot(
|
|
42
|
+
series, window, _kernel, dist=dist, title=title, ylabel="differential entropy"
|
|
43
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Multiscale entropy — sample entropy across coarse-grained time scales."""
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
|
|
5
|
+
from . import _core, sample
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _coarse_grain(values, scale):
|
|
9
|
+
"""Average non-overlapping blocks of length `scale`."""
|
|
10
|
+
n = len(values) // scale
|
|
11
|
+
trimmed = values[: n * scale]
|
|
12
|
+
return trimmed.reshape(n, scale).mean(axis=1)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute(series, scales=range(1, 10), method="sample"):
|
|
16
|
+
"""Return {scale: entropy} by coarse-graining then applying `method`.
|
|
17
|
+
|
|
18
|
+
Scales that coarse-grain the series below sample entropy's minimum
|
|
19
|
+
length are skipped (omitted from the result).
|
|
20
|
+
"""
|
|
21
|
+
if method != "sample":
|
|
22
|
+
raise ValueError("only method='sample' is supported")
|
|
23
|
+
arr, _ = _core.as_array(series)
|
|
24
|
+
result = {}
|
|
25
|
+
for scale in scales:
|
|
26
|
+
if scale == 1:
|
|
27
|
+
grained = arr
|
|
28
|
+
else:
|
|
29
|
+
grained = _coarse_grain(arr, scale)
|
|
30
|
+
if len(grained) < 4: # sample entropy needs n > m+1 (m=2 default)
|
|
31
|
+
continue
|
|
32
|
+
result[int(scale)] = sample.compute(grained)
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def plot(series, scales=range(1, 10), title=None):
|
|
37
|
+
"""Plot the complexity profile (entropy vs. scale)."""
|
|
38
|
+
profile = compute(series, scales=scales)
|
|
39
|
+
fig, ax = plt.subplots(figsize=(8, 4))
|
|
40
|
+
ax.plot(list(profile.keys()), list(profile.values()), marker="o")
|
|
41
|
+
ax.set_title(title or "Multiscale entropy")
|
|
42
|
+
ax.set_xlabel("scale")
|
|
43
|
+
ax.set_ylabel("sample entropy")
|
|
44
|
+
fig.tight_layout()
|
|
45
|
+
return fig
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Permutation entropy — complexity from ordinal patterns (Bandt & Pompe)."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from itertools import permutations
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from . import _core
|
|
9
|
+
from .utils import normalize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _kernel(values, order=3, delay=1):
|
|
13
|
+
"""Permutation entropy (base 2) of ordinal patterns of length `order`."""
|
|
14
|
+
if order < 2:
|
|
15
|
+
raise ValueError("order must be >= 2")
|
|
16
|
+
if delay < 1:
|
|
17
|
+
raise ValueError("delay must be >= 1")
|
|
18
|
+
values = np.asarray(values, dtype=float)
|
|
19
|
+
n = len(values)
|
|
20
|
+
span = delay * (order - 1)
|
|
21
|
+
if n - span <= 0:
|
|
22
|
+
raise ValueError("series too short for given order/delay")
|
|
23
|
+
perm_index = {p: i for i, p in enumerate(permutations(range(order)))}
|
|
24
|
+
counts = np.zeros(len(perm_index))
|
|
25
|
+
for i in range(n - span):
|
|
26
|
+
window = values[i : i + span + 1 : delay]
|
|
27
|
+
pattern = tuple(np.argsort(window, kind="stable"))
|
|
28
|
+
counts[perm_index[pattern]] += 1
|
|
29
|
+
total = counts.sum()
|
|
30
|
+
p = counts[counts > 0] / total
|
|
31
|
+
return float(-np.sum(p * np.log2(p)))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _check_window(window, order, delay):
|
|
35
|
+
"""Raise ValueError if window is too small for the given order/delay."""
|
|
36
|
+
span = delay * (order - 1)
|
|
37
|
+
if window <= span:
|
|
38
|
+
raise ValueError(f"window ({window}) must exceed delay*(order-1) = {span}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def compute(series, order=3, delay=1):
|
|
42
|
+
arr, _ = _core.as_array(series)
|
|
43
|
+
return _kernel(arr, order=order, delay=delay)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rolling(series, window=20, order=3, delay=1):
|
|
47
|
+
_check_window(window, order, delay)
|
|
48
|
+
return _core.rolling(series, window, _kernel, order=order, delay=delay)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def delta(series, window=20, order=3, delay=1):
|
|
52
|
+
_check_window(window, order, delay)
|
|
53
|
+
return _core.delta(series, window, _kernel, order=order, delay=delay)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def normalized(series, order=3, delay=1):
|
|
57
|
+
"""Entropy scaled to [0, 1] by log2(order!)."""
|
|
58
|
+
return normalize.by_max(
|
|
59
|
+
compute(series, order=order, delay=delay), math.log2(math.factorial(order))
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def plot(series, window=20, order=3, delay=1, title=None):
|
|
64
|
+
_check_window(window, order, delay)
|
|
65
|
+
return _core.make_plot(
|
|
66
|
+
series, window, _kernel, order=order, delay=delay, title=title, ylabel="permutation entropy"
|
|
67
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Sample entropy (SampEn) — regularity/predictability of a time series."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from . import _core
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _count_matches(values, m, tol):
|
|
9
|
+
"""Count template-vector pairs (length m) within Chebyshev distance `tol`."""
|
|
10
|
+
n = len(values)
|
|
11
|
+
templates = np.array([values[i : i + m] for i in range(n - m + 1)])
|
|
12
|
+
count = 0
|
|
13
|
+
for i in range(len(templates) - 1):
|
|
14
|
+
dist = np.max(np.abs(templates[i + 1 :] - templates[i]), axis=1)
|
|
15
|
+
count += np.count_nonzero(dist <= tol)
|
|
16
|
+
return count
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _kernel(values, m=2, r=0.2):
|
|
20
|
+
"""Sample entropy: -ln(A/B) of length-(m+1) vs length-m matches."""
|
|
21
|
+
if r <= 0:
|
|
22
|
+
raise ValueError("r must be positive")
|
|
23
|
+
if m < 1:
|
|
24
|
+
raise ValueError("m must be >= 1")
|
|
25
|
+
values = np.asarray(values, dtype=float)
|
|
26
|
+
n = len(values)
|
|
27
|
+
if n <= m + 1:
|
|
28
|
+
raise ValueError("series too short for given m")
|
|
29
|
+
tol = r * np.std(values)
|
|
30
|
+
if tol == 0:
|
|
31
|
+
return 0.0 # constant signal: perfectly regular
|
|
32
|
+
b = _count_matches(values, m, tol)
|
|
33
|
+
a = _count_matches(values, m + 1, tol)
|
|
34
|
+
if b == 0 or a == 0:
|
|
35
|
+
# no regularity detected; return a large-but-finite ceiling
|
|
36
|
+
return float(np.log((n - m) * (n - m - 1)))
|
|
37
|
+
return float(-np.log(a / b))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def compute(series, m=2, r=0.2):
|
|
41
|
+
arr, _ = _core.as_array(series)
|
|
42
|
+
return _kernel(arr, m=m, r=r)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def rolling(series, window=50, m=2, r=0.2):
|
|
46
|
+
return _core.rolling(series, window, _kernel, m=m, r=r)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def delta(series, window=50, m=2, r=0.2):
|
|
50
|
+
return _core.delta(series, window, _kernel, m=m, r=r)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def plot(series, window=50, m=2, r=0.2, title=None):
|
|
54
|
+
return _core.make_plot(series, window, _kernel, m=m, r=r, title=title, ylabel="sample entropy")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Shannon entropy — classic information entropy over a binned distribution."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from . import _core
|
|
6
|
+
from .utils import normalize
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _kernel(values, bins=10):
|
|
10
|
+
"""Shannon entropy (base 2) of `values` histogrammed into `bins`."""
|
|
11
|
+
if bins <= 0:
|
|
12
|
+
raise ValueError("bins must be a positive integer")
|
|
13
|
+
values = np.asarray(values, dtype=float)
|
|
14
|
+
counts, _ = np.histogram(values, bins=bins)
|
|
15
|
+
total = counts.sum()
|
|
16
|
+
if total == 0:
|
|
17
|
+
return 0.0
|
|
18
|
+
p = counts[counts > 0] / total
|
|
19
|
+
return float(-np.sum(p * np.log2(p)))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def compute(series, bins=10):
|
|
23
|
+
arr, _ = _core.as_array(series)
|
|
24
|
+
return _kernel(arr, bins=bins)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def rolling(series, window=20, bins=10):
|
|
28
|
+
return _core.rolling(series, window, _kernel, bins=bins)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def delta(series, window=20, bins=10):
|
|
32
|
+
return _core.delta(series, window, _kernel, bins=bins)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def normalized(series, bins=10):
|
|
36
|
+
"""Entropy scaled to [0, 1] by the maximum possible (log2(bins))."""
|
|
37
|
+
return normalize.by_max(compute(series, bins=bins), np.log2(bins))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def geographic(region_df, col="interest"):
|
|
41
|
+
"""Shannon entropy of a spatial distribution (e.g. Google Trends by region)."""
|
|
42
|
+
values = np.asarray(region_df[col], dtype=float)
|
|
43
|
+
total = values.sum()
|
|
44
|
+
if total == 0:
|
|
45
|
+
return 0.0
|
|
46
|
+
p = values[values > 0] / total
|
|
47
|
+
return float(-np.sum(p * np.log2(p)))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def plot(series, window=20, bins=10, title=None):
|
|
51
|
+
return _core.make_plot(
|
|
52
|
+
series, window, _kernel, bins=bins, title=title, ylabel="Shannon entropy"
|
|
53
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Spectral entropy — Shannon entropy of the normalized power spectrum."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy import signal as _signal
|
|
5
|
+
|
|
6
|
+
from . import _core
|
|
7
|
+
from .utils import normalize
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _psd(values, sf):
|
|
11
|
+
"""Return the (positive-frequency) power spectral density of `values`."""
|
|
12
|
+
values = np.asarray(values, dtype=float)
|
|
13
|
+
values = values - values.mean()
|
|
14
|
+
freqs, psd = _signal.periodogram(values, fs=sf)
|
|
15
|
+
return freqs, psd
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _kernel(values, sf=1.0):
|
|
19
|
+
"""Spectral entropy (base 2) of the normalized power spectrum."""
|
|
20
|
+
_, psd = _psd(values, sf)
|
|
21
|
+
total = psd.sum()
|
|
22
|
+
if total == 0:
|
|
23
|
+
return 0.0
|
|
24
|
+
p = psd[psd > 0] / total
|
|
25
|
+
return float(-np.sum(p * np.log2(p)))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compute(series, sf=1.0):
|
|
29
|
+
arr, _ = _core.as_array(series)
|
|
30
|
+
return _kernel(arr, sf=sf)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def rolling(series, window=50, sf=1.0):
|
|
34
|
+
return _core.rolling(series, window, _kernel, sf=sf)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def delta(series, window=50, sf=1.0):
|
|
38
|
+
return _core.delta(series, window, _kernel, sf=sf)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalized(series, sf=1.0):
|
|
42
|
+
"""Entropy scaled to [0, 1] by log2(number of frequency bins)."""
|
|
43
|
+
arr, _ = _core.as_array(series)
|
|
44
|
+
freqs, _psd_vals = _psd(arr, sf)
|
|
45
|
+
n_bins = int(np.count_nonzero(_psd_vals > 0))
|
|
46
|
+
if n_bins <= 1:
|
|
47
|
+
return 0.0
|
|
48
|
+
return normalize.by_max(_kernel(arr, sf=sf), np.log2(n_bins))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def plot(series, window=50, sf=1.0, title=None):
|
|
52
|
+
return _core.make_plot(series, window, _kernel, sf=sf, title=title, ylabel="spectral entropy")
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Normalization helpers: scale an entropy value into [0, 1]."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def by_max(value, max_value):
|
|
5
|
+
"""Return value / max_value clipped to [0, 1]; 0.0 if max_value == 0."""
|
|
6
|
+
if max_value == 0:
|
|
7
|
+
return 0.0
|
|
8
|
+
ratio = value / max_value
|
|
9
|
+
if ratio < 0.0:
|
|
10
|
+
return 0.0
|
|
11
|
+
if ratio > 1.0:
|
|
12
|
+
return 1.0
|
|
13
|
+
return float(ratio)
|