eletrotupi / tcc/ api/src/lib/queue/processors/insights/energySleep.ts4e5ac8210eca36b8c2a4073c8eaac1d4998d8039
3.3 KB | 100 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, returns value in [-1, 1]
6
// TODO: Document this more and write to thesis
7
function pearson(xs: number[], ys: number[]): number {
8
  const n = xs.length;
9
  if (n < 2) return 0;
10

11
  const meanX = xs.reduce((a, b) => a + b, 0) / n;
12
  const meanY = ys.reduce((a, b) => a + b, 0) / n;
13

14
  const num = xs.reduce((sum, x, i) => sum + (x - meanX) * (ys[i] - meanY), 0);
15
  const denX = Math.sqrt(xs.reduce((sum, x) => sum + (x - meanX) ** 2, 0));
16
  const denY = Math.sqrt(ys.reduce((sum, y) => sum + (y - meanY) ** 2, 0));
17

18
  if (denX === 0 || denY === 0) return 0;
19
  return num / (denX * denY);
20
}
21

22
export async function processEnergySleep({
23
  userId,
24
  periodStart,
25
  periodEnd,
26
}: InsightPeriodPayload): Promise<void> {
27
  const start = new Date(periodStart);
28
  const end = new Date(periodEnd);
29

30
  const sleepRecords = await prisma.sleepRecord.findMany({
31
    where: { userId, date: { gte: start, lte: end } },
32
    orderBy: { date: "asc" },
33
  });
34

35
  if (sleepRecords.length < 3) return;
36

37
  // For each sleep record, find the closest mood entry on the following day
38
  // XXX: What if the user logs two sleep entries on the same day?
39
  const pairs: Array<{ sleep: number; energy: number }> = [];
40

41
  for (const record of sleepRecords) {
42
    const nextDayStart = new Date(record.date);
43
    nextDayStart.setDate(nextDayStart.getDate() + 1);
44
    const nextDayEnd = new Date(nextDayStart);
45
    nextDayEnd.setDate(nextDayEnd.getDate() + 1);
46

47
    const mood = await prisma.mood.findFirst({
48
      where: { userId, moment: { gte: nextDayStart, lt: nextDayEnd } },
49
      orderBy: { moment: "asc" },
50
      select: { energyLevel: true },
51
    });
52

53
    if (mood) {
54
      pairs.push({ sleep: record.average, energy: mood.energyLevel });
55
    }
56
  }
57

58
  if (pairs.length < 3) return;
59

60
  const sleepValues = pairs.map((p) => p.sleep);
61
  const energyValues = pairs.map((p) => p.energy);
62
  const score = parseFloat(pearson(sleepValues, energyValues).toFixed(3));
63

64
  await prisma.insight.upsert({
65
    where: {
66
      userId_type_periodStart: {
67
        userId,
68
        type: InsightType.ENERGY_SLEEP_CORRELATION,
69
        periodStart: start,
70
      },
71
    },
72
    update: {
73
      body: buildBody(score, pairs.length),
74
      metadata: { correlationScore: score, sampleSize: pairs.length },
75
      generatedAt: new Date(),
76
    },
77
    create: {
78
      userId,
79
      type: InsightType.ENERGY_SLEEP_CORRELATION,
80
      period: InsightPeriod.WEEKLY,
81
      title: "Sono e Energia",
82
      body: buildBody(score, pairs.length),
83
      metadata: { correlationScore: score, sampleSize: pairs.length },
84
      periodStart: start,
85
      periodEnd: end,
86
    },
87
  });
88
}
89

90
function buildBody(score: number, sampleSize: number): string {
91
  const base = sampleSize < 5 ? " (dados limitados, continue registrando)" : "";
92

93
  if (score > 0.6)
94
    return `Seu sono tem forte impacto positivo na sua energia no dia seguinte.${base}`;
95
  if (score > 0.3)
96
    return `Seu sono tem impacto moderado na sua energia no dia seguinte.${base}`;
97
  if (score < -0.3)
98
    return `Curiosamente, mais sono está associado a menos energia — vale investigar.${base}`;
99
  return `Não encontramos uma relação clara entre seu sono e energia ainda.${base}`;
100
}