In honour of the start of the 2026 Fifa World Cup and my competitive nature to try and win my office score prediction pool, I decided to quickly throw together a “fun” analysis using some of my math, programming, and most importantly, claude background.

Scoring

The league is pretty straight forward. Players make a score prediction for every game in the world cup and are scored based on the following criteria:

  1. 3 points if you predict the exact score
  2. 1.5 points if you get the result right and your score is close
  3. 1 point if your result is right but your score isn’t close
  4. 0 points if you don’t get the result right

What “your score is close” means is beyond me and I didn’t feel like checking so here we are. Also note that you can make the predictions anytime before the game starts. Anyways shoutout SuperBru for this prediction game and let’s get into it.

My Approach

I aim to go for a hybrid approach of using data as well as fan intuition to make my predictions. The goal will be to create baseline predictions using available data and then develop a framework that uses added information to hopefully enhance the predictions. To keep things simple I am going to use 1X2 and over/under betting odds for each game to base the predictions off of. Then I will use some simple historical facts/stats to create a guideline to complete my decision making for the final scores to predict.

Betting Odds

Term

1X2 Odds

A three-way market where 1 = home win, X = draw, 2 = away win. Each outcome is priced in decimal odds, so dividing 1 by the odds to get the implied probability. A price of 1.65 implies a 60.6% chance.

Term

Over / Under

A market on total goals scored by both teams. The most common line is 2.5 — you bet on whether the game finishes with 3 or more goals (over) or 2 or fewer (under). Same idea as 1X2 for finding implied probability.

I found The Odds Api, which is a super cool api that gives you access to upcoming and historical sports betting odds. It has a free subscription tier where you get 500 free credits and get decent access to their data. Unfortunately, only paid members can get historical odds so I was not able to check that out, but otherwise it was easy to use and had everything I needed. Below is a simple python script showing how to extract 1X2 and over/under odds from the api. You can also check out the website above which has detailed documentation about using the API.

import requests

#get you key from their website when signing up and reference documenation for sport
API_KEY = "your_key_here"
SPORT   = "soccer__fifa_world_cup"  

#request the correct API endpoint for odds you want
response = requests.get(
    "https://api.the-odds-api.com/v4/sports/{}/odds".format(SPORT),
    params={
        "apiKey":      API_KEY,
        "regions":     "uk",
        "markets":     "h2h,totals",
        "oddsFormat":  "decimal",
    }
)

# to check how many credits you have left (optional)
response.raise_for_status()
    remaining = response.headers.get("x-requests-remaining", "?")
    print(f"  [API] Requests remaining this month: {remaining}\n") 

# get each home and away odds from the first book maker
for match in response.json():
    home = match["home_team"]
    away = match["away_team"]

    for bookmaker in match["bookmakers"]:
        for market in bookmaker["markets"]:

            if market["key"] == "h2h":
                odds = {o["name"]: o["price"] for o in market["outcomes"]}
                print(f"{home} vs {away}")
                print(f"  H: {odds[home]}  D: {odds['Draw']}  A: {odds[away]}")

            if market["key"] == "totals":
                for o in market["outcomes"]:
                    if o["name"] == "Over":
                        print(f"  O/U {o['point']}: {o['price']}")
        break  # first bookmaker only

My idea was that if I had the bookies odds for every team’s winning chances, as well as the line of over/under 2.5 goals in a game, then I figured I could do some sort of reverse engineering to predict each team’s winning probability and number of goals expected to score. A main theme was trying to keep the model as simple, but meaningful as possible.

Math Behind the Model

The model uses the Poisson distribution to estimate underlying win and goal probabilites from the betting odds. The poisson distribution is a discrete probability distribution, meaning that it gives the likelihood of a countable outcome occurring (definition here). The outcome in our case is the number of goals scored in a given fixture and this value is represented by λ. The poisson distribution can be used if the following two conditions are met:

  1. Individual events happen at random and are independent.
  2. We know the mean (average) number of events occurring within a given time interval.

Number 1 is an assumption of our model. What it means is that to use the poisson distribution, goals in a soccer game must happen randomly and the impact of a goal must not affect the likelihood of who scores the next goal. In reality this is not necessarily the case as a team that concedes might push extra hard for an equalizer or blow up and concede more, however scoring in soccer is random enough to justify making this assumption.

Number 2 says we must know the average number of goals occurring in a game and although we do not know this, by using the over/under 2.5 goals odds, we can solve and estimate it.


Step 1: Vig Removal

Just before we dive into estimating the number of goals per game, we are going to address something called vigorish (or vig for short). Vig is a commission added to the betting odds to ensure the bookmakers can take a profit. For a three-way market (home/draw/away) the implied probabilities, calculated as 1 / decimal_odds, sum to more than 1. That excess is the bookmaker’s cut. Before using the odds for anything we strip it out by normalising:

\[\hat{p}_i = \frac{p_i}{\sum_j p_j}\]

