// AiLicita — o codigo do calculo de sobrepreco (metodologia v1.0.0) // Fonte: packages/ingest/src/precos/stats.ts — arquivo real da deteccao, publicado verbatim. // Regua: comparamos apenas o comparavel (mesmo CATMAT/CATSER, mesma UF, mesmo trimestre, // minimo de 8 amostras). Indicio estatistico, nunca acusacao. https://ailicita.com/metodologia /** * Estatística robusta do banco de preços (F4). Funções puras — testadas em test/stats.test.ts. * Sobrepreço = desvio robusto vs. a mediana do mesmo item CATMAT/CATSER na mesma UF e trimestre. */ /** Constante que torna o MAD comparável ao desvio-padrão sob normalidade. */ const MAD_SCALE = 0.6745; /** Z robusto mínimo para classificar como outlier (limiar clássico de Iglewicz-Hoaglin). */ export const LIMIAR_OUTLIER = 3.5; /** Amostras mínimas para uma referência de preço confiável. */ export const MIN_AMOSTRAS = 8; export function quantile(valoresOrdenados: number[], q: number): number { const n = valoresOrdenados.length; if (n === 0) return NaN; const pos = (n - 1) * q; const base = Math.floor(pos); const resto = pos - base; const a = valoresOrdenados[base]!; const b = valoresOrdenados[Math.min(base + 1, n - 1)]!; return a + resto * (b - a); } export function mediana(valores: number[]): number { return quantile([...valores].sort((a, b) => a - b), 0.5); } /** Median Absolute Deviation — robusto a até 50% de valores absurdos (a "sujeira" não contamina a régua). */ export function mad(valores: number[]): number { const m = mediana(valores); return mediana(valores.map((v) => Math.abs(v - m))); } /** Z-score robusto (modified z-score). MAD 0 → NaN (série degenerada, não classificável). */ export function zRobusto(valor: number, medianaRef: number, madRef: number): number { if (madRef === 0) return NaN; return (MAD_SCALE * (valor - medianaRef)) / madRef; } export interface ReferenciaPreco { nAmostras: number; mediana: number; p25: number; p75: number; mad: number; } export function calcularReferencia(valores: number[]): ReferenciaPreco { const ordenados = [...valores].sort((a, b) => a - b); return { nAmostras: ordenados.length, mediana: quantile(ordenados, 0.5), p25: quantile(ordenados, 0.25), p75: quantile(ordenados, 0.75), mad: mad(ordenados), }; } /** Trimestre fiscal de uma data ISO (AAAA-MM-DD…) → "AAAA-Qn". */ export function trimestreDe(dataIso: string): string { const ano = dataIso.slice(0, 4); const mes = Number(dataIso.slice(5, 7)); return `${ano}-Q${Math.ceil(mes / 3)}`; } export function isOutlier(z: number): boolean { return Number.isFinite(z) && Math.abs(z) >= LIMIAR_OUTLIER; } /** Fator de Tukey: sobrepreço se valor > p75 + 1,5·IQR. */ export const FATOR_IQR = 1.5; /** Razão mínima vs. mediana para flag direto (preço 2× o mercado). */ export const LIMIAR_RATIO = 2; /** Limite superior de Tukey. IQR 0 (série degenerada) → NaN, não classificável. */ export function limiteSuperiorIqr(ref: Pick): number { const iqr = ref.p75 - ref.p25; if (iqr === 0) return NaN; return ref.p75 + FATOR_IQR * iqr; } export type MetodoOutlier = "zscore" | "iqr" | "ratio"; /** * Métodos que classificam o valor como sobrepreço (v3 exige os três). * zscore olha os dois lados (|z|); iqr e ratio só para cima — sobrepreço é acima do mercado. */ export function metodosOutlier(valor: number, ref: ReferenciaPreco): MetodoOutlier[] { const metodos: MetodoOutlier[] = []; if (isOutlier(zRobusto(valor, ref.mediana, ref.mad))) metodos.push("zscore"); const limite = limiteSuperiorIqr(ref); if (Number.isFinite(limite) && valor > limite) metodos.push("iqr"); if (ref.mediana > 0 && valor / ref.mediana >= LIMIAR_RATIO) metodos.push("ratio"); return metodos; }