eletrotupi / tcc/ api/src/lib/queue/processors/insights/energySleep.ts7ff50aa722f838fa59d0afb52d1c2e2b96bf3e22
3.4 KB | 101 lines Raw
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
}