where p_i = 1 / decimal_odds_i is the raw implied probability for outcome i, and the sum in the denominator runs over all outcomes j in the market. For example, odds of 1.65 / 3.80 / 5.50 produce raw probabilities that sum to 1.051. After normalisation: home 57.7%, draw 25.0%, away 17.3%.


Step 2: Solving for Expected Goals (λ)

We model total goals as a Poisson random variable with unknown mean λ (the average number of goals we expect in the match). The Poisson PMF is:

\[P(X = k) = \frac{\lambda^k e^{-\lambda}}{k!}\]

where X is the total number of goals scored, k is a specific goal count, and e is Euler’s number. The over/under 2.5 market gives us P(goals ≥ 3) directly. We need the λ that satisfies:

\[P(X \geq 3 \mid \lambda) = 1 - \sum_{k=0}^{2} \frac{\lambda^k e^{-\lambda}}{k!} = p_{\text{over}}\]

where p_over is the normalised implied probability from the over 2.5 odds. There is no closed-form solution, so we define:

\[f(\lambda) = P(X \geq 3 \mid \lambda) - p_{\text{over}} = 0\]

and solve numerically using Brent’s method. There is no possible way to rearrange for λ, so we have to use a numerical method like Brent’s. A market implying 55% chance of over 2.5 goals solves to λ ≈ 2.88.


Step 3: Splitting λ Between Teams

We now have total expected goals but need to assign them to each team. We use the 1X2 win probabilities as a proxy for relative team strength, the stronger team gets a proportionally larger share:

\[s = \frac{\hat{p}_{\text{home}}}{\hat{p}_{\text{home}} + \hat{p}_{\text{away}}}\] \[\lambda_h = \lambda \cdot s \qquad \lambda_a = \lambda \cdot (1 - s)\]

where s is the home team’s share of the two-outcome (home/away) probability, p_home and p_away are the normalised win probabilities from Step 1, and lambda_h, lambda_a are the resulting expected goals for the home and away team respectively. For our example: s = 0.577 / (0.577 + 0.173) = 0.769, giving λ_h = 2.21 and λ_a = 0.67.


Known Limitations

Draw probability is discarded in the split. The proportional split only uses win probabilities. A 25% draw probability implies the two teams’ lambdas should be close together; 10% implies they should be far apart. This information is currently unused.

No team news or lineups. The model doesn’t know if a key player is injured or if a team is already qualified or knocked out. The market prices result probabilities but not always the scoreline distribution cleanly.

Making the predictions

The following structure will be used to make predictions:

  1. For the first round use models predictions as is regardless of team news or injury.
  2. At the conclusion of round 1, look over injuries/team news to see if any outliers can be identified. I.e predictions under or over predicting goals.
  3. Get updated betting odds from API before the start of round 2.
  4. Use model predictions again, but adjust by a goal up or down depending on whether teams are missing key players.
  5. Same idea for final round, depending on how predictions are going two options. If good then continue with the current method, then take a look at most common score lines so far in tournament and use those for each prediction in hopes to make up some points by hitting correct scores.

Below are the following predictions for each group stage game and will be updated and dated as they are played.

Group Stage 1

Last updated: June 19, 2026

Match Predicted xG Actual

Group Stage 1 Review

After 24 games the model is holding up reasonably well on results (46% correct W/D/L vs 33% random baseline) but is systematically underestimating goals, missing by 0.42 per game on average.

The bias is largely outlier-driven — Germany 7-1, Sweden 5-1, England 4-2, and USA 4-1 account for 11 of the 23 missing goals. Strip those blowouts out and the model is close to calibrated. For competitive games it’s performing as expected.

The bigger pattern is a goal distribution mismatch: the model clusters predictions around 3-goal games but the tournament is running hot, with 42% of matches producing 4+ goals and 1-1 appearing six times as the most common scoreline. The model generated zero sub-2-goal predictions; in reality 4 games finished with 1 goal or fewer.

For matchday 2 I’ll be nudging λ upward selectively for mismatched fixtures rather than applying a global adjustment — the blowout games were all heavy favourites against weak opposition, so that’s where the model leaves the most on the table. Although 1-1 is the most common scoreline and my model rarely predicting, I am not going to adjust anything here yet and let the model run on the updated odds once again.

Group Stage 2

Updated June 24

Only change I’ve made is a few 3-0 games bumped to 4-0. Purely based on strong teams playing weaker and the estimated xG being above 3 for the stronger team.

Match Predicted xG Actual

After round 2, a few notes. The model has predicted 29/48 games, increasing accuracy to around 60%. I can’t be mad at this and it is probably performing a bit better than I expected. Unfortunately, my idea of bumping some 3-0 games up to 4-0 has backfired because I bumped the wrong 3-0’s up and left the ones that actually went 4-0 as 3-0. I should have probably either bumped all to 4-0 or left all at 3-0, to try and collect as many exact scores as possible.

