From 4d7b8392024ae5cc588c87040143cb7e1b9d69df Mon Sep 17 00:00:00 2001 From: jjscripts Date: Tue, 22 Sep 2026 18:56:20 +0800 Subject: [PATCH 1/2] docs: rewrite the README; fix the crashing quickstart examples Laya-style layout: centered banner (light and dark, drawn with entroscope's own rolling spectral entropy), badges including the docs site, installation with extras, a quickstart whose comments show real outputs, a validation table, sections on rolling entropy, two-signal measures and ML/polars, a guide to choosing a measure, and an "Honest limits" section. The quickstart in the README and on the docs site called rolling with window=20 on an 8-point series and raised ValueError; the other README examples used undefined variables or a missing CSV. Every example now runs, and tests/test_readme.py executes them in CI and checks the 14 numbers they print. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 7 + README.md | 326 ++++++++++++++++++++--------------- docs/assets/banner-dark.png | Bin 0 -> 42724 bytes docs/assets/banner-light.png | Bin 0 -> 43645 bytes docs/assets/make_banner.py | 104 +++++++++++ docs/quickstart.md | 48 ++++-- tests/test_readme.py | 43 +++++ 7 files changed, 369 insertions(+), 159 deletions(-) create mode 100644 docs/assets/banner-dark.png create mode 100644 docs/assets/banner-light.png create mode 100644 docs/assets/make_banner.py create mode 100644 tests/test_readme.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b161ec2..619bea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- Docs: README rewritten with a banner, runnable examples that show their real + outputs, a "Why trust the numbers" validation table, a guide to choosing a + measure, and an "Honest limits" section. The quickstart examples in the README + and on the docs site crashed (`window=20` on an 8-point series); they now run, + and `tests/test_readme.py` executes every example and checks the numbers it + claims. + - CI: removed the tag-triggered PyPI publish workflow (`publish.yml`). Releases are uploaded to PyPI manually; see "Releasing" in `CONTRIBUTING.md`. diff --git a/README.md b/README.md index 09b59aa..3a8b74a 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,231 @@ -# entroscope - -[![CI](https://img.shields.io/github/actions/workflow/status/Par-python/entroscope/ci.yml?style=flat-square&logo=githubactions&logoColor=white&label=CI&labelColor=24292e)](https://github.com/Par-python/entroscope/actions/workflows/ci.yml) -[![PyPI](https://img.shields.io/pypi/v/entroscope.svg?style=flat-square&logo=pypi&logoColor=white&labelColor=24292e&color=blue)](https://pypi.org/project/entroscope/) -[![Downloads](https://img.shields.io/pepy/dt/entroscope.svg?style=flat-square&logo=python&logoColor=white&labelColor=24292e&color=blue)](https://pepy.tech/project/entroscope) -[![Python](https://img.shields.io/pypi/pyversions/entroscope.svg?style=flat-square&logo=python&logoColor=white&labelColor=24292e&color=blue)](https://pypi.org/project/entroscope/) -[![Stars](https://img.shields.io/github/stars/Par-python/entroscope.svg?style=flat-square&logo=github&logoColor=white&labelColor=24292e&color=yellow)](https://github.com/Par-python/entroscope/stargazers) -[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square&logo=opensourceinitiative&logoColor=white&labelColor=24292e)](https://opensource.org/licenses/MIT) - -**The definitive entropy toolkit for time series data.** Nine entropy measures, -one consistent interface, working directly on pandas and polars Series and numpy -arrays. Results are [validated against antropy, EntropyHub and scipy](#validated-results). - - - - Two stacked charts. Top: a synthetic signal that is random noise until step 180, then a regular cycle. Bottom: its rolling spectral entropy, high during the noise and falling sharply shortly after step 180. - - -It started in [NextOnMenu](https://github.com/Par-python/nextonmenu): a falling -Shannon entropy of a food's regional search interest turned out to be an early -signal that it was about to trend. Computing it meant re-writing the same -histogram-and-log boilerplate every time. entroscope is that code, written once. +

+ + + entroscope: the definitive entropy toolkit for time series data + +

+ +

+ Entropy for time series, over time.
+ Nine entropy measures behind one API, rolling windows on pandas, polars and numpy,
+ and every result checked against antropy, EntropyHub and scipy. +

+ +

+ PyPI + Downloads + Python versions + CI + Docs + Stars + License: MIT +

+ +Most entropy libraries hand you one number per array. entroscope is built for +the question that comes next: **how is the entropy of my series changing, and +when did it change?** Every measure computes on a single window, rolls across a +whole series, and plots, with the same call shape. pandas and polars Series keep +their index or name. + +## Installation ```bash pip install entroscope ``` -## Quick start +| Extra | Adds | +| --- | --- | +| `pip install "entroscope[sklearn]"` | `EntropyFeatures`, a scikit-learn transformer | +| `pip install "entroscope[polars]"` | polars Series input and output | -```python -import pandas as pd -from entroscope import shannon +Requires Python 3.9+. numpy, pandas, scipy and matplotlib install automatically. -s = pd.Series([10, 20, 15, 80, 90, 85, 88, 92]) +## Quickstart -shannon.compute(s) # 1.75 (a single entropy value, in bits) -shannon.rolling(s, window=20) # rolling entropy over time (a Series) -shannon.delta(s, window=20) # rate of change of entropy -shannon.normalized(s) # entropy scaled to [0, 1] -shannon.plot(s, window=20) # a matplotlib Figure -``` +A series that is pure noise for 200 steps, then turns into a clean 20-step cycle: -Every method accepts a `pd.Series` **or** a `np.ndarray`. Pass a Series and you -get a Series back with its index preserved; pass an array and you get an array. +```python +import numpy as np +import pandas as pd +from entroscope import shannon, spectral -### Headless environments (Docker, CI) +rng = np.random.default_rng(0) +t = np.arange(400) +noise = rng.normal(size=400) +s = pd.Series(np.where(t < 200, noise, np.sin(2 * np.pi * t / 20) + 0.2 * noise)) -`entroscope` does not change your matplotlib backend on import, so interactive -plotting in notebooks keeps working. In a headless environment (a Docker -container or CI runner) where you want a guaranteed non-interactive backend, set -the standard environment variable: +spectral.compute(s[:200]) # -> 6.05 bits: power spread over every frequency +spectral.compute(s[200:]) # -> 0.88 bits: power concentrated in one +spectral.normalized(s[200:]) # -> 0.13 on a 0-1 scale -```bash -export MPLBACKEND=Agg # or, in a Dockerfile: ENV MPLBACKEND=Agg +roll = spectral.rolling(s, window=40) # -> pd.Series, same index, NaN for the first 39 steps +roll[100], roll[300] # -> (3.64, 0.70): the drop marks the change +spectral.delta(s, window=40) # -> step-to-step change in the rolling entropy +fig = spectral.plot(s, window=40) # -> matplotlib Figure (never calls plt.show()) + +shannon.compute(s[:200]), shannon.compute(s[200:]) # -> (3.08, 3.23): can't tell them apart ``` -## The nine measures +The last line is why there are nine measures. Shannon entropy only sees the spread +of values, not their order, so it misses the rhythm that spectral entropy picks up +immediately. [The nine measures](#the-nine-measures) says which to reach for. -| Measure | Import | Captures | -| ---------------- | ------------------------- | ------------------------------------------------ | -| **Shannon** | `entroscope.shannon` | Uncertainty in a binned distribution | -| **Permutation** | `entroscope.permutation` | Ordinal-pattern complexity (robust to noise) | -| **Sample** | `entroscope.sample` | Regularity / predictability | -| **Approximate** | `entroscope.approximate` | Regularity (less noise-sensitive, faster) | -| **Spectral** | `entroscope.spectral` | Spread of the power spectrum (frequency domain) | -| **Differential** | `entroscope.differential` | Continuous entropy via a fitted distribution | -| **Multiscale** | `entroscope.multiscale` | Sample entropy across coarse-grained time scales | -| **Transfer** | `entroscope.transfer` | Directional information flow X → Y (KSG/binned) | -| **Divergence** | `entroscope.divergence` | KL and Jensen-Shannon distance between samples | - -## One consistent API - -Every measure exposes the same methods, so switching measures is a one-word change: - -| Method | Input | Returns | -| ------------------------------ | ----------------- | ----------------------------------------------------- | -| `compute(x, **params)` | Series or ndarray | `float` | -| `rolling(x, window, **params)` | Series or ndarray | Series/ndarray, same length (NaN warm-up) | -| `delta(x, window, **params)` | Series or ndarray | Series/ndarray (first difference) | -| `normalized(x, **params)` | Series or ndarray | `float` in [0, 1] (shannon/permutation/spectral only) | -| `plot(x, window, **params)` | Series or ndarray | `matplotlib.figure.Figure` | - -Shannon additionally provides `geographic(df, col=...)` for spatial distributions -(e.g. search interest by region). Multiscale provides `compute` and `plot`. - -**Missing values.** Any NaN or ±inf in the input makes the result NaN, never a -made-up number. `rolling` and `delta` are NaN only for the windows that contain -the gap, so the rest of the series is unaffected. `rolling` is causal: the value -at position t uses positions t−window+1 through t. - -The two-input measures take a pair of series. `transfer.compute(x, y)` (plus -`rolling`, `delta`, `plot`) estimates how much `x`'s past tells you about `y`'s -future; `divergence.kl(p, q)` and `divergence.js(p, q)` (plus `plot`) compare two -samples' distributions over shared bins. - -## Visualization +

+ + + Two stacked charts. Top: a synthetic signal that is random noise until step 180, then a regular cycle. Bottom: its rolling spectral entropy, high during the noise and falling sharply shortly after step 180. + +

-```python -from entroscope import plot +### Why trust the numbers -# overlay several measures on one axis -plot.compare(s, measures=["shannon", "permutation", "spectral"], window=20) +Every measure is checked against an independent implementation on every CI run, +on four kinds of signal (white noise, a noisy sine, an AR(1) process and the chaotic +logistic map): -# a grid of every measure at once -plot.dashboard(s, window=20) +| Measure | Checked against | Agreement | +| --- | --- | --- | +| sample, approximate, permutation | antropy and EntropyHub | exact (relative error < 1e-10) | +| spectral | antropy | exact | +| multiscale | EntropyHub `MSEn` | exact | +| shannon, differential (normal) | scipy | exact | +| transfer | bivariate-Gaussian closed form, Kraskov et al. (2004) | within 0.05 bits | -# highlight where entropy drops sharply (trend / regime-change detection) -plot.drop_events(s, measure="shannon", window=20, threshold=0.4) -``` +Cross-checking also found two places where entroscope had drifted from the +published definitions (sample entropy and multiscale entropy); both are fixed. +Details are on the [validation page](https://par-python.github.io/entroscope/validation/), +and you can rerun the checks with `pip install -e ".[dev,reference]"` then +`pytest tests/test_reference.py`. -All plot functions return a `matplotlib.figure.Figure` and never call -`plt.show()`, so they're safe in scripts, notebooks, and CI alike. +## Watching entropy over time -## Integrations +```python +from entroscope import plot -**polars**: pass a polars Series anywhere a pandas Series works; rolling and -delta results come back as a polars Series with the same name. +fig = plot.compare(s, measures=["shannon", "permutation", "spectral"], window=40) +fig = plot.dashboard(s, window=40) # one panel per measure +fig = plot.drop_events(s, measure="spectral", window=40, threshold=0.5) # marks sharp drops +``` -**scikit-learn**: `EntropyFeatures` turns time-series windows into entropy -features inside a pipeline: +`rolling` is causal: the value at step t uses only steps t−window+1 through t, so +it is safe to use for live monitoring. Missing data never turns into a made-up +number: ```python -from sklearn.ensemble import RandomForestClassifier -from sklearn.pipeline import make_pipeline -from entroscope.features import EntropyFeatures +gappy = s.copy() +gappy[250] = np.nan -# windows: shape (n_windows, window_length); one row per window -model = make_pipeline(EntropyFeatures(), RandomForestClassifier()) -model.fit(windows, labels) +spectral.compute(gappy) # -> nan +spectral.rolling(gappy, window=40).isna().sum() # -> 79: the 39 warm-up steps + the 40 windows holding the gap ``` -Install the optional dependencies with `pip install "entroscope[sklearn]"` or -`"entroscope[polars]"`. See the [integrations guide](docs/integrations.md). - -## Validated results +## Two signals: direction and drift -Every measure is checked against an independent implementation, on every CI run: +```python +from entroscope import divergence, transfer -| Measure | Checked against | -| -------------------------------- | ------------------------------------------------- | -| sample, approximate, permutation | antropy and EntropyHub: exact match (< 1e-10) | -| spectral | antropy: exact match | -| multiscale | EntropyHub `MSEn`: exact match | -| shannon, differential (normal) | scipy: exact match | -| transfer | analytic Gaussian closed form and Kraskov (2004) | +rng = np.random.default_rng(1) +driver = rng.normal(size=1000) +follower = 0.8 * np.roll(driver, 1) + 0.6 * rng.normal(size=1000) # follows driver, one step behind -Details, including two definitions corrected along the way, are in -[docs/validation.md](docs/validation.md). Run the checks yourself with -`pip install -e ".[dev,reference]" && pytest tests/test_reference.py`. +transfer.compute(driver, follower) # -> 0.73 bits: driver's past predicts follower +transfer.compute(follower, driver) # -> 0.02 bits: not the other way round -## Real-world examples +train = rng.normal(0, 1, 5000) +live = rng.normal(0.5, 1.2, 5000) +divergence.js(train, live) # -> 0.04 bits (0 = identical, 1 = no overlap) +divergence.kl(live, train) # -> 0.23 bits (directional) +``` -Runnable scripts live in [`examples/`](examples/); worked write-ups are in -[`docs/examples/`](docs/examples/): +## Machine learning and polars -- **[Food trends](docs/examples/food_trends.md)**: detect when search interest - stops being random (the original NextOnMenu use case). -- **[Finance](docs/examples/finance.md)**: market uncertainty via permutation - and spectral entropy. -- **[Medical](docs/examples/medical.md)**: HRV, EEG seizure onset, respiration, - and continuous glucose. -- **[Business](docs/examples/business.md)**: sales demand, web-traffic anomalies, - price volatility, and manufacturing QC. +`EntropyFeatures` turns time-series windows into entropy features inside a +scikit-learn pipeline. Each row of `X` is one window: ```python -# food-trend analysis: entropy drops before a trend goes mainstream -import pandas as pd -from entroscope import shannon +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import cross_val_score +from sklearn.pipeline import make_pipeline +from entroscope.features import EntropyFeatures + +rng = np.random.default_rng(2) +t = np.arange(128) +cycles = [np.sin(2 * np.pi * t / rng.uniform(8, 32)) + rng.normal(size=128) for _ in range(100)] +noise = [rng.normal(size=128) for _ in range(100)] +X, y = np.vstack(cycles + noise), np.repeat([0, 1], 100) -matcha = pd.read_csv("matcha_trends.csv")["interest"] -shannon.plot(matcha, window=20, title="Matcha entropy over time") +model = make_pipeline(EntropyFeatures(measures=("spectral", "permutation")), LogisticRegression()) +cross_val_score(model, X, y, cv=5).mean() # -> 0.945 from two features per window ``` -A sustained drop in rolling entropy means a signal is becoming structured rather -than noisy, an early indicator of a forming pattern. +Pass a polars Series anywhere a pandas Series works: -## Requirements +```python +import polars as pl -Python 3.9+, with numpy, pandas, scipy, and matplotlib (installed automatically). +spectral.rolling(pl.Series("sensor", s.to_numpy()), window=40) # -> polars Series named "sensor" +``` -## Contributing +## The nine measures -Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the -test/lint commands, and how to add a new entropy measure. +| Measure | Captures | Reach for it when | +| --- | --- | --- | +| `shannon` | Spread of values in a histogram | Order doesn't matter, e.g. how evenly interest spreads across regions (`shannon.geographic`) | +| `permutation` | Complexity of ordinal patterns | Noisy data where the order of ups and downs matters; robust to scale and outliers | +| `spectral` | Spread of power across frequencies | Detecting rhythms and cycles appearing or disappearing | +| `sample` | Regularity: do repeating patterns keep repeating? | Short-to-medium series where predictability is the question | +| `approximate` | Regularity, like sample, with a gentler bias | You want sample-style regularity on shorter series | +| `differential` | Entropy of a continuous distribution (normal fit or KDE) | Continuous data without natural bins | +| `multiscale` | Sample entropy across coarse-grained time scales | Structure that only shows up at some resolutions | +| `transfer` | Directional information flow X → Y | Lead–lag questions: which series drives which | +| `divergence` | KL and Jensen-Shannon distance between two samples | Data drift, e.g. training vs production distributions | + +## One API + +| Method | Input | Returns | +| --- | --- | --- | +| `compute(x, **params)` | pandas, polars or numpy | `float` | +| `rolling(x, window, **params)` | pandas, polars or numpy | same type and length as the input, NaN during warm-up | +| `delta(x, window, **params)` | pandas, polars or numpy | first difference of `rolling` | +| `normalized(x, **params)` | pandas, polars or numpy | `float` in [0, 1] (shannon, permutation, spectral) | +| `plot(x, window, **params)` | pandas, polars or numpy | `matplotlib.figure.Figure` | + +`transfer` takes two series (`x`, `y`) and has the same four methods except +`normalized`. `divergence` takes two samples and provides `kl`, `js` and `plot`. +`multiscale` provides `compute` and `plot`. + +## Honest limits + +- **Entropy is not always the best detector.** In a + [60-run study](https://github.com/Par-python/training-early-warning) of neural + networks diverging during training, a one-line gradient-norm spike rule warned + on every run with no false alarms and beat both entropy detectors. Compare + against a simple baseline before trusting entropy for a new job. +- **`rolling` recomputes every window in Python.** On 3,000 points, shannon, + permutation and spectral take about 0.1–0.4 s; sample entropy with a 100-step + window takes about 3 s, and sample and approximate grow with the square of the + window. Very long series or wide windows need patience. +- **Sample entropy can be undefined.** With no matching templates (short series or + a tiny `r`), `sample.compute` returns a finite ceiling, + `ln((n−m)(n−m−1))`, instead of infinity. Check `r` if you see identical high values. +- **Spectral entropy follows antropy's periodogram definition.** EntropyHub's + `SpecEn` uses a different estimator, so its numbers differ by design. +- **Transfer entropy needs data.** Below about 30 samples it warns, and k-NN + estimates on short windows are noisy. + +## Resources + +- **Docs:** https://par-python.github.io/entroscope/ ([quickstart](https://par-python.github.io/entroscope/quickstart/), [validation](https://par-python.github.io/entroscope/validation/), [integrations](https://par-python.github.io/entroscope/integrations/)) +- **Worked examples:** [food trends](docs/examples/food_trends.md), [finance](docs/examples/finance.md), + [business](docs/examples/business.md), [medical](docs/examples/medical.md); runnable scripts in [`examples/`](examples/) +- **Case study:** [Can entropy warn that training is about to diverge?](https://github.com/Par-python/training-early-warning) +- **Changelog:** [CHANGELOG.md](CHANGELOG.md) · **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md) +- **Headless use (Docker, CI):** entroscope never changes your matplotlib backend; + set `MPLBACKEND=Agg` in the environment if you need a non-interactive one. + +entroscope started in [NextOnMenu](https://github.com/Par-python/nextonmenu), where a +falling Shannon entropy of a food's regional search interest turned out to be an +early sign it was about to trend. ## License diff --git a/docs/assets/banner-dark.png b/docs/assets/banner-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..07067e22521522711eefda7ddc3f45edb42a1f79 GIT binary patch literal 42724 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

iu6Vpbps>u}Si^!@tz<4@o_eU^;0@zm5xgMTwr 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 RKTYK6Objgy%Jk0N{4X7K2G{@q literal 0 HcmV?d00001 diff --git a/docs/assets/banner-light.png b/docs/assets/banner-light.png new file mode 100644 index 0000000000000000000000000000000000000000..64b79f5826e6be75e8cc4389eee6242b7fe81105 GIT binary patch literal 43645 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

|8RpMgYqY+Us~Q1 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∾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 A^8f$< literal 0 HcmV?d00001 diff --git a/docs/assets/make_banner.py b/docs/assets/make_banner.py new file mode 100644 index 0000000..de413b6 --- /dev/null +++ b/docs/assets/make_banner.py @@ -0,0 +1,104 @@ +"""Regenerate the README banner (banner-light.png and banner-dark.png next to this file). + + python docs/assets/make_banner.py + +Adapted from the original banner design in assets/make_banner.py. The curve is +entroscope's own rolling spectral entropy of a signal that starts as noise and +settles into a clean oscillation, so the banner shows the library doing its job. +""" + +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") + +import matplotlib.font_manager as fm +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt + +from entroscope import spectral + +OUT = Path(__file__).resolve().parent + +THEMES = { + "light": { + "bg": "#ffffff", "ink": "#1b1f24", "sub": "#3a4149", "mute": "#6b7280", + "line": "#2a78d6", "grid": "#e7e9ec", "rule": "#d7dbe0", + }, + "dark": { + "bg": "#0d1117", "ink": "#e6edf3", "sub": "#c9d1d9", "mute": "#8b949e", + "line": "#4493f8", "grid": "#21262d", "rule": "#30363d", + }, +} # fmt: skip + +MEASURES = ( + "shannon · permutation · sample · approximate · spectral · " + "differential · multiscale · transfer · divergence" +) + + +def _pick(names, default="DejaVu Sans"): + available = {f.name for f in fm.fontManager.ttflist} + return next((name for name in names if name in available), default) + + +SANS = _pick(["Helvetica Neue", "Helvetica", "Arial"]) +MONO = _pick(["SF Mono", "Menlo", "DejaVu Sans Mono"], default="DejaVu Sans Mono") + + +def make_signal(n=600, seed=7): + """Pure noise that ramps into a clean sine over the middle of the series.""" + rng = np.random.default_rng(seed) + t = np.linspace(0, 24 * np.pi, n) + emergence = np.clip((np.arange(n) - n * 0.30) / (n * 0.40), 0, 1) + return pd.Series((1 - emergence) * 1.6 * rng.standard_normal(n) + emergence * 2.2 * np.sin(t)) + + +def draw(entropy, c): + fig = plt.figure(figsize=(12.8, 4.6), dpi=100) + fig.patch.set_facecolor(c["bg"]) + + fig.text(0.05, 0.84, "entroscope", color=c["ink"], fontsize=44, fontweight="bold", + fontfamily=SANS, va="center") # fmt: skip + fig.text(0.05, 0.69, "the definitive entropy toolkit for time series data", + color=c["sub"], fontsize=15, fontfamily=SANS, va="center") # fmt: skip + fig.text(0.05, 0.605, MEASURES, color=c["mute"], fontsize=10.5, fontfamily=SANS, + va="center") # fmt: skip + fig.text(0.95, 0.84, "pip install entroscope", color=c["ink"], fontsize=13, + fontfamily=MONO, ha="right", va="center") # fmt: skip + fig.add_artist(plt.Line2D([0.05, 0.95], [0.53, 0.53], transform=fig.transFigure, + color=c["rule"], lw=1.0)) # fmt: skip + + ax = fig.add_axes([0.05, 0.08, 0.90, 0.38]) + ax.set_facecolor(c["bg"]) + ax.grid(axis="y", color=c["grid"], lw=1.0) + ax.set_axisbelow(True) + ax.plot(entropy.index, entropy.to_numpy(), color=c["line"], lw=1.8, + solid_capstyle="round") # fmt: skip + for side, spine in ax.spines.items(): + spine.set_visible(side == "bottom") + ax.spines["bottom"].set_color(c["rule"]) + ax.set_xticks([]) + ax.set_yticks([]) + ax.margins(x=0.0) + ax.set_ylim(bottom=-0.06 * float(entropy.max())) # keep the flat tail off the baseline + ax.text(0.99, 0.95, "rolling spectral entropy falls as noise turns into a rhythm", + transform=ax.transAxes, color=c["mute"], fontsize=9.5, fontfamily=SANS, + ha="right", va="top") # fmt: skip + return fig + + +def main(): + entropy = spectral.rolling(make_signal(), window=100) # two full cycles + for name, colors in THEMES.items(): + fig = draw(entropy, colors) + path = OUT / f"banner-{name}.png" + fig.savefig(path, dpi=100, facecolor=colors["bg"]) + plt.close(fig) + print("wrote", path.relative_to(OUT.parent.parent)) + + +if __name__ == "__main__": + main() diff --git a/docs/quickstart.md b/docs/quickstart.md index 02bf19c..270c74c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -8,17 +8,25 @@ pip install entroscope ## Compute entropy +A series that is pure noise for 200 steps, then turns into a clean 20-step cycle: + ```python +import numpy as np import pandas as pd -from entroscope import shannon, permutation, spectral - -s = pd.Series([10, 20, 15, 80, 90, 85, 88, 92]) - -shannon.compute(s) # single value -shannon.rolling(s, window=20) # rolling Series (index preserved) -shannon.delta(s, window=20) # rate of change -shannon.normalized(s) # 0-1 scaled -fig = shannon.plot(s, window=20) # matplotlib Figure +from entroscope import shannon, spectral + +rng = np.random.default_rng(0) +t = np.arange(400) +noise = rng.normal(size=400) +s = pd.Series(np.where(t < 200, noise, np.sin(2 * np.pi * t / 20) + 0.2 * noise)) + +spectral.compute(s[:200]) # -> 6.05 bits: noise spreads power over every frequency +spectral.compute(s[200:]) # -> 0.88 bits: the cycle concentrates it +spectral.normalized(s[200:]) # -> 0.13 on a 0-1 scale +roll = spectral.rolling(s, window=40) # -> Series, same index, NaN for the first 39 steps +spectral.delta(s, window=40) # -> step-to-step change in the rolling entropy +fig = spectral.plot(s, window=40) # -> matplotlib Figure +shannon.compute(s) # every measure has the same call shape ``` ## Compare measures @@ -26,16 +34,20 @@ fig = shannon.plot(s, window=20) # matplotlib Figure ```python from entroscope import plot -fig = plot.compare(s, measures=["shannon", "permutation", "spectral"], window=20) -fig = plot.dashboard(s, window=20) +fig = plot.compare(s, measures=["shannon", "permutation", "spectral"], window=40) +fig = plot.dashboard(s, window=40) +fig = plot.drop_events(s, measure="spectral", window=40, threshold=0.5) ``` Every measure follows the same contract: -| Method | Returns | -| ------------- | ------------------------------------ | -| `compute` | `float` | -| `rolling` | Series/ndarray, same length | -| `delta` | Series/ndarray (first difference) | -| `normalized` | `float` in [0, 1] (where defined) | -| `plot` | `matplotlib.figure.Figure` | +| Method | Returns | +| ------------- | ----------------------------------------------------------- | +| `compute` | `float` | +| `rolling` | same type and length as the input, NaN during warm-up | +| `delta` | first difference of `rolling` | +| `normalized` | `float` in [0, 1] (shannon, permutation, spectral) | +| `plot` | `matplotlib.figure.Figure` | + +Inputs can be a pandas Series (index kept), a polars Series (name kept) or a numpy +array. Any NaN or ±inf in a window makes that window's result NaN. diff --git a/tests/test_readme.py b/tests/test_readme.py new file mode 100644 index 0000000..a5a2473 --- /dev/null +++ b/tests/test_readme.py @@ -0,0 +1,43 @@ +"""Every Python example in the README and the docs quickstart must run as written. + +Blocks in one file run in order in a shared namespace, the way a reader works +through the page. `# -> value` comments that start with a number are checked +against what the line actually returns. +""" + +import re +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +import pytest + +ROOT = Path(__file__).resolve().parent.parent +PAGES = ["README.md", "docs/quickstart.md"] +BLOCK = re.compile(r"```python\n(.*?)```", re.S) +# `expr # -> 6.05 ...` or `expr # -> (3.64, 0.70) ...`: the numbers to check. +CLAIM = re.compile(r"^(?P[^#]+?)\s+# -> (?P\(?-?\d[\d.,\s-]*\)?)") +ASSIGNMENT = re.compile(r"^\s*[A-Za-z_][\w.\[\]]*\s*=(?!=)") + + +def _numbers(text): + return [float(v) for v in re.findall(r"-?\d+(?:\.\d+)?", text)] + + +@pytest.mark.parametrize("page", PAGES) +def test_examples_run_and_claims_hold(page, monkeypatch): + monkeypatch.setenv("MPLBACKEND", "Agg") + blocks = BLOCK.findall((ROOT / page).read_text()) + assert blocks, f"no python blocks in {page}" + namespace = {} + for i, block in enumerate(blocks, 1): + exec(compile(block, f"{page}:block{i}", "exec"), namespace) + plt.close("all") + for line in block.splitlines(): + claim = CLAIM.match(line) + if not claim or ASSIGNMENT.match(line): + continue + expected = _numbers(claim["value"]) + actual = np.atleast_1d(np.asarray(eval(claim["expr"], namespace), dtype=float)) + # Values are shown rounded to 2-3 significant decimals. + assert actual == pytest.approx(expected, abs=0.006), f"{page}: {line.strip()}" From 1355539523f2b00f89ada8c3ca3de75a8240ffb5 Mon Sep 17 00:00:00 2001 From: jjscripts Date: Tue, 22 Sep 2026 19:24:36 +0800 Subject: [PATCH 2/2] test: satisfy ruff 0.16 rules in test_readme Use re.DOTALL instead of the re.S alias (FURB167), and mark the deliberate exec of the docs' example code with a scoped noqa (S102). Co-Authored-By: Claude Opus 5 --- tests/test_readme.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_readme.py b/tests/test_readme.py index a5a2473..7653f36 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -14,7 +14,7 @@ ROOT = Path(__file__).resolve().parent.parent PAGES = ["README.md", "docs/quickstart.md"] -BLOCK = re.compile(r"```python\n(.*?)```", re.S) +BLOCK = re.compile(r"```python\n(.*?)```", re.DOTALL) # `expr # -> 6.05 ...` or `expr # -> (3.64, 0.70) ...`: the numbers to check. CLAIM = re.compile(r"^(?P[^#]+?)\s+# -> (?P\(?-?\d[\d.,\s-]*\)?)") ASSIGNMENT = re.compile(r"^\s*[A-Za-z_][\w.\[\]]*\s*=(?!=)") @@ -31,7 +31,8 @@ def test_examples_run_and_claims_hold(page, monkeypatch): assert blocks, f"no python blocks in {page}" namespace = {} for i, block in enumerate(blocks, 1): - exec(compile(block, f"{page}:block{i}", "exec"), namespace) + # Running the docs' own example code is the point of this test. + exec(compile(block, f"{page}:block{i}", "exec"), namespace) # noqa: S102 plt.close("all") for line in block.splitlines(): claim = CLAIM.match(line)