/ concept-collection / timeseries-entropy
Sign in
concept-collection / timeseries-entropy
timeseries-entropy / README.md
6.4 KBPreviewCodeBlameHistoryRaw

timeseries-entropy#

Unbiased Monte-Carlo estimation of the entropy of a quantized filtered Gaussian time series:

x iid N(0, sigma^2)  ->  y = h * x  ->  z = round(y)

The estimand is the conditional entropy H(z_{M+1} | z_1..z_M) in bits, which decreases to the entropy rate of z — the true lossless compression limit in bits/sample — as the past window M grows beyond the memory of the process.

Companion to timeseries-compressibility, which carries a hand-synced TypeScript port of this estimator in src/entropy/ (run in the browser from a worker) — when changing the algorithm here, change it there too.

Method#

  1. Stationary conditional sampling. Draw x from the prior and push it through the pipeline to get a past z_1..z_M. The generating x is an exact draw from p(x | z), so a Gibbs chain started there is already in stationarity — no burn-in bias. Each Gibbs conditional is a box-truncated normal; after each sweep the free tail latent is drawn fresh, emitting one exact sample of z_{M+1}. The samples form a stationary, autocorrelated discrete chain. By default each chain is auto-thinned: a short probe measures the draws' integrated autocorrelation time tau and the chain then takes ceil(tau) sweeps per emitted sample (capped), so downstream levels see approximately independent draws.
  2. Unbiased entropy of the chain's marginal. Plug-in entropies of blocks whose sizes double per level are combined by Rhee–Glynn randomized telescoping with antithetic half-block corrections Delta_m = h(B_m) - [h(B_m^1) + h(B_m^2)]/2, truncated at a random level N with P(N >= m) = 2^(-r m) and reweighted. The expectation is exactly H(z_{M+1} | that past) despite the plug-in bias at every finite block size and despite the autocorrelation, which affects only the variance.
  3. Average over independent pasts to get H(z_{M+1} | z_1..z_M) with a valid standard error. The only remaining approximation to the entropy rate is the finite window M. Pasts are independent, so they run in parallel across processes (workers=, default all cores); each past has its own spawned RNG stream, making results deterministic per seed for any worker count.

Install#

pip install -e .

Requires numpy and scipy.

Usage#

from timeseries_entropy import estimate_conditional_entropy, kernels

est = estimate_conditional_entropy(kernels.moving_average(8), sigma=4.0)
print(est.mean, est.se)   # bits/sample, over independent pasts

Lower level: ConditionalChain(kernel, sigma, past, rng).draw(k) yields the stationary chain of z_{M+1} samples, and unbiased_entropy(draw, n0, r, rng) is one randomized-telescoping realization for any stationary discrete chain.

CLI#

timeseries-entropy --sigma 4 --filter moving-average --width 8
timeseries-entropy --sigma 8 --filter lowpass --high 3000 --rate 30000
timeseries-entropy --sigma 2 --filter none --pasts 8

Filters match the web app: none, moving-average, lowpass, bandpass, first-difference.

Tuning#

Cached estimates#

The estimates branch caches a grid of estimates so the common settings need not be recomputed: four filters (none, moving-average 8, lowpass 3000 Hz, bandpass 300-6000 Hz at 30 kHz) crossed with sigma in {1, 2, 4, 8, 16, 32}.

https://raw.githubusercontent.com/concept-collection/timeseries-entropy/estimates/estimates.json

The estimates workflow fills it every two hours, and can also be dispatched by hand. Each run draws 8 fresh, independent pasts per cell, appends one record per cell to runs.jsonl (including the per-past values and resolved thinning, so any outlier can be traced to its seed and replayed), and rebuilds estimates.json by pooling every past ever drawn — so the means keep tightening the more often it runs. The cache was reset on 2026-08-01: draws made before auto-thinning landed had heavy-tailed outliers on the narrowband large-sigma cells (see Tuning) and were discarded. The grid lives in scripts/grid.py; adding a cell does not invalidate the cache, since each record stores its own parameters. To fill the cache locally instead:

python scripts/run_sweep.py --data-dir data

Analytic prediction#

timeseries_entropy.theory predicts the rate from the Fourier modes H(f) = sum_j h_j e^(-2 pi i f j) of the kernel; the CLI prints it before each run. Szego's theorem gives the Gaussian one-step prediction error sigma_inf^2 = sigma^2 exp(int_0^1 ln|H(f)|^2 df) and the high-resolution rate (1/2) log2(2 pi e sigma_inf^2), which fails when H(f) has near-zero modes (the integral diverges negative while the true rate stays >= 0). Two quantization corrections fix it: the observed past is quantized, so the uniform roundoff power 1/12 is added to the spectrum before the geometric mean — which also keeps the integral finite in stopbands — and the next sample is quantized, so the Gaussian-uniform convolution entropy G(s) = h(N(0, s^2) + U(-1/2, 1/2)) replaces the Gaussian log term:

H_rate ~ G(s*),   s*^2 = exp( int_0^1 ln(sigma^2 |H(f)|^2 + 1/12) df ) - 1/12

See THEORY.md for the full derivation, its assumptions, and Monte-Carlo validation across filters and sigmas.

moveopenescclose