El problema
Queríamos poner a prueba un modelo predictivo en público, con una regla sencilla: publicar las probabilidades al arranque del Mundial 2026, sin usar ningún resultado del torneo, y compararlas después con lo que pasó, aciertos y fallos incluidos. El torneo estrenaba formato de 48 selecciones, así que no servía un simulador pensado para 32.
Qué construimos
Escribimos un simulador Monte Carlo en TypeScript que juega el torneo completo 100.000 veces: fase de grupos con el sorteo oficial y eliminatorias según el formato de 48 equipos. La fuerza de cada selección parte de su Elo, ajustado con el valor de mercado de la plantilla (Transfermarkt), la calidad de liga, los títulos de la temporada y el rendimiento continental, más una ventaja para los anfitriones. El resultado se guarda como JSON y lo muestra una página estática que, terminado el torneo, enfrenta cada predicción con el resultado real.
Cómo está hecho
- Simulador por lotes
- Un script de TypeScript de unas 450 líneas (scripts/simulate-world-cup.ts) sin dependencias externas: solo node:fs y node:path. Juega 100.000 torneos completos y escribe los resultados agregados en un JSON.
- Datos como código
- Las 48 selecciones, el sorteo oficial de grupos y los índices de forma viven en ficheros .ts tipados dentro de src/data/, no en una base de datos. Se versionan con el resto de la web.
- Modelo de partido
- Cada equipo marca goles según una distribución de Poisson con media base de 1,4, desplazada por la diferencia de Elo efectivo. El Elo efectivo suma al Elo base un bonus de forma calculado con cuatro índices; los anfitriones reciben 80 puntos extra en la fase de grupos.
- Cuadro oficial FIFA 2026
- Los dieciseisavos reproducen el cuadro oficial hueco a hueco. Los ocho mejores terceros se colocan en sus huecos con backtracking, respetando de qué grupos puede venir cada uno.
- Página estática
- La página /mundial-2026 importa el JSON al compilar (Next.js con export estático en Cloudflare Pages), así que en producción no se calcula nada: se sirve HTML ya generado.
- Simulador en el navegador
- Un componente de cliente vuelve a implementar el modelo para que cualquiera juegue su propio Mundial con un botón. Es una reimplementación, no el mismo código del script.
Decisiones y contrapartidas
Qué decidimos, por qué y qué cedimos a cambio.
Calcular una vez, en local, y congelar la salida con su fecha.
- Por qué:
- Una predicción solo se puede auditar si no se toca después: el JSON lleva generatedAt del 12 de junio de 2026 y no incorpora ningún resultado del torneo.
- A cambio:
- La predicción no se entera de nada de lo que pasa durante el torneo: lesiones, sanciones o cambios de forma.
Generador pseudoaleatorio propio (Mulberry32) con semilla fija en lugar de Math.random().
- Por qué:
- Hace el resultado reproducible. El 9 de noviembre de 2026 volvimos a ejecutar el script sin cambios y los recuentos de las 48 selecciones coinciden con los publicados.
- A cambio:
- El simulador del navegador sigue usando Math.random() sin semilla, así que sus partidas no se pueden reproducir.
Un script de Node sin dependencias y datos en ficheros tipados, sin notebooks ni infraestructura.
- Por qué:
- El problema es pequeño: un proceso que tardó 3,7 segundos según el propio JSON y un fichero que se publica con la web.
- A cambio:
- Los ajustes a mano de los datos no dejaron rastro: no se guardó el Elo original de cada selección ni la ejecución anterior a la calibración.
Reproducir el cuadro oficial de dieciseisavos en lugar de emparejar a los 32 clasificados por ranking.
- Por qué:
- El camino de cada selección depende de su puesto en el grupo; emparejar por fuerza daría otro torneo.
- A cambio:
- El backtracking se queda con la primera asignación válida de terceros, que no hemos comprobado que coincida siempre con la de FIFA, y si no encontrara ninguna dejaría un hueco vacío sin avisar.
Ajustar a mano el Elo base de algunas selecciones mirando las probabilidades del supercomputador de Opta.
- Por qué:
- El Elo publicado arrastraba rachas pasadas; el caso más claro era Argentina.
- A cambio:
- El modelo queda anclado en parte a la salida de otro modelo, así que la comparación con Opta no es independiente, y concentra más probabilidad en los favoritos que Opta.
Límites y qué haríamos distinto
- No hay evaluación partido a partido con una métrica de puntuación (Brier o log-loss). Que ganara la favorita es una sola muestra: no valida ni refuta el modelo.
- Cada selección tiene un único rating fijo durante todo el torneo, sin separar ataque y defensa.
- No modela la prórroga: un empate en eliminatorias va directo a penaltis. Los desempates de grupo usan el Elo en lugar de todos los criterios de FIFA.
- El simulador del navegador se desvió del script (empareja por Elo y usa otra escala para los penaltis). Lo correcto sería un único módulo de modelo importado por los dos.
- El script no tiene tests.
Estado
Publicado el 12 de junio de 2026, al día siguiente del partido inaugural y sin usar ningún resultado del torneo. El torneo terminó el 19 de julio y la página incluye la comparación completa entre predicción y resultados.
Todo lo que cuenta este caso sale del script del simulador, de sus datos y del JSON publicado. El repositorio no es público; el build log enlazado abajo muestra el código de cada pieza.