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 — Team Strength

Skip to content

Football > World Cup 2026 > Team Strength

Football · World Cup 2026 · Team Ratings

How strong is each squad?

One headline number per team: Tiento — goals better (or worse) than the average World Cup side on a neutral pitch, blended from our four independent rating systems and named for the ball that played the first half of the first-ever World Cup final. The full seven-system table sits underneath. Panna / offense / defense aggregate squad player ratings; EPR converts to per-90 expected possession value; PSR captures box-score skills; Elo is the classic team rating; BT compresses the match model’s predictions for all 1,128 possible pairings into a single Bradley-Terry strength per team (host advantage for the USA, Canada and Mexico is baked in — it reads as expected strength at this tournament).

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 2026 home &uarr;</a></span>
  <span>&approx; 4 min read</span>
</div>`
Show code
window.editorial.sidebarToggle()
Show code
statsEsc = window.statsEsc

// The strength ratings come from the parquet, but the Champ % column is
// overlaid from the LIVE in-browser sim (wc-live-sim.js) so it tracks real +
// in-progress results, re-run every minute while games are live.
_wcLiveTeams = window.wcLiveSim.simStream()
_wcStrengthRaw = {
  try { return await window.fetchParquet(window.DATA_BASE_URL + "football/wc2026_team_strength.parquet") }
  catch (e) { console.error("[wc2026] team_strength load failed:", e); return null }
}
_wcStrength = {
  if (_wcStrengthRaw == null) return null
  const champ = new Map((_wcLiveTeams || []).map(t => [t.team, t.p_champ]))
  return _wcStrengthRaw.map(r => champ.has(r.team) ? { ...r, p_champ: champ.get(r.team) } : r)
}
// Fixture predictions feed the goals-scale calibration of the aggregate rating
_wcStrengthPreds = {
  try { return await window.fetchParquet(window.DATA_BASE_URL + "football/wc2026_predictions.parquet") }
  catch (e) { console.warn("[wc2026] predictions load failed (rating uses fallback scale):", e); return null }
}
// Aggregate rating: z-blend of panna/EPR/PSR/Elo on a goals-above-average
// scale (see wcMaps.computeTeamRating for weights + why BT is excluded)
wcRating = {
  if (_wcStrength == null) return null
  return window.wcMaps.computeTeamRating(_wcStrength, _wcStrengthPreds)
}
Show code
viewof wcStrengthCategory = {
  const cats = [
    ["rating", window.wcMaps.RATING_NAME],
    ["panna", "Panna"], ["offense", "Offense"], ["defense", "Defense"],
    ["epr", "EPR"], ["psr", "PSR"], ["elo", "Elo"], ["bt", "BT"], ["p_champ", "Champ %"]
  ]
  const wrap = document.createElement("div")
  wrap.className = "epv-toggle"
  wrap.style.cssText = "margin:0.5rem 0"
  wrap.value = "rating"
  for (const [val, label] of cats) {
    const btn = document.createElement("button")
    btn.className = "epv-toggle-btn" + (val === "rating" ? " active" : "")
    btn.textContent = label
    btn.addEventListener("click", () => {
      wrap.querySelectorAll(".epv-toggle-btn").forEach(b => b.classList.remove("active"))
      btn.classList.add("active")
      wrap.value = val
      wrap.dispatchEvent(new Event("input", { bubbles: true }))
    })
    wrap.appendChild(btn)
  }
  return wrap
}

{
  if (_wcStrength == null) return html`<p class="text-muted">Data failed to load — try refreshing (see console for details).</p>`
  if (_wcStrength.length === 0) return html`<p class="text-muted">No strength data available.</p>`

  const sortCol = wcStrengthCategory
  const fmt3 = x => x?.toFixed(3) ?? ""
  const fmtGoals = x => x == null ? "" : (x >= 0 ? "+" : "−") + Math.abs(x).toFixed(2)

  const rows = _wcStrength.map(r => ({
    ...r,
    rating: wcRating ? wcRating.ratings.get(r.team) ?? null : null
  }))

  return window.statsTable(rows, {
    columns: ["team", "group", "rating", "panna", "offense", "defense", "epr", "psr", "elo", "bt", "p_champ"],
    header: {
      team: "Team", group: "Grp", rating: window.wcMaps.RATING_NAME,
      panna: "Panna", offense: "Off", defense: "Def",
      epr: "EPR", psr: "PSR", elo: "Elo", bt: "BT", p_champ: "Champ %"
    },
    groups: [
      { label: "", span: 2 },
      { label: "Overall", span: 1 },
      { label: "Player ratings", span: 5 },
      { label: "Team ratings", span: 2 },
      { label: "", span: 1 }
    ],
    format: {
      rating: fmtGoals,
      panna: fmt3, offense: fmt3, defense: fmt3,
      epr: fmt3, psr: fmt3, elo: x => x?.toFixed(0) ?? "", bt: fmt3,
      p_champ: x => x != null ? x.toFixed(1) + "%" : ""
    },
    render: {
      team: (v, r) => window.wcMaps.teamLinkHtml(r.team, { flag: false, cls: "player-link" })
    },
    heatmap: {
      // `rating` is goals above/below the average WC team — diverging, centred 0.
      // `defense` in wc2026_team_strength.parquet is PRE-SIGN-FLIPPED at the
      // pannadata layer ("positive = good" per WC2026_BLOG_IMPLEMENTATION.md).
      // Diverges from football/stat-defs.js + team-ratings.qmd which use
      // "low-good" because their defense column is raw xG-conceded. Don't
      // align these by reflex — the underlying columns aren't comparable.
      rating: "diverging",
      panna: "high-good", offense: "high-good", defense: "high-good",
      epr: "high-good", psr: "high-good", elo: "high-good", bt: "high-good",
      p_champ: "high-good"
    },
    sort: sortCol,
    reverse: true,
    rows: 48
  })
}
Show code
wcStrengthAsAt = window.editorial.dataUpdated(window.DATA_BASE_URL + "football/wc2026_team_strength.parquet")
Show code
{
  const inner = document.createElement("div")
  inner.className = "side-rail-inner"
  const { railBlock, btnTile, tableSource } = window.editorial
  const asAt = await wcStrengthAsAt

  if (_wcStrength && _wcStrength.length > 0) {
    // Lead tiles with self-explanatory numbers (agreement count, Elo, champ %)
    // — raw panna / BT decimals mean nothing without the table for scale.
    const systems = ["panna", "offense", "defense", "epr", "psr", "elo", "bt"]
    const counts = new Map()
    for (const k of systems) {
      const top = [..._wcStrength].sort((a, b) => (b[k] ?? -Infinity) - (a[k] ?? -Infinity))[0]
      if (top) counts.set(top.team, (counts.get(top.team) || 0) + 1)
    }
    const [topTeam, nAgree] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]
    const byElo = [..._wcStrength].sort((a, b) => b.elo - a.elo)[0]
    const byChamp = [..._wcStrength].sort((a, b) => (b.p_champ ?? 0) - (a.p_champ ?? 0))[0]
    const btn = railBlock("By the numbers")
    btn.appendChild(btnTile(`${nAgree} of 7`, [
      { text: "systems rank " }, { text: topTeam, bold: true }, { text: " the strongest squad" }
    ]))
    btn.appendChild(btnTile(byElo.elo.toFixed(0), [
      { text: "Highest Elo · " }, { text: byElo.team, bold: true }
    ]))
    if (byChamp?.p_champ != null) {
      btn.appendChild(btnTile(`${byChamp.p_champ.toFixed(0)}%`, [
        { text: "Best title odds · " }, { text: byChamp.team, bold: true }
      ]))
    }
    btn.appendChild(btnTile("48", [{ text: "Teams · 7 rating systems" }]))
    inner.appendChild(btn)
  }

  const links = railBlock("Read next")
  const l1 = document.createElement("div"); l1.innerHTML = `<a href="world-cup-title-race.html"><strong>Title Race</strong></a><br><span class="text-muted" style="font-size:0.78rem">Champion probabilities</span>`
  links.appendChild(l1)
  const l2 = document.createElement("div"); l2.style.marginTop = "0.7rem"
  l2.innerHTML = `<a href="world-cup-groups.html"><strong>Group Projections</strong></a><br><span class="text-muted" style="font-size:0.78rem">12 groups · advance %</span>`
  links.appendChild(l2)
  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: "7 rating systems"
  }))

  return inner
}
 

Pete Owen · Sydney · © 2026 · Source

My Teams | Settings | Photo Credits | Privacy | Disclaimer