In The Game
  • Home
  • World Cup
  • Blog
  • Games
  • AFL
    • Overview

    • Matches
    • Ladder

    • Player Stats
    • Player Ratings
    • Player Game Logs
    • Player Comparison
    • Card Deck
    • Age Curves

    • Team Stats
    • Team Game Logs
    • Team Ratings

    • Definitions
  • Football
    • Overview

    • Matches
    • Leagues

    • World Cup 2026
    • Simulator
    • Wall Chart
    • Pick Your Bracket
    • Title Race
    • Group Projections
    • Match Predictions
    • Player Stats
    • Player Ratings
    • Venues
    • Team Strength
    • Head to Head

    • Player Stats
    • Player Ratings
    • Player Game Logs
    • Player Comparison
    • Card Deck

    • Team Stats
    • Team Game Logs
    • Team Ratings

    • Definitions
  • About

World Cup 2026 — Head to Head

Skip to content

Football > World Cup 2026 > Head to Head

Football · World Cup 2026 · Head to Head

Show code
statsEsc = window.statsEsc

// Per-team rating systems + group letter (panna/offense/defense/epr/psr/elo/bt
// and their rank_* columns), straight from the parquet.
_wchStrength = {
  try { return await window.fetchParquet(window.DATA_BASE_URL + "football/wc2026_team_strength.parquet") }
  catch (e) { console.error("[wc-compare] team_strength load failed:", e); return null }
}
// Group fixtures: prob_home/draw/away + pred_*_goals, used to spot a direct
// group meeting and to calibrate Tiento's goals scale.
_wchPredictions = {
  try { return await window.fetchParquet(window.DATA_BASE_URL + "football/wc2026_predictions.parquet") }
  catch (e) { console.error("[wc-compare] predictions load failed:", e); return null }
}
// Tournament odds from the LIVE in-browser sim (wc-live-sim.js): real +
// in-progress results baked in, re-run every minute while games are live.
// _wchFull carries matchups (per-KO-round ties) + meta; _wchSim is teams[].
_wchFull = window.wcLiveSim.fullStream()
_wchSim = window.wcLiveSim.simStream()
Show code
// Tiento aggregate rating — z-blend of panna/EPR/PSR/Elo on a goals-above-
// average scale (see wcMaps.computeTeamRating for weights + why BT is excluded).
wchRating = {
  if (_wchStrength == null) return null
  return window.wcMaps.computeTeamRating(_wchStrength, _wchPredictions)
}
Show code
wchPick = {
  const wc = window.wcMaps
  const known = new Set([
    ...(_wchStrength || []).map(r => r.team),
    ...(_wchSim || []).map(r => r.team)
  ])
  // canonicalise a raw hash value against the known team set
  const resolve = (raw) => {
    if (!raw) return null
    if (known.has(raw)) return raw
    const n = wc.normalizeWcTeam(raw)
    return known.has(n) ? n : null
  }
  let a = resolve(window._getHashParam("a"))
  let b = resolve(window._getHashParam("b"))

  // Default ordering by champion probability (live sim), else parquet order.
  const ranked = (_wchSim && _wchSim.length)
    ? [..._wchSim].sort((x, y) => (y.p_champ ?? 0) - (x.p_champ ?? 0)).map(r => r.team)
    : (_wchStrength || []).map(r => r.team)

  if (!a) a = ranked.find(t => t !== b) ?? null
  if (!b) b = ranked.find(t => t !== a) ?? null
  // guard against A === B
  if (a && b && a === b) b = ranked.find(t => t !== a) ?? null

  if (a && b) document.title = `${a} vs ${b} — World Cup 2026 — In The Game`
  return { a, b }
}

wchA = wchPick.a
wchB = wchPick.b

