mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(insights): scan the nightly RMSSD series for correlations and illness
HRV_RMSSD was read nowhere HRV is analysed: the correlation scan and the illness engine only knew the SDNN series, so a strap or ring wearer's HRV never took part in either. RMSSD is now its own outcome channel (owned by the recovery module, like the summary tile) and its own illness scan type with a drop read as adverse. It is not merged into the SDNN channel: the estimators differ in scale, and one series of both would correlate the change of device rather than the body.
This commit is contained in:
1 parent
c5d2053e5a
commit
a66c54a1f1
14 files changed
+43
No files matched your search
@@ -10392,6 +10392,7 @@
|
||||
"vital": {
|
||||
"RESTING_HEART_RATE": "Ruheherzfrequenz",
|
||||
"HEART_RATE_VARIABILITY": "Herzfrequenzvariabilität",
|
||||
"HRV_RMSSD": "HRV (RMSSD)",
|
||||
"RECOVERY_SCORE": "Recovery",
|
||||
"OXYGEN_SATURATION": "Sauerstoffsättigung",
|
||||
"BODY_TEMPERATURE": "Körpertemperatur",
|
||||
|
||||
@@ -10392,6 +10392,7 @@
|
||||
"vital": {
|
||||
"RESTING_HEART_RATE": "Resting heart rate",
|
||||
"HEART_RATE_VARIABILITY": "Heart-rate variability",
|
||||
"HRV_RMSSD": "HRV (RMSSD)",
|
||||
"RECOVERY_SCORE": "Recovery",
|
||||
"OXYGEN_SATURATION": "Oxygen saturation",
|
||||
"BODY_TEMPERATURE": "Body temperature",
|
||||
|
||||
@@ -10392,6 +10392,7 @@
|
||||
"vital": {
|
||||
"RESTING_HEART_RATE": "Frecuencia cardíaca en reposo",
|
||||
"HEART_RATE_VARIABILITY": "Variabilidad de la frecuencia cardíaca",
|
||||
"HRV_RMSSD": "VFC (RMSSD)",
|
||||
"RECOVERY_SCORE": "Recovery",
|
||||
"OXYGEN_SATURATION": "Saturación de oxígeno",
|
||||
"BODY_TEMPERATURE": "Temperatura corporal",
|
||||
|
||||
@@ -10392,6 +10392,7 @@
|
||||
"vital": {
|
||||
"RESTING_HEART_RATE": "Fréquence cardiaque au repos",
|
||||
"HEART_RATE_VARIABILITY": "Variabilité de la fréquence cardiaque",
|
||||
"HRV_RMSSD": "VFC (RMSSD)",
|
||||
"RECOVERY_SCORE": "Recovery",
|
||||
"OXYGEN_SATURATION": "Saturation en oxygène",
|
||||
"BODY_TEMPERATURE": "Température corporelle",
|
||||
|
||||
@@ -10392,6 +10392,7 @@
|
||||
"vital": {
|
||||
"RESTING_HEART_RATE": "Frequenza cardiaca a riposo",
|
||||
"HEART_RATE_VARIABILITY": "Variabilità della frequenza cardiaca",
|
||||
"HRV_RMSSD": "HRV (RMSSD)",
|
||||
"RECOVERY_SCORE": "Recovery",
|
||||
"OXYGEN_SATURATION": "Saturazione di ossigeno",
|
||||
"BODY_TEMPERATURE": "Temperatura corporea",
|
||||
|
||||
@@ -10392,6 +10392,7 @@
|
||||
"vital": {
|
||||
"RESTING_HEART_RATE": "안정 시 심박수",
|
||||
"HEART_RATE_VARIABILITY": "심박변이도",
|
||||
"HRV_RMSSD": "심박변이도 (RMSSD)",
|
||||
"RECOVERY_SCORE": "회복",
|
||||
"OXYGEN_SATURATION": "산소포화도",
|
||||
"BODY_TEMPERATURE": "체온",
|
||||
|
||||
@@ -10392,6 +10392,7 @@
|
||||
"vital": {
|
||||
"RESTING_HEART_RATE": "Rusthartslag",
|
||||
"HEART_RATE_VARIABILITY": "Hartslagvariabiliteit",
|
||||
"HRV_RMSSD": "HRV (RMSSD)",
|
||||
"RECOVERY_SCORE": "Herstel",
|
||||
"OXYGEN_SATURATION": "Zuurstofverzadiging",
|
||||
"BODY_TEMPERATURE": "Lichaamstemperatuur",
|
||||
|
||||
@@ -10392,6 +10392,7 @@
|
||||
"vital": {
|
||||
"RESTING_HEART_RATE": "Spoczynkowe tętno",
|
||||
"HEART_RATE_VARIABILITY": "Zmienność rytmu serca",
|
||||
"HRV_RMSSD": "HRV (RMSSD)",
|
||||
"RECOVERY_SCORE": "Recovery",
|
||||
"OXYGEN_SATURATION": "Saturacja tlenem",
|
||||
"BODY_TEMPERATURE": "Temperatura ciała",
|
||||
|
||||
@@ -98,6 +98,7 @@ const LEGIT_IDENTICAL = new Set<string>([
|
||||
"insights.derived.composite.READINESS.component.hrv",
|
||||
"labs.catalog.hs-crp",
|
||||
"measurements.typeHrvRmssd",
|
||||
"illness.vital.HRV_RMSSD",
|
||||
"settings.sections.sources.metrics.hrvRmssd",
|
||||
"measurements.typeVo2Max",
|
||||
// "{hours} h {minutes} min": the SI symbols for hour and minute.
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
computeIllnessCorrelation,
|
||||
dayDiff,
|
||||
FUNCTIONAL_IMPACT_RETURN_KEY,
|
||||
ILLNESS_SCAN_TYPES,
|
||||
isAdverseDeviation,
|
||||
MIN_BASELINE_DAYS,
|
||||
MIN_EPISODE_COVERAGE_DAYS,
|
||||
@@ -803,6 +804,12 @@ describe("isAdverseDeviation", () => {
|
||||
expect(isAdverseDeviation("HEART_RATE_VARIABILITY", "above")).toBe(false);
|
||||
});
|
||||
|
||||
it("reads a drop in the nightly RMSSD series as adverse, like SDNN", () => {
|
||||
expect(ILLNESS_SCAN_TYPES).toContain("HRV_RMSSD");
|
||||
expect(isAdverseDeviation("HRV_RMSSD", "below")).toBe(true);
|
||||
expect(isAdverseDeviation("HRV_RMSSD", "above")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats neutral-direction vitals (WEIGHT) as never adverse", () => {
|
||||
expect(isAdverseDeviation("WEIGHT", "above")).toBe(false);
|
||||
expect(isAdverseDeviation("WEIGHT", "below")).toBe(false);
|
||||
|
||||
@@ -118,6 +118,7 @@ export const ADVERSE_DIRECTION: Partial<
|
||||
BLOOD_PRESSURE_SYS: "up",
|
||||
BLOOD_PRESSURE_DIA: "up",
|
||||
HEART_RATE_VARIABILITY: "down",
|
||||
HRV_RMSSD: "down",
|
||||
RECOVERY_SCORE: "down",
|
||||
OXYGEN_SATURATION: "down",
|
||||
};
|
||||
@@ -130,6 +131,9 @@ export const ADVERSE_DIRECTION: Partial<
|
||||
export const ILLNESS_SCAN_TYPES: MeasurementType[] = [
|
||||
"RESTING_HEART_RATE",
|
||||
"HEART_RATE_VARIABILITY",
|
||||
// RMSSD, scanned on its own baseline: a nightly strap or ring reading drops
|
||||
// during illness the same way SDNN does.
|
||||
"HRV_RMSSD",
|
||||
"RECOVERY_SCORE",
|
||||
"OXYGEN_SATURATION",
|
||||
"BODY_TEMPERATURE",
|
||||
|
||||
@@ -127,6 +127,21 @@ describe("assembleDiscoveryMatrix — switched-off modules", () => {
|
||||
expect(keys).toContain("CUSTOM_METRIC:abc");
|
||||
});
|
||||
|
||||
it("scans the nightly RMSSD series as an outcome owned by recovery", async () => {
|
||||
const on = await assembleDiscoveryMatrix("u1", OPTS);
|
||||
expect(
|
||||
on.series.find((s) => s.key === "HRV_RMSSD" && s.role === "outcome"),
|
||||
).toBeDefined();
|
||||
const off = await assembleDiscoveryMatrix("u1", {
|
||||
...OPTS,
|
||||
modules: { recovery: false },
|
||||
});
|
||||
const keys = keysOf(off.series);
|
||||
expect(keys).not.toContain("HRV_RMSSD");
|
||||
// SDNN is a plain vital and stays.
|
||||
expect(keys).toContain("HEART_RATE_VARIABILITY");
|
||||
});
|
||||
|
||||
it("does not read the weather at all with the environment module off", async () => {
|
||||
const read = vi.mocked(fetchEnvironmentSeries);
|
||||
read.mockClear();
|
||||
|
||||
@@ -662,6 +662,12 @@ export const DISCOVERY_BEHAVIOURS = [
|
||||
export const DISCOVERY_OUTCOMES = [
|
||||
"SLEEP_DURATION",
|
||||
"HEART_RATE_VARIABILITY",
|
||||
// The nightly RMSSD series (WHOOP, Oura, Polar, Fitbit, Google Health) as
|
||||
// its own outcome. It is not merged into the SDNN channel: the estimators
|
||||
// differ in scale, so one series of both would correlate the switch between
|
||||
// devices rather than the body. Neither HRV channel is a behaviour, so the
|
||||
// two never pair with each other.
|
||||
"HRV_RMSSD",
|
||||
"RESTING_HEART_RATE",
|
||||
"WEIGHT",
|
||||
// v1.22 — blood pressure as an OUTCOME (it was behaviour-only). This makes
|
||||
|
||||
@@ -212,6 +212,8 @@ const STATIC_SURFACE_MODULE = {
|
||||
"correlation:SLEEP_DURATION": "sleep",
|
||||
"correlation:BLOOD_GLUCOSE": "glucose",
|
||||
"correlation:RESTING_HEART_RATE": "recovery",
|
||||
// The RMSSD series is the recovery module's (see `summary:HRV_RMSSD`).
|
||||
"correlation:HRV_RMSSD": "recovery",
|
||||
"correlation:MEDICATION_COMPLIANCE": "medications",
|
||||
// Symptom burden is read from the illness journal.
|
||||
"correlation:SYMPTOM_SEVERITY": "illness",
|
||||
|
||||
Reference in new issue
Block a user