Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .claude/skills/prediccion-mundial-2026/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ World Cup, using the **Elo rating** of each team plus a **recent-form** adjustme
- `data/grupos-resultados-2026.json` — the 12 groups, played results and the
remaining fixtures.
- `data/knockout-2026.json` — official Round-of-32 to Final bracket structure.
- `data/knockout-resultados-2026.json` — real results of the knockout rounds already
played (Round of 32, Round of 16, …). When present, the projection and the Monte
Carlo condition on them: the real winners advance and only the unplayed rounds are
predicted/simulated. Add new results here as each round finishes.
- `data/elo-series-completo-1901-2026.js` — full historical series (195 teams).

**Source:** World Football Elo Ratings (eloratings.net), via the El Atlas chart.
Expand Down Expand Up @@ -95,4 +99,6 @@ recent results via `--formA/--formB`. Tested: it does not improve accuracy.
## Updating the data

`bash scripts/actualizar.sh` (from repo root) refreshes the live Elo and regenerates
everything. New match results go into `data/grupos-resultados-2026.json`.
everything. New group-stage results go into `data/grupos-resultados-2026.json`; new
knockout results go into `data/knockout-resultados-2026.json` (keyed by bracket match
id, e.g. `M97`).
128 changes: 64 additions & 64 deletions .claude/skills/prediccion-mundial-2026/data/2026.tsv

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
"exact": true,
"predScore": "1-1",
"p1": 0.5767237828343034,
"pX": 0.25832693095676523,
"p2": 0.1649492862089314
"pX": 0.2583269309567652,
"p2": 0.16494928620893137
},
{
"group": "A",
Expand Down Expand Up @@ -168,7 +168,7 @@
"predScore": "2-0",
"p1": 0.6397348695901068,
"pX": 0.2371109633580704,
"p2": 0.1231541670518227
"p2": 0.12315416705182271
},
{
"group": "B",
Expand Down Expand Up @@ -1344,7 +1344,7 @@
"hit": true,
"exact": true,
"predScore": "0-2",
"p1": 0.12196219514229964,
"p1": 0.12196219514229965,
"pX": 0.23639331540889114,
"p2": 0.6416444894488093
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@
"2026": 13
},
"in_index": true,
"elo_live": 1882,
"elo_live": 1881,
"rank_live": 16,
"en_name": "Croatia"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"meta": {
"descripcion": "Resultados REALES de la fase eliminatoria del Mundial 2026 (32avos y octavos jugados).",
"fuente": "Recopilado y cruzado multi-fuente (Yahoo Sports, Al Jazeera, Olympics.com, Wikipedia). Verificado contra la topologia oficial del bracket (knockout-2026.json) y las tablas finales de grupos.",
"as_of": "2026-07-08",
"generado": "2026-07-08",
"nota": "home/away = orientacion real del partido; 'pens' = tanda de penales [local, visitante] cuando hubo empate en el tiempo reglamentario; 'winner' = ISO3 del equipo que avanzo. Cuartos, semis y final todavia no se jugaron.",
"rondas_jugadas": ["Round of 32", "Round of 16"]
},
"played": [
{ "id": "M73", "round": "Round of 32", "home": "CAN", "away": "ZAF", "home_goals": 1, "away_goals": 0, "winner": "CAN" },
{ "id": "M74", "round": "Round of 32", "home": "PRY", "away": "DEU", "home_goals": 1, "away_goals": 1, "pens": [4, 3], "winner": "PRY" },
{ "id": "M75", "round": "Round of 32", "home": "MAR", "away": "NLD", "home_goals": 1, "away_goals": 1, "pens": [3, 2], "winner": "MAR" },
{ "id": "M76", "round": "Round of 32", "home": "BRA", "away": "JPN", "home_goals": 2, "away_goals": 1, "winner": "BRA" },
{ "id": "M77", "round": "Round of 32", "home": "FRA", "away": "SWE", "home_goals": 3, "away_goals": 0, "winner": "FRA" },
{ "id": "M78", "round": "Round of 32", "home": "NOR", "away": "CIV", "home_goals": 2, "away_goals": 1, "winner": "NOR" },
{ "id": "M79", "round": "Round of 32", "home": "MEX", "away": "ECU", "home_goals": 2, "away_goals": 0, "winner": "MEX" },
{ "id": "M80", "round": "Round of 32", "home": "GBR", "away": "COD", "home_goals": 2, "away_goals": 1, "winner": "GBR" },
{ "id": "M81", "round": "Round of 32", "home": "USA", "away": "BIH", "home_goals": 2, "away_goals": 0, "winner": "USA" },
{ "id": "M82", "round": "Round of 32", "home": "BEL", "away": "SEN", "home_goals": 3, "away_goals": 2, "winner": "BEL" },
{ "id": "M83", "round": "Round of 32", "home": "PRT", "away": "HRV", "home_goals": 2, "away_goals": 1, "winner": "PRT" },
{ "id": "M84", "round": "Round of 32", "home": "ESP", "away": "AUT", "home_goals": 3, "away_goals": 0, "winner": "ESP" },
{ "id": "M85", "round": "Round of 32", "home": "CHE", "away": "DZA", "home_goals": 2, "away_goals": 0, "winner": "CHE" },
{ "id": "M86", "round": "Round of 32", "home": "ARG", "away": "CPV", "home_goals": 3, "away_goals": 2, "winner": "ARG" },
{ "id": "M87", "round": "Round of 32", "home": "COL", "away": "GHA", "home_goals": 1, "away_goals": 0, "winner": "COL" },
{ "id": "M88", "round": "Round of 32", "home": "EGY", "away": "AUS", "home_goals": 1, "away_goals": 1, "pens": [4, 2], "winner": "EGY" },

{ "id": "M89", "round": "Round of 16", "home": "FRA", "away": "PRY", "home_goals": 1, "away_goals": 0, "winner": "FRA" },
{ "id": "M90", "round": "Round of 16", "home": "MAR", "away": "CAN", "home_goals": 3, "away_goals": 0, "winner": "MAR" },
{ "id": "M91", "round": "Round of 16", "home": "NOR", "away": "BRA", "home_goals": 2, "away_goals": 1, "winner": "NOR" },
{ "id": "M92", "round": "Round of 16", "home": "GBR", "away": "MEX", "home_goals": 3, "away_goals": 2, "winner": "GBR" },
{ "id": "M93", "round": "Round of 16", "home": "ESP", "away": "PRT", "home_goals": 1, "away_goals": 0, "winner": "ESP" },
{ "id": "M94", "round": "Round of 16", "home": "BEL", "away": "USA", "home_goals": 4, "away_goals": 1, "winner": "BEL" },
{ "id": "M95", "round": "Round of 16", "home": "ARG", "away": "EGY", "home_goals": 3, "away_goals": 2, "winner": "ARG" },
{ "id": "M96", "round": "Round of 16", "home": "CHE", "away": "COL", "home_goals": 0, "away_goals": 0, "pens": [4, 3], "winner": "CHE" }
]
}
69 changes: 54 additions & 15 deletions .claude/skills/prediccion-mundial-2026/genera-predicciones.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const eloOf = (iso) => byIso[iso].elo_live ?? byIso[iso].elo_2026;
const en = (iso) => byIso[iso].en_name ?? byIso[iso].name;

