mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
feat(insights): comprehensive upgrade — RPP, seasonal BP, body composition, sleep/activity, fitness ladder, clinical milestones
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7b5fabe46c
commit
3bb3837938
7 files changed
+159
-5
No files matched your search
@@ -32,6 +32,17 @@ KORRELATIONSANALYSE:
|
||||
- r < 0.4: schwache/keine Korrelation — nicht erwähnen
|
||||
- Korrelation ≠ Kausalität: immer als "Zusammenhang" formulieren, nicht als "Ursache"
|
||||
|
||||
ERWEITERTE METRIKEN:
|
||||
- ratePressureProduct.rpp30: Puls × systolischer RR. Normal: 7.000-10.000. > 12.000: erhöhter myokardialer Sauerstoffbedarf.
|
||||
* Wenn beide steigen ("Double Jeopardy"): höchstes Risiko
|
||||
* Wenn nur Puls steigt (RR stabil): Stress/Dekonditionierung
|
||||
* Wenn nur RR steigt (Puls stabil): Gefäßwiderstand
|
||||
- bodyCompositionDivergence.flag: Gewicht stabil + Körperfett steigt = stille Muskelmasse-Abnahme (sarkopenische Adipositas-Frühzeichen).
|
||||
- moodAdherenceRisk: Stimmung ≤ 2.5 über 7 Tage + fallend = Adhärenz-Einbruch in den nächsten 5 Tagen wahrscheinlich. Proaktiv ansprechen.
|
||||
- seasonalVariation: Winter-Sommer-Differenz des systolischen RR. > 5 mmHg ist physiologisch normal. Den User beruhigen — dies ist keine Verschlechterung.
|
||||
- sleep: Zielwert ≥ 7h/Nacht (ESC). < 6h: Risikofaktor für Hypertonie und Gewichtszunahme.
|
||||
- activity: WHO-Ziel ≥ 8.000 Schritte/Tag.
|
||||
|
||||
HISTORISCHER VERGLEICH:
|
||||
- Vergleiche aktuelle 7-Tage-Werte mit dem 30-Tage-Durchschnitt der Vorperiode
|
||||
- Klinisch relevante Veränderungen benennen:
|
||||
|
||||
@@ -21,7 +21,15 @@ FACHSPEZIFISCH — BLUTDRUCK:
|
||||
- weightVsSystolic-Korrelation: Nur analysieren wenn im Snapshot vorhanden und |r| > 0.4. Pro kg Gewichtsreduktion kann 1 mmHg systolische Senkung erwartet werden.
|
||||
- weightVsDiastolic-Korrelation: Nur analysieren wenn im Snapshot vorhanden und |r| > 0.4.
|
||||
- Vergleiche avgSys30 und avgSys90 sowie allTimeAvg um langfristige Trends zu erkennen.
|
||||
- Nutze historicalComparison.systolic und historicalComparison.diastolic: Bei ≥5 mmHg systolischer bzw. ≥3 mmHg diastolischer Veränderung klinisch bewerten.`;
|
||||
- Nutze historicalComparison.systolic und historicalComparison.diastolic: Bei ≥5 mmHg systolischer bzw. ≥3 mmHg diastolischer Veränderung klinisch bewerten.
|
||||
- Morgen-Risikoleiter (J-HOP Studie):
|
||||
* Morgen-RR 135-144: Schlaganfall-HR 2.45
|
||||
* Morgen-RR 145-154: HR 2.80
|
||||
* Morgen-RR 155-164: HR 3.58
|
||||
* Morgen-RR ≥ 165: HR 6.52
|
||||
- Rate-Pressure Product: Wenn ratePressureProduct.rpp30 > 12.000: "Erhöhter kardialer Sauerstoffbedarf" mit assessment "warning" bewerten.
|
||||
- Saisonale Variation: Falls seasonalVariation vorhanden und delta > 5 mmHg: "Physiologisch normale saisonale Schwankung — kein Grund zur Sorge, ggf. Winter-Dosisanpassung besprechen."
|
||||
- Salz-Signal: Akuter Gewichtsanstieg ≥ 1 kg in 3 Tagen + systolischer Anstieg ≥ 5 mmHg = mögliche erhöhte Natriumzufuhr.`;
|
||||
}
|
||||
|
||||
export function getBloodPressureUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
|
||||
@@ -13,7 +13,12 @@ FACHSPEZIFISCH — GESAMTBEWERTUNG:
|
||||
- Positive Entwicklungen explizit hervorheben — Motivation ist therapeutisch relevant.
|
||||
- Korrelationen nur erwähnen wenn der jeweilige r-Wert im Snapshot vorhanden und |r| > 0.4 ist. Falls ein Korrelationsfeld nicht im Snapshot vorhanden ist, keine Korrelation interpretieren oder erfinden. Vergleiche die aktuellen 7-Tage-Werte mit den Langzeit-Durchschnitten (avg90, allTime).
|
||||
- Nutze historicalComparison um aktuelle Veränderungen gegenüber der etablierten Baseline einzuordnen.
|
||||
- Falls Alter und Geschlecht bekannt sind, alters- und geschlechtsspezifische Risikobewertung anwenden.`;
|
||||
- Falls Alter und Geschlecht bekannt sind, alters- und geschlechtsspezifische Risikobewertung anwenden.
|
||||
- Schlaf: Falls sleep-Daten vorhanden, Schlafqualität in die Gesamtbewertung einbeziehen. < 6h/Nacht als Risikofaktor für Hypertonie und Gewichtszunahme benennen.
|
||||
- Aktivität: Falls activity-Daten vorhanden, Schrittzahl bewerten (WHO-Ziel ≥ 8.000/Tag). Zusammenhang mit Puls- und Gewichtstrend herstellen.
|
||||
- Rate-Pressure Product: Falls ratePressureProduct vorhanden, als Indikator für kardiale Belastung in die Risikostratifizierung einbeziehen. > 12.000 = erhöhter myokardialer Sauerstoffbedarf.
|
||||
- Body-Composition-Divergenz: Falls bodyCompositionDivergence.flag = true, als Frühzeichen für sarkopenische Adipositas in die Gesamtbewertung aufnehmen.
|
||||
- Saisonale Variation: Falls seasonalVariation vorhanden, saisonale Blutdruckschwankungen kontextualisieren und ggf. beruhigend einordnen.`;
|
||||
}
|
||||
|
||||
export function getGeneralStatusUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
|
||||
@@ -19,7 +19,9 @@ FACHSPEZIFISCH — MEDIKAMENTEN-ADHÄRENZ:
|
||||
- Vergleiche Perioden hoher Adhärenz (>90%) mit den zugehörigen Vitalwerten. Zeige konkret: "In Wochen mit >90% Einnahmetreue war der systolische RR X mmHg niedriger."
|
||||
- Korrelationen nur erwähnen wenn der r-Wert im Snapshot vorhanden und |r| > 0.4 ist. Falls das Feld nicht im Snapshot vorhanden ist, keine Korrelation interpretieren oder erfinden.
|
||||
- Nutze historicalComparison um den Einfluss der Adhärenz auf Blutdruck und Puls zu quantifizieren, sofern die Daten im Snapshot vorhanden sind.
|
||||
- Falls Stimmungsdaten verfügbar und Korrelation |r| > 0.4: Prüfe ob niedrige Adhärenz mit schlechterer Stimmung korreliert.`;
|
||||
- Falls Stimmungsdaten verfügbar und Korrelation |r| > 0.4: Prüfe ob niedrige Adhärenz mit schlechterer Stimmung korreliert.
|
||||
- Chronotherapie-Hinweis: Falls ein Blutdruck-Medikament vorhanden ist UND Compliance > 90% ABER BP nicht im Zielbereich: "Einnahme-Zeitpunkt mit dem Arzt besprechen — abendliche Einnahme kann bei einigen Patienten die nächtliche Blutdruckkontrolle verbessern."
|
||||
- Mood-Adhärenz-Risiko: Falls moodAdherenceRisk = true: "Deine Stimmung war in den letzten Tagen niedrig. Erfahrungsgemäß kann das die Einnahmetreue in den kommenden Tagen beeinflussen. Tipp: Lege die Medikamente abends schon bereit."`;
|
||||
}
|
||||
|
||||
export function getMedicationComplianceUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
|
||||
@@ -18,7 +18,13 @@ FACHSPEZIFISCH — PULS/HERZFREQUENZ:
|
||||
- moodVsPulse-Korrelation: Nur analysieren wenn im Snapshot vorhanden und |r| > 0.4. Falls nicht vorhanden, keine Korrelation interpretieren.
|
||||
- pulseVsSystolic-Korrelation: Nur analysieren wenn im Snapshot vorhanden und |r| > 0.4. Bewertung der hämodynamischen Kopplung.
|
||||
- Vergleiche avg7 vs avg30 vs avg90 vs allTimeAvg um kurzfristige Abweichungen von der Langzeit-Baseline zu erkennen.
|
||||
- Nutze historicalComparison.pulse: Bei ≥5 bpm Veränderung gegenüber der Baseline klinisch bewerten.`;
|
||||
- Nutze historicalComparison.pulse: Bei ≥5 bpm Veränderung gegenüber der Baseline klinisch bewerten.
|
||||
- Fitness-Interpretation:
|
||||
* slope30 < -0.2 bpm/Tag (≈ -6 bpm/Monat): Positives Fitness-Signal, kardiovaskuläre Verbesserung
|
||||
* slope30 > +0.2 bpm/Tag (≈ +6 bpm/Monat) OHNE Stimmungsabfall: Mögliche Dekonditionierung
|
||||
* slope30 > +0.2 bpm/Tag MIT moodVsPulse r > 0.4: Stressbedingte Pulserhöhung
|
||||
- Risikoband 80-100 bpm: Technisch "normal", aber erhöhtes CV-Risiko (+6-9% Mortalität pro 10 bpm, Meta-Analyse).
|
||||
- Resting HR > 90 bpm: Unabhängiger Mortalitätsprädiktor — als eigenes Finding mit assessment "warning" erfassen.`;
|
||||
}
|
||||
|
||||
export function getPulseUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
|
||||
@@ -14,7 +14,14 @@ FACHSPEZIFISCH — GEWICHT:
|
||||
- Tageszeit-Schwankungen: Morgen- vs. Abendmessungen differenzieren (1-2 kg normal).
|
||||
- Vergleiche das aktuelle 7-Tage-Mittel (historicalComparison.weight.current7dAvg) mit dem 30-Tage-Baseline (previous30dAvg). Bei >2 kg Differenz: klinisch bewerten und Ursachen diskutieren.
|
||||
- Plateau-Definition: <0.5 kg Veränderung über >14 Tage (prüfe ob avg7 ≈ avg30 ≈ avg90).
|
||||
- Nutze allTimeMin/allTimeMax/allTimeAvg um den aktuellen Wert historisch einzuordnen.`;
|
||||
- Nutze allTimeMin/allTimeMax/allTimeAvg um den aktuellen Wert historisch einzuordnen.
|
||||
- Gewichtsmeilensteine (klinisch signifikant):
|
||||
* 5% Verlust vom Startgewicht (allTimeMax): Metabolischer Benefit, KV-Risikoreduktion
|
||||
* 10% Verlust: Erheblicher klinischer Nutzen
|
||||
* Wenn erreicht: Explizit anerkennen und klinischen Kontext geben
|
||||
- Zu schneller Verlust: > 1 kg/Woche über > 2 Wochen = Risiko für Muskelmasseverlust, nicht nachhaltig
|
||||
- Body-Composition-Divergenz: Falls bodyCompositionDivergence.flag = true: "Gewicht stabil, aber Körperfettanteil steigend — möglicher Muskelmasseverlust. Krafttraining empfehlen."
|
||||
- Schlaf-Gewicht-Verbindung: Falls sleep.avg7 < 6h UND weight.slope30 > 0: "Schlafmangel stört metabolische Regulation und kann Gewichtszunahme begünstigen."`;
|
||||
}
|
||||
|
||||
export function getWeightUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
|
||||
@@ -82,12 +82,44 @@ export interface AggregatedFeatures {
|
||||
totalEntries: number;
|
||||
coverage: DataCoverage;
|
||||
};
|
||||
sleep?: {
|
||||
avg7: number | null;
|
||||
avg30: number | null;
|
||||
latest: number | null;
|
||||
coverage: DataCoverage;
|
||||
};
|
||||
activity?: {
|
||||
avg7: number | null;
|
||||
avg30: number | null;
|
||||
latest: number | null;
|
||||
coverage: DataCoverage;
|
||||
};
|
||||
ratePressureProduct?: {
|
||||
rpp7: number | null;
|
||||
rpp30: number | null;
|
||||
risk: 'normal' | 'elevated' | null;
|
||||
};
|
||||
bodyCompositionDivergence?: {
|
||||
weightStable: boolean;
|
||||
bodyFatRising: boolean;
|
||||
flag: boolean;
|
||||
};
|
||||
moodAdherenceRisk?: boolean;
|
||||
seasonalVariation?: {
|
||||
winterAvgSys: number | null;
|
||||
summerAvgSys: number | null;
|
||||
delta: number | null;
|
||||
significance: 'normal' | 'elevated' | null;
|
||||
};
|
||||
correlations?: {
|
||||
weightVsSystolic: CorrelationResult | null;
|
||||
weightVsDiastolic: CorrelationResult | null;
|
||||
pulseVsSystolic: CorrelationResult | null;
|
||||
moodVsPulse: CorrelationResult | null;
|
||||
moodVsSystolic: CorrelationResult | null;
|
||||
moodVsWeight: CorrelationResult | null;
|
||||
sleepVsPulse: CorrelationResult | null;
|
||||
sleepVsSystolic: CorrelationResult | null;
|
||||
};
|
||||
historicalComparison?: {
|
||||
weight?: {
|
||||
@@ -406,6 +438,30 @@ export async function extractFeatures(
|
||||
};
|
||||
}
|
||||
|
||||
// Sleep Duration
|
||||
const sleepData = byType("SLEEP_DURATION");
|
||||
if (sleepData.length > 0) {
|
||||
const summary = summarize(toDataPoints(sleepData));
|
||||
features.sleep = {
|
||||
avg7: summary.avg7,
|
||||
avg30: summary.avg30,
|
||||
latest: summary.latest,
|
||||
coverage: computeCoverage(sleepData, now),
|
||||
};
|
||||
}
|
||||
|
||||
// Activity Steps
|
||||
const activityData = byType("ACTIVITY_STEPS");
|
||||
if (activityData.length > 0) {
|
||||
const summary = summarize(toDataPoints(activityData));
|
||||
features.activity = {
|
||||
avg7: summary.avg7,
|
||||
avg30: summary.avg30,
|
||||
latest: summary.latest,
|
||||
coverage: computeCoverage(activityData, now),
|
||||
};
|
||||
}
|
||||
|
||||
// Mood
|
||||
const moodEntries = await prisma.moodEntry.findMany({
|
||||
where: { userId },
|
||||
@@ -491,14 +547,73 @@ export async function extractFeatures(
|
||||
const computeCorr = (a: DataPoint[], b: DataPoint[]) =>
|
||||
pearsonCorrelation(pairByTimestamp(a, b));
|
||||
|
||||
const sleepPoints = toDataPoints(sleepData);
|
||||
|
||||
features.correlations = {
|
||||
weightVsSystolic: computeCorr(weightPoints, sysPoints),
|
||||
weightVsDiastolic: computeCorr(weightPoints, diaPoints),
|
||||
pulseVsSystolic: computeCorr(pulsePoints, sysPoints),
|
||||
moodVsPulse: computeCorr(moodPoints, pulsePoints),
|
||||
moodVsSystolic: computeCorr(moodPoints, sysPoints),
|
||||
moodVsWeight: computeCorr(moodPoints, weightPoints),
|
||||
sleepVsPulse: sleepData.length > 0 ? computeCorr(sleepPoints, pulsePoints) : null,
|
||||
sleepVsSystolic: sleepData.length > 0 ? computeCorr(sleepPoints, sysPoints) : null,
|
||||
};
|
||||
|
||||
// Rate-Pressure Product (RPP) — myocardial oxygen demand indicator
|
||||
if (features.pulse && features.bloodPressure) {
|
||||
const rpp7 =
|
||||
features.pulse.avg7 !== null && features.bloodPressure.avgSys30 !== null
|
||||
? Math.round(features.pulse.avg7 * (avgInWindow(sysData, now, 7) ?? features.bloodPressure.avgSys30))
|
||||
: null;
|
||||
const rpp30 =
|
||||
features.pulse.avg30 !== null && features.bloodPressure.avgSys30 !== null
|
||||
? Math.round(features.pulse.avg30 * features.bloodPressure.avgSys30)
|
||||
: null;
|
||||
const rppRef = rpp30 ?? rpp7;
|
||||
features.ratePressureProduct = {
|
||||
rpp7,
|
||||
rpp30,
|
||||
risk: rppRef !== null ? (rppRef > 12000 ? 'elevated' : 'normal') : null,
|
||||
};
|
||||
}
|
||||
|
||||
// Body Composition Divergence
|
||||
if (features.weight && features.bodyFat) {
|
||||
const weightStable = features.weight.slope30 !== null && Math.abs(features.weight.slope30) < 0.01;
|
||||
const bodyFatRising = features.bodyFat.slope30 !== null && features.bodyFat.slope30 > 0;
|
||||
features.bodyCompositionDivergence = {
|
||||
weightStable,
|
||||
bodyFatRising,
|
||||
flag: weightStable && bodyFatRising,
|
||||
};
|
||||
}
|
||||
|
||||
// Mood-Adherence Risk Flag
|
||||
if (features.mood && features.medications && features.medications.length > 0) {
|
||||
features.moodAdherenceRisk =
|
||||
features.mood.avg7 !== null &&
|
||||
features.mood.avg7 <= 2.5 &&
|
||||
features.mood.trend30 === 'declining';
|
||||
}
|
||||
|
||||
// Seasonal BP Variation (only if > 180 days of data)
|
||||
if (features.context.dataSpanDays > 180 && sysData.length > 0) {
|
||||
const winterMonths = [11, 0, 1]; // Dec, Jan, Feb (0-indexed)
|
||||
const summerMonths = [5, 6, 7]; // Jun, Jul, Aug
|
||||
const winterVals = sysData.filter(m => winterMonths.includes(m.measuredAt.getMonth())).map(m => m.value);
|
||||
const summerVals = sysData.filter(m => summerMonths.includes(m.measuredAt.getMonth())).map(m => m.value);
|
||||
const winterAvg = winterVals.length > 0 ? Math.round((winterVals.reduce((s, v) => s + v, 0) / winterVals.length) * 10) / 10 : null;
|
||||
const summerAvg = summerVals.length > 0 ? Math.round((summerVals.reduce((s, v) => s + v, 0) / summerVals.length) * 10) / 10 : null;
|
||||
const delta = winterAvg !== null && summerAvg !== null ? Math.round((winterAvg - summerAvg) * 10) / 10 : null;
|
||||
features.seasonalVariation = {
|
||||
winterAvgSys: winterAvg,
|
||||
summerAvgSys: summerAvg,
|
||||
delta,
|
||||
significance: delta !== null ? (Math.abs(delta) > 5 ? 'elevated' : 'normal') : null,
|
||||
};
|
||||
}
|
||||
|
||||
// Historical comparison: current 7d avg vs previous 30d avg (days 7-37)
|
||||
features.historicalComparison = {};
|
||||
if (weightData.length > 0) {
|
||||
|
||||
Reference in new issue
Block a user