// Per-team rows used by every section below
wchStrA = _wchStrength?.find(r => r.team === wchA) ?? null
wchStrB = _wchStrength?.find(r => r.team === wchB) ?? null
wchSimA = _wchSim?.find(r => r.team === wchA) ?? null
wchSimB = _wchSim?.find(r => r.team === wchB) ?? null
wchGroupA = wchStrA?.group ?? wchSimA?.group ?? null
wchGroupB = wchStrB?.group ?? wchSimB?.group ?? null
Show code
// ── Hero: two flags + names + champ %, with a "vs" divider ─────────
{
  if (_wchStrength == null && _wchSim == null) return html`<h1>World Cup 2026 head to head</h1><p class="text-muted">Data failed to load — check the console.</p>`
  if (wchA == null || wchB == null) return html`<h1>World Cup 2026 head to head</h1><p class="text-muted">Loading…</p>`
  const wc = window.wcMaps
  const side = (team, sim) => `
    <div class="wch-hero-side">
      ${wc.flagImg(team, "wch-hero-flag")}
      <div class="wch-hero-name">${statsEsc(team)}</div>
      <div class="wch-hero-champ">${sim ? `<b>${sim.p_champ.toFixed(1)}%</b> to win it` : `<span class="text-muted">odds loading…</span>`}</div>
    </div>`
  const wrap = document.createElement("div")
  wrap.className = "wch-hero"
  wrap.innerHTML = `
    ${side(wchA, wchSimA)}
    <div class="wch-hero-vs">vs</div>
    ${side(wchB, wchSimB)}`
  return wrap
}

Two nations, side by side — champion odds, all seven rating systems plus the Tiento aggregate, each team’s road to the final, and the most likely knockout round they could meet.

