World Cup 2026 - Revenge of the French

This year’s World Cup has been fantastic. There was quite a bit of negative discourse about the size of the tournament (too many small nations will make for lots of blowouts blah blah blah), but I’d argue that it was the bigger “darkhorse” teams that disappointed. Türkiye, Belgium, and Uruguay all underperformed, while darlings like Curaçao (first World Cup and World Cup goal!), Cape Verde (shoutout Instagram star Vozinha), and DR Congo (welcome to the knockouts!) all suprised and delighted.

Before play kicked off, I predicted the outcome of all 72 Group Stage matches - here’s how I faired:

Overall accuracy 55.6% 40 of 72 correct
Best group Group C 5 of 6 correct
Toughest group Group D 1 of 6 correct
Group A
2/6
Group B
3/6
Group C
5/6
Group D
1/6
Group E
3/6
Group F
5/6
Group G
3/6
Group H
3/6
Group I
4/6
Group J
4/6
Group K
4/6
Group L
3/6

I’d say that 55.6% accuracy with no predictive model or any (thorough) research isn’t too shabby - especially since these matches can have draws. That said, Group D was brutal. Türkiye decided to just do cardio in their first two games and then turn into Pep’s Barça against the USMNT B Team, Paraguay absolutely underperformed, and Australia was excellent.

The knockouts begin today and I decided to make predictions again, but this time using a predictive model. Here’s my bracket:

Loading bracket...

I’d say Mexico over England in the Round of 16 is the only “spicy” take here, but I fully agree with the model - I don’t like England’s chances at ALL at the Azteca, 7,200ft above sea level, etc.).

Saying I have an emotional dependence on the performance of the U.S. Men’s National Team is an understatement. With this being the first World Cup on home soil since ‘94, and our quality of play vs. Paraguay and Australia, a loss to Bosnia in the Round of 32 would be catastrophic. I give us the edge against Belgium in the Round of 16 with the game being in Seattle, but I could see us pissing ourselves in true USMNT fashion (hopefully I’m wrong).

The model has the final as a coin flip, but I give the edge to France. They’re perhaps the most complete squad we’ve ever seen. Messi’s performance throughout the Group Stage has been nothing short of heroic (he’s the Greatest of All Time for a reason) but I think the French handle the Argentines somewhat comfortably (France 3 - Argentina 1).

I’ve loved every second of this tournament so far and I can’t wait for more!

Model Breakdown

Knockout Stage Prediction Model

Intermediate model using group stage performance + FIFA rankings.

Weights:

  • Group stage points 40%
  • Goal differential 25%
  • Goals scored 15%
  • FIFA ranking 20%
teams = {
    "France":       {"pts": 9,  "gd": 8,  "gs": 10, "rank": 2},
    "Argentina":    {"pts": 9,  "gd": 6,  "gs": 8,  "rank": 1},
    "Spain":        {"pts": 7,  "gd": 5,  "gs": 5,  "rank": 8},
    "Netherlands":  {"pts": 7,  "gd": 8,  "gs": 10, "rank": 7},
    "England":      {"pts": 7,  "gd": 5,  "gs": 6,  "rank": 5},
    "Brazil":       {"pts": 7,  "gd": 6,  "gs": 7,  "rank": 4},
    "Belgium":      {"pts": 5,  "gd": 4,  "gs": 7,  "rank": 3},
    "Germany":      {"pts": 6,  "gd": 7,  "gs": 10, "rank": 13},
    "Portugal":     {"pts": 6,  "gd": 5,  "gs": 6,  "rank": 6},
    "Colombia":     {"pts": 7,  "gd": 4,  "gs": 4,  "rank": 11},
    "USA":          {"pts": 6,  "gd": 4,  "gs": 6,  "rank": 12},
    "Mexico":       {"pts": 9,  "gd": 5,  "gs": 6,  "rank": 15},
    "Norway":       {"pts": 6,  "gd": 4,  "gs": 7,  "rank": 22},
    "Switzerland":  {"pts": 7,  "gd": 5,  "gs": 7,  "rank": 19},
    "Australia":    {"pts": 6,  "gd": 2,  "gs": 4,  "rank": 23},
    "Croatia":      {"pts": 6,  "gd": 2,  "gs": 5,  "rank": 10},
    "Japan":        {"pts": 5,  "gd": 4,  "gs": 7,  "rank": 18},
    "Morocco":      {"pts": 5,  "gd": 4,  "gs": 5,  "rank": 14},
    "Senegal":      {"pts": 4,  "gd": 4,  "gs": 7,  "rank": 20},
    "Egypt":        {"pts": 5,  "gd": 2,  "gs": 5,  "rank": 34},
    "Ivory Coast":  {"pts": 4,  "gd": 1,  "gs": 3,  "rank": 27},
    "Austria":      {"pts": 4,  "gd": 2,  "gs": 6,  "rank": 26},
    "Algeria":      {"pts": 4,  "gd": -1, "gs": 5,  "rank": 39},
    "Congo DR":     {"pts": 4,  "gd": -1, "gs": 4,  "rank": 55},
    "Cape Verde":   {"pts": 5,  "gd": -1, "gs": 2,  "rank": 70},
    "Ghana":        {"pts": 4,  "gd": 0,  "gs": 1,  "rank": 60},
    "S. Africa":    {"pts": 1,  "gd": -2, "gs": 1,  "rank": 66},
    "Canada":       {"pts": 4,  "gd": 4,  "gs": 7,  "rank": 37},
    "Bosnia":       {"pts": 4,  "gd": -2, "gs": 4,  "rank": 61},
    "Paraguay":     {"pts": 3,  "gd": -3, "gs": 2,  "rank": 64},
    "Sweden":       {"pts": 4,  "gd": 4,  "gs": 7,  "rank": 25},
    "Ecuador":      {"pts": 3,  "gd": 1,  "gs": 3,  "rank": 43},
    "S. Korea":     {"pts": 3,  "gd": 0,  "gs": 1,  "rank": 21},
}

