mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(insights): enrich features with correlations, mood, historical comparisons and improve prompts
Add 90-day averages, all-time stats, mood data, cross-metric Pearson correlations, historical comparisons (7d vs previous 30d baseline), and age/gender context to the AI feature extraction. Update all prompts with correlation analysis instructions, clinical significance thresholds, and temporal layer comparisons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
360a11523e
commit
64c72d241c
7 files changed
+287
-6
No files matched your search
@@ -10,6 +10,32 @@ GRUNDREGELN:
|
||||
* Neueste Messung > 7 Tage alt: "Daten möglicherweise nicht aktuell."
|
||||
- Sprache: Deutsch, medizinisch präzise aber allgemeinverständlich.
|
||||
- Disclaimer: Immer den Standardhinweis verwenden.
|
||||
- Alter/Geschlecht: Falls in context.ageYears und context.gender vorhanden, alters- und geschlechtsspezifische Referenzwerte verwenden.
|
||||
|
||||
KORRELATIONSANALYSE:
|
||||
- Du erhältst vorberechnete Pearson-Korrelationen zwischen Metriken.
|
||||
- r > 0.7: starke Korrelation — klinisch relevant, detailliert kommentieren
|
||||
- r 0.4-0.7: moderate Korrelation — erwähnen, vorsichtig interpretieren
|
||||
- r < 0.4: schwache/keine Korrelation — nicht überinterpretieren
|
||||
- Korrelation ≠ Kausalität: immer als "Zusammenhang" formulieren, nicht als "Ursache"
|
||||
|
||||
HISTORISCHER VERGLEICH:
|
||||
- Vergleiche aktuelle 7-Tage-Werte mit dem 30-Tage-Durchschnitt der Vorperiode
|
||||
- Klinisch relevante Veränderungen benennen:
|
||||
* Gewicht: ±2 kg relevant
|
||||
* Systolisch: ±5 mmHg relevant
|
||||
* Diastolisch: ±3 mmHg relevant
|
||||
* Puls: ±5 bpm relevant
|
||||
|
||||
STIMMUNGSDATEN:
|
||||
- Stimmung (1-5 Skala: 1=LAUSIG, 5=SUPER_GUT) als kontextuellen Faktor einbeziehen
|
||||
- Korrelation mit Vitalwerten nur erwähnen wenn statistisch auffällig (r > 0.3)
|
||||
- Stress/Stimmung beeinflusst nachweislich Blutdruck und Herzfrequenz
|
||||
|
||||
TEMPORALE SCHICHTEN:
|
||||
- Vergleiche kurzfristig (7d) vs. mittelfristig (30d) vs. langfristig (90d/allTime)
|
||||
- avg7 vs avg30 zeigt aktuelle Tendenz, avg90/allTime zeigt Langzeit-Baseline
|
||||
- Abweichungen von der Langzeit-Baseline sind klinisch aussagekräftiger als kurzfristige Schwankungen
|
||||
|
||||
AUSGABEFORMAT: Antworte ausschließlich mit validem JSON im folgenden Schema:
|
||||
{
|
||||
|
||||
@@ -17,13 +17,18 @@ FACHSPEZIFISCH — BLUTDRUCK:
|
||||
- Pulsdruck: (Systolisch - Diastolisch) > 60 mmHg als Marker für arterielle Steifigkeit bewerten.
|
||||
- Medikamenten-Korrelation: Einnahmetreue von Antihypertensiva mit Blutdruckverlauf korrelieren.
|
||||
- Gewichts-Korrelation: Gewichtstrend mit Blutdruckverlauf vergleichen (pro kg Gewichtsverlust ca. 1 mmHg Senkung erwartet).
|
||||
- Stimmungs-Korrelation: Falls Stimmungsdaten verfügbar, Zusammenhang mit Blutdruckvariabilität prüfen.`;
|
||||
- Stimmungs-Korrelation: Falls Stimmungsdaten verfügbar, Zusammenhang mit Blutdruckvariabilität prüfen.
|
||||
- Analysiere die weightVsSystolic-Korrelation: Pro kg Gewichtsreduktion kann 1 mmHg systolische Senkung erwartet werden. Bewerte ob der berechnete Zusammenhang diesem klinischen Modell entspricht.
|
||||
- Analysiere die weightVsDiastolic-Korrelation analog und bewerte die klinische Plausibilität.
|
||||
- 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.`;
|
||||
}
|
||||
|
||||
export function getBloodPressureUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Analysiere die folgenden Blutdruck-Daten mit Fokus auf Trends, Zielwerterreichung und Medikamentenwirksamkeit.
|
||||
Berücksichtige die Messzeiträume und Datendichte für die Konfidenzeinschätzung.
|
||||
Nutze die vorberechneten Korrelationen (correlations) und den historischen Vergleich (historicalComparison) für eine fundierte temporale Analyse.
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -10,7 +10,10 @@ FACHSPEZIFISCH — GESAMTBEWERTUNG:
|
||||
- Risikostratifizierung: Bewerte das kardiovaskuläre Gesamtrisiko basierend auf der Kombination aller Faktoren.
|
||||
- Medikamenten-Adhärenz: Gesamteinnahmetreue bewerten und Zusammenhang mit Vitalwerten benennen.
|
||||
- Stimmung: Falls vorhanden, als kontextuellen Faktor einbeziehen (nicht übergewichten).
|
||||
- Positive Entwicklungen explizit hervorheben — Motivation ist therapeutisch relevant.`;
|
||||
- Positive Entwicklungen explizit hervorheben — Motivation ist therapeutisch relevant.
|
||||
- Nutze die correlations-Daten um Zusammenhänge zwischen Gewicht, Blutdruck, Puls und Stimmung zu identifizieren. 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.`;
|
||||
}
|
||||
|
||||
export function getGeneralStatusUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
@@ -32,6 +35,7 @@ export function buildGeneratePrompts(featuresJson: string, privacyMode: string):
|
||||
userPrompt: `Analysiere die folgenden Gesundheitsdaten.
|
||||
Datenmodus: ${modeLabel}
|
||||
Beachte die coverage-Objekte jeder Metrik für Informationen zu Messhäufigkeit und Zeiträumen.
|
||||
Nutze die correlations-Daten für Kreuzanalysen und historicalComparison für temporale Einordnung.
|
||||
|
||||
${featuresJson}`,
|
||||
};
|
||||
|
||||
@@ -15,12 +15,16 @@ FACHSPEZIFISCH — MEDIKAMENTEN-ADHÄRENZ:
|
||||
- Wirksamkeits-Korrelation: Direkte Verbindung zwischen Einnahmetreue und Vitalwert-Veränderungen herstellen.
|
||||
* Beispiel: "In Wochen mit > 90% Einnahmetreue war der mittlere systolische RR 5 mmHg niedriger."
|
||||
- Verpasste Dosen: Konsequenzen differenzieren (Antihypertensiva-Rebound vs. Statine = weniger zeitkritisch).
|
||||
- Motivierende Gesprächsführung: Positive Formulierungen bevorzugen, Fortschritte anerkennen.`;
|
||||
- Motivierende Gesprächsführung: Positive Formulierungen bevorzugen, Fortschritte anerkennen.
|
||||
- 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."
|
||||
- Nutze die vorberechneten Korrelationen (correlations) und historicalComparison um den Einfluss der Adhärenz auf Blutdruck und Puls zu quantifizieren.
|
||||
- Falls Stimmungsdaten verfügbar: Prüfe ob niedrige Adhärenz mit schlechterer Stimmung korreliert.`;
|
||||
}
|
||||
|
||||
export function getMedicationComplianceUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Analysiere die Medikamenten-Einnahmetreue mit Fokus auf Muster, Wirksamkeitskorrelation und konkrete Verbesserungsvorschläge.
|
||||
Nutze die Korrelationsdaten und den historischen Vergleich um den Zusammenhang zwischen Adhärenz und Vitalwerten zu belegen.
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -14,12 +14,17 @@ FACHSPEZIFISCH — PULS/HERZFREQUENZ:
|
||||
- Erholung: Sinkender Ruhepuls über Wochen deutet auf verbesserte kardiovaskuläre Fitness hin.
|
||||
- Medikamenten-Einfluss: Betablocker senken die Herzfrequenz — Compliance-Korrelation prüfen.
|
||||
- Blutdruck-Korrelation: Puls × systolischen Druck = Doppelprodukt als Herzbelastungsindikator.
|
||||
- Stimmungs-Korrelation: Chronischer Stress kann Ruhepuls erhöhen.`;
|
||||
- Stimmungs-Korrelation: Chronischer Stress kann Ruhepuls erhöhen.
|
||||
- Nutze die moodVsPulse-Korrelation um Stress-Einfluss auf den Ruhepuls einzuschätzen. Bei r > 0.3 (negative Korrelation = schlechtere Stimmung → höherer Puls) den Zusammenhang klinisch einordnen.
|
||||
- Nutze die pulseVsSystolic-Korrelation für die 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.`;
|
||||
}
|
||||
|
||||
export function getPulseUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Analysiere die Puls-/Herzfrequenzdaten mit Fokus auf Ruhepulstrend, Variabilität und Zusammenhänge mit Medikation und Stimmung.
|
||||
Nutze die vorberechneten Korrelationen und den historischen Vergleich für eine fundierte Analyse.
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -11,12 +11,16 @@ FACHSPEZIFISCH — GEWICHT:
|
||||
- Realistische Zielprojektion: Maximal 0.5-1.0 kg/Woche als nachhaltiger Gewichtsverlust (DGE-Empfehlung).
|
||||
- Gewichts-Blutdruck-Korrelation: Pro kg Gewichtsreduktion ca. 1 mmHg systolische Senkung.
|
||||
- Medikamenten-Einfluss: Gewichtsrelevante Medikamente identifizieren (z.B. Betablocker, Cortison).
|
||||
- Tageszeit-Schwankungen: Morgen- vs. Abendmessungen differenzieren (1-2 kg normal).`;
|
||||
- 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.`;
|
||||
}
|
||||
|
||||
export function getWeightUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Analysiere die Gewichtsentwicklung mit Fokus auf Trends, BMI-Klassifikation und Zusammenhang mit anderen Vitalwerten.
|
||||
Nutze die temporalen Schichten (avg7, avg30, avg90, allTime) und den historischen Vergleich für eine differenzierte Bewertung.
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -5,8 +5,14 @@
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
import { summarize } from "@/lib/analytics/trends";
|
||||
import type { DataPoint } from "@/lib/analytics/trends";
|
||||
import { calculateCompliance } from "@/lib/analytics/compliance";
|
||||
import { getBpTargets } from "@/lib/analytics/bp-targets";
|
||||
import {
|
||||
pairByTimestamp,
|
||||
pearsonCorrelation,
|
||||
type CorrelationResult,
|
||||
} from "@/lib/analytics/correlations";
|
||||
|
||||
interface DataCoverage {
|
||||
count: number;
|
||||
@@ -21,6 +27,10 @@ export interface AggregatedFeatures {
|
||||
latest: number;
|
||||
avg7: number | null;
|
||||
avg30: number | null;
|
||||
avg90: number | null;
|
||||
allTimeAvg: number | null;
|
||||
allTimeMin: number | null;
|
||||
allTimeMax: number | null;
|
||||
slope30: number | null;
|
||||
outlierCount: number;
|
||||
bmi: number | null;
|
||||
@@ -29,13 +39,26 @@ export interface AggregatedFeatures {
|
||||
bloodPressure?: {
|
||||
avgSys30: number | null;
|
||||
avgDia30: number | null;
|
||||
avgSys90: number | null;
|
||||
avgDia90: number | null;
|
||||
allTimeAvgSys: number | null;
|
||||
allTimeAvgDia: number | null;
|
||||
allTimeMinSys: number | null;
|
||||
allTimeMaxSys: number | null;
|
||||
allTimeMinDia: number | null;
|
||||
allTimeMaxDia: number | null;
|
||||
slopeSys30: number | null;
|
||||
slopeDia30: number | null;
|
||||
pctInTarget: number | null;
|
||||
coverage: DataCoverage;
|
||||
};
|
||||
pulse?: {
|
||||
avg7: number | null;
|
||||
avg30: number | null;
|
||||
avg90: number | null;
|
||||
allTimeAvg: number | null;
|
||||
allTimeMin: number | null;
|
||||
allTimeMax: number | null;
|
||||
slope30: number | null;
|
||||
anomalyCount: number;
|
||||
coverage: DataCoverage;
|
||||
@@ -46,6 +69,43 @@ export interface AggregatedFeatures {
|
||||
slope30: number | null;
|
||||
coverage: DataCoverage;
|
||||
};
|
||||
mood?: {
|
||||
avg7: number | null;
|
||||
avg30: number | null;
|
||||
latest: number | null;
|
||||
trend30: "improving" | "declining" | "stable" | null;
|
||||
totalEntries: number;
|
||||
coverage: DataCoverage;
|
||||
};
|
||||
correlations?: {
|
||||
weightVsSystolic: CorrelationResult | null;
|
||||
weightVsDiastolic: CorrelationResult | null;
|
||||
pulseVsSystolic: CorrelationResult | null;
|
||||
moodVsPulse: CorrelationResult | null;
|
||||
moodVsSystolic: CorrelationResult | null;
|
||||
};
|
||||
historicalComparison?: {
|
||||
weight?: {
|
||||
current7dAvg: number | null;
|
||||
previous30dAvg: number | null;
|
||||
change: number | null;
|
||||
};
|
||||
systolic?: {
|
||||
current7dAvg: number | null;
|
||||
previous30dAvg: number | null;
|
||||
change: number | null;
|
||||
};
|
||||
diastolic?: {
|
||||
current7dAvg: number | null;
|
||||
previous30dAvg: number | null;
|
||||
change: number | null;
|
||||
};
|
||||
pulse?: {
|
||||
current7dAvg: number | null;
|
||||
previous30dAvg: number | null;
|
||||
change: number | null;
|
||||
};
|
||||
};
|
||||
medications?: Array<{
|
||||
name: string;
|
||||
dose: string;
|
||||
@@ -61,6 +121,8 @@ export interface AggregatedFeatures {
|
||||
dataSpanDays: number;
|
||||
oldestMeasurementDaysAgo: number | null;
|
||||
newestMeasurementDaysAgo: number | null;
|
||||
ageYears: number | null;
|
||||
gender: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,7 +136,7 @@ export interface RawFeatures extends AggregatedFeatures {
|
||||
|
||||
function toDataPoints(
|
||||
records: Array<{ value: number; measuredAt: Date }>,
|
||||
): Array<{ date: Date; value: number }> {
|
||||
): DataPoint[] {
|
||||
return records.map((r) => ({ date: r.measuredAt, value: r.value }));
|
||||
}
|
||||
|
||||
@@ -107,6 +169,38 @@ function computeCoverage(
|
||||
};
|
||||
}
|
||||
|
||||
/** Compute average of values within a time window (days ago from now). */
|
||||
function avgInWindow(
|
||||
records: Array<{ value: number; measuredAt: Date }>,
|
||||
now: number,
|
||||
fromDaysAgo: number,
|
||||
toDaysAgo: number = 0,
|
||||
): number | null {
|
||||
const fromMs = now - fromDaysAgo * 24 * 60 * 60 * 1000;
|
||||
const toMs = now - toDaysAgo * 24 * 60 * 60 * 1000;
|
||||
const filtered = records.filter((r) => {
|
||||
const t = r.measuredAt.getTime();
|
||||
return t >= fromMs && t <= toMs;
|
||||
});
|
||||
if (filtered.length === 0) return null;
|
||||
const sum = filtered.reduce((s, r) => s + r.value, 0);
|
||||
return Math.round((sum / filtered.length) * 100) / 100;
|
||||
}
|
||||
|
||||
/** Compute historical comparison: current 7d avg vs previous 30d avg (days 7-37). */
|
||||
function computeHistoricalComparison(
|
||||
records: Array<{ value: number; measuredAt: Date }>,
|
||||
now: number,
|
||||
): { current7dAvg: number | null; previous30dAvg: number | null; change: number | null } {
|
||||
const current7dAvg = avgInWindow(records, now, 7, 0);
|
||||
const previous30dAvg = avgInWindow(records, now, 37, 7);
|
||||
const change =
|
||||
current7dAvg !== null && previous30dAvg !== null
|
||||
? Math.round((current7dAvg - previous30dAvg) * 100) / 100
|
||||
: null;
|
||||
return { current7dAvg, previous30dAvg, change };
|
||||
}
|
||||
|
||||
export async function extractFeatures(
|
||||
userId: string,
|
||||
includeRaw: boolean,
|
||||
@@ -116,6 +210,7 @@ export async function extractFeatures(
|
||||
select: {
|
||||
heightCm: true,
|
||||
dateOfBirth: true,
|
||||
gender: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -146,6 +241,18 @@ export async function extractFeatures(
|
||||
)
|
||||
: 0;
|
||||
|
||||
// Compute age
|
||||
let ageYears: number | null = null;
|
||||
if (user?.dateOfBirth) {
|
||||
const dob = user.dateOfBirth;
|
||||
const today = new Date();
|
||||
ageYears = today.getFullYear() - dob.getFullYear();
|
||||
const monthDiff = today.getMonth() - dob.getMonth();
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) {
|
||||
ageYears--;
|
||||
}
|
||||
}
|
||||
|
||||
const features: AggregatedFeatures = {
|
||||
context: {
|
||||
heightCm: user?.heightCm ?? null,
|
||||
@@ -162,6 +269,8 @@ export async function extractFeatures(
|
||||
(now - newestMeasurement.getTime()) / (24 * 60 * 60 * 1000),
|
||||
)
|
||||
: null,
|
||||
ageYears,
|
||||
gender: user?.gender ?? null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -178,6 +287,10 @@ export async function extractFeatures(
|
||||
latest: summary.latest!,
|
||||
avg7: summary.avg7,
|
||||
avg30: summary.avg30,
|
||||
avg90: avgInWindow(weightData, now, 90),
|
||||
allTimeAvg: summary.count > 0 ? summary.mean : null,
|
||||
allTimeMin: summary.count > 0 ? summary.min : null,
|
||||
allTimeMax: summary.count > 0 ? summary.max : null,
|
||||
slope30: summary.slope30?.slope ?? null,
|
||||
outlierCount: summary.anomalyCount,
|
||||
bmi,
|
||||
@@ -211,6 +324,14 @@ export async function extractFeatures(
|
||||
features.bloodPressure = {
|
||||
avgSys30: sysSummary?.avg30 ?? null,
|
||||
avgDia30: diaSummary?.avg30 ?? null,
|
||||
avgSys90: sysData.length > 0 ? avgInWindow(sysData, now, 90) : null,
|
||||
avgDia90: diaData.length > 0 ? avgInWindow(diaData, now, 90) : null,
|
||||
allTimeAvgSys: sysSummary?.count ? sysSummary.mean : null,
|
||||
allTimeAvgDia: diaSummary?.count ? diaSummary.mean : null,
|
||||
allTimeMinSys: sysSummary?.count ? sysSummary.min : null,
|
||||
allTimeMaxSys: sysSummary?.count ? sysSummary.max : null,
|
||||
allTimeMinDia: diaSummary?.count ? diaSummary.min : null,
|
||||
allTimeMaxDia: diaSummary?.count ? diaSummary.max : null,
|
||||
slopeSys30: sysSummary?.slope30?.slope ?? null,
|
||||
slopeDia30: diaSummary?.slope30?.slope ?? null,
|
||||
pctInTarget,
|
||||
@@ -228,7 +349,12 @@ export async function extractFeatures(
|
||||
if (pulseData.length > 0) {
|
||||
const summary = summarize(toDataPoints(pulseData));
|
||||
features.pulse = {
|
||||
avg7: summary.avg7,
|
||||
avg30: summary.avg30,
|
||||
avg90: avgInWindow(pulseData, now, 90),
|
||||
allTimeAvg: summary.count > 0 ? summary.mean : null,
|
||||
allTimeMin: summary.count > 0 ? summary.min : null,
|
||||
allTimeMax: summary.count > 0 ? summary.max : null,
|
||||
slope30: summary.slope30?.slope ?? null,
|
||||
anomalyCount: summary.anomalyCount,
|
||||
coverage: computeCoverage(pulseData, now),
|
||||
@@ -247,6 +373,113 @@ export async function extractFeatures(
|
||||
};
|
||||
}
|
||||
|
||||
// Mood
|
||||
const moodEntries = await prisma.moodEntry.findMany({
|
||||
where: { userId },
|
||||
orderBy: { moodLoggedAt: "asc" },
|
||||
});
|
||||
|
||||
if (moodEntries.length > 0) {
|
||||
const moodNow = Date.now();
|
||||
const last7 = moodEntries.filter(
|
||||
(e) => moodNow - e.moodLoggedAt.getTime() < 7 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const last30 = moodEntries.filter(
|
||||
(e) => moodNow - e.moodLoggedAt.getTime() < 30 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
|
||||
const avg = (entries: typeof moodEntries) =>
|
||||
entries.length > 0
|
||||
? Math.round(
|
||||
(entries.reduce((s, e) => s + e.score, 0) / entries.length) * 100,
|
||||
) / 100
|
||||
: null;
|
||||
|
||||
// Compute 30-day trend: compare first half vs second half of last 30 days
|
||||
let trend30: "improving" | "declining" | "stable" | null = null;
|
||||
if (last30.length >= 4) {
|
||||
const firstHalf = last30.filter(
|
||||
(e) => moodNow - e.moodLoggedAt.getTime() >= 15 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const secondHalf = last30.filter(
|
||||
(e) => moodNow - e.moodLoggedAt.getTime() < 15 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
if (firstHalf.length >= 2 && secondHalf.length >= 2) {
|
||||
const avgFirst = avg(firstHalf)!;
|
||||
const avgSecond = avg(secondHalf)!;
|
||||
const diff = avgSecond - avgFirst;
|
||||
if (diff > 0.3) trend30 = "improving";
|
||||
else if (diff < -0.3) trend30 = "declining";
|
||||
else trend30 = "stable";
|
||||
}
|
||||
}
|
||||
|
||||
const oldest = moodEntries[0].moodLoggedAt;
|
||||
const newest = moodEntries[moodEntries.length - 1].moodLoggedAt;
|
||||
const spanDays = Math.round(
|
||||
(newest.getTime() - oldest.getTime()) / (24 * 60 * 60 * 1000),
|
||||
);
|
||||
const avgDaysBetween =
|
||||
moodEntries.length > 1
|
||||
? Math.round((spanDays / (moodEntries.length - 1)) * 10) / 10
|
||||
: null;
|
||||
|
||||
features.mood = {
|
||||
avg7: avg(last7),
|
||||
avg30: avg(last30),
|
||||
latest: moodEntries[moodEntries.length - 1].score,
|
||||
trend30,
|
||||
totalEntries: moodEntries.length,
|
||||
coverage: {
|
||||
count: moodEntries.length,
|
||||
spanDays,
|
||||
avgDaysBetween,
|
||||
oldestDaysAgo: Math.round(
|
||||
(moodNow - oldest.getTime()) / (24 * 60 * 60 * 1000),
|
||||
),
|
||||
newestDaysAgo: Math.round(
|
||||
(moodNow - newest.getTime()) / (24 * 60 * 60 * 1000),
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Cross-metric correlations
|
||||
const weightPoints = toDataPoints(weightData);
|
||||
const sysPoints = toDataPoints(sysData);
|
||||
const diaPoints = toDataPoints(diaData);
|
||||
const pulsePoints = toDataPoints(pulseData);
|
||||
const moodPoints: DataPoint[] = moodEntries.map((e) => ({
|
||||
date: e.moodLoggedAt,
|
||||
value: e.score,
|
||||
}));
|
||||
|
||||
const computeCorr = (a: DataPoint[], b: DataPoint[]) =>
|
||||
pearsonCorrelation(pairByTimestamp(a, b));
|
||||
|
||||
features.correlations = {
|
||||
weightVsSystolic: computeCorr(weightPoints, sysPoints),
|
||||
weightVsDiastolic: computeCorr(weightPoints, diaPoints),
|
||||
pulseVsSystolic: computeCorr(pulsePoints, sysPoints),
|
||||
moodVsPulse: computeCorr(moodPoints, pulsePoints),
|
||||
moodVsSystolic: computeCorr(moodPoints, sysPoints),
|
||||
};
|
||||
|
||||
// Historical comparison: current 7d avg vs previous 30d avg (days 7-37)
|
||||
features.historicalComparison = {};
|
||||
if (weightData.length > 0) {
|
||||
features.historicalComparison.weight = computeHistoricalComparison(weightData, now);
|
||||
}
|
||||
if (sysData.length > 0) {
|
||||
features.historicalComparison.systolic = computeHistoricalComparison(sysData, now);
|
||||
}
|
||||
if (diaData.length > 0) {
|
||||
features.historicalComparison.diastolic = computeHistoricalComparison(diaData, now);
|
||||
}
|
||||
if (pulseData.length > 0) {
|
||||
features.historicalComparison.pulse = computeHistoricalComparison(pulseData, now);
|
||||
}
|
||||
|
||||
// Medications
|
||||
const medications = await prisma.medication.findMany({
|
||||
where: { userId, active: true },
|
||||
|
||||
Reference in new issue
Block a user