Files
HealthLog/src/lib/stats/spread.ts
T
Marc-André Bombeck ac86199808 fix(stats): one standard deviation with its kind named at the call site
About a dozen hand-rolled spreads each picked population or sample
variance without saying so, and the two surfaces a person compares
picked differently: the series API reported the population SD of a
blood-pressure series, the MCP tools and the AI payload the sample SD of
the same readings, about 22 % apart at three readings.

`lib/stats/spread` holds `variance` and `standardDeviation` with an
explicit `population | sample` kind and `READING_SPREAD`, the kind every
surface uses to report the spread of a person's readings. The series
route and the feature payload both report it; the spreads that feed a
verdict (the normal-swing checks, the changepoint threshold) and the
clinical glucose SD stay the sample estimate, now said out loud. A guard
fails on a new hand-rolled spread and on the two surfaces drifting apart.
2026-10-20 04:36:54 +02:00

50 lines
1.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Variance and standard deviation, with the kind named at every call site.
*
* - `population` divides by n: the spread of exactly these readings. It is
* what a surface reports when it describes a series the person can see.
* - `sample` divides by n − 1: an estimate of the spread that readings like
* these come from. It belongs where the spread feeds a decision (a
* significance verdict, a changepoint threshold) or where a clinical
* definition asks for it.
*
* The two differ by sqrt(n / (n − 1)), about 22 % at n = 3, so one series
* shown with both rules looks like two different series. Every hand-rolled
* spread used to pick its rule silently; the API reported the population SD
* of a blood-pressure series while the MCP and AI payload reported the
* sample SD of the same readings.
*/
export type VarianceKind = "population" | "sample";
/**
* The kind every surface uses to report the spread of a person's readings,
* so the API, the MCP tools, the Coach and the AI payload say the same
* number for the same series.
*/
export const READING_SPREAD: VarianceKind = "population";
/** `null` when the kind is undefined for this many values (n < 1, or n < 2 for a sample). */
export function variance(
values: readonly number[],
kind: VarianceKind,
): number | null {
const n = values.length;
const divisor = kind === "sample" ? n - 1 : n;
if (divisor < 1) return null;
let sum = 0;
for (const v of values) sum += v;
const mean = sum / n;
let squares = 0;
for (const v of values) squares += (v - mean) ** 2;
return squares / divisor;
}
export function standardDeviation(
values: readonly number[],
kind: VarianceKind,
): number | null {
const v = variance(values, kind);
return v === null ? null : Math.sqrt(v);
}