round_of_32 = [
    ("S. Africa",   "Canada"),
    ("Brazil",      "Japan"),
    ("Germany",     "Paraguay"),
    ("Netherlands", "Morocco"),
    ("Ivory Coast", "Norway"),
    ("France",      "Sweden"),
    ("Mexico",      "Ecuador"),
    ("England",     "Congo DR"),
    ("Belgium",     "Senegal"),
    ("USA",         "Bosnia"),
    ("Spain",       "Austria"),
    ("Portugal",    "Croatia"),
    ("Switzerland", "Algeria"),
    ("Australia",   "Egypt"),
    ("Argentina",   "Cape Verde"),
    ("Colombia",    "Ghana"),
]


def team_score(team: dict) -> float:
    pts_score  = team["pts"]  * 40
    gd_score   = (team["gd"] + 10) * (25 / 20)
    gs_score   = team["gs"]  * (15 / 10)
    rank_score = max(0, 100 - team["rank"]) * (20 / 99)
    return pts_score + gd_score + gs_score + rank_score


def win_probability(team_a: dict, team_b: dict) -> float:
    score_a = team_score(team_a)
    score_b = team_score(team_b)
    raw_prob = score_a / (score_a + score_b)
    return round(max(0.20, min(0.80, raw_prob)), 3)


def predict_match(name_a: str, name_b: str) -> dict:
    team_a = teams[name_a]
    team_b = teams[name_b]
    prob_a = win_probability(team_a, team_b)
    winner = name_a if prob_a >= 0.5 else name_b
    loser  = name_b if prob_a >= 0.5 else name_a
    prob_w = prob_a if prob_a >= 0.5 else 1 - prob_a
    return {
        "winner": winner,
        "loser":  loser,
        "prob_winner": round(prob_w * 100),
        "prob_loser":  round((1 - prob_w) * 100),
    }


def simulate_bracket(first_round: list[tuple]) -> dict:
    results = {}
    current_round = first_round
    round_names = ["Round of 32", "Round of 16", "Quarterfinals", "Semifinals", "Final"]

    for round_name in round_names:
        round_results = [predict_match(a, b) for a, b in current_round]
        results[round_name] = round_results
        winners = [r["winner"] for r in round_results]
        current_round = list(zip(winners[::2], winners[1::2]))
        if len(current_round) == 0:
            break

    return results


def print_bracket(bracket: dict) -> None:
    for round_name, matches in bracket.items():
        print(f"\n{'='*40}")
        print(f"  {round_name}")
        print(f"{'='*40}")
        for m in matches:
            print(
                f"  {m['winner']:>14}  ({m['prob_winner']}%)  "
                f"def.  {m['loser']:<14} ({m['prob_loser']}%)"
            )


if __name__ == "__main__":
    bracket = simulate_bracket(round_of_32)
    print_bracket(bracket)
    champion = bracket["Final"][0]["winner"]
    print(f"\n🏆 Projected champion: {champion}\n")