Pearson correlation (r)
r = Σ(x-x̄)(y-ȳ) / √(Σ(x-x̄)² · Σ(y-ȳ)²), clamped to [-1, 1]
- Window
- Whatever paired series the caller passes in — the function has no lookback of its own. The platform's primary caller, the weekly correlation compute, passes a 90-day rolling window (LOOKBACK_DAYS).
- Limitations
- Requires n ≥ min_n (default 3, callers commonly raise this to 5); returns None below that threshold or when either series has zero variance. Measures linear association only — a real non-linear relationship can score near zero. Not corrected for day-to-day autocorrelation on its own (see effective_sample_size) — a raw r without the effective-n-based p-value/CI overstates confidence on daily physiological series.
- Minimum n
- 3
- Used by
- The correlation engine (/method/intelligence/), tools_training zone-2 and exercise-efficiency correlations, get_cross_source_correlation.
source: stats_core.py::pearson_r
Lag-1 autocorrelation
r1 = Σ(xₜ-x̄)(xₜ₊₁-x̄) / Σ(xₜ-x̄)², clamped to [-1, 1]
- Window
- Caller-supplied series, in the order given — treats the series as one continuous sequence with no gap-awareness (a missing day is not distinguished from a lag of 1).
- Limitations
- Returns 0.0 (no detectable memory) below n=3 or zero variance, which makes the downstream effective-n correction a no-op rather than an error. A helper, not a reported statistic on its own — it feeds effective_sample_size.
source: stats_core.py::lag1_autocorr
Effective sample size (n_eff)
Single series: n_eff = n·(1-r1)/(1+r1). Paired (Pyper & Peterman 1998): n_eff = n·(1-r1x·r1y)/(1+r1x·r1y)
- Window
- Same series/window as the r or mean it's correcting.
- Limitations
- Clamped to [2, n] — a negative-autocorrelation 'bonus' (n_eff > n) is discarded so the correction only ever moves toward conservatism, never inflates apparent evidence. First-order (Bartlett/AR(1)) only — does not model higher-order or seasonal autocorrelation. The core answer to the ADR-105 rule that daily physiological series (recovery, HRV, weight) are not i.i.d. and raw n overstates the evidence.
- Used by
- Every correlation surface: correlation_report (mcp/helpers.py), tools_training, the weekly correlation compute.
source: stats_core.py::effective_sample_size
Pearson p-value
Two-tailed via the t-distribution: t = r·√df / √(1-r²), df = n-2; erf-based normal approximation with a small-df shrink z = t·√(df/(df+2)) below df=30
- Window
- Takes fractional n, so it composes directly with effective_sample_size's autocorrelation-corrected n_eff rather than the raw observation count.
- Limitations
- None when |r| ≥ 1 or n ≤ 2. Accurate to ~3 decimals for df > 10, conservative (slightly wider) below — a stated intentional trade-off, not an unhandled edge case. Always compute on n_eff for daily series, never raw n (ADR-105).
- Used by
- correlation_report (the ONE correlation-reporting helper, replacing 6 duplicate copies), tools_training.
source: stats_core.py::pearson_p_value
Correlation confidence interval (Fisher z)
z_r = atanh(r); CI = tanh(z_r ± z_crit·SE), SE = 1/√(n-3)
- Window
- Same n as the r it's bounding — pass effective_sample_size's n_eff for autocorrelated daily series.
- Limitations
- None when |r| ≥ 1 or n ≤ 3. Supports exactly four confidence levels (0.80, 0.90, 0.95, 0.99) — anything else raises ValueError rather than silently approximating.
- Used by
- correlation_report, tools_training correlation reads.
source: stats_core.py::fisher_ci
Correlation evidence score (Evidence Bar)
score = 0.5·s_n + 0.3·s_ci + 0.2·s_fdr, s_n = min(1, log1p(n_eff-2)/log1p(88)) (0 at n_eff≤2, ~0.67 at n_eff=21, 1.0 at n_eff≥90), s_ci = max(0, 1 - width of the 95% Fisher CI on r), s_fdr = 1 if fdr_significant is True else 0
- Window
- The same (r, n, n_eff, fdr_significant) a caller already computed for one correlation claim — no lookback of its own.
- Limitations
- Tolerant of None/garbage inputs (returns the honest low/zero shape rather than raising) since it sits on a public serving path. `level` (high/medium/low) mirrors charts.js's client-side n thresholds (≥21/≥8) so the served level and every existing confidence treatment read the same grammar. `ci95`/`ci_width` are None (never a fabricated band) when no CI is computable; `fdr_significant` is None (never coerced to False) when the batch was never checked. #1372's ONE deterministic source for the public Evidence Bar — no authored strength grades anywhere, every component computed.
- Used by
- The Evidence Bar on /api/vitals correlations (site_api_discovery.py) and the correlation ledger surface (site_api_ledger.py) — every published correlation claim's per-claim evidence readout.
source: stats_core.py::correlation_evidence
Moving-block bootstrap CI
Percentile CI over 1000 resamples of contiguous blocks (default block length n^(1/3), floored at 2) — preserves short-range autocorrelation that i.i.d. resampling would destroy
- Window
- Caller-supplied series (paired or single); a fixed seed (1337) makes the interval reproducible for identical input — same data always yields the same interval.
- Limitations
- Needs n ≥ 5, and at least 100 of the 1000 replicates must produce a valid statistic or the call returns None (e.g. degenerate resamples with zero variance). Default statistic is Pearson r (paired) or the mean (single series); a custom stat may be passed in. Only 4 confidence levels supported (see fisher_ci).
- Minimum n
- 5
- Used by
- weight_trend.py (the weight-rate confidence interval on /api/journey, #535).
source: stats_core.py::moving_block_bootstrap_ci
Bootstrap CI for a mean difference
Block-resamples baseline and window series independently (1000 replicates each), returns the percentile CI of mean(window) - mean(baseline)
- Window
- The two series the caller defines as 'baseline' and 'window' — the n-of-1 experiment primitive: is this metric different in the test window than before it started, and by how much?
- Limitations
- Both series need n ≥ 5 or the call returns None. Independent block-resampling means it does not account for any correlation between the baseline and window periods themselves (e.g. a slow seasonal drift spanning both).
- Minimum n
- 5
- Used by
- experiment_design.py (evaluate_design) — the n-of-1 pre-registered experiment analysis (#539).
source: stats_core.py::bootstrap_mean_diff_ci
Start-point randomization test (SCED)
For every candidate start k the pre-declared window could have produced: T(k) = mean(post_k) - mean(pre_k), washout excluded identically at each; one-sided p = #{T(k) at least as extreme as the observed split} / k_valid
- Window
- The full daily series of the pre-registered criterion metric, from baseline_days before the declared window's first candidate through the experiment's end — one continuous series, so every candidate start sees the same data.
- Limitations
- Only valid when the start was actually DRAWN at random from the frozen window (experiment_design.draw_start_date) — applied to a hand-picked start it is decorative, not inferential. Exact under the randomization performed, so no independence assumption is needed (the autocorrelation objection to parametric tests on N=1 daily series does not apply), but resolution is capped at p = 1/k: a 7-14 day window can never report below 1/7-1/14, which is a granularity floor, not high precision. Candidates with a thin arm (< 5 points after gaps) are excluded and reported in n_excluded; a pure pre-existing linear trend scores p ≈ 1 by construction — the coincident-trend confound the design exists to defeat.
- Minimum n
- 2
- Used by
- experiment_design.randomization_test — the end_experiment close-path analysis for randomized-start designs (#1413); reported on /data/ experiment cards next to the effect + CI, and in the analysis summary sentence.
source: stats_core.py::start_point_randomization_test
BSTS-lite synthetic-control counterfactual (the Ghost)
y_t = beta·[1, controls_t] + mu_t + eps_t with mu a local-level random walk; beta by OLS on the pre-period, level variances by maximum innovations likelihood over a fixed signal-to-noise grid; post-period ghost = frozen level + regression, variance P_T + t·sigma_eta² + sigma_eps² + OLS prediction covariance
- Window
- The pre-period declared in the FROZEN design spec (pre_days before the intervention start, default 28) for fitting; the washout-trimmed post-period for the effect. Only days where the criterion AND every declared control have values — gaps dropped and counted, never imputed.
- Limitations
- Spec (controls, pre-window, MAPE gate) is frozen at pre-registration — applied post-hoc it is spec shopping, which the design gate exists to forbid. The ghost is withheld with a stated reason when the pre-period one-step-ahead MAPE exceeds the frozen gate, when fewer than 14 aligned pre-days exist, or when MAPE is unevaluable (criterion near zero). First-order only: a local level plus static regression — no seasonality or dynamic coefficients, so a strong weekly cycle the controls don't carry inflates MAPE and (correctly) withholds the ghost. The effect CI uses the full forecast-error covariance (shared level error correlates post days); the interval widens with distance from start by construction — the model's honesty about extrapolation, drawn as-is on the card.
- Minimum n
- 14
- Used by
- experiment_design.counterfactual_analysis — the end_experiment close path for designs that froze a counterfactual spec (#1410); rendered on /data/ experiment cards as the ghost trace + widening band, and in the analysis summary sentence.
source: bsts_lite.py::fit_counterfactual
EWMA fit (simple exponential smoothing)
level update: levelₜ = levelₜ₋₁ + α·(xₜ - levelₜ₋₁); α chosen by deterministic grid search over 0.05–0.95 (step 0.05) minimizing one-step-ahead squared error when not given explicitly
- Window
- Caller-supplied series in chronological order.
- Limitations
- Needs ≥ 4 clean points or returns None. The grid search always picks the same α for the same data (ties go to the smaller α) — deterministic, no randomness, per ADR-105's 'deterministic computation before any LLM verdict' rule.
- Used by
- ewma_forecast (below); indirectly, the forecast engine.
source: stats_core.py::ewma_fit
EWMA h-step-ahead forecast
Point forecast = final smoothed level; interval width σ_h = σ·√(1 + (h-1)·α²) where σ is the sample SD of one-step-ahead residuals
- Window
- Caller-supplied series; the platform's forecast engine runs this daily at 0.80 confidence — chosen deliberately below the more common 0.95 so the 'did the interval cover the outcome ~80% of the time?' calibration question is answerable in weeks, not months.
- Limitations
- Needs ≥ min_n clean points (default 10) and ≥ 3 residuals or returns None. An EXPECTATION extrapolated from the series' own recent pattern — never a causal claim. Supports only the 4 fisher_ci confidence levels.
- Minimum n
- 10
- Used by
- forecast_engine_lambda.py — daily expectations graded into the calibration ledger.
source: stats_core.py::ewma_forecast
EWMA series (exponentially-weighted moving average)
ewaₜ = α·xₜ + (1-α)·ewaₜ₋₁, with α = 1 - exp(-1/decay_days); optional warm-start seed
- Window
- Caller-supplied chronological series; EWMA-ACWR runs it with a 7-day (acute) and 28-day (chronic) time-constant over the daily Whoop-strain series.
- Limitations
- Recent observations are weighted most, older ones decay smoothly — unlike a flat rolling mean, which weights its whole window equally and drops days off a cliff at the edge. ACWR = EWMA(acute)/EWMA(chronic) is a COUPLED ratio: the acute load is a mathematical component of the chronic load, so they move together by construction (Lolli et al. 2019) — a directional signal, not a precise injury predictor. The Gabbett zone thresholds it is compared against are population-derived (ADR-105 r4).
- Used by
- acwr_compute_lambda.py (EWMA-ACWR, #543); mcp/helpers.compute_ewa.
source: stats_core.py::ewma_series
Brier score
mean((p - y)²) over all (stated probability, realized outcome) pairs
- Window
- Every resolved prediction to date for the surface being scored (a coach, the hypothesis engine, or platform-wide) — grows as more predictions resolve, never a fixed lookback.
- Limitations
- 0.0 is perfect, 0.25 is the always-say-50% baseline, 1.0 is confidently-wrong-every-time. None when there are no valid (probability in [0,1], outcome in {0,1}) pairs — an unresolved or too-new coach shows no score rather than a misleading 0.
- Used by
- The calibration scoreboard (/method/calibration/), /api/calibration, /api/coach_team, the coach track-record MCP tool.
source: stats_core.py::brier_score
Brier skill score
1 - (Brier score / Brier score of the base-rate climatology forecast)
- Window
- Same pair set as the Brier score it's compared against.
- Limitations
- None when fewer than 2 pairs or every outcome is identical (skill is undefined against a degenerate base rate). 1.0 is perfect, 0.0 means no better than always guessing the observed base rate, negative means worse than that baseline — the honest 'does stated confidence beat just guessing the average?' number.
- Used by
- The calibration scoreboard.
source: stats_core.py::brier_skill_score
Reliability curve (calibration bins)
Splits [0,1] into n_bins equal bands (default 10); each non-empty bin reports mean stated confidence vs. observed outcome rate
- Window
- Same pair set as the Brier score for that surface.
- Limitations
- Empty list when there are no valid pairs. Bins with very few points can show a noisy observed rate — the bin's n is always reported alongside so a 1-of-1 bin isn't read as strong evidence.
- Used by
- The calibration scoreboard's reliability curve.
source: stats_core.py::reliability_bins
Calibration summary (n, Brier, reliability, verdict)
Composes brier_score + brier_skill_score + reliability_bins over the same (confidence, outcome) pairs into one summary dict
- Window
- Every resolved prediction for the coach/surface being scored, to date.
- Limitations
- Needs at least 1 resolved pair to report accuracy_pct; the calibration verdict (over/under-confident) needs ≥ 5. Below those thresholds the relevant field is None rather than a guess dressed as a number. Also reports `skilled` (Brier skill > 0?) — calibrated (reliability: stated confidence tracks observed rates) and skilled (beats the base rate) are different claims, and the summary carries both so no surface can conflate them (#1370). Reports `accuracy_ci95` — the 95% Wilson interval on the same confirmed/n, same percentage-point units as accuracy_pct — whenever n > 0 (#3450, ADR-105): a bare accuracy_pct off a small n reads as more precise than it is (21.6% vs. 'somewhere between 11% and 37%' are different decisions at n=37).
- Used by
- /api/calibration, /api/coach_team's per-coach calibration line.
source: calibration_core.py::score_pairs
Pooled calibration card scored against a stratified base rate
Pooled Brier over every stratum's pairs; reference Brier = Σ n_i · bs_ref_i / Σ n_i where bs_ref_i is stratum i's OWN base-rate Brier; skill = 1 − brier / reference. Over/under-confidence is tripped by the worst stratum's n-weighted reliability gap (among strata with n ≥ 5), not the pooled gap
- Window
- Every resolved pair on the platform, partitioned by stratum (coach calls · hypothesis bets · interval forecasts), to date.
- Limitations
- Pooling strata with different base rates and scoring the pool against ONE pooled base rate can manufacture a positive skill no stratum has (the pooled reference is worse than either stratum's own, so knowing the stratum beats it) — the 2026-09-05 lifetime card read skilled / well-calibrated while both strata were unskilled (#3550). Against the stratified reference, pooled skill > 0 is impossible unless a stratum beats its own climatology; `skilled` is additionally forced False when no stratum is skilled, and each stratum's own n / Brier / skill / gap is served on the card. Same n ≥ 5 verdict floor and the same fixed ±0.15 gap trip as calibration_verdict (a documented ADR-105 exception). A stratum with n < 2 or every outcome identical contributes no reference — unknown, never counted as unskilled.
- Minimum n
- 5
- Used by
- /api/calibration's platform + platform.lifetime cards; the State of Matthew calibration section.
source: calibration_core.py::score_strata
Directional trend verdict (up / down / flat)
slope = (EWMA_now - EWMA_prior_lag7) / |EWMA_prior_lag7| over the metric's trailing 30d series (decay 0.87); slope > +0.02 → up, < -0.02 → down, else flat
- Window
- Trailing 30 days of the predicted metric, minimum 9 observations (else no verdict, not a flat one).
- Limitations
- The ±2% band is a FIXED editorial choice, not derived from each metric's own trailing variance — a documented exception to the ADR-105 'thresholds from personal variance' rule, dated 2026-09-02 (#3448). Its reach is the dominant live scoring path: every machine spec re-routed to directional evaluation by the #813 rescue (1,207/1,207 live specs at stamping) is judged against this one band, so a high-variance metric can clear it on noise and a low-variance metric's real signal can be absorbed as flat. Re-derive trigger: once a per-metric trailing-variance baseline exists over a clean cycle window (the September 2026 n≥30 read is the first candidate), the band is derived from it as its own measured change.
- Minimum n
- 9
- Used by
- Coach prediction grading — since #4617 only a direction call with no filing date (every dated call is graded by directional_window_verdict).
source: coach_prediction_evaluator.py::_get_ewma_trend
Direction call graded across its own window (up / down / flat)
change = last reading − first reading of [filed day, filed day + stated window]; band = median |day-to-day change| of the window's readings (÷ √k for an N-day-average call, comparing the mean of the first k and last k readings, k = min(N, readings ÷ 2)); change > band → up, < −band → down, else flat
- Window
- The call's own stated window (the domain-clamped window when none is stated), from the day it was filed — genesis for a call sealed before day one. Readings after the window's last day never enter. At least 4 readings, the first and last within 2 days of their edges, else no verdict (never a flat one).
- Limitations
- The band is derived from the subject's own day-to-day variance inside the window (ADR-105), about one standard deviation of the first-minus-last difference a no-change series would produce — so a single noisy edge reading (a partial day of steps) can still decide a raw-reading call; the N-day-average calls smooth it. The median step includes any real trend per day, which widens the band slightly on a steadily moving series. Replaced the latest-readings EWMA check (#4617): two sealed weight calls it graded flat sat on a window where the served weigh-ins fell more than ten pounds.
- Minimum n
- 4
- Used by
- Coach prediction grading — every dated direction call, and every machine spec the #813 rescue re-routes.
source: prediction_direction_window.py::measure
Over/under-confident verdict
Weighted mean gap = Σ(bin_n · (bin_mean_confidence - bin_observed_rate)) / Σ(bin_n); gap > 0.15 → over-confident, gap < -0.15 → under-confident; otherwise well-calibrated only when Brier skill > 0 — a skill ≤ 0 surface reads not-yet-skillful instead (#1370)
- Window
- Same reliability bins as reliability_bins for that surface.
- Limitations
- Requires n ≥ 5 resolved predictions AND at least one non-empty bin, else 'insufficient_data'. The ±0.15 threshold is a fixed editorial choice (not derived from this platform's own variance) — a documented exception to the ADR-105 'thresholds from personal variance' rule, tracked as a candidate for future recalibration once more predictions resolve. 'Well-calibrated' asserts reliability AND skill: a forecaster whose stated confidence tracks observed rates but whose Brier skill is ≤ 0 (worse than always guessing the base rate) reads 'not_yet_skillful' — reliability alone never earns the flattering verdict (ADR-104/105, #1370). An undefined skill (degenerate base rate) is treated as unknown, not as unskilled.
- Minimum n
- 5
source: calibration_core.py::score_pairs
Coarse credibility label (nascent / not-yet-skillful / developing / reliable / authoritative)
n < 3 → nascent. Brier skill ≤ 0 → not-yet-skillful. Brier ≤ 0.15 AND n ≥ 12 → authoritative. Brier ≤ 0.20 → reliable. Else → developing
- Window
- Same pair set as the Brier score for that surface.
- Limitations
- A coarse, backward-compatible label kept for surfaces that pre-date the Brier-based scorer — always shown alongside the underlying Brier score and n, never as a substitute for them. The 0.15/0.20 cut points are fixed, not personal-variance-derived. A negative Brier skill (worse than the base rate) can never render the reliable/authoritative rungs, however good the raw Brier looks against an extreme base rate — it reads not-yet-skillful (#1370).
- Used by
- Coach cards that need a single at-a-glance credibility word.
source: calibration_core.py::score_pairs
Conversational enrichment signals — analysis-only scope
scope = analysis_only (v1, #1577): LLM-coded signals from coach check-in answers, habit reflections, field-note responses and — since #1708 — reactions to the coach's weekly Horizons pick (channel prescription_reaction) seed HYPO_CANDIDATE# rows (channel-tagged) and are visible as enriched context, but they do NOT enter character or flourishing scoring. Absence of conversation is 'no data', never a zero (ADR-104). Dedup vs the journal (AC4): content-hash equality, or a ≥40-char normalized containment match.
- Window
- Rolling 14-day sweep on the daily 6:30 AM enrichment cadence; each record is coded once (re-coded only on a schema bump or explicit force).
- Limitations
- These numbers are a language model's (Haiku) reading of short conversational prose — never sensor data, and shorter/noisier input than a journal entry. ANALYSIS-ONLY by declared policy: promotion into character or flourishing scoring requires personal-variance thresholds derived from an observed baseline FIRST (ADR-105) plus a human re-review of this entry — the fingerprint below trips if the policy function changes without that review. Causal hints survive only the deterministic verbatim-quote grounding gate (ADR-104). Enrichment pauses at budget tier 1 (internal band, ADR-125); paused windows are visible as unenriched records, never fabricated. The #1708 prescription_reaction channel inherits this scope unchanged: it calibrates the DETERMINISTIC Horizons reaction ledger (counts only, no model verdict) and moves no scoring.
- Used by
- journal_analyzer_lambda HYPO_CANDIDATE# aggregation (channel provenance on every quote) → the weekly hypothesis engine's candidate seeding; enriched context wherever enriched_* fields are read.
source: conversation_enrichment.py::enrichment_policy