Predict Euro Cup matches with simple statistics
By: Sijmen Van Der Willik
Tags: Statistics, Football, Data Science, Eurocup, Predictions

Predict Euro Cup matches with simple statistics

We’re all just pawns until we win the friendly football pool [image by klimkin]

EDIT: by popular demand, the code and data are now available on GitHub, ready to use code can also be found on Kaggle. I also added parsed versions of datasets for World Cup and Euro Cup games. Feel free to use any or all that you like. — Sijmen

My friends and I start a football pool for every World and Euro cup. There’s not much on the line except for grandiose glory and an eternal feeling of victory. I guess you could argue there’s actually a lot on the line.

Anyway, I’m a data scientist, let’s use that for our football pool.

The idea

[Assumption 1] Football doesn’t change, and a similar distribution of scores will occur in every tournament.

[Assumption 2] FIFA ranking is a good indicator of a country’s performance.

I will look at a typical score distribution for a Euro Cup, and distribute the scores according to their occurrence and the difference in FIFA rank between countries.

For example: in a typical Euro Cup the most extreme score is 5–0, and it occurs once. I will predict this score for the match with the largest difference in FIFA ranking between the countries.

After that, I look for the second most extreme score and predict it for the match with countries having the second-largest difference in FIFA ranking.

I continue doing this until all matches have a predicted score.

If this doesn’t make sense, don’t worry, more examples below.

The data

I was hoping to find a website with an option to export all Euro Cup matches, but alas, I didn’t find it. OpenFootball looked very promising, but I ended up not using it because 1. their text files are in an annoying format and 2. I didn’t want to install their tools to build the DB.

For the match data I ended up using LinguaSport, simple format, easy to parse, seems to have everything on there. Love it.

For the FIFA rankings I used this Kaggle dataset, thank you very much, Alex.

Lastly, the games to be played… I entered this by hand.

Implementation

The main table looks like this:

+-------------+-----------------------+------------------+-------+

| edition | team 1 | team 2 | score |

+-------------+-----------------------+------------------+-------+

| 2016-FRANCE | Wales (Cymru) | Belgium (België) | 3-1 |

| 2016-FRANCE | Germany (Deutschland) | Italy (Italia) | 1-1 |

| 2016-FRANCE | France | Iceland (Ísland) | 5-2 |

| 2016-FRANCE | Portugal | Wales (Cymru) | 2-0 |

| 2016-FRANCE | Germany (Deutschland) | France | 0-2 |

+-------------+-----------------------+------------------+-------+

Each played match is in the table, using value_counts() and some formatting we get this cool heatmap showing which scores occurred most at each edition. Note that only the combination matters, i.e. 2–1 is the same as 1–2.

Occurrence of each score for each edition of the Euro Cup [image by author]

Below is the same data shown in a heatmap, normalized per edition.

Heatmap showing relative frequency of a match outcome for each edition of the Euro Cup [image by author]

We can easily see the scores which occur most often: 1–0, 1–1, 2–0 and 2–1. To be even more precise below is the percentage occurrence of each score of all Euro Cups:

+-------+--------+

| score | freq. |

+-------+--------+

| 0-0 | 9.6% |

| 1-0 | 16.25% |

| 1-1 | 13.87% |

| 2-0 | 15.49% |

| 2-1 | 14.41% |

| 2-2 | 3.3% |

| 3-0 | 6.6% |

| 3-1 | 5.4% |

| 3-2 | 6.02% |

| 3-3 | 0.73% |

| 4-0 | 0.98% |

| 4-1 | 0.81% |

| 4-2 | 1.47% |

| 4-3 | 0.17% |

| 5-0 | 1.78% |

| 5-1 | 1.09% |

| 5-2 | 0.69% |

| 5-4 | 0.23% |

| 6-0 | 0.41% |

| 6-1 | 0.29% |

| 6-2 | 0.17% |

| 7-1 | 0.23% |

+-------+--------+

Our goal is to create predictions that will have the same distribution of scores as we’ve found above. For this, our next step will be turning the percentage into the number of occurrences (we will call N from now on).

For the first stage of the tournament, we will predict 36 matches. Multiplying the values by 36 gives a good indication of N:

+-------+------+

| score | ~N |

+-------+------+

| 0-0 | 3.46 |

| 1-0 | 5.85 |

| 1-1 | 4.99 |

| 2-0 | 5.58 |

| 2-1 | 5.19 |

| 2-2 | 1.19 |

| 3-0 | 2.37 |

| 3-1 | 1.95 |

| 3-2 | 2.17 |

| 3-3 | 0.26 |

| 4-0 | 0.35 |

| 4-1 | 0.29 |

| 4-2 | 0.53 |

| 4-3 | 0.06 |

| 5-0 | 0.64 |

| 5-1 | 0.39 |

| 5-2 | 0.25 |

| 5-4 | 0.08 |

| 6-0 | 0.15 |

| 6-1 | 0.11 |

| 6-2 | 0.06 |

| 7-1 | 0.08 |

+-------+------+

Now we have to turn these into discrete values because we cannot predict a fraction of matches. We cannot simply round the values, because this would give the wrong total number. I.e. rounding [0.3, 0.4, 0.3] will give [0, 0, 0], which doesn’t have the same sum.

Instead, we will add the floor of the numbers with a fraction, then keep adding one to the largest fraction until we have reached the same sum, see the code below.

