|
|
@@ -0,0 +1,188 @@
|
|
|
+import fs from "node:fs";
|
|
|
+import path from "node:path";
|
|
|
+import { fileURLToPath } from "node:url";
|
|
|
+import { AtendimentoGoldenLabel } from "../src/models/AtendimentoGoldenLabel.model.js";
|
|
|
+import { media, desvioPadrao, correlacaoPearson } from "./utils/estatistica.js";
|
|
|
+
|
|
|
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
+
|
|
|
+const setorIdx = process.argv.indexOf("--setor");
|
|
|
+const SETOR = setorIdx > -1 ? process.argv[setorIdx + 1] : null;
|
|
|
+
|
|
|
+const CAMPOS_CATEGORICOS = ["Resolvido", "Sentimento", "StatusCancelamento", "HorarioVisitaInformado", "VisitaAgendada"];
|
|
|
+
|
|
|
+// usa o snapshot da IA congelado em atendimento_golden_labels (Ia*) no momento em que
|
|
|
+// cada item foi rotulado — não junta com atendimento_avaliacoes ao vivo de propósito,
|
|
|
+// pra comparação nunca ser afetada por reavaliações posteriores do mesmo atendimento
|
|
|
+async function buscarPares() {
|
|
|
+ let query = AtendimentoGoldenLabel.query()
|
|
|
+ .where("atendimento_golden_labels.Status", "rotulado")
|
|
|
+ .whereNotNull("IaSnapshotEm")
|
|
|
+ .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId")
|
|
|
+ .select("atendimento_golden_labels.*", "a.Codigo as Codigo", "a.Setor as Setor");
|
|
|
+
|
|
|
+ if (SETOR) query = query.where("a.Setor", SETOR);
|
|
|
+
|
|
|
+ return query;
|
|
|
+}
|
|
|
+
|
|
|
+// Jaccard entre dois conjuntos de strings (comparação textual simples, não crítica)
|
|
|
+function jaccard(a, b) {
|
|
|
+ const setA = new Set((a ?? []).map((s) => s.toLowerCase().trim()));
|
|
|
+ const setB = new Set((b ?? []).map((s) => s.toLowerCase().trim()));
|
|
|
+ if (setA.size === 0 && setB.size === 0) return 1;
|
|
|
+ const intersecao = [...setA].filter((x) => setB.has(x)).length;
|
|
|
+ const uniao = new Set([...setA, ...setB]).size;
|
|
|
+ return uniao === 0 ? 1 : intersecao / uniao;
|
|
|
+}
|
|
|
+
|
|
|
+function metricasCategoricas(pares) {
|
|
|
+ const resultado = {};
|
|
|
+ for (const campo of CAMPOS_CATEGORICOS) {
|
|
|
+ const campoIa = `Ia${campo}`;
|
|
|
+ const comparaveis = pares.filter((p) => p[campo] !== null && p[campo] !== undefined);
|
|
|
+ const matriz = {};
|
|
|
+ let acertos = 0;
|
|
|
+
|
|
|
+ for (const p of comparaveis) {
|
|
|
+ const humano = String(p[campo]);
|
|
|
+ const ia = String(p[campoIa]);
|
|
|
+ matriz[humano] = matriz[humano] ?? {};
|
|
|
+ matriz[humano][ia] = (matriz[humano][ia] ?? 0) + 1;
|
|
|
+ if (humano === ia) acertos += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ resultado[campo] = {
|
|
|
+ amostra: comparaveis.length,
|
|
|
+ exactMatchPercent: comparaveis.length ? Number(((acertos / comparaveis.length) * 100).toFixed(1)) : null,
|
|
|
+ matrizConfusao: matriz
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return resultado;
|
|
|
+}
|
|
|
+
|
|
|
+function metricasScore(pares) {
|
|
|
+ const comparaveis = pares.filter((p) => p.ScoreAtendente !== null && p.IaScoreAtendente !== null);
|
|
|
+ if (comparaveis.length === 0) {
|
|
|
+ return { amostra: 0, mae: null, correlacao: null, distribuicaoDiferenca: null };
|
|
|
+ }
|
|
|
+
|
|
|
+ const diferencas = comparaveis.map((p) => Math.abs(p.ScoreAtendente - p.IaScoreAtendente));
|
|
|
+ const mae = Number(media(diferencas).toFixed(2));
|
|
|
+ const correlacao = correlacaoPearson(
|
|
|
+ comparaveis.map((p) => p.ScoreAtendente),
|
|
|
+ comparaveis.map((p) => p.IaScoreAtendente)
|
|
|
+ );
|
|
|
+
|
|
|
+ const distribuicaoDiferenca = { diferenca0: 0, diferenca1: 0, diferenca2: 0, diferenca3mais: 0 };
|
|
|
+ for (const d of diferencas) {
|
|
|
+ if (d === 0) distribuicaoDiferenca.diferenca0 += 1;
|
|
|
+ else if (d === 1) distribuicaoDiferenca.diferenca1 += 1;
|
|
|
+ else if (d === 2) distribuicaoDiferenca.diferenca2 += 1;
|
|
|
+ else distribuicaoDiferenca.diferenca3mais += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ amostra: comparaveis.length,
|
|
|
+ mae,
|
|
|
+ desvioPadraoDiferenca: Number(desvioPadrao(diferencas).toFixed(2)),
|
|
|
+ correlacao: correlacao !== null ? Number(correlacao.toFixed(2)) : null,
|
|
|
+ distribuicaoDiferenca
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function metricasListas(pares) {
|
|
|
+ const comAmbos = pares.filter((p) => (p.Atendentes?.length || p.IaAtendentes?.length));
|
|
|
+ const jaccardMedioAtendentes = comAmbos.length
|
|
|
+ ? Number(media(comAmbos.map((p) => jaccard(p.Atendentes, p.IaAtendentes))).toFixed(2))
|
|
|
+ : null;
|
|
|
+
|
|
|
+ const comAmbosSugestoes = pares.filter((p) => (p.Sugestoes?.length || p.IaSugestoes?.length));
|
|
|
+ const jaccardMedioSugestoes = comAmbosSugestoes.length
|
|
|
+ ? Number(media(comAmbosSugestoes.map((p) => jaccard(p.Sugestoes, p.IaSugestoes))).toFixed(2))
|
|
|
+ : null;
|
|
|
+
|
|
|
+ return {
|
|
|
+ atendentes: { amostra: comAmbos.length, jaccardMedio: jaccardMedioAtendentes },
|
|
|
+ sugestoes: { amostra: comAmbosSugestoes.length, jaccardMedio: jaccardMedioSugestoes }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function main() {
|
|
|
+ const pares = await buscarPares();
|
|
|
+ console.log(`[comparar-golden-set] ${pares.length} atendimentos rotulados${SETOR ? ` no setor ${SETOR}` : ""} comparados contra a avaliação da IA.`);
|
|
|
+
|
|
|
+ if (pares.length === 0) {
|
|
|
+ console.log("[comparar-golden-set] nada para comparar ainda — rotule alguns atendimentos no golden set primeiro.");
|
|
|
+ process.exit(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ const categoricas = metricasCategoricas(pares);
|
|
|
+ const score = metricasScore(pares);
|
|
|
+ const listas = metricasListas(pares);
|
|
|
+
|
|
|
+ console.log("\n=== Score do atendente ===");
|
|
|
+ console.log(`Amostra: ${score.amostra} | MAE: ${score.mae} | desvio da diferença: ${score.desvioPadraoDiferenca} | correlação: ${score.correlacao}`);
|
|
|
+ if (score.distribuicaoDiferenca) {
|
|
|
+ console.log(
|
|
|
+ `Distribuição da diferença |humano - IA|: 0 = ${score.distribuicaoDiferenca.diferenca0} | 1 = ${score.distribuicaoDiferenca.diferenca1} | 2 = ${score.distribuicaoDiferenca.diferenca2} | ≥3 = ${score.distribuicaoDiferenca.diferenca3mais}`
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log("\n=== Campos categóricos (exact-match humano vs. IA) ===");
|
|
|
+ for (const [campo, m] of Object.entries(categoricas)) {
|
|
|
+ console.log(` ${campo}: ${m.exactMatchPercent}% (amostra: ${m.amostra})`);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log("\n=== Listas (similaridade de Jaccard, informativo) ===");
|
|
|
+ console.log(` Atendentes: ${listas.atendentes.jaccardMedio} (amostra: ${listas.atendentes.amostra})`);
|
|
|
+ console.log(` Sugestões: ${listas.sugestoes.jaccardMedio} (amostra: ${listas.sugestoes.amostra})`);
|
|
|
+
|
|
|
+ const backupDir = path.resolve(__dirname, "logs");
|
|
|
+ fs.mkdirSync(backupDir, { recursive: true });
|
|
|
+ const backupPath = path.join(backupDir, `comparar-golden-set-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
|
|
|
+ fs.writeFileSync(
|
|
|
+ backupPath,
|
|
|
+ JSON.stringify(
|
|
|
+ {
|
|
|
+ config: { setor: SETOR },
|
|
|
+ resumo: { score, categoricas, listas },
|
|
|
+ detalhes: pares.map((p) => ({
|
|
|
+ atendimentoId: p.AtendimentoId,
|
|
|
+ codigo: p.Codigo,
|
|
|
+ setor: p.Setor,
|
|
|
+ humano: {
|
|
|
+ ScoreAtendente: p.ScoreAtendente,
|
|
|
+ Resolvido: p.Resolvido,
|
|
|
+ Sentimento: p.Sentimento,
|
|
|
+ StatusCancelamento: p.StatusCancelamento,
|
|
|
+ HorarioVisitaInformado: p.HorarioVisitaInformado,
|
|
|
+ VisitaAgendada: p.VisitaAgendada,
|
|
|
+ Atendentes: p.Atendentes,
|
|
|
+ Sugestoes: p.Sugestoes
|
|
|
+ },
|
|
|
+ ia: {
|
|
|
+ ScoreAtendente: p.IaScoreAtendente,
|
|
|
+ Resolvido: p.IaResolvido,
|
|
|
+ Sentimento: p.IaSentimento,
|
|
|
+ StatusCancelamento: p.IaStatusCancelamento,
|
|
|
+ HorarioVisitaInformado: p.IaHorarioVisitaInformado,
|
|
|
+ VisitaAgendada: p.IaVisitaAgendada,
|
|
|
+ Atendentes: p.IaAtendentes,
|
|
|
+ Sugestoes: p.IaSugestoes
|
|
|
+ }
|
|
|
+ }))
|
|
|
+ },
|
|
|
+ null,
|
|
|
+ 2
|
|
|
+ )
|
|
|
+ );
|
|
|
+ console.log(`\n[comparar-golden-set] resultado completo salvo em ${backupPath}`);
|
|
|
+
|
|
|
+ process.exit(0);
|
|
|
+}
|
|
|
+
|
|
|
+main().catch((err) => {
|
|
|
+ console.error("[comparar-golden-set] falha fatal:", err);
|
|
|
+ process.exit(1);
|
|
|
+});
|