It started as these things often do: in a group chat after a particularly interesting game.
Arsenal had just kept Liverpool to a 0-0 draw on the 8th of January. For most clubs, holding Liverpool to a clean sheet at home would be cause for celebration – but this was Arsenal, and a draw felt like a loss. The conversation in the chat quickly turned critical: Arsenal’s attack was too blunt, too predictable. They lacked the manpower to break down any resilient side. How could they claim to be the best team in Europe when they couldn’t score against anyone good?
I pushed back. The argument grew. Soon we weren’t just talking about Arsenal, but about all the Champions League contenders: domestic rivals Manchester City, the defending champions PSG, German league leaders Bayern Munich, and the Spanish contingent of Barcelona and Real Madrid. Each of them had their cases, and each of us tried our best to shoot holes through their claims as the debate dragged on for hours.
Eventually though, the discussion stalled. It was a constant back-and-forth: someone brought metrics that suited their argument, and someone else shot it down. One person cited league position, but that gets inflated in easier leagues. Another pointed to UCL performances, but with the new format, strength of schedule varies wildly amongst teams. A third person brought up one-off match results, but those are too small a sample size to make such judgements. Every metric had a counterargument. I wanted something more robust: a rating system for European teams whose methodology was consistent and interpretable, and one that used as much available evidence as possible.
That was the original goal: build a model that could estimate team strength and use it to answer the question of who the best team in Europe actually is. It was only in the process of building this model that I developed an interest in a slightly adjacent question: how likely is it to win the Champions League anyway? What is Arsenal’s probability of winning the whole thing versus getting to the final? I had seen a lot of these types of simulations for the Premier League, but none for the Champions League (for reasons I found out later in the project).
The Plan
My intention here is to give a slightly more technical description of what I did (this is a “How I Built” piece, after all). In my mind, a team strength estimator needed two things: a model, and the data.
The Model. By model, I do not just mean the statistical object that generates a best estimate. I also needed a theoretical model that aligns with my understanding of the sport and that can generate interpretable predictions. I considered Elo ratings and betting odds, but eventually came to settle on using a Dixon-Coles model to estimate team quality. It had a lot going for it: it is well researched, based on easily acceptable assumptions, and most importantly a Dixon-Coles model can generate sample results between two teams, which becomes more important once we get to the UCL simulator portion of this article.
For those unfamiliar: Dixon-Coles assigns attack and defense ratings to teams based on goals scored and conceded, weighting recent games more heavily than older ones. You can plug these ratings into a formula to get expected goals for any hypothetical matchup. I used the penaltyblog package to implement it.
The Data. Any model that estimates team strength needs something to estimate from. In the case of Dixon-Coles, it estimates rating parameters from previous match results. I scraped five years of those results (from the 2021/22 season through January 8th, 2026) from the top five European leagues plus the Champions League itself, using the soccerdata package. This gives the model enough history to generate reliable estimates, and ensures that teams moving between competitions still have a track record to draw on.
At this point, we have all that we need to build a Dixon-Coles model. However, at some point in the process I expanded the project to include a UCL sim, and that requires one more thing: an environment.
The Environment. In reinforcement learning, the “environment” refers to everything an agent can interact with.
The analogy carries over here: if the agents are the 36 UCL teams, then the environment is the system that allows these teams to interact with each other according to the rules of the Champions League. It contains information like the fixtures yet to be played, the group standings, draw logic, knockout progression, and ultimately spits out a winner. I can then use this environment and run a bunch of simulations to estimate the probability of PSG winning the tournament vs Bayern Munich.
This was by far the most time-consuming part of the project, and to understand why, it helps to explain how my existing Premier League probability tables work. The algorithm is simple:
- Take the current league fixtures and a model estimating team strength
- Split fixtures into played and unplayed games
- Simulate the results of all unplayed games
- Total each team’s points (simulated plus actual) and rank them
- Repeat steps 2-4 many times
At the end, you get a table saying Arsenal wins the league in X% of simulations, or Ipswich gets relegated Y% of the time.
In this format, the fixture list serves as the “environment”: you can derive the current league table from it, and you also have a list of all the unplayed fixtures to simulate.
With the UCL, however, things are different. For starters, a complete fixture list does not exist. The knockout stage pairings depend on league phase results that haven’t happened yet, so you need to first simulate the remaining league phase fixtures. But even then, the knockout stage matchups are non-deterministic, so you also need to implement the draw logic yourself. If you want to stay true to the UCL logic, you technically need to implement these draws in very particular ways, but I simplified it, ending up with this modified algorithm:
- Take the UCL league phase fixtures and the model
- Simulate any unplayed league phase games
- Build the final league table from played and simulated results
- Apply UCL draw logic to generate knockout pairings
- Simulate the knockout round
- Repeat steps 4-5 until a winner emerges
- Repeat steps 1-6 a thousand times
Steps 4 through 6 required building a helper class to handle bracket progression: tracking which teams advance, generating valid pairings according to UEFA’s seeding rules, and so on. Nothing conceptually difficult, but a lot of edge cases to get right.
One technical wrinkle: the Dixon-Coles model includes home advantage, and while it’s theoretically easy to remove that for neutral-venue matches, I could not find a clean way to do this in the package I was using. In order to get around that, I treated the final as a two-legged tie where both teams got a game with home advantage. Not perfect, but the effect on win probabilities should be minimal.
The Results
Let’s start with the headline question: which team is the best in Europe?
Based on the past five seasons of league and UCL games, our model has Arsenal as the strongest team in Europe right now, with an expected goal difference of 1.60 per game against the average UCL opponent. Luckily, I am vindicated.
From here, though, our model gets spicier, putting City in second place, slightly ahead of treble-winning PSG. I was taken aback at first, but thinking about how the model works, wins in the Premier League are generally worth more than wins in Ligue 1 due to the overall strength of competition. This model was also trained on several seasons where City were league winners, even if the time decay partially accounts for this.
We also see some other potential oddities: Liverpool ranks above Barcelona and Real Madrid, while Newcastle and Chelsea rank above Inter. Some would point out that this suggests an overrating of the Premier League, which may be true. However, I am inclined to believe that these estimates are not too far from the real thing – and it does not hurt as much knowing that Liverpool beat Real Madrid earlier in the tournament.
Since we are talking so much about leagues, let’s rank them too:
We can see that the Premier League contingent is by far the strongest. This follows from the previous graphic where we saw that five of the top ten strongest UCL teams are English. Surprisingly, though, the next best league is actually Ligue 1, according to the estimates. Doing a deeper dive into the team ratings, it seems that Athletic Bilbao and Villarreal appear to have had a noticeable impact on Spain’s average GD, allowing PSG to carry the French group to second place.
Now, for the most important question: what does this mean for the Champions League? Here are the top ten teams by Champions League win probability according to my simulations:
Let’s get this out of the way: Arsenal’s 31% win probability is really high. Kalshi, a prediction market, has them at around 21% at the time of writing – our model is way more bullish. City at 18% is also above market rates: Kalshi has them at 12%, behind Bayern.
Continuing from the earlier trends of Premier League dominance, five of the top ten teams by UCL win probability are from England. I do think that there is an argument for Dortmund or Atletico Madrid to displace Newcastle from the top ten, and Atletico actually have higher odds of getting to the quarter-finals and round of 16, compared to Newcastle, so the difference between the two teams is probably not significant.
One final thing that the simulation makes clear: uncertainty compounds quickly in knockout tournaments. Using a standard 95% confidence interval, we can’t be certain that any team makes even the quarter-finals. And if we use “more likely than not” as our threshold (50%), then only four teams clear that bar for the quarters, with only Arsenal doing the same for the semis.
This aligns with intuition. The Champions League is famous for upsets. The “best” team doesn’t always win. The model only lays bare how much randomness is involved.
Conclusion
So that’s how I went from an argument with friends to a UCL simulator. In retrospect, it really was not that difficult, especially with the simplifying assumptions I made. Maybe in the future I’ll edit the code to be more true to the UCL draw.
I had intentions of putting up a visualization of my UCL simulations while the tournament progressed, but with the FBRef news (RIP), I’ll have to put that on the back burner while finding a new data source. Nevertheless, I do intend to make the source code available on my GitHub at some point if anyone else is interested. And even if this does not quite find the football analytics crowd, I hope that some of the numbers play a part in your expectations as we get to see the rest of the tournament unfold.