Skip to content

0057. Motor de scoring KPI: umbrales configurables en BD, "sin dato" uniforme y recomendación por reglas

Estado

Aceptada

Contexto

El Excel WL_Aqua_Intelligence_v4_data_actualizada.xlsx de Winston Loaiza extiende la sábana ya modelada (referencia/sabana-calculos.md) con una capa nueva: 5 semáforos por criterio (FCA, supervivencia, crecimiento diario, crecimiento 3 semanas, carga), un índice de salud (0-100), una prioridad operativa y una recomendación textual — sobre un consolidado de 47 lagunas / 4 fincas.

Al planificar la tarea (motor-kpis-scoring, ver backlog) el análisis inicial dio cobertura ≈65% y dejó tres preguntas abiertas para Winston, incluidos los cortes exactos de prioridad. Al releer el xlsx con data_only=False (en vez de solo los valores calculados) aparecieron las fórmulas reales de las columnas AB2:AI2, así que las tres preguntas se resolvieron leyendo el archivo en vez de esperar respuesta — pero eso mismo obligó a revisar tres asunciones que se habían dado por buenas con la lectura "solo valores":

  1. La prioridad se había asumido función pura del índice (cortes 58/80/88/90). Es falsa: la fórmula real (AH2) combina el índice con el conteo de semáforos Rojo/Crítico y Amarillo — una piscina con índice 78 puede ser "Alta" por tener un solo Rojo, no por el índice. La inferencia anterior coincidía en las 47 filas por casualidad.
  2. La columna 35 (Recomendación) es la única sin fórmula — texto plano en las 47 filas. No estaba claro si convenía generarla por reglas o esperar el catálogo de frases de Winston.
  3. El trato de "Sin dato" en la hoja del socio es inconsistente entre criterios: FCA y crecimiento 3 semanas tienen rama explícita ("Sin dato", penaliza 0); supervivencia, crecimiento diario y carga no la tienen, así que un dato en blanco falla la comparación y cae en Rojo, penalizando a una piscina por falta de datos.

Decisión

1. Umbrales en BD (KpiThresholdSet), no constantes de código. Cada organización tiene su propio set, editable desde PATCH /kpi-thresholds (solo Administrador), sembrado con los valores exactos del Excel v4 al crear la organización. Se descartó hardcodear los umbrales: el socio ya pidió ajustar cortes distintos por finca en conversaciones previas del proyecto, y los 6 cortes de prioridad + 5×4 penalizaciones + tramos por criterio son exactamente el tipo de parámetro de negocio que cambia sin tocar código. La dirección de cada criterio (menor-es-mejor vs. mayor-es-mejor) NO es configurable — es semántica del dominio (KPI_CRITERION_DIRECTION, código), no un umbral: dejarla en BD permitiría invertir el significado de un criterio editando una fila por error.

2. Prioridad reproduce la fórmula real, no la inferencia por índice. Suma los conteos de Rojo+Crítico y de Amarillo, con cortes configurables (urgenteRedCount/altaRedCount/mediaYellowCount). Verificado 47/47 contra el archivo tras la corrección.

3. "Sin dato" se extiende a los 5 criterios (no solo FCA y crecimiento 3 semanas como en la hoja original) — decisión deliberada, no un fix silencioso: nuestros módulos ya devuelven null cuando falta el insumo (ver 08-reporte-semanal-flow.md), y penalizar por falta de dato en vez de tratarlo como "sin dato" castigaría a piscinas recién sembradas o con biometría atrasada, no a piscinas con mal desempeño. Se aceptó que esto hace el índice no bit-a-bit idéntico al Excel en filas con datos incompletos (ninguna de las 47 filas del archivo real los tiene, así que no afectó la verificación 47/47).

Además, un 0 en crecimiento 3 semanas se trata como "sin dato" (no crecimiento nulo), replicando OR(S2="",S2=0) del Excel: sin un muestreo distinto 21 días atrás, el peso actual y el de hace 3 semanas resuelven al mismo dato y la resta da 0 — ese 0 no es una medición real de estancamiento.

4. La recomendación se genera por reglas ahora, no se difiere. Al reconstruir la regla (una frase fija por criterio en estado no-verde, orden de columnas del Excel, unidas por ; , tope de 3) y correrla contra las 47 filas, reproduce 47/47 exacto — dejó de ser una apuesta. El catálogo de frases queda como constante editable en código (KPI_RECOMENDACION_FRASES), no en BD: es texto de producto, no un umbral numérico, y cambia con mucha menos frecuencia que los umbrales.

Consecuencias

  • El modelo de umbrales soporta tramos de tamaño distinto por criterio (FCA: 4, supervivencia: 5) representando los criterios planos como un tramo único con peso sentinel (UNBOUNDED_BRACKET_WEIGHT_G = 9999) — el motor de scoring (kpi-scoring.ts) no necesita una rama de código aparte para "criterio sin tramos".
  • Organizaciones creadas antes de este módulo no tienen KpiThresholdSet: el primer GET /kpi-thresholds (o el primer GET /reports/weekly) lo siembra al vuelo con los defaults, sin migración de datos necesaria y sin romper el reporte semanal existente.
  • El tope de 3 frases en la recomendación se apoya en evidencia de n=1 (una sola de las 47 filas del archivo tiene 4 criterios en alerta simultáneo) — es la explicación más simple que reproduce ese caso, no una regla que Winston confirmó explícitamente. Queda pendiente que confirme el catálogo definitivo; no bloquea, porque el catálogo es editable sin migración.
  • PlatformOrganizationsService y AuthService ahora dependen de KpiThresholdsService (mismo patrón que FeedingCurvesService) — al implementar se detectó que PlatformModule no importaba ReportesModule (solo AuthModule, que no lo reexporta), lo que rompía el alta de organizaciones desde plataforma por un error de resolución de dependencias de NestJS. Corregido agregando el import directo, mismo patrón que AlimentacionModule en ese módulo.
  • WeeklyReportRowDto gana 8 campos nuevos (5 estados + índice + prioridad + recomendación) — cambio de contrato aditivo, no rompe consumidores existentes del reporte semanal (el admin es el único cliente hoy).

Referencias

  • Tarea motor-kpis-scoring (2026-08-04), _planning/motor-kpis-scoring-plan.md / -progress.md.
  • camaroneras_backend/src/modules/reportes/kpi-scoring.ts, kpi-thresholds.defaults.ts, kpi-thresholds/ (modelo + service + controller).
  • camaroneras_backend/src/modules/reportes/__fixtures__/wl-aqua-v4-consolidado.ts — las 47 filas reales del xlsx, usadas como fixture de regresión (kpi-scoring.spec.ts).
  • camaroneras_docs/modulos/10-parametros-kpi-flow.md.
  • Backlog del proyecto → "WL Aqua Intelligence v4 — brecha vs. el doc del socio" (contexto completo de las 5 tareas derivadas del Excel v4 y las respuestas de Winston).