html`<div class="sim-kpis">
<div class="sim-kpi local"><div class="etq">Gana ${local}</div><div class="val">${fmtp(partido.p_local)}</div>
<div class="det">${partido.lambda_local.toFixed(2)} goles esperados</div></div>
<div class="sim-kpi"><div class="etq">Empate</div><div class="val">${fmtp(partido.p_empate)}</div>
<div class="det">Marcador más probable: ${partido.marcador} (${fmtp(partido.p_marcador)})</div></div>
<div class="sim-kpi visita"><div class="etq">Gana ${visita}</div><div class="val">${fmtp(partido.p_visita)}</div>
<div class="det">${partido.lambda_visita.toFixed(2)} goles esperados</div></div>
</div>`
Probabilidad de cada marcador (%)
{
const celdas = partido.matriz.flatMap((fila, i) => fila.map((p, j) => ({gl: i, gv: j, p})));
const [mh, mv] = partido.marcador.split("-").map(Number);
return Plot.plot({
width: 540, height: 420, marginLeft: 64, marginBottom: 48, marginTop: 8,
style: {fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif', fontSize: "12px", color: "#52514e"},
x: {label: `Goles de ${visita} →`, domain: d3.range(6), labelAnchor: "center", labelOffset: 38, tickSize: 0},
y: {label: `↓ Goles de ${local}`, domain: d3.range(6), labelAnchor: "center", labelOffset: 50, tickSize: 0},
color: {type: "linear", range: ["#eef5fd", "#184f95"]},
marks: [
Plot.cell(celdas, {x: "gv", y: "gl", fill: "p", inset: 1.5, rx: 4, tip: true,
title: d => `${local} ${d.gl} – ${d.gv} ${visita}: ${(d.p * 100).toFixed(1)}%`}),
Plot.text(celdas, {x: "gv", y: "gl", text: d => (d.p * 100).toFixed(1),
fill: d => d.p > 0.075 ? "white" : "#0b0b0b"}),
Plot.cell([{gl: mh, gv: mv}], {x: "gv", y: "gl", fill: "none", stroke: "#0b0b0b", strokeWidth: 2, inset: 1, rx: 4})
]
});
}
¿De dónde sale el pronóstico?
{
const L = sim.stats.find(s => s.equipo === local);
const V = sim.stats.find(s => s.equipo === visita);
return html`<table class="tabla">
<thead><tr><th>Variable de M0</th><th>${local}</th><th>${visita}</th></tr></thead>
<tbody>
<tr><td>Elo antes del partido</td><td>${L.elo.toFixed(0)}</td><td>${V.elo.toFixed(0)}</td></tr>
<tr><td>Goles a favor por partido (temporada)</td><td>${L.gf.toFixed(2)}</td><td>${V.gf.toFixed(2)}</td></tr>
<tr><td>Goles en contra por partido (temporada)</td><td>${L.ga.toFixed(2)}</td><td>${V.ga.toFixed(2)}</td></tr>
<tr><td>Partidos jugados en ${sim.temporada}</td><td>${L.partidos_temporada}</td><td>${V.partidos_temporada}</td></tr>
</tbody>
</table>
<p class="nota" style="margin-top:.8rem">Diferencia de Elo: <b>${Math.abs(L.elo - V.elo).toFixed(0)} puntos</b> a favor de
${L.elo >= V.elo ? local : visita}. ${sim.k_shrinkage === 0
? html`Los goles a favor y en contra son promedios de la temporada en curso
(<i>shrinkage</i> k = ${sim.k_shrinkage}: la calibración del equipo mostró que mezclarlos con la temporada anterior no
mejora el pronóstico). Antes del primer partido de la temporada se usa la anterior, y un equipo recién ascendido
recibe el promedio de la liga. Con pocos partidos jugados, estos promedios todavía varían mucho.`
: html`Los goles a favor y en contra mezclan la temporada en curso con la anterior
(<i>shrinkage</i> k = ${sim.k_shrinkage}, peso n/(n+k) a la actual): con pocos partidos jugados pesa más la
temporada pasada, y un equipo recién ascendido recibe el promedio de la liga.`}</p>
<p class="nota">El marcador más probable (recuadro negro) suele ser corto. Un equipo puede ser favorito aunque
el marcador individual más probable sea un empate, porque su victoria suma muchos marcadores distintos.</p>`;
}