| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- 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"];
- 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;
- }
- async function buscarConcordanciaDireta() {
- let query = AtendimentoGoldenLabel.query()
- .whereIn("atendimento_golden_labels.Status", ["rotulado", "aceito"])
- .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId")
- .select("atendimento_golden_labels.Status as Status");
- if (SETOR) query = query.where("a.Setor", SETOR);
- const linhas = await query;
- const aceito = linhas.filter((l) => l.Status === "aceito").length;
- const rotulado = linhas.filter((l) => l.Status === "rotulado").length;
- const total = aceito + rotulado;
- return { aceito, rotulado, total, percentAceito: total ? Number(((aceito / total) * 100).toFixed(1)) : null };
- }
- 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 concordancia = await buscarConcordanciaDireta();
- console.log(
- `[comparar-golden-set] concordância direta (aceitos sem correção / total revisado)${SETOR ? ` no setor ${SETOR}` : ""}: ` +
- `${concordancia.aceito}/${concordancia.total} (${concordancia.percentAceito}%)`
- );
- const pares = await buscarPares();
- 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).`);
- if (pares.length === 0) {
- console.log("[comparar-golden-set] nada para comparar na acurácia fina ainda — rotule alguns atendimentos à mão 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: { concordancia, 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);
- });
|