Where the model has dissapointed is in exact scores. I have only got 3 exact scores so far, which means I am missing out on a lot of points. The tournament has seen nineteen 4+ goal games and I have only predicted 3. There were also a combined twelve 0 or 1 goal games where I predicted none. The model predicted thirty-one 3 goal games and only six occurred. 2 goal games were predicted fourteen times and actually happened eleven times. It clear we are predicting 3 goal games far too often and neglecting low scoring games. I think due to variance its much harder to predict high scoring games, so for round 3 we should focus our efforts on predicting more of the low-scoring outcomes.

Group Stage 3

Updated June 24

So now what? Going into the final group stage games I have created a decision tree to help make my predictions. I will go back to the model predictions in the knockouts.

Are win probabilities within 20% of each other?

  • → Yes: does one team have a clear incentive?
    • → Yes: predict 1-0
    • → No: predict 0-0 or 1-1, whichever’s closer to prediction.
  • → No (clear favourite): Does model predict 3-0 or higher?
    • → Yes: drop to 2-0
    • → No: drop predicted goals by 1 (2-1 → 1-0, 2-0 → 1-0)

With this set of logic the idea is to target the low-scoring games we have missed out on thus far. Given the final round I think a lot of teams will be set up very defensively with their tournaments on the line so I don’t see why low-scoring shouldn’t continue to happen. With that being said here are my picks:

Match Predicted xG Actual

Round of 32

Updated June 29

Round of 32 up next. I ended up going 17/24 correct outcomes with 3 exact last round. The method of predicting low scoring, got us a good return but not much different that the model in round 2. Because of this and the fact that knockouts begin, I am going to go back to using the model predictions as is. We will reassess after the round of 32. In the league I am playing in I currently in 67/411. So not great but not terrible either. Last round shot us up a bunch so hopefully we will conintue to climb. Anyways here are my predictions.

Match Predicted xG Actual

We continue to climb getting up to around 30th in my office pool. Best round yet with 68% accuracy and 37.5% exact scorelines. I’ll chalk it up that we were just due for some good fortune, but we will leave the model as is going into the next round and see how it holds.

Round of 16

Updated July 4

Nothing new here, model predictions are as follows:

Match Predicted xG Actual

Quarter Finals

Updated July 9

No exact scores for the round of 16. The model had England and Belgium tying which personally I would’ve picked wins for both, but other than that some pretty unpredictable scorelines.

The most common scorelines are 1-0 with 14, 1-1 with 12, 2-1 with 11, 2-0 with 9, and 0-0 with 8. The model has the favorites winning 2-1 in every game of the quarterfinals. Although personally I would go with 1-0 for some games given it’s late in the world cup and teams will probably tend to start favoring defense, I think picking the same score for every match gives me a good chance of getting exact scores, especially since 2-1 is common. Part of me wants to go 1-0 for each, but I will put some faith in the model and let it try and prove itself once again.

Match Predicted xG Actual

Semi Finals

Updated July 12

Not bad quarter finals. 2 exact and 2 close. I am currently sitting around 30th out of 250 in the office league. Trusting the model paid off, I think picking the same score for every game at this point is a good strategy. However, I will still follow the model for the semis. Both are close games and I think the only way to really make up any serious ground is to get exact for both.

Match Predicted xG Actual

Final

Updated July 20

Was slacking a bit and forgot to update. Previous round was rough, as neither of the bookies favorites made the final. Not much I think can be done at this stage as there’s only 2 games and the team’s are so close we shall just let it play out.

Match Predicted xG Actual

I increased the England France scoring predictions from 2-1 France, to 3-2, as I figured the third place match would be high scoring, but truly the unthinkable happened haha. Also pure domination from Spain and I had them myself down as 1-0 win but obviously took the models prediction.

Wrap Up

Updated June 12, 2026

I don’t have much to say right now other than GO CANADA, and hopefully these predictions can serve me well. I will at the least update this post at the end of every groupstage round, but hopefully more often than that. Anyways I appreciate those who made it this far and best of luck to you and your team you will be cheering on this World Cup!

Updated July 20, 2026

Well the world cup is finished, congrats to Spain, and every other team that created history and special moments for their countries.

I ended up finishing 42/408 in my office league, narrowly missing the top 10%. 104 games predicted, 21 results correct, 30 close scores, and 14 exact scores. A prediction success rate of 63%. A few takeaways/lessons/ideas to learn from and hopefully improve upon next time.

  • Use a method of comparison. I should have made my predictions for each game as an avid football fan, seeing how my predictions would have stacked up vs the model.
  • Compare the results of the model to other common approaches. Does this model actually meaningfully improve predictions, or would I have had better success predicting the most common scoreline historically for every game?
  • In a game scenario like this office pool, are there oppurtunites to sytematically hedge my prediction to try and make ground? I.e if the model had two teams with relatively close odds, and one team was heavily being chosen to win more than the other, picking the less common choice may statistically have given me advantage.

Overall, this was a fun experience and I am glad I did it. Although I know it was not thourougly thought through or planned, it is nice to have seen it through from start to end, and more importantly it has piqued my interest in continuing to do more predictions and learning how to improve them. Thanks for following along!

Jack Sears