compararGoldenSet.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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. async function buscarPares() {
  11. let query = AtendimentoGoldenLabel.query()
  12. .where("atendimento_golden_labels.Status", "rotulado")
  13. .whereNotNull("IaSnapshotEm")
  14. .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId")
  15. .select("atendimento_golden_labels.*", "a.Codigo as Codigo", "a.Setor as Setor");
  16. if (SETOR) query = query.where("a.Setor", SETOR);
  17. return query;
  18. }
  19. async function buscarConcordanciaDireta() {
  20. let query = AtendimentoGoldenLabel.query()
  21. .whereIn("atendimento_golden_labels.Status", ["rotulado", "aceito"])
  22. .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId")
  23. .select("atendimento_golden_labels.Status as Status");
  24. if (SETOR) query = query.where("a.Setor", SETOR);
  25. const linhas = await query;
  26. const aceito = linhas.filter((l) => l.Status === "aceito").length;
  27. const rotulado = linhas.filter((l) => l.Status === "rotulado").length;
  28. const total = aceito + rotulado;
  29. return { aceito, rotulado, total, percentAceito: total ? Number(((aceito / total) * 100).toFixed(1)) : null };
  30. }
  31. function jaccard(a, b) {
  32. const setA = new Set((a ?? []).map((s) => s.toLowerCase().trim()));
  33. const setB = new Set((b ?? []).map((s) => s.toLowerCase().trim()));
  34. if (setA.size === 0 && setB.size === 0) return 1;
  35. const intersecao = [...setA].filter((x) => setB.has(x)).length;
  36. const uniao = new Set([...setA, ...setB]).size;
  37. return uniao === 0 ? 1 : intersecao / uniao;
  38. }
  39. function metricasCategoricas(pares) {
  40. const resultado = {};
  41. for (const campo of CAMPOS_CATEGORICOS) {
  42. const campoIa = `Ia${campo}`;
  43. const comparaveis = pares.filter((p) => p[campo] !== null && p[campo] !== undefined);
  44. const matriz = {};
  45. let acertos = 0;
  46. for (const p of comparaveis) {
  47. const humano = String(p[campo]);
  48. const ia = String(p[campoIa]);
  49. matriz[humano] = matriz[humano] ?? {};
  50. matriz[humano][ia] = (matriz[humano][ia] ?? 0) + 1;
  51. if (humano === ia) acertos += 1;
  52. }
  53. resultado[campo] = {
  54. amostra: comparaveis.length,
  55. exactMatchPercent: comparaveis.length ? Number(((acertos / comparaveis.length) * 100).toFixed(1)) : null,
  56. matrizConfusao: matriz
  57. };
  58. }
  59. return resultado;
  60. }
  61. function metricasScore(pares) {
  62. const comparaveis = pares.filter((p) => p.ScoreAtendente !== null && p.IaScoreAtendente !== null);
  63. if (comparaveis.length === 0) {
  64. return { amostra: 0, mae: null, correlacao: null, distribuicaoDiferenca: null };
  65. }
  66. const diferencas = comparaveis.map((p) => Math.abs(p.ScoreAtendente - p.IaScoreAtendente));
  67. const mae = Number(media(diferencas).toFixed(2));
  68. const correlacao = correlacaoPearson(
  69. comparaveis.map((p) => p.ScoreAtendente),
  70. comparaveis.map((p) => p.IaScoreAtendente)
  71. );
  72. const distribuicaoDiferenca = { diferenca0: 0, diferenca1: 0, diferenca2: 0, diferenca3mais: 0 };
  73. for (const d of diferencas) {
  74. if (d === 0) distribuicaoDiferenca.diferenca0 += 1;
  75. else if (d === 1) distribuicaoDiferenca.diferenca1 += 1;
  76. else if (d === 2) distribuicaoDiferenca.diferenca2 += 1;
  77. else distribuicaoDiferenca.diferenca3mais += 1;
  78. }
  79. return {
  80. amostra: comparaveis.length,
  81. mae,
  82. desvioPadraoDiferenca: Number(desvioPadrao(diferencas).toFixed(2)),
  83. correlacao: correlacao !== null ? Number(correlacao.toFixed(2)) : null,
  84. distribuicaoDiferenca
  85. };
  86. }
  87. function metricasListas(pares) {
  88. const comAmbos = pares.filter((p) => (p.Atendentes?.length || p.IaAtendentes?.length));
  89. const jaccardMedioAtendentes = comAmbos.length
  90. ? Number(media(comAmbos.map((p) => jaccard(p.Atendentes, p.IaAtendentes))).toFixed(2))
  91. : null;
  92. const comAmbosSugestoes = pares.filter((p) => (p.Sugestoes?.length || p.IaSugestoes?.length));
  93. const jaccardMedioSugestoes = comAmbosSugestoes.length
  94. ? Number(media(comAmbosSugestoes.map((p) => jaccard(p.Sugestoes, p.IaSugestoes))).toFixed(2))
  95. : null;
  96. return {
  97. atendentes: { amostra: comAmbos.length, jaccardMedio: jaccardMedioAtendentes },
  98. sugestoes: { amostra: comAmbosSugestoes.length, jaccardMedio: jaccardMedioSugestoes }
  99. };
  100. }
  101. async function main() {
  102. const concordancia = await buscarConcordanciaDireta();
  103. console.log(
  104. `[comparar-golden-set] concordância direta (aceitos sem correção / total revisado)${SETOR ? ` no setor ${SETOR}` : ""}: ` +
  105. `${concordancia.aceito}/${concordancia.total} (${concordancia.percentAceito}%)`
  106. );
  107. const pares = await buscarPares();
  108. console.log(`[comparar-golden-set] ${pares.length} atendimentos rotulados à mão${SETOR ? ` no setor ${SETOR}` : ""} comparados contra a avaliação da IA (aceitos não entram nessa parte).`);
  109. if (pares.length === 0) {
  110. console.log("[comparar-golden-set] nada para comparar na acurácia fina ainda — rotule alguns atendimentos à mão no golden set primeiro.");
  111. process.exit(0);
  112. }
  113. const categoricas = metricasCategoricas(pares);
  114. const score = metricasScore(pares);
  115. const listas = metricasListas(pares);
  116. console.log("\n=== Score do atendente ===");
  117. console.log(`Amostra: ${score.amostra} | MAE: ${score.mae} | desvio da diferença: ${score.desvioPadraoDiferenca} | correlação: ${score.correlacao}`);
  118. if (score.distribuicaoDiferenca) {
  119. console.log(
  120. `Distribuição da diferença |humano - IA|: 0 = ${score.distribuicaoDiferenca.diferenca0} | 1 = ${score.distribuicaoDiferenca.diferenca1} | 2 = ${score.distribuicaoDiferenca.diferenca2} | ≥3 = ${score.distribuicaoDiferenca.diferenca3mais}`
  121. );
  122. }
  123. console.log("\n=== Campos categóricos (exact-match humano vs. IA) ===");
  124. for (const [campo, m] of Object.entries(categoricas)) {
  125. console.log(` ${campo}: ${m.exactMatchPercent}% (amostra: ${m.amostra})`);
  126. }
  127. console.log("\n=== Listas (similaridade de Jaccard, informativo) ===");
  128. console.log(` Atendentes: ${listas.atendentes.jaccardMedio} (amostra: ${listas.atendentes.amostra})`);
  129. console.log(` Sugestões: ${listas.sugestoes.jaccardMedio} (amostra: ${listas.sugestoes.amostra})`);
  130. const backupDir = path.resolve(__dirname, "logs");
  131. fs.mkdirSync(backupDir, { recursive: true });
  132. const backupPath = path.join(backupDir, `comparar-golden-set-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
  133. fs.writeFileSync(
  134. backupPath,
  135. JSON.stringify(
  136. {
  137. config: { setor: SETOR },
  138. resumo: { concordancia, score, categoricas, listas },
  139. detalhes: pares.map((p) => ({
  140. atendimentoId: p.AtendimentoId,
  141. codigo: p.Codigo,
  142. setor: p.Setor,
  143. humano: {
  144. ScoreAtendente: p.ScoreAtendente,
  145. Resolvido: p.Resolvido,
  146. Sentimento: p.Sentimento,
  147. StatusCancelamento: p.StatusCancelamento,
  148. HorarioVisitaInformado: p.HorarioVisitaInformado,
  149. VisitaAgendada: p.VisitaAgendada,
  150. Atendentes: p.Atendentes,
  151. Sugestoes: p.Sugestoes
  152. },
  153. ia: {
  154. ScoreAtendente: p.IaScoreAtendente,
  155. Resolvido: p.IaResolvido,
  156. Sentimento: p.IaSentimento,
  157. StatusCancelamento: p.IaStatusCancelamento,
  158. HorarioVisitaInformado: p.IaHorarioVisitaInformado,
  159. VisitaAgendada: p.IaVisitaAgendada,
  160. Atendentes: p.IaAtendentes,
  161. Sugestoes: p.IaSugestoes
  162. }
  163. }))
  164. },
  165. null,
  166. 2
  167. )
  168. );
  169. console.log(`\n[comparar-golden-set] resultado completo salvo em ${backupPath}`);
  170. process.exit(0);
  171. }
  172. main().catch((err) => {
  173. console.error("[comparar-golden-set] falha fatal:", err);
  174. process.exit(1);
  175. });