# get the floor of each value with fraction

n = [int(x) for x in e] # create a list with only fractions

f = [x % 1 for x in e] # loop until the right sum is reached

while sum(n) < 36: # find the largest fraction, add one to N there and set to zero

idx = np.argmax(f)

f[idx] = 0

n[idx] += 1

This results in our final N:

+-------+------+---+

| score | ~N | N |

+-------+------+---+

| 0-0 | 3.46 | 4 |

| 1-0 | 5.85 | 6 |

| 1-1 | 4.99 | 5 |

| 2-0 | 5.58 | 6 |

| 2-1 | 5.19 | 5 |

| 2-2 | 1.19 | 1 |

| 3-0 | 2.37 | 2 |

| 3-1 | 1.95 | 2 |

| 3-2 | 2.17 | 2 |

| 3-3 | 0.26 | 0 |

| 4-0 | 0.35 | 0 |

| 4-1 | 0.29 | 0 |

| 4-2 | 0.53 | 1 |

| 4-3 | 0.06 | 0 |

| 5-0 | 0.64 | 1 |

| 5-1 | 0.39 | 1 |

| 5-2 | 0.25 | 0 |

| 5-4 | 0.08 | 0 |

| 6-0 | 0.15 | 0 |

| 6-1 | 0.11 | 0 |

| 6-2 | 0.06 | 0 |

| 7-1 | 0.08 | 0 |

+-------+------+---+

The predictions

Now there’s just one step left: match the distribution of the scores with the matches to be played.

To do this, we will create a DataFrame with the matches to be played and add the FIFA ranks for the countries so we can sort for this. The table looks like this:

+-----------+-------------+--------+--------+-----------+

| country 1 | country 2 | rank 1 | rank 2 | rank diff |

+-----------+-------------+--------+--------+-----------+

| turkey | italy | 29 | 7 | 22 |

| wales | switzerland | 17 | 13 | 4 |

| denmark | finland | 10 | 54 | -44 |

| belgium | russia | 1 | 38 | -37 |

| england | croatia | 4 | 14 | -10 |

+-----------+-------------+--------+--------+-----------+

Rank 1 and rank 2 describe the country’s current FIFA rank, the rank diff describes the difference between the two.

Finally, we sort the scores N by goal differences and the matches by FIFA rank differences and we put them next to each other, obtaining our final predictions:

+-----------------+-----------------+-----+-----+--------+------+

| country 1 | country 2 | r 1 | r 2 | r diff | PRED |

+-----------------+-----------------+-----+-----+--------+------+

| turkey | italy | 29 | 7 | 22 | 1-2 |

| wales | switzerland | 17 | 13 | 4 | 0-0 |

| denmark | finland | 10 | 54 | -44 | 3-0 |

| belgium | russia | 1 | 38 | -37 | 3-1 |

| england | croatia | 4 | 14 | -10 | 1-0 |

| austria | north macedonia | 23 | 62 | -39 | 4-2 |

| netherlands | ukraine | 16 | 24 | -8 | 1-1 |

| scotland | czech | 44 | 40 | 4 | 1-1 |

| poland | slovakia | 21 | 36 | -15 | 1-0 |

| spain | sweden | 6 | 18 | -12 | 1-0 |

| hungary | portugal | 37 | 5 | 32 | 0-2 |

| france | germany | 2 | 12 | -10 | 1-0 |

| finland | russia | 54 | 38 | 16 | 1-2 |

| turkey | wales | 29 | 17 | 12 | 0-1 |

| italy | switzerland | 7 | 13 | -6 | 1-1 |

| ukraine | north macedonia | 24 | 62 | -38 | 3-1 |

| denmark | belgium | 10 | 1 | 9 | 2-2 |

| netherlands | austria | 16 | 23 | -7 | 1-1 |

| sweden | slovakia | 18 | 36 | -18 | 2-1 |

| croatia | czech | 14 | 40 | -26 | 3-2 |

| england | scotland | 4 | 44 | -40 | 3-0 |

| hungary | france | 37 | 2 | 35 | 0-2 |

| portugal | germany | 5 | 12 | -7 | 1-1 |

| spain | poland | 6 | 21 | -15 | 2-1 |

| italy | wales | 7 | 17 | -10 | 1-0 |

| switzerland | turkey | 13 | 29 | -16 | 2-1 |

| ukraine | austria | 24 | 23 | 1 | 0-0 |

| north macedonia | netherlands | 62 | 16 | 46 | 1-5 |

| finland | belgium | 54 | 1 | 53 | 0-5 |

| russia | denmark | 38 | 10 | 28 | 0-2 |

| czech | england | 40 | 4 | 36 | 0-2 |

| croatia | scotland | 14 | 44 | -30 | 2-0 |

| slovakia | spain | 36 | 6 | 30 | 0-2 |

| sweden | poland | 18 | 21 | -3 | 0-0 |

| portugal | france | 5 | 2 | 3 | 0-0 |

| germany | hungary | 12 | 37 | -25 | 3-2 |

+-----------------+-----------------+-----+-----+--------+------+

As you can see from the table, when there’s a large difference in FIFA ranking, a large goal difference will be predicted in favour of the country with the better FIFA rank. The matches where both countries have a similar FIFA rank are predicted to end in ties.

Now just wait until Belgium actually beats Finland 5–0 and I’ll look like a genius.