Show code
html`<div class="byline">
  <span>By <strong>Pete Owen</strong></span>
  <span>Updated · <strong>Weekly</strong></span>
  <span><a href="world-cup-2026.html">World Cup hub &uarr;</a></span>
</div>`
Show code
window.editorial.sidebarToggle()
Show code
// ── Two team selectors — each sets its hash param; data-loader reloads ──
{
  if (_wchStrength == null || !_wchStrength.length) return html``
  const teams = [..._wchStrength].map(r => r.team).sort((a, b) => a.localeCompare(b))

  const selA = Inputs.select(teams, { label: "Team A", value: wchA })
  selA.addEventListener("input", () => {
    window.location.hash = "a=" + encodeURIComponent(selA.value) +
      "&b=" + encodeURIComponent(wchB ?? "")
  })
  const selB = Inputs.select(teams, { label: "Team B", value: wchB })
  selB.addEventListener("input", () => {
    window.location.hash = "a=" + encodeURIComponent(wchA ?? "") +
      "&b=" + encodeURIComponent(selB.value)
  })

  const wrap = document.createElement("div")
  wrap.className = "wch-selectors"
  wrap.append(selA, selB)
  return wrap
}
Show code
// ── Tiento + 7 ratings face-off: diverging bars, A left / B right ──
{
  if (wchA == null || wchB == null) return html``
  if (wchStrA == null || wchStrB == null) return html`<p class="text-muted">No rating data for one of these teams.</p>`
  const a = wchStrA, b = wchStrB
  const fmt3 = x => x != null ? x.toFixed(3) : "—"
  const fmt0 = x => x != null ? x.toFixed(0) : "—"
  const fmtGoals = x => x == null ? "—" : (x >= 0 ? "+" : "−") + Math.abs(x).toFixed(2)

  const tA = wchRating ? wchRating.ratings.get(wchA) ?? null : null
  const tB = wchRating ? wchRating.ratings.get(wchB) ?? null : null

  // Each system: value getter + fmt + which direction wins. defense is
  // pre-sign-flipped at the pannadata layer (positive = good), like Tiento it
  // diverges from the raw-xG defense elsewhere — see world-cup-strength.qmd.
  const systems = [
    { key: "rating", label: window.wcMaps.RATING_NAME, fmt: fmtGoals, valA: tA, valB: tB, rankA: null, rankB: null, hint: "Aggregate goals-above-average rating" },
    { key: "panna",   label: "Panna",   fmt: fmt3, valA: a.panna,   valB: b.panna,   rankA: a.rank_panna,   rankB: b.rank_panna,   hint: "Squad player ratings" },
    { key: "offense", label: "Offense", fmt: fmt3, valA: a.offense, valB: b.offense, rankA: a.rank_offense, rankB: b.rank_offense, hint: "Attacking squad value" },
    { key: "defense", label: "Defense", fmt: fmt3, valA: a.defense, valB: b.defense, rankA: a.rank_defense, rankB: b.rank_defense, hint: "Defensive squad value (positive = good)" },
    { key: "epr",     label: "EPR",     fmt: fmt3, valA: a.epr,     valB: b.epr,     rankA: a.rank_epr,     rankB: b.rank_epr,     hint: "Expected possession value / 90" },
    { key: "psr",     label: "PSR",     fmt: fmt3, valA: a.psr,     valB: b.psr,     rankA: a.rank_psr,     rankB: b.rank_psr,     hint: "Box-score skill rating" },
    { key: "elo",     label: "Elo",     fmt: fmt0, valA: a.elo,     valB: b.elo,     rankA: a.rank_elo,     rankB: b.rank_elo,     hint: "Team Elo rating" },
    { key: "bt",      label: "BT",      fmt: fmt3, valA: a.bt,      valB: b.bt,      rankA: a.rank_bt,      rankB: b.rank_bt,      hint: "Bradley-Terry tournament strength" }
  ]

  const wrap = document.createElement("div")
  const h = document.createElement("h2"); h.textContent = "Rating systems, head to head"
  wrap.appendChild(h)
  const note = document.createElement("p"); note.className = "text-muted"
  note.style.cssText = "font-size:0.84rem;margin:0 0 0.6rem"
  note.innerHTML = `Each row pits ${statsEsc(wchA)} (left) against ${statsEsc(wchB)} (right). The wider half is the higher value; rank is out of the 48 qualified nations. <a href="world-cup-strength.html">Full sortable table &rarr;</a>`
  wrap.appendChild(note)

  // Per-row bar: split track centred on a divider, each half scaled to the
  // larger of the two absolute values (so the winner fills its half).
  const card = document.createElement("div")
  card.className = "wch-faceoff"
  for (const s of systems) {
    const vA = s.valA, vB = s.valB
    const have = vA != null && vB != null && isFinite(vA) && isFinite(vB)
    const max = have ? Math.max(Math.abs(vA), Math.abs(vB), 1e-9) : 1
    const wA = have ? Math.min(100, Math.abs(vA) / max * 100) : 0
    const wB = have ? Math.min(100, Math.abs(vB) / max * 100) : 0
    const aWins = have && vA > vB
    const bWins = have && vB > vA

    const row = document.createElement("div")
    row.className = "wch-fo-row"
    row.title = s.hint
    row.innerHTML = `
      <div class="wch-fo-a${aWins ? " win" : ""}">
        <span class="wch-fo-rank">${s.rankA != null ? `#${s.rankA}` : ""}</span>
        <span class="wch-fo-val">${s.fmt(vA)}</span>
        <div class="wch-fo-track"><div class="wch-fo-fill a" style="width:${wA.toFixed(1)}%"></div></div>
      </div>
      <div class="wch-fo-lbl">${statsEsc(s.label)}</div>
      <div class="wch-fo-b${bWins ? " win" : ""}">
        <div class="wch-fo-track"><div class="wch-fo-fill b" style="width:${wB.toFixed(1)}%"></div></div>
        <span class="wch-fo-val">${s.fmt(vB)}</span>
        <span class="wch-fo-rank">${s.rankB != null ? `#${s.rankB}` : ""}</span>
      </div>`
    card.appendChild(row)
  }
  wrap.appendChild(card)
  return wrap
}
Show code
// ── Road to the final face-off: paired bars per stage ─────────────
{
  if (wchA == null || wchB == null) return html``
  if (wchSimA == null && wchSimB == null) return html``

  const stages = [
    { key: "advance", label: "Reach R32", sub: "advance from group" },
    { key: "p_R16",   label: "Reach R16", sub: "last 16" },
    { key: "p_QF",    label: "Reach QF",  sub: "quarter-final" },
    { key: "p_SF",    label: "Reach SF",  sub: "semi-final" },
    { key: "p_final", label: "Reach Final", sub: "Jul 19" },
    { key: "p_champ", label: "Champions", sub: "lift the trophy" }
  ]

  const wrap = document.createElement("div")
  wrap.style.marginTop = "1.6rem"
  const head = document.createElement("div")
  head.style.cssText = "display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap"
  const h = document.createElement("h2"); h.textContent = "Road to the final"
  head.append(h, window.wcLiveSim.liveBadge(_wchFull ? _wchFull.meta : null))
  wrap.appendChild(head)
  const note = document.createElement("p"); note.className = "text-muted"
  note.style.cssText = "font-size:0.84rem;margin:0 0 0.6rem"
  note.innerHTML = `Probability of reaching each round, from a 10,000-tournament Monte Carlo simulation re-run live in your browser. <span class="wch-key"><span class="wch-key-dot a"></span>${statsEsc(wchA)}</span> <span class="wch-key"><span class="wch-key-dot b"></span>${statsEsc(wchB)}</span>`
  wrap.appendChild(note)

  const card = document.createElement("div")
  card.className = "wch-road"
  for (const s of stages) {
    const vA = wchSimA?.[s.key]
    const vB = wchSimB?.[s.key]
    const row = document.createElement("div")
    row.className = "wch-road-row"
    const pct = (v) => v != null ? (v >= 10 ? v.toFixed(0) : v.toFixed(1)) + "%" : "—"
    row.innerHTML = `
      <div class="wch-road-a">
        <span class="wch-road-pct">${pct(vA)}</span>
        <div class="wch-road-track"><div class="wch-road-fill a" style="width:${vA != null ? Math.min(100, vA).toFixed(1) : 0}%"></div></div>
      </div>
      <div class="wch-road-lbl"><span class="wch-road-stage">${statsEsc(s.label)}</span><span class="wch-road-sub">${statsEsc(s.sub)}</span></div>
      <div class="wch-road-b">
        <div class="wch-road-track"><div class="wch-road-fill b" style="width:${vB != null ? Math.min(100, vB).toFixed(1) : 0}%"></div></div>
        <span class="wch-road-pct">${pct(vB)}</span>
      </div>`
    card.appendChild(row)
  }
  wrap.appendChild(card)
  return wrap
}
Show code
// ── If they meet ───────────────────────────────────────────────────
// Same group -> they meet in the group stage (show the direct fixture if one
// exists). Otherwise, scan the live sim's matchups (per-KO-round {a,b,pct}
// ties): P(meet) = sum of pct across all rounds where the tie contains both,
// and the single most-likely round to meet. matchups is null in the static-
// parquet fallback -> that part hides, group meetings still show.
{
  if (wchA == null || wchB == null) return html``
  const wc = window.wcMaps
  const wrap = document.createElement("div")
  wrap.style.marginTop = "1.6rem"
  const h = document.createElement("h2"); h.textContent = "If they meet"
  wrap.appendChild(h)

  const sameGroup = wchGroupA != null && wchGroupA === wchGroupB

  // A direct group fixture (either ordering) from the predictions parquet
  const directFx = (_wchPredictions || []).find(f =>
    (f.home_team === wchA && f.away_team === wchB) ||
    (f.home_team === wchB && f.away_team === wchA)) || null

  if (sameGroup) {
    const note = document.createElement("p")
    note.innerHTML = `${statsEsc(wchA)} and ${statsEsc(wchB)} are both in <a href="world-cup-group.html#group=${statsEsc(wchGroupA)}">Group ${statsEsc(wchGroupA)}</a>, so they meet in the group stage${directFx ? "" : " — but the fixture isn't in the prediction set"}.`
    wrap.appendChild(note)
    if (directFx) {
      const card = document.createElement("div")
      card.className = "match-cards-container"
      card.innerHTML = wc.fixtureCardHtml(directFx, null, { showDay: true, showGroupChip: true })
      wrap.appendChild(card)
    }
    return wrap
  }

  // Different groups -> knockout-meeting odds from the live sim matchups
  const full = _wchFull
  if (!full || !full.matchups) {
    const note = document.createElement("p"); note.className = "text-muted"
    note.textContent = "Knockout meeting odds load with the live simulation."
    wrap.appendChild(note)
    return wrap
  }

  const ROUND_LABELS = ["Round of 32", "Round of 16", "Quarter-final", "Semi-final", "Final"]
  const perRound = []
  let total = 0
  for (let r = 0; r < full.matchups.length; r++) {
    let pct = 0
    for (const m of full.matchups[r]) {
      const hit = (m.a === wchA && m.b === wchB) || (m.a === wchB && m.b === wchA)
      if (hit) pct += m.pct
    }
    perRound.push({ round: ROUND_LABELS[r] || `Round ${r}`, pct })
    total += pct
  }
  const likeliest = [...perRound].filter(x => x.pct > 0).sort((a, b) => b.pct - a.pct)[0] || null

  const note = document.createElement("p")
  note.innerHTML = total > 0
    ? `Across 10,000 live simulations, ${statsEsc(wchA)} and ${statsEsc(wchB)} meet in the knockout rounds <b>${total.toFixed(1)}%</b> of the time` +
      (likeliest ? ` — most often in the <b>${statsEsc(likeliest.round)}</b> (${likeliest.pct.toFixed(1)}%).` : ".")
    : `In different groups, ${statsEsc(wchA)} and ${statsEsc(wchB)} hardly ever meet — under 0.1% of the 10,000 live simulations.`
  wrap.appendChild(note)

  if (total > 0) {
    const rows = perRound.filter(x => x.pct > 0).sort((a, b) => b.pct - a.pct)
    const table = window.statsTable(rows, {
      columns: ["round", "pct"],
      header: { round: "Round", pct: "P(meet)" },
      tooltip: { pct: "Share of all 10,000 sims where they meet in this round" },
      format: { pct: x => x.toFixed(1) + "%" },
      heatmap: { pct: "high-good" },
      sort: "pct", reverse: true, rows: 6
    })
    wrap.appendChild(table)
  }
  return wrap
}
Show code
wcCompareAsAt = window.editorial.dataUpdated(window.DATA_BASE_URL + "football/wc2026_predictions.parquet")
Show code
{
  const inner = document.createElement("div")
  inner.className = "side-rail-inner"
  const { railBlock, btnTile, tableSource } = window.editorial
  const asAt = await wcCompareAsAt

  if (wchA != null && wchB != null) {
    const btn = railBlock("By the numbers")

    // Champ % gap
    if (wchSimA && wchSimB) {
      const gap = Math.abs(wchSimA.p_champ - wchSimB.p_champ)
      const lead = wchSimA.p_champ >= wchSimB.p_champ ? wchA : wchB
      btn.appendChild(btnTile(`${gap.toFixed(1)}pp`, [
        { text: "Champion-odds gap · " }, { text: lead, bold: true }, { text: " ahead" }
      ]))
    }

    // Tiento gap
    if (wchRating) {
      const tA = wchRating.ratings.get(wchA), tB = wchRating.ratings.get(wchB)
      if (tA != null && tB != null) {
        const gap = Math.abs(tA - tB)
        const lead = tA >= tB ? wchA : wchB
        btn.appendChild(btnTile(`${gap.toFixed(2)}`, [
          { text: `${window.wcMaps.RATING_NAME} gap (goals) · ` }, { text: lead, bold: true }, { text: " stronger" }
        ]))
      }
    }

    inner.appendChild(btn)
  }

  const links = railBlock("Read next")
  const hrefA = wchA ? "#team=" + encodeURIComponent(wchA) : ""
  const hrefB = wchB ? "#team=" + encodeURIComponent(wchB) : ""
  links.innerHTML = `
    <div><a href="world-cup-team.html${hrefA}"><strong>${wchA ? statsEsc(wchA) : "Team A"} profile</strong></a><br><span class="text-muted" style="font-size:0.78rem">Road, group odds, squad, ratings</span></div>
    <div style="margin-top:0.7rem"><a href="world-cup-team.html${hrefB}"><strong>${wchB ? statsEsc(wchB) : "Team B"} profile</strong></a><br><span class="text-muted" style="font-size:0.78rem">Road, group odds, squad, ratings</span></div>
    <div style="margin-top:0.7rem"><a href="world-cup-simulator.html"><strong>Simulator</strong></a><br><span class="text-muted" style="font-size:0.78rem">Lock in results, watch the odds move</span></div>
    <div style="margin-top:0.7rem"><a href="world-cup-strength.html"><strong>Team Strength</strong></a><br><span class="text-muted" style="font-size:0.78rem">All 48 squads, seven rating systems</span></div>`
  inner.appendChild(links)

  inner.appendChild(tableSource({
    source: "pannadata",
    sourceUrl: "https://github.com/peteowen1/pannadata",
    sourceNote: "Opta scrape",
    license: "CC BY 4.0",
    asAt: asAt || "Refreshed weekly",
    hint: "10K-simulation Monte Carlo"
  }))
  return inner
}
 

Pete Owen · Sydney · © 2026 · Source

My Teams | Settings | Photo Credits | Privacy | Disclaimer