| 1 | import { prisma } from "@app/lib/prisma";
|
| 2 | import { InsightType, InsightPeriod } from "@prisma/client";
|
| 3 | import { InsightPeriodPayload } from "@app/lib/queue/types";
|
| 4 |
|
| 5 | // Pearson correlation coefficient for two paired samples, in [-1, 1]. Returns
|
| 6 | // 0 when there are fewer than two samples or either axis has no variance, so
|
| 7 | // an absent signal reads as no correlation rather than a divide by zero.
|
| 8 | export function pearson(xs: number[], ys: number[]): number {
|
| 9 | const n = xs.length;
|
| 10 | if (n < 2) return 0;
|
| 11 |
|
| 12 | const meanX = xs.reduce((a, b) => a + b, 0) / n;
|
| 13 | const meanY = ys.reduce((a, b) => a + b, 0) / n;
|
| 14 |
|
| 15 | const num = xs.reduce((sum, x, i) => sum + (x - meanX) * (ys[i] - meanY), 0);
|
| 16 | const denX = Math.sqrt(xs.reduce((sum, x) => sum + (x - meanX) ** 2, 0));
|
| 17 | const denY = Math.sqrt(ys.reduce((sum, y) => sum + (y - meanY) ** 2, 0));
|
| 18 |
|
| 19 | if (denX === 0 || denY === 0) return 0;
|
| 20 | return num / (denX * denY);
|
| 21 | }
|
| 22 |
|
| 23 | export function energySleepBody(score: number, sampleSize: number): string {
|
| 24 | const base = sampleSize < 5 ? " (dados limitados, continue registrando)" : "";
|
| 25 |
|
| 26 | if (score > 0.6)
|
| 27 | return `Seu sono tem forte impacto positivo na sua energia no dia seguinte.${base}`;
|
| 28 | if (score > 0.3)
|
| 29 | return `Seu sono tem impacto moderado na sua energia no dia seguinte.${base}`;
|
| 30 | if (score < -0.3)
|
| 31 | return `Curiosamente, mais sono está associado a menos energia — vale investigar.${base}`;
|
| 32 | return `Não encontramos uma relação clara entre seu sono e energia ainda.${base}`;
|
| 33 | }
|
| 34 |
|
| 35 | export async function processEnergySleep({
|
| 36 | userId,
|
| 37 | periodStart,
|
| 38 | periodEnd,
|
| 39 | }: InsightPeriodPayload): Promise<void> {
|
| 40 | const start = new Date(periodStart);
|
| 41 | const end = new Date(periodEnd);
|
| 42 |
|
| 43 | const sleepRecords = await prisma.sleepRecord.findMany({
|
| 44 | where: { userId, date: { gte: start, lte: end } },
|
| 45 | orderBy: { date: "asc" },
|
| 46 | });
|
| 47 |
|
| 48 | if (sleepRecords.length < 3) return;
|
| 49 |
|
| 50 | // For each sleep record, find the closest mood entry on the following day
|
| 51 | // XXX: What if the user logs two sleep entries on the same day?
|
| 52 | const pairs: Array<{ sleep: number; energy: number }> = [];
|
| 53 |
|
| 54 | for (const record of sleepRecords) {
|
| 55 | const nextDayStart = new Date(record.date);
|
| 56 | nextDayStart.setDate(nextDayStart.getDate() + 1);
|
| 57 | const nextDayEnd = new Date(nextDayStart);
|
| 58 | nextDayEnd.setDate(nextDayEnd.getDate() + 1);
|
| 59 |
|
| 60 | const mood = await prisma.mood.findFirst({
|
| 61 | where: { userId, moment: { gte: nextDayStart, lt: nextDayEnd } },
|
| 62 | orderBy: { moment: "asc" },
|
| 63 | select: { energyLevel: true },
|
| 64 | });
|
| 65 |
|
| 66 | if (mood) {
|
| 67 | pairs.push({ sleep: record.average, energy: mood.energyLevel });
|
| 68 | }
|
| 69 | }
|
| 70 |
|
| 71 | if (pairs.length < 3) return;
|
| 72 |
|
| 73 | const sleepValues = pairs.map((p) => p.sleep);
|
| 74 | const energyValues = pairs.map((p) => p.energy);
|
| 75 | const score = parseFloat(pearson(sleepValues, energyValues).toFixed(3));
|
| 76 |
|
| 77 | await prisma.insight.upsert({
|
| 78 | where: {
|
| 79 | userId_type_periodStart: {
|
| 80 | userId,
|
| 81 | type: InsightType.ENERGY_SLEEP_CORRELATION,
|
| 82 | periodStart: start,
|
| 83 | },
|
| 84 | },
|
| 85 | update: {
|
| 86 | body: energySleepBody(score, pairs.length),
|
| 87 | metadata: { correlationScore: score, sampleSize: pairs.length },
|
| 88 | generatedAt: new Date(),
|
| 89 | },
|
| 90 | create: {
|
| 91 | userId,
|
| 92 | type: InsightType.ENERGY_SLEEP_CORRELATION,
|
| 93 | period: InsightPeriod.WEEKLY,
|
| 94 | title: "Sono e Energia",
|
| 95 | body: energySleepBody(score, pairs.length),
|
| 96 | metadata: { correlationScore: score, sampleSize: pairs.length },
|
| 97 | periodStart: start,
|
| 98 | periodEnd: end,
|
| 99 | },
|
| 100 | });
|
| 101 | }
|