compararGoldenSet.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import fs from "node:fs";
  2. import path from "node:path";
  3. import { fileURLToPath } from "node:url";
  4. import { AtendimentoGoldenLabel } from "../src/models/AtendimentoGoldenLabel.model.js";
  5. import { media, desvioPadrao, correlacaoPearson } from "./utils/estatistica.js";
  6. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  7. const setorIdx = process.argv.indexOf("--setor");
  8. const SETOR = setorIdx > -1 ? process.argv[setorIdx + 1] : null;
  9. const CAMPOS_CATEGORICOS = ["Resolvido", "Sentimento", "StatusCancelamento", "HorarioVisitaInformado", "VisitaAgendada"];
  10. // usa o snapshot da IA congelado em atendimento_golden_labels (Ia*) no momento em que
  11. // cada item foi rotulado — não junta com atendimento_avaliacoes ao vivo de propósito,
  12. // pra comparação nunca ser afetada por reavaliações posteriores do mesmo atendimento
  13. async function buscarPares() {
  14. let query = AtendimentoGoldenLabel.query()
  15. .where("atendimento_golden_labels.Status", "rotulado")
  16. .whereNotNull("IaSnapshotEm")
  17. .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId")
  18. .select("atendimento_golden_labels.*", "a.Codigo as Codigo", "a.Setor as Setor");
  19. if (SETOR) query = query.where("a.Setor", SETOR);
  20. return query;
  21. }
  22. // Jaccard entre dois conjuntos de strings (comparação textual simples, não crítica)
  23. function jaccard(a, b) {
  24. const setA = new Set((a ?? []).map((s) => s.toLowerCase().trim()));
  25. const setB = new Set((b ?? []).map((s) => s.toLowerCase().trim()));
  26. if (setA.size === 0 && setB.size === 0) return 1;
  27. const intersecao = [...setA].filter((x) => setB.has(x)).length;
  28. const uniao = new Set([...setA, ...setB]).size;
  29. return uniao === 0 ? 1 : intersecao / uniao;
  30. }
  31. function metricasCategoricas(pares) {
  32. const resultado = {};
  33. for (const campo of CAMPOS_CATEGORICOS) {
  34. const campoIa = `Ia${campo}`;
  35. const comparaveis = pares.filter((p) => p[campo] !== null && p[campo] !== undefined);
  36. const matriz = {};
  37. let acertos = 0;
  38. for (const p of comparaveis) {
  39. const humano = String(p[campo]);
  40. const ia = String(p[campoIa]);
  41. matriz[humano] = matriz[humano] ?? {};
  42. matriz[humano][ia] = (matriz[humano][ia] ?? 0) + 1;
  43. if (humano === ia) acertos += 1;
  44. }
  45. resultado[campo] = {
  46. amostra: comparaveis.length,
  47. exactMatchPercent: comparaveis.length ? Number(((acertos / comparaveis.length) * 100).toFixed(1)) : null,
  48. matrizConfusao: matriz
  49. };
  50. }
  51. return resultado;
  52. }
  53. function metricasScore(pares) {
  54. const comparaveis = pares.filter((p) => p.ScoreAtendente !== null && p.IaScoreAtendente !== null);
  55. if (comparaveis.length === 0) {
  56. return { amostra: 0, mae: null, correlacao: null, distribuicaoDiferenca: null };
  57. }
  58. const diferencas = comparaveis.map((p) => Math.abs(p.ScoreAtendente - p.IaScoreAtendente));
  59. const mae = Number(media(diferencas).toFixed(2));
  60. const correlacao = correlacaoPearson(
  61. comparaveis.map((p) => p.ScoreAtendente),
  62. comparaveis.map((p) => p.IaScoreAtendente)
  63. );
  64. const distribuicaoDiferenca = { diferenca0: 0, diferenca1: 0, diferenca2: 0, diferenca3mais: 0 };
  65. for (const d of diferencas) {
  66. if (d === 0) distribuicaoDiferenca.diferenca0 += 1;
  67. else if (d === 1) distribuicaoDiferenca.diferenca1 += 1;
  68. else if (d === 2) distribuicaoDiferenca.diferenca2 += 1;
  69. else distribuicaoDiferenca.diferenca3mais += 1;
  70. }
  71. return {
  72. amostra: comparaveis.length,
  73. mae,
  74. desvioPadraoDiferenca: Number(desvioPadrao(diferencas).toFixed(2)),
  75. correlacao: correlacao !== null ? Number(correlacao.toFixed(2)) : null,
  76. distribuicaoDiferenca
  77. };
  78. }
  79. function metricasListas(pares) {
  80. const comAmbos = pares.filter((p) => (p.Atendentes?.length || p.IaAtendentes?.length));
  81. const jaccardMedioAtendentes = comAmbos.length
  82. ? Number(media(comAmbos.map((p) => jaccard(p.Atendentes, p.IaAtendentes))).toFixed(2))
  83. : null;
  84. const comAmbosSugestoes = pares.filter((p) => (p.Sugestoes?.length || p.IaSugestoes?.length));
  85. const jaccardMedioSugestoes = comAmbosSugestoes.length
  86. ? Number(media(comAmbosSugestoes.map((p) => jaccard(p.Sugestoes, p.IaSugestoes))).toFixed(2))
  87. : null;
  88. return {
  89. atendentes: { amostra: comAmbos.length, jaccardMedio: jaccardMedioAtendentes },
  90. sugestoes: { amostra: comAmbosSugestoes.length, jaccardMedio: jaccardMedioSugestoes }
  91. };
  92. }
  93. async function main() {
  94. const pares = await buscarPares();
  95. console.log(`[comparar-golden-set] ${pares.length} atendimentos rotulados${SETOR ? ` no setor ${SETOR}` : ""} comparados contra a avaliação da IA.`);
  96. if (pares.length === 0) {
  97. console.log("[comparar-golden-set] nada para comparar ainda — rotule alguns atendimentos no golden set primeiro.");
  98. process.exit(0);
  99. }
  100. const categoricas = metricasCategoricas(pares);
  101. const score = metricasScore(pares);
  102. const listas = metricasListas(pares);
  103. console.log("\n=== Score do atendente ===");
  104. console.log(`Amostra: ${score.amostra} | MAE: ${score.mae} | desvio da diferença: ${score.desvioPadraoDiferenca} | correlação: ${score.correlacao}`);
  105. if (score.distribuicaoDiferenca) {
  106. console.log(
  107. `Distribuição da diferença |humano - IA|: 0 = ${score.distribuicaoDiferenca.diferenca0} | 1 = ${score.distribuicaoDiferenca.diferenca1} | 2 = ${score.distribuicaoDiferenca.diferenca2} | ≥3 = ${score.distribuicaoDiferenca.diferenca3mais}`
  108. );
  109. }
  110. console.log("\n=== Campos categóricos (exact-match humano vs. IA) ===");
  111. for (const [campo, m] of Object.entries(categoricas)) {
  112. console.log(` ${campo}: ${m.exactMatchPercent}% (amostra: ${m.amostra})`);
  113. }
  114. console.log("\n=== Listas (similaridade de Jaccard, informativo) ===");
  115. console.log(` Atendentes: ${listas.atendentes.jaccardMedio} (amostra: ${listas.atendentes.amostra})`);
  116. console.log(` Sugestões: ${listas.sugestoes.jaccardMedio} (amostra: ${listas.sugestoes.amostra})`);
  117. const backupDir = path.resolve(__dirname, "logs");
  118. fs.mkdirSync(backupDir, { recursive: true });
  119. const backupPath = path.join(backupDir, `comparar-golden-set-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
  120. fs.writeFileSync(
  121. backupPath,
  122. JSON.stringify(
  123. {
  124. config: { setor: SETOR },
  125. resumo: { score, categoricas, listas },
  126. detalhes: pares.map((p) => ({
  127. atendimentoId: p.AtendimentoId,
  128. codigo: p.Codigo,
  129. setor: p.Setor,
  130. humano: {
  131. ScoreAtendente: p.ScoreAtendente,
  132. Resolvido: p.Resolvido,
  133. Sentimento: p.Sentimento,
  134. StatusCancelamento: p.StatusCancelamento,
  135. HorarioVisitaInformado: p.HorarioVisitaInformado,
  136. VisitaAgendada: p.VisitaAgendada,
  137. Atendentes: p.Atendentes,
  138. Sugestoes: p.Sugestoes
  139. },
  140. ia: {
  141. ScoreAtendente: p.IaScoreAtendente,
  142. Resolvido: p.IaResolvido,
  143. Sentimento: p.IaSentimento,
  144. StatusCancelamento: p.IaStatusCancelamento,
  145. HorarioVisitaInformado: p.IaHorarioVisitaInformado,
  146. VisitaAgendada: p.IaVisitaAgendada,
  147. Atendentes: p.IaAtendentes,
  148. Sugestoes: p.IaSugestoes
  149. }
  150. }))
  151. },
  152. null,
  153. 2
  154. )
  155. );
  156. console.log(`\n[comparar-golden-set] resultado completo salvo em ${backupPath}`);
  157. process.exit(0);
  158. }
  159. main().catch((err) => {
  160. console.error("[comparar-golden-set] falha fatal:", err);
  161. process.exit(1);
  162. });