411 REFERENCE · Wikibooks
Scientific Calculations with Julia/Basic Model of Sports Betting
Reference material by Wikibooks contributors. This retains its source identity and is not 411 news reporting.
Scientific Calculations with Julia Basic Model of Sports Betting Portfolio Optimization in Finance Supply and Demand
A basic model for soccer betting can be created using the Poisson distribution to estimate goals, the outcome probability matrix for scores 0-0, 1-0, 2-1, etc., and the expected value of the bet (Expected Value).
thumb|left|Poisson Distribution - The horizontal axis is the index k or number of occurrences. λ is the expected occurrence rate. The vertical axis is the probability of events k given λ. The function is defined only for integer values of k.
The Poisson distribution is often used in sports betting (especially soccer) because it well models the number of rare events that occur in a fixed time interval.
In a soccer match, goals are few, occur discretely, and are counted as 0, 1, 2, 3...
Typical example:
| goals | probability |
| --- | ----------- |
| 0 | high |
| 1 | high |
| 2 | medium |
| 3 | low |
| 4+ | very low |
This type of phenomenon is exactly what the Poisson distribution was designed to address.
The probability of scoring (k) goals is:
for each
where
(λ) = average goals
(k) = number of goals
Example:
if (λ = 1.5)
| goals | probability |
| --- | ----------- |
| 0 | 22% |
| 1 | 33% |
| 2 | 25% |
| 3 | 12% |
A distribution very similar to real soccer data.
The outcome probability matrix relating to scores 0-0, 1-0, 2-1, etc. is the most used in soccer models. For example, the element (1,0) of the matrix is given by the probability that the home team scores 1 goal, knowing that its average goal total is λ = 1.6 according to the relevant Poisson probability, multiplied by the probability that the away team scores 0 goals, knowing that its average goal total is λ = 1.1 according to the relevant Poisson probability. So essentially, you need to multiply the values of two separate Poisson probability matrices. In the basic model, it is assumed that the home team's goals and the away team's goals are independent, so the probabilities multiply: P(i,j) = P(i) X P(j).
The resulting matrix becomes, for example:
| | 0 | 1 | 2 | 3 |
| - | ---- | ---- | ---- | --- |
| 0 | 0.07 | 0.08 | 0.04 | ... |
| 1 | 0.10 | 0.11 | 0.06 | ... |
| 2 | 0.08 | 0.09 | 0.05 | ... |
so the outcome (0,1) has a probability of 8%, etc.
The expected value of the bet is the most important calculation because it indicates how much you win or lose on average with each bet.
EV = (P X Odds) - 1
where P = estimated true probability and Odds = bookmaker's odds
For example, if the estimated probability is 50% and the odds are 2.50:
EV = (0.50 X 2.50) - 1 = + 0.25, it is a theoretically profitable bet, because you theoretically earn €0.25 for every euro wagered on average.
So:
| EV | meaning |
| ------ | ---------------------------- |
| EV > 0 | theoretically profitable bet |
| EV = 0 | fair game |
| EV < 0 | unfavorable bet |
Implementation in Julia
First, I install the Julia libraries I need:
using Pkg
Pkg.add("Distributions")
Pkg.add("CSV")
Pkg.add("DataFrames")
First, I plot the results matrix with a maximum of 5 goals per team:
max_goals = 5
matrix = [(i,j) for i in 0:max_goals, j in 0:max_goals]
matrix
6×6 Matrix{Tuple{Int64, Int64}}:
(0, 0) (0, 1) (0, 2) (0, 3) (0, 4) (0, 5)
(1, 0) (1, 1) (1, 2) (1, 3) (1, 4) (1, 5)
(2, 0) (2, 1) (2, 2) (2, 3) (2, 4) (2, 5)
(3, 0) (3, 1) (3, 2) (3, 3) (3, 4) (3, 5)
(4, 0) (4, 1) (4, 2) (4, 3) (4, 4) (4, 5)
(5, 0) (5, 1) (5, 2) (5, 3) (5, 4) (5, 5)
Suppose that the home team has an average goal per game λ = 1.6 and the away team has an average goal per game λ = 1.1 and plot the probability matrix of the outcomes:
using Distributions
using CSV
using DataFrames
lambda_home = 1.6
lambda_away = 1.1
home_goals = Poisson(lambda_home)
away_goals = Poisson(lambda_away)
prob_matrix = zeros(max_goals+1, max_goals+1)
for i in 0:max_goals
for j in 0:max_goals
prob_matrix[i+1, j+1] = pdf(home_goals, i) * pdf(away_goals, j)
end
end
prob_matrix
6×6 Matrix{Float64}:
0.0672055 0.0739261 0.0406593 0.0149084 0.00409982 0.00090196
0.107529 0.118282 0.0650549 0.0238535 0.00655971 0.00144314
0.0860231 0.0946254 0.0520439 0.0190828 0.00524776 0.00115451
0.045879 0.0504669 0.0277568 0.0101775 0.00279881 0.000615738
0.0183516 0.0201867 0.0111027 0.00407099 0.00111952 0.000246295
0.00587251 0.00645976 0.00355287 0.00130272 0.000358247 7.88144e-5
Let's calculate the probabilitiesof win, draw, and loss:
p_home = 0.0
p_draw = 0.0
p_away = 0.0
for i in 0:max_goals
for j in 0:max_goals
p = prob_matrix[i+1, j+1]
if i > j
p_home += p
elseif i == j
p_draw += p
else
p_away += p
end
end
end
println("Home win probability: ", p_home)
println("Draw probability: ", p_draw)
println("Away win probability: ", p_away)
Home win probability: 0.48353796379688296
Draw probability: 0.24890698520246393
Away win probability: 0.260552747649295
So a home win is more likely at 48.35%.
Let's assume the following bookmaker odds and calculate the expected value:
Home = 2.30
Draw = 3.40
Away = 3.10
odds_home = 2.30
odds_draw = 3.40
odds_away = 3.10
ev_home = p_home * odds_home - 1
ev_draw = p_draw * odds_draw - 1
ev_away = p_away * odds_away - 1
println("EV home: ", ev_home)
println("EV draw: ", ev_draw)
println("EV away: ", ev_away)
EV home: 0.11213731673283078
EV draw: -0.15371625031162262
EV away: -0.1922864822871856
Then:
home → favorable
draw → unfavorable
away → unfavorable
We can also find the most likely exact result:
max_prob = 0
score = (0,0)
for i in 0:max_goals
for j in 0:max_goals
p = prob_matrix[i+1,j+1]
if p > max_prob
max_prob = p
score = (i,j)
end
end
end
println("Most likely result: ", score)
println("Probability: ", max_prob)
Most likely outcome: (1, 1)
Probability: 0.1182817024219596
Estimate of lambda λ (goal averages) with Serie A data
Let's download the data with these columns:
| Date | HomeTeam | AwayTeam | FTHG | FTAG |
| ---------- | -------- | -------- | ---- | ---- |
| 2023-08-19 | Empoli | Verona | 0 | 1 |
FTHG = Full Time Home Goals
FTAG = Full Time Away Goals
from: https://www.football-data.co.uk/mmz4281/2324/I1.csv and create the relevant DataFrame:
df = CSV.read("I1.csv", DataFrame)
df = select(df, :HomeTeam, :AwayTeam, :FTHG, :FTAG)
rename!(df,
:FTHG => :HomeGoals,
:FTAG => :AwayGoals
)
dropmissing!(df)
teams = unique(vcat(df.HomeTeam, df.AwayTeam))
# Let's build a dataset with **goals scored and conceded**:
stats = DataFrame(
team = teams,
goals_scored = zeros(length(teams)),
goals_granted = zeros(length(teams)),
matches = zeros(Int,length(teams))
)
# Calculation of averages:
for row in each row(df)
home = row.HomeTeam
away = row.AwayTeam
hg = row.HomeGoals
ag = row.AwayGoals
stats[stats.team .== home, :goals_scored] .+= hg
stats[stats.team .== home, :goals_granted] .+= ag
stats[stats.team .== home, :matches] .+= 1
stats[stats.team .== away, :goals_scored] .+= adj
stats[stats.team .== away, :goals_allowed] .+= hg
stats[stats.team .== away, :matches] .+= 1
end
# Average goals (λ):
stats.lambda_attack = stats.goals_scored ./ stats.matches
stats.lambda_defense = stats.goals_granted ./ stats.matches
Example result:
| team | attack λ | defense λ |
| ----- | -------- | --------- |
| Inter | 1.9 | 0.8 |
| Milan | 1.6 | 1.0 |
| Roma | 1.4 | 1.2 |
Now, for example, we can estimate the two λs for Inter vs. Milan:
team_home = "Inter"
team_away = "Milan"
lambda_home = stats[stats.team .== team_home, :lambda_attack][1]
lambda_away = stats[stats.team .== team_away, :lambda_attack][1]
println("lambda_home =",lambda_home)
println("lambda_away =",lambda_away)
lambda_home =2.3421052631578947
lambda_away =2.0
Once we have the two lambdas, we can proceed by calculating the outcome probability matrix as before...
Conclusions
This basic model does not take into account the In fact, there is actually a dependence between the two teams' goals, because if one team scores, it can retreat into defense; if a team loses, it attacks more; red cards change the dynamics; and the strategy changes after a goal. Therefore, the Dixon-Coles model introduces a correction for results like 0-0, 1-0, 1-1, which the independent Poisson model tends to estimate poorly. Furthermore, this new model estimates lambda differently and introduces two new parameters, mu and ro (Dixon-Coles parameter), with:
λ = attack_home x defense_away x home_advantage
μ = attack_away x defense_home
This is because it assumes that the home team has a stronger attack, the away team a stronger defense, and there is a home advantage.
However, the model with two independent Poisson models is widely used because it is simple, easy to estimate, works surprisingly well, and requires few parameters, making it the basic model in the literature.Sports betting analysis.