fix(insights): read a same-hours day by its day mean

The same-hours standing averaged each day as sum / n. Pulse already folds
its day as the mean of its local hours' means, and the day-aggregate reader
returns that as dayMean; a type moved onto that reader would have been
compared raw. The standing now reads dayMean where the reader carries one.
This commit is contained in:
Marc-André Bombeck committed 2026-10-05 13:12:06 +02:00
1 parent abb1c9f8a9
commit 22cb792379
2 files changed
+45 -5

No files matched your search

@@ -33,6 +33,7 @@ import {
computeCoincidentDeviation,
classifyDeviation,
COINCIDENT_FIRE_THRESHOLD,
sameHoursStanding,
} from "../coincident-deviation";
const PROFILE = { ageYears: 40, sex: "MALE" as const };
@@ -351,3 +352,40 @@ describe("glucose — a day in progress is compared with the same hours", () =>
).toBeUndefined();
});
});
describe("sameHoursStanding", () => {
it("reads an hourly-mean day by its day mean, not by sum / n", () => {
// Twelve dense readings in one hour and one in another: the mean of the
// hours' means is 100, the raw mean is 140. A type on the hourly-mean
// reader carries `dayMean`, and that is the day's value.
const day = (d: string, dayMean: number) => ({
day: d,
n: 13,
sum: 140 * 13,
dayMean,
});
const days = [
...Array.from({ length: 8 }, (_, i) =>
day(`2026-05-${String(20 + i).padStart(2, "0")}`, 100),
),
day("2026-06-02", 101),
];
const standing = sameHoursStanding(days, "2026-06-02", "BLOOD_GLUCOSE");
expect(standing?.value).toBe(101);
expect(standing?.center).toBe(100);
});
it("falls back to sum / n where the reader has no day mean", () => {
const days = [
...Array.from({ length: 8 }, (_, i) => ({
day: `2026-05-${String(20 + i).padStart(2, "0")}`,
n: 2,
sum: 180,
})),
{ day: "2026-06-02", n: 1, sum: 88 },
];
const standing = sameHoursStanding(days, "2026-06-02", "BLOOD_GLUCOSE");
expect(standing?.value).toBe(88);
expect(standing?.center).toBe(90);
});
});
@@ -94,20 +94,22 @@ export const SAME_HOURS_MIN_DAYS = 7;
* there is nothing like-for-like to compare today with, and no verdict.
*/
export function sameHoursStanding(
days: readonly { day: string; n: number; sum: number }[],
days: readonly { day: string; n: number; sum: number; dayMean?: number }[],
todayKey: string,
type: MeasurementType,
): { value: number; low: number; high: number; center: number } | null {
// A type on the hourly-mean reader carries `dayMean` (the mean of its
// hours' means); every other type's day is the mean of its readings.
const value = (d: { n: number; sum: number; dayMean?: number }) =>
d.dayMean ?? d.sum / d.n;
const today = days.find((d) => d.day === todayKey && d.n > 0);
if (!today) return null;
const earlier = days
.filter((d) => d.day < todayKey && d.n > 0)
.map((d) => d.sum / d.n);
const earlier = days.filter((d) => d.day < todayKey && d.n > 0).map(value);
if (earlier.length < SAME_HOURS_MIN_DAYS) return null;
const band = buildBaselineBand(earlier, type);
if (!band) return null;
return {
value: today.sum / today.n,
value: value(today),
low: band.low,
high: band.high,
center: band.center,