PartidosClasificaciónHistorial

Datos de FBref, Understat, y API-Football. Código fuente en GitHub · Documentación de la API · RSS

← Volver a los partidos

Lecciones aprendidas

Un proyecto real tiene errores reales. Estos son los que vale la pena contar — no porque fueran dramáticos, sino por lo que enseñaron una y otra vez sobre la misma lección: la brecha entre creer que algo funciona y realmente verificar que funciona.

Una restricción de unicidad que nunca hizo cumplir la unicidad

Al principio, el registro de predicciones tenía una restricción SQL estándar UNIQUE pensada para evitar que la misma predicción se escribiera dos veces. Parecía correcta en cada revisión de código. No lo era. SQL trata NULL como nunca igual a nada — incluyendo otro NULL. Como cada mercado tiene al menos una columna de sujeto anulable (una predicción a nivel de equipo no tiene jugador, una predicción a nivel de jugador no tiene equipo), la restricción nunca deduplicó nada silenciosamente, para ningún mercado, desde el primer día. La solución fue un índice basado en expresión usando COALESCE en su lugar. La lección real no era sobre sintaxis SQL — era que una restricción que existe en el esquema no es lo mismo que una restricción que realmente funciona.

Petey lee un porcentaje como un conteo de eventos

La primera vez que Petey respondió una pregunta real sobre un mercado con una tasa de acierto del 0%, dijo: “no anotamos ningún gol en el torneo.” Literalmente no se anotó nada — leyó un número de confianza declarado como un conteo de eventos del mundo real. La solución fue un prompt más claro explicando qué significa realmente “tasa de acierto”. Eso resolvió exactamente un error más: incluso con instrucciones cuidadosas de no editorializar, Petey seguía describiendo números neutrales como “le costó,” “moderado,” “estable” — ninguno fundamentado en lo que realmente se le dio. Pedirle al modelo más educadamente no funcionó. Lo que sí funcionó: un filtro estructural que descarta cualquier respuesta que contenga una palabra de juicio no respaldada y recurre a una oración plana y determinista en su lugar. Seguridad mediante arquitectura, no mediante una solicitud mejor redactada.

Un nombre de host que solo existía dentro del clúster

Petey funcionaba perfectamente en cada prueba local. Falló por completo en el momento en que se puso en producción. La razón: las llamadas a la API del frontend apuntaban al nombre de servicio interno de Kubernetes para el backend — un nombre de host que solo se resuelve dentro de la propia red del clúster. Un navegador real, en la internet pública, no tiene forma de buscarlo. Cada página renderizada en el servidor funcionaba bien, porque esas solicitudes se ejecutan desde dentro de un pod. Solo las solicitudes del lado del navegador fallaban. La solución fue enrutar esas llamadas a través de pequeñas rutas de API de Next.js que se ejecutan del lado del servidor y hacen proxy al backend real — el mismo patrón que ya usaba cada otra página funcional, solo que aún no se había aplicado a la página construida de forma diferente.

Quitar un secreto del código no es eliminarlo

Un commit temprano tenía una contraseña de base de datos real y una clave de API de terceros escritas directamente en el script de automatización nocturna. La solución parecía completa: ambas se extrajeron a variables de entorno, el script ahora falla rápido con un mensaje claro si falta alguna en lugar de ejecutarse silenciosamente con una cadena de conexión rota, y .gitignore se amplió para que no pueda volver a pasar de la misma manera. Lo que el commit de la solución no hizo — y lo dijo explícitamente en su propio mensaje, como un paso de seguimiento separado — fue purgar los valores antiguos del historial de git. Permanecieron ahí, en texto plano, en un commit accesible desde el espejo público de GitHub, hasta que ambas credenciales fueron rotadas. Un commit que elimina un secreto solo evita que se lea de ahí en adelante; cada commit anterior todavía lo tiene, mientras exista el repositorio. Los dos son problemas diferentes con soluciones diferentes, y solo uno de ellos fue realmente un cambio de código.

Corregido, probado, publicado — excepto que no lo estaba

Esta es la que vale la pena detenerse a considerar. Se construyeron dos correcciones separadas, verificadas de forma aislada, y genuinamente creídas como en producción: una reescritura de las declaraciones de córners/tiros para que las predicciones se lean claramente por equipo en lugar de ambiguamente, y una capa de calibración isotónica fuera de pliegue construida específicamente para corregir un problema confirmado de exceso de confianza en el modelo de córners. Ambas se reportaron, en su momento, como completadas. Ninguna de las dos había sido realmente conectada al script que ejecuta el pipeline nocturno real. El código de calibración permaneció en un script de entrenamiento validado y funcional durante semanas mientras las predicciones en producción seguían publicándose con probabilidades sin corregir. Nadie lo detectó porque nada estaba mal de una forma obviamente visible — las predicciones seguían viéndose razonables, el sitio seguía funcionando, los mensajes de commit se leían como una función terminada. Se necesitó buscar directamente en el archivo real que ejecuta el CronJob, en lugar de confiar en la memoria o en un resumen pasado, para descubrirlo. Ese es el verdadero hábito que este proyecto reforzó: lo único que confirma que una corrección funciona es revisar el código que realmente se ejecuta en producción — no el código que se escribió, no lo que afirma un mensaje de commit, no lo que se creía la última vez que alguien miró.

