statistics — inference layer for fragility/sensitivity studies

Purpose

The inference owner for the analysis pipeline: Pearson/Spearman/tail correlation, OLS
regression, Cliff’s delta effect size, ensemble dispersion (CI/spread/exceedance), cross-correlation
peak/lag, the near-singular-fraction band, and seeded event-study controls.
Split out of data_analyzer (Jun 18) so that module stays math-primitives-only.

Role in the system

  • Companion to data_analyzer (math primitives) and metric_catalog (catalog access) — the three
    files the Jun-18 split produced.
  • data_analyzer re-exports every public name here (# noqa: E402,F401) so the
    signal_measurement facade and test_data_primitives.py keep importing through data_analyzer.
  • signal_measurement imports it as _stats and delegates all correlation/effect-size math to it.
  • Consumes a few reduction helpers (finite_numeric_samples, percentile, Reductions,
    longest_run_length) lazily from data_analyzer — see Footguns.
  • Pure consumer of NPZ-derived arrays; never reaches across pipeline boundaries.

Inputs / Outputs

  • In: plain 1-D NumPy-coercible signals (paired samples, ensembles of per-run scalars, boolean
    episode masks, event-index sets); all functions are NaN-aware and trim to the common finite support.
  • Out: floats (correlations, effect sizes, fractions), Package records (linfit,
    episode_contrast), (values, survival) arrays (exceedance_curve), and seeded index/window sets
    (event-study controls).

Key functions

  • CorrMethod — Pearson/Spearman selector; coerce permissive (default PEARSON), from_value strict — analysis/statistics.py:23
  • pearson_r / spearman_rho — NaN-aware correlation (Spearman = Pearson on average ranks) — :60 / :86
  • tail_corr — correlation conditioned on the top-q% of a (worst-decile association) — :94
  • xcorr_peak_lag — normalized cross-correlation peak and the lag at which it occurs — :107
  • linfit — OLS deg-1 fit → Package(slope, intercept, max_rel_resid):124
  • cliffs_delta — nonparametric effect size in [-1, 1] between two samples — :136
  • episode_contrast — the near-singular/clip/dwell fingerprint (Cliff’s delta inside vs outside a mask) — :149
  • ci95 / rel_spread / exceedance_curve — ensemble dispersion ACROSS runs/seeds — :171 / :181 / :191
  • near_singular_fraction — swept-onset fraction below threshold, delegates to Reductions.FRAC_BELOW:203
  • matched_control_pool / matched_control_windows — seeded controls off the forbidden event spans — :219 / :227

Footguns

The lazy imports are a deliberate cycle-break, not style

tail_corr, cliffs_delta, episode_contrast, ci95, rel_spread, exceedance_curve, and
near_singular_fraction import finite_numeric_samples / percentile / Reductions /
longest_run_length function-locally. Those live in data_analyzer, which RE-EXPORTS this
module’s names — a module-level import would be a cycle. This file has no module-level dependency
on data_analyzer, so it always imports cleanly regardless of load order. (analysis/INSIGHTS.md)

xcorr_peak_lag lag sign is reversed from np.correlate

Positive lag means a precedes b (b is the delayed copy). The lag axis is flipped relative to
np.correlate’s center-relative convention. (analysis/INSIGHTS.md)

near_singular_fraction must delegate, not re-implement

It routes through Reductions.FRAC_BELOW (the canonical np.mean(x < t)) so a swept-onset
measurement and the catalog’s pinned-0.025 frac_below band (on s_min_J in metrics.yaml)
can never drift apart. This function takes an arbitrary onset on any singular-value signal.
(analysis/INSIGHTS.md)

CorrMethod.coerce defaults to PEARSON silently

Any value that is not "spearman" or a CorrMethod member maps to PEARSON (the historical default).
Use from_value when an unknown selector should raise. Enum values are lowercase names
(SPEARMAN.value == "spearman"). (analysis/INSIGHTS.md)

Pseudocode (the episode fingerprint)

sig, m = align(signal, mask)                 # common length, mask → bool
near    = finite(sig[m])                      # samples during the episode
healthy = finite(sig[~m])                     # samples outside it
return Package(
    delta        = cliffs_delta(near, healthy),   # > 0 ⇒ LARGER during the episode
    near_mean, healthy_mean,                       # both finite means
    near_frac    = mean(m),                         # episode duty cycle
    longest_run  = longest_run_length(m),          # longest contiguous episode
    n_near, n_healthy)                             # sample counts

data_analyzer · metric_catalog · signal_measurement · orchestrator · terminology

  • Long-form derivations: generated_reports/math/analysis_correlations.md.
  • Golden tests pin every function against numpy/scipy: validation/test_data_primitives.py.