const pct = (x) => x.toFixed(1) + "%";
const date = fix.meta.generado;
const date = proj.koMeta?.as_of ?? fix.meta.generado;

// Predicted scoreline + pick for an upcoming match (live Elo).
function predRow(homeIso, awayIso) {
Expand Down Expand Up @@ -89,23 +89,60 @@ if (fix.remaining_fixtures.length === 0 && existsSync(btMatchesPath)) {
}
}

// 2) Projected knockout bracket (match by match)
md += `## 2) Projected knockout bracket (match by match)\n\n`;
const groupsDone = fix.remaining_fixtures.length === 0;
md += `Single most-likely path: ${groupsDone ? "the **final** group standings" : "real group results + the most-likely score for the remaining group games"} `;
md += `${groupsDone ? "set the bracket" : "decide the standings"}; then every knockout tie is predicted with a decisive `;
md += `score and a winner (in reality many ties go to extra time/penalties; "(tight)" marks the `;
md += `near coin-flips).\n\n`;
const realTies = proj.ties.filter((t) => t.real);
const predTies = proj.ties.filter((t) => !t.real);

// 2) Knockout stage so far: our predictions vs the real results
let secNum = 2;
if (realTies.length) {
const koHits = realTies.filter((t) => t.hit).length;
md += `## ${secNum}) Knockout stage so far: our predictions vs the results\n\n`;
md += `The rounds already played are shown with their **real result** and the model's `;
md += `pre-match **pick** (favourite by live Elo; knockout ties have no draw). The **Hit** `;
md += `column marks where the pick matched who actually advanced. Penalty shootouts are noted `;
md += `as _(pens x-y)_.\n\n`;
md += `**Scoreboard: the model called ${koHits} of ${realTies.length} knockout ties right `;
md += `(${((100 * koHits) / realTies.length).toFixed(1)}%)** so far. ✓ = correct, ✗ = miss.\n\n`;
let cr = "";
for (const t of realTies) {
if (t.round !== cr) {
md += `\n### ${t.round}\n\n`;
md += `| Match | Result | Advanced | Our pick | Hit |\n|---|---|---|---|---|\n`;
cr = t.round;
}
const score = `${t.score}${t.pens ? ` (pens ${t.pens})` : ""}`;
md += `| ${t.home} vs ${t.away} | **${score}** | ${t.winner} | ${t.pick} | ${t.hit ? "✓" : "✗"} |\n`;
}
md += `\n`;
secNum++;
}

