mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
fix(pulse): make a day of pulse the mean of its hours' means
A watch samples pulse about every 5 to 10 minutes at rest and about every 5 seconds in a workout. The daily value was the plain mean over a day's readings, so a workout hour of several hundred readings outweighed the other 23 hours: a day with one hard hour and 23 resting hours (60 bpm) read about 128 bpm, not 64. A day of pulse is now the mean of its per-hour means, in the rollup DAY bucket, the live per-day read the chart serves, and the series route's day grain. Windows over several days weigh each day once instead of by its reading count, so the bias does not return one level up. Reading counts, min/max, and the regression sums still run over every reading. The chart tooltip and the data table name the statistic, in all seven locales. The statistic is limited to pulse; HRV, SpO2 and glucose keep the plain mean (see src/lib/measurements/day-statistic.ts). Stored DAY rows are rewritten by the next rollup recompute (admin recompute route), not on read. Fixes #1107
This commit is contained in:
1 parent
808b2672cd
commit
497da7ca8b
25 files changed
+816
-50
No files matched your search
@@ -26,6 +26,7 @@ import type { MeasurementType, SleepStage } from "@/generated/prisma/client";
|
||||
import { reconstructSleepNights } from "@/lib/analytics/sleep-night";
|
||||
import { VALUE_RANGES } from "@/lib/validations/measurement";
|
||||
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
||||
import { usesHourlyMeanDay } from "@/lib/measurements/day-statistic";
|
||||
import { seriesRowsFrom } from "@/lib/measurements/series-canonical";
|
||||
import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver";
|
||||
import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
|
||||
@@ -410,6 +411,11 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
]);
|
||||
const type = KIND_TO_TYPE[kind];
|
||||
const seriesRows = seriesRowsFrom(priorityJson, type, days);
|
||||
// A day of a type with an activity-dependent sampling rate (pulse) is the
|
||||
// mean of its local hours' means, each hour once, so a workout hour with
|
||||
// hundreds of times the readings does not outweigh the rest of the day (see
|
||||
// `day-statistic.ts`). The hour grain is already one mean per hour.
|
||||
const hourWeighted = grain === "day" && usesHourlyMeanDay(type);
|
||||
const bucketRows = await prisma.$queryRawUnsafe<
|
||||
Array<{
|
||||
bucket_start: Date;
|
||||
@@ -418,7 +424,39 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
max_value: number;
|
||||
}>
|
||||
>(
|
||||
`
|
||||
hourWeighted
|
||||
? `
|
||||
WITH localized AS (
|
||||
SELECT
|
||||
m."value",
|
||||
m."value_min",
|
||||
m."value_max",
|
||||
date_trunc(
|
||||
'hour',
|
||||
(m."measured_at" AT TIME ZONE 'UTC') AT TIME ZONE $3
|
||||
) AS local_hour
|
||||
FROM ${seriesRows}
|
||||
WHERE m."measured_at" >= $4
|
||||
),
|
||||
hourly AS (
|
||||
SELECT
|
||||
date_trunc($2, local_hour) AS local_day,
|
||||
AVG("value") AS hour_mean,
|
||||
MIN(COALESCE("value_min", "value")) AS min_value,
|
||||
MAX(COALESCE("value_max", "value")) AS max_value
|
||||
FROM localized
|
||||
GROUP BY local_hour
|
||||
)
|
||||
SELECT
|
||||
local_day AT TIME ZONE $3 AS bucket_start,
|
||||
AVG(hour_mean)::double precision AS mean,
|
||||
MIN(min_value)::double precision AS min_value,
|
||||
MAX(max_value)::double precision AS max_value
|
||||
FROM hourly
|
||||
GROUP BY local_day
|
||||
ORDER BY bucket_start ASC
|
||||
`
|
||||
: `
|
||||
WITH localized AS (
|
||||
SELECT
|
||||
m."value",
|
||||
|
||||
Reference in new issue
Block a user