Un paso que asumió que el paso anterior había terminado

El pipeline nocturno tiene tres etapas — ingesta, construcción de características, generación de la lista de predicciones — ejecutadas como trabajos programados separados, cada uno simplemente confiando en que el anterior se completó a tiempo. Durante semanas, el paso de reconstrucción de características se quedó silenciosamente atrás de las estadísticas crudas sobre las que debía construirse, y nadie lo notó, porque la ingesta misma resultó estar pausada por una razón no relacionada al mismo tiempo — así que la brecha nunca se manifestó como una salida visiblemente incorrecta. Tuvo que detectarse manualmente, dos veces, antes de que quedara clara la causa real: un script de reconstrucción correctamente escrito y correctamente documentado como necesario de ejecutar después de cada lote de ingesta, pero no conectado a nada que realmente lo llamara de esa manera. Un tiempo que coincide por casualidad no es lo mismo que una dependencia realmente exigida.

Un filtro de seguridad con una palabra escondida dentro de otra palabra

El filtro de palabras de juicio de Petey — el construido para detectar al modelo editorializando con palabras como “le costó” o “estable” — usaba coincidencia de subcadenas simple. Funcionó, hasta que apareció una categoría de estadística real cuyo nombre contenía por casualidad una palabra prohibida como subcadena. Cada respuesta que usaba legítimamente esas palabras fue redirigida silenciosamente a la oración de respaldo genérica en lugar de la redacción real de Ollama — no exactamente roto, solo silenciosamente peor, justo para las preguntas que deberían haber funcionado mejor. La solución fue cambiar a coincidencia por límites de palabra. La lección fue más estrecha de lo que parece: un filtro de seguridad es código en sí mismo, y el código que filtra por subcadenas eventualmente marcará la subcadena equivocada.

Un cambio de nombre de base de datos del que el sitio en producción nunca se enteró

Un cambio de nombre de columna se integró limpiamente en el código, y la migración se ejecutó limpiamente contra la base de datos en producción. El sitio se cayó de todos modos. El cambio de base de datos era real y correcto; los contenedores de API y frontend en ejecución seguían siendo las imágenes antiguas, construidas antes del cambio de nombre, todavía esperando los nombres de columna antiguos. Que una migración sea correcta no dice nada sobre si lo que realmente sirve el tráfico ha sido reconstruido para coincidir con ella — son dos pasos separados, y solo uno de ellos había ocurrido.

Construir el modelo fue la parte fácil

El modelo xG personalizado se lanzó para EPL, Serie A y La Liga — las ligas con datos reales a nivel de tiro detrás de ellas. La MLS, la liga que este ejercicio pretendía en parte dejar de depender de un tercero, no está incluida. No porque construir el modelo para la MLS sea difícil — al código no le importa de qué liga son las filas con las que entrena — sino porque los tiros mismos no existen en ningún lugar accesible. Understat, la única fuente ya conectada a este proyecto con coordenadas reales de tiros, nunca ha cubierto la MLS.

Así que la pregunta real se convirtió en: ¿hay alguna otra fuente real de coordenadas de tiros de la MLS, gratuita o no? Verificado directamente en lugar de asumido. Cada proveedor que publica xG agregado a un nivel asequible — la misma API ya usada para partidos y cuotas, más dos de sus alternativas más cercanas — resultó publicar solo eso: un único número por tiro o por partido, nunca las coordenadas subyacentes con las que un modelo puede realmente entrenarse. La única excepción real es la publicación gratuita de datos abiertos de StatsBomb en GitHub, que sí incluye datos reales de tiros de la MLS (temporada 2023) con posiciones de defensores en el momento de cada tiro — más ricos que lo que este proyecto tiene para cualquier liga en este momento. Pero es una publicación histórica única, no un flujo en vivo, así que puede demostrar que el modelo funciona con tiros de la MLS sin poder generar una sola predicción en vivo a partir de ella. Los datos reales de coordenadas de tiros en vivo, para cualquier liga, resultaron estar restringidos detrás de contactos de ventas empresariales en toda la industria — no un nivel que este proyecto vaya a alcanzar para un sitio de portafolio.

Lanzar el modelo para tres ligas y explícitamente no para una cuarta se sintió como la decisión más honesta que abandonar silenciosamente la idea o retener toda la función por una brecha que no se va a cerrar pronto. La ausencia es el hallazgo: a veces el verdadero obstáculo de una función no es el código, es que los datos que necesita nunca fueron gratuitos para empezar.

Ninguno de estos se detectó volviéndose más hábil para escribir errores. Se detectaron construyendo la disciplina de seguir verificando — contra datos reales, código real en ejecución, comportamiento real en producción — en lugar de confiar en que algo ya confirmado una vez seguirá siendo verdad después.

Cómo funciona →