// 3) Projected rest of the bracket (match by match)
const projTitle = realTies.length ? "Projected rest of the bracket (from the quarter-finals)" : "Projected knockout bracket (match by match)";
md += `## ${secNum}) ${projTitle}\n\n`;
if (realTies.length) {
md += `Single most-likely path from the **eight real quarter-finalists**: every remaining tie is `;
md += `predicted with live Elo (decisive score + winner; in reality many go to extra time/penalties, `;
md += `"(tight)" marks the near coin-flips).\n\n`;
} else {
md += `Single most-likely path: ${groupsDone ? "the **final** group standings" : "real group results + the most-likely score for the remaining group games"} `;
md += `${groupsDone ? "set the bracket" : "decide the standings"}; then every knockout tie is predicted with a decisive `;
md += `score and a winner (in reality many ties go to extra time/penalties; "(tight)" marks the `;
md += `near coin-flips).\n\n`;
}
let curRound = "";
for (const t of proj.ties) {
for (const t of predTies) {
if (t.round !== curRound) { md += `\n### ${t.round}\n\n`; curRound = t.round; }
md += `- **${t.home} ${t.score} ${t.away}** → advances **${t.winner}**${t.tight ? " _(tight)_" : ""}\n`;
}
md += `\n### 🏆 Projected champion: **${proj.champion}**\n\n`;
md += `Projected final: ${proj.final.home} ${proj.final.score} ${proj.final.away} (winner ${proj.final.winner}, without the actual result).\n\n`;
secNum++;

// 3) Who advances (Monte Carlo probabilities)
md += `## 3) Who advances? Group probabilities\n\n`;
// Who advances (Monte Carlo probabilities)
md += `## ${secNum}) Who advances? Group probabilities\n\n`;
secNum++;
md += `Probability of reaching the Round of 32 (top 2 per group + 8 best third-placed teams), `;
md += `from ${sim.meta.simulaciones.toLocaleString("en")} Monte Carlo runs. ✓ = group already decided.\n\n`;
for (const g of Object.keys(sim.grupos)) {
Expand All @@ -118,13 +155,14 @@ for (const g of Object.keys(sim.grupos)) {
md += `\n`;
}

// 4) Finalist probabilities
md += `## 4) Finalist probabilities (Monte Carlo)\n\n`;
md += `From ${sim.meta.simulaciones.toLocaleString("en")} simulations of the whole tournament (official bracket, live Elo).\n\n`;
// Finalist probabilities
md += `## ${secNum}) Finalist probabilities (Monte Carlo)\n\n`;
md += `From ${sim.meta.simulaciones.toLocaleString("en")} simulations${realTies.length ? " conditioned on the real results so far (the eight real quarter-finalists)" : " of the whole tournament (official bracket, live Elo)"}.\n\n`;
md += `**Most likely final: ${sim.prediccion_finalistas}** `;
md += `(occurs in ${pct(sim.final_mas_probable[0].prob)} of simulations).\n\n`;
md += `| Team | Reaches final | Champion |\n|---|---|---|\n`;
for (const r of sim.finalistas.slice(0, 10)) md += `| ${r.name} | ${pct(r.final)} | ${pct(r.champ)} |\n`;
const finalistRows = sim.finalistas.filter((r) => r.final > 0);
for (const r of (finalistRows.length ? finalistRows : sim.finalistas).slice(0, 10)) md += `| ${r.name} | ${pct(r.final)} | ${pct(r.champ)} |\n`;
md += `\n**Most likely finals:**\n\n`;
for (const p of sim.final_mas_probable) md += `- ${p.final} — ${pct(p.prob)}\n`;
md += `\n> Finalist probabilities are low and tightly bunched: the model has NO strong favorite, `;
Expand All @@ -142,7 +180,8 @@ resumen += `Projected final: **${proj.final.home} ${proj.final.score} ${proj.fin
resumen += `**Most likely finalists (Monte Carlo):** ${sim.prediccion_finalistas} `;
resumen += `(${pct(sim.final_mas_probable[0].prob)} of simulations).\n\n`;
resumen += `| Team | Reaches final | Champion |\n|---|---|---|\n`;
for (const r of sim.finalistas.slice(0, 6)) resumen += `| ${r.name} | ${pct(r.final)} | ${pct(r.champ)} |\n`;
const resumenRows = sim.finalistas.filter((r) => r.final > 0);
for (const r of (resumenRows.length ? resumenRows : sim.finalistas).slice(0, 6)) resumen += `| ${r.name} | ${pct(r.final)} | ${pct(r.champ)} |\n`;
resumen += `\nFull detail (${groupsDone ? "our group-stage predictions vs the results" : "every group, every upcoming match"}, the match-by-match bracket) in `;
resumen += `[**predicciones/PREDICCIONES.md**](predicciones/PREDICCIONES.md).\n`;
resumen += `<!-- PRED:END -->`;
Expand Down
38 changes: 35 additions & 3 deletions .claude/skills/prediccion-mundial-2026/proyeccion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* This is the "predicted bracket" (one concrete path). For probabilities
* (who is LIKELY to reach the final), see simular.mjs (Monte Carlo).
*/
import { readFileSync } from "node:fs";
import { readFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { loadData, predictFromEff, PARAMS } from "./model.mjs";
Expand All @@ -18,6 +18,16 @@ export function proyectar() {
const elo = loadData();
const fix = JSON.parse(readFileSync(join(__dir, "data", "grupos-resultados-2026.json"), "utf8"));
const bracket = JSON.parse(readFileSync(join(__dir, "data", "knockout-2026.json"), "utf8"));
// Resultados REALES de eliminatorias ya jugadas (opcional). Si existen, se usan
// para avanzar de ronda (la proyeccion continua desde los equipos verdaderos).
const koPath = join(__dir, "data", "knockout-resultados-2026.json");
const koResults = {};
let koMeta = null;
if (existsSync(koPath)) {
const ko = JSON.parse(readFileSync(koPath, "utf8"));
koMeta = ko.meta || null;
for (const r of ko.played || []) koResults[r.id] = r;
}
const byIso = Object.fromEntries(elo.equipos.map((t) => [t.iso3, t]));
const HOSTS = new Set(["MEX", "CAN", "USA"]);
const ha = (iso) => (HOSTS.has(iso) ? PARAMS.HOME_ADV : 0);
Expand Down Expand Up @@ -114,10 +124,32 @@ export function proyectar() {
const home = /^M\d+$/.test(m.home) ? winners[m.home] : resolveSlot(m.home);
const away = /^M\d+$/.test(m.away) ? winners[m.away] : resolveSlot(m.away);
if (home == null || away == null) { winners[m.id] = home || away; continue; }

// Partido ya jugado: usar resultado REAL y comparar con lo que el modelo
// habria predicho (pick pre-partido con Elo en vivo).
const res = koResults[m.id];
if (res) {
const real = new Set([res.home, res.away]);
if (!real.has(home) || !real.has(away)) {
throw new Error(`Resultado real de ${m.id} (${res.home} vs ${res.away}) no coincide con el bracket (${home} vs ${away}).`);
}
winners[m.id] = res.winner;
const pick = predKO(res.home, res.away).winner; // favorito del modelo
ties.push({
round: roundNames[ri], id: m.id, real: true,
home: name(res.home), away: name(res.away),
score: `${res.home_goals}-${res.away_goals}`,
pens: res.pens ? `${res.pens[0]}-${res.pens[1]}` : null,
winner: name(res.winner),
pick: name(pick), hit: pick === res.winner,
});
continue;
}

const r = predKO(home, away);
winners[m.id] = r.winner;
ties.push({
round: roundNames[ri], id: m.id,
round: roundNames[ri], id: m.id, real: false,
home: name(home), away: name(away),
score: `${r.a}-${r.b}`, tight: r.tight,
winner: name(r.winner),
Expand All @@ -133,5 +165,5 @@ export function proyectar() {
equipos: tables[g].map((t, i) => ({ pos: i + 1, name: t.name, pts: t.pts, gd: t.gd })),
};

return { standings, projectedGroupMatches, ties, roundNames, final: finalTie, champion };
return { standings, projectedGroupMatches, ties, roundNames, final: finalTie, champion, koMeta };
}
15 changes: 13 additions & 2 deletions .claude/skills/prediccion-mundial-2026/simular.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
* Usa elo_live (eloratings.net) para los partidos futuros. Heurística orientativa.
*/
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { loadData, predictFromEff, autoForm, PARAMS } from "./model.mjs";
Expand All @@ -22,6 +22,15 @@ const ROOT = join(__dir, "..", "..", "..");
const elo = loadData();
const fix = JSON.parse(readFileSync(join(__dir, "data", "grupos-resultados-2026.json"), "utf8"));
const bracket = JSON.parse(readFileSync(join(__dir, "data", "knockout-2026.json"), "utf8"));
// Resultados REALES de eliminatorias ya jugadas: si existen, la simulacion se
// condiciona a ellos (los ganadores reales avanzan; solo se muestrean las rondas
// aun no jugadas), de modo que las probabilidades reflejen el bracket verdadero.
const KO_RESULTS = (() => {
const p = join(__dir, "data", "knockout-resultados-2026.json");
if (!existsSync(p)) return {};
const ko = JSON.parse(readFileSync(p, "utf8"));
return Object.fromEntries((ko.played || []).map((r) => [r.id, r.winner]));
})();
const byIso = Object.fromEntries(elo.equipos.map((t) => [t.iso3, t]));
const HOSTS = new Set(["MEX", "CAN", "USA"]);
const ha = (iso) => (HOSTS.has(iso) ? PARAMS.HOME_ADV : 0);
Expand Down Expand Up @@ -161,8 +170,10 @@ function simulate(rnd) {
for (const m of round) {
const a = winnerOf(m.home, rnd), b = winnerOf(m.away, rnd);
if (a == null || b == null) { alive[m.id] = a || b; continue; }
alive[m.id] = knockoutWinner(a, b, rnd);
if (m.id === bracket.final_id) finalists = [a, b];
// Ronda ya jugada: el ganador real avanza (no se muestrea).
if (KO_RESULTS[m.id]) { alive[m.id] = KO_RESULTS[m.id]; continue; }
alive[m.id] = knockoutWinner(a, b, rnd);
}
}
return { tables, thirds, bestThirdGroups, finalists, champion: alive[bracket.final_id] };
Expand Down
7 changes: 5 additions & 2 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ bash scripts/actualizar.sh
```

Baja el Elo en vivo de eloratings.net, refresca todo y regenera las predicciones.
Un **GitHub Action** lo corre **todos los días**. Los resultados de partidos nuevos
se agregan a `data/grupos-resultados-2026.json` (único paso manual).
Un **GitHub Action** lo corre **todos los días**. Los resultados nuevos se agregan
a mano: los de fase de grupos en `data/grupos-resultados-2026.json` y los de
eliminatorias en `data/knockout-resultados-2026.json` (por id de llave del cuadro).
Cuando hay resultados de eliminatorias, la proyección y el Monte Carlo se condicionan
a ellos y solo predicen las rondas que faltan jugar.

---

Expand Down
Loading