Fit football models using CmdStan
Description
Fits football goal-based models using Stan via the CmdStan backend. Supported models include: double Poisson, bivariate Poisson, Dixon-Coles, Skellam, Student's t, diagonal-inflated bivariate Poisson, zero-inflated Skellam, and negative Binomial.
Usage
stan_foot(
data,
model,
predict = 0,
ranking,
dynamic_type,
dynamic_weight = FALSE,
dynamic_par = list(common_sd = FALSE, kl_variance = FALSE, periods_per_season = 2,
spike = normal(9, 1.5), slab = normal(0, 3)),
prior_par = list(ability = normal(0, NULL), ability_sd = cauchy(0, 5), home = normal(0,
5)),
home_effect = TRUE,
norm_method = "none",
ranking_map = NULL,
method = "MCMC",
...
)Arguments
data |
A data frame containing match data with columns:
|
model |
A character string specifying the Stan model to fit. Options are:
|
predict |
An integer specifying the number of out-of-sample matches for prediction. If missing, the function fits the model to the entire dataset without making predictions. |
ranking |
An optional
The ranking points are matched to the teams of |
dynamic_type |
A character string specifying the type of dynamics in the model. Options are:
|
dynamic_weight |
A logical value indicating whether to fit the weighted dynamic model of Macrì Demartino, Egidi and Torelli (2026), where the evolution of the attack and defence abilities is governed by team- and period-specific commensurate priors with spike-and-slab hyperpriors (default |
dynamic_par |
A list of options for the evolution variance of the dynamic models (ignored when
The options |
prior_par |
A list specifying the prior distributions for the parameters of interest:
See the rstanarm package for more details on specifying priors. |
home_effect |
A logical value indicating the inclusion of a home effect in the model. (default is |
norm_method |
A character string specifying the method used to normalize team-specific ranking points. Options are:
|
ranking_map |
An optional vector mapping ranking periods to data periods. If not provided and the number of ranking periods matches the number of data periods, a direct mapping is assumed. |
method |
A character string specifying the method used to obtain the Bayesian estimates. Options are:
|
... |
Additional arguments passed to |
Details
Let \((y^{H}_{n}, y^{A}_{n})\) denote the observed number of goals scored by the home and the away team in the \(n\)-th game, respectively. A general bivariate Poisson model allowing for goals' correlation (Karlis & Ntzoufras, 2003) is the following:
\[ Y^H_n, Y^A_n| \lambda_{1n}, \lambda_{2n}, \lambda_{3n} \sim \mathsf{BivPoisson}(\lambda_{1n}, \lambda_{2n}, \lambda_{3n})\]
\[\log(\lambda_{1n}) = \mu+att_{h_n} + def_{a_n}\]
\[\log(\lambda_{2n}) = att_{a_n} + def_{h_n}\]
\[\log(\lambda_{3n}) =\beta_0,\]
where the case \(\lambda_{3n}=0\) reduces to the double Poisson model (Baio & Blangiardo, 2010). \(\lambda_{1n}, \lambda_{2n}\) represent the scoring rates for the home and the away team, respectively, where: \(\mu\) is the home effect; the parameters \(att_T\) and \(def_T\) represent the attack and the defence abilities, respectively, for each team \(T\), \(T=1,\ldots,N_T\); the nested indexes \(h_{n}, a_{n}=1,\ldots,N_T\) denote the home and the away team playing in the \(n\)-th game, respectively. Attack/defence parameters are imposed a sum-to-zero constraint to achieve identifiability and assigned some weakly-informative prior distributions:
\[att_T \sim \mathrm{N}(\mu_{att}, \sigma_{att})\]
\[def_T \sim \mathrm{N}(\mu_{def}, \sigma_{def}),\]
with hyperparameters \(\mu_{att}, \sigma_{att}, \mu_{def}, \sigma_{def}\).
The Dixon and Coles (1997) model keeps the two scoring rates \(\lambda_{1n}, \lambda_{2n}\) of the double Poisson model but multiplies the joint probability of low-scoring results by a dependence factor \(\tau\) to better capture the observed correlation in the \(0\text{-}0, 1\text{-}0, 0\text{-}1\) and \(1\text{-}1\) outcomes:
\[P(Y^H_n = y^H_n, Y^A_n = y^A_n) = \tau_{\lambda_{1n}, \lambda_{2n}}(y^H_n, y^A_n) \, \mathsf{Poisson}(y^H_n | \lambda_{1n}) \, \mathsf{Poisson}(y^A_n | \lambda_{2n}),\]
where
\[\tau_{\lambda_1, \lambda_2}(y^H, y^A) = \begin{cases} 1 - \lambda_1 \lambda_2 \rho & y^H = y^A = 0 \\ 1 + \lambda_1 \rho & y^H = 0, y^A = 1 \\ 1 + \lambda_2 \rho & y^H = 1, y^A = 0 \\ 1 - \rho & y^H = y^A = 1 \\ 1 & \text{otherwise,} \end{cases}\]
and \(\rho\) is the low-score dependence parameter, assigned a weakly-informative \(\mathrm{N}(0, 0.1)\) prior and constrained to keep the adjustment factor positive.
Instead of using the marginal number of goals, another alternative is to modelling directly the score difference \((y^{H}_{n}- y^{A}_{n})\). We can use the Poisson-difference distribution (or Skellam distribution) to model goal difference in the \(n\)-th match (Karlis & Ntzoufras, 2009):
\[y^{H}_{n}- y^{A}_{n}| \lambda_{1n}, \lambda_{2n} \sim PD(\lambda_{1n}, \lambda_{2n}),\]
and the scoring rates \(\lambda_{1n}, \lambda_{2n}\) are unchanged with respect to the bivariate/double Poisson model. If we want to use a continue distribution, we can use a student t distribution with 7 degrees of freedom (Gelman, 2014):
\[y^{H}_{n}- y^{A}_{n} \sim t(7, ab_{h_{n}}-ab_{a(n)}, \sigma_y)\]
\[ab_t \sim \mathrm{N}(\mu + b \times {prior\_score}_t, sigma_{ab}),\]
where \(ab_t\) is the overall ability for the \(t\)-th team, whereas \(prior\_score_t\) is a prior measure of team's strength (for instance a ranking).
These model rely on the assumption of static parameters.
However, we could assume dynamics in the attach/defence
abilities (Owen, 2011; Egidi et al., 2018, Macri Demartino et al., 2024) in terms of weeks or seasons through the argument
dynamic_type. In such a framework, for a given
number of times \(1, \ldots, \mathcal{T}\), the models
above would be unchanged, but the priors for the abilities
parameters at each time \(\tau, \tau=2,\ldots, \mathcal{T},\) would be:
\[att_{T, \tau} \sim \mathrm{N}({att}_{T, \tau-1}, \sigma_{att})\]
\[def_{T, \tau} \sim \mathrm{N}({def}_{T, \tau-1}, \sigma_{def}),\]
whereas for \(\tau=1\) we have:
\[att_{T, 1} \sim \mathrm{N}(\mu_{att}, \sigma_{att})\]
\[def_{T, 1} \sim \mathrm{N}(\mu_{def}, \sigma_{def}).\]
Of course, the identifiability constraint must be imposed for each time \(\tau\).
The evolution standard deviations of the dynamic models can be specified in
four alternative ways, through the arguments dynamic_par and dynamic_weight:
Default (Egidi et al., 2018): two distinct standard deviations \(\sigma_{att}\) and \(\sigma_{def}\), shared by all the teams and periods (parameters
sigma_attandsigma_def).-
dynamic_par = list(common_sd = TRUE)(Owen, 2011): a single evolution standard deviation \(\sigma_{att} = \sigma_{def} = \sigma\) (parametersigma_common). -
dynamic_par = list(kl_variance = TRUE)(Koopman and Lit, 2015): the evolution variance is inflated in the periods that follow a summer break,\[\sigma^2_{k,\tau} = \sigma^2_{k} + \sigma^2_{break} \, I_{\tau}, \quad k \in \{att, def\},\]
where \(I_{\tau} = 1\) if period \(\tau\) follows a summer break and \(0\) otherwise. The package assumes that each season is made of
periods_per_seasonconsecutive periods (default 2, i.e. two half-seasons), so that a summer break precedes the periods \(1 + m \, \mathrm{periods\_per\_season}\), \(m = 1, 2, \ldots\) (parameterssigma_att_kl,sigma_def_klandsigma_break). -
dynamic_weight = TRUE(Macrì Demartino et al., 2026): weighted dynamic model with commensurate priors. For each team \(T\), ability \(k \in \{att, def\}\) and period \(\tau \ge 2\),\[k_{T, \tau} \sim \mathrm{N}(k_{T, \tau-1}, 1/\phi_{k, T, \tau}),\]
where the commensurate precision \(\phi_{k, T, \tau}\) is assigned a two-component mixture of half-normal distributions (spike-and-slab),
\[\phi_{k, T, \tau} \sim p_T \, \mathrm{N}^{+}(\mu_s, \psi_s) + (1 - p_T) \, \mathrm{N}^{+}(\mu_l, \psi_l), \quad p_T \sim \mathrm{Beta}(1, 1).\]
The spike \(\mathrm{N}^{+}(\mu_s, \psi_s)\) is concentrated on large precisions (strong borrowing of information from the previous period), whereas the slab \(\mathrm{N}^{+}(\mu_l, \psi_l)\) is diffuse on small precisions (weak borrowing). The hyperparameters are set through
dynamic_par$spikeanddynamic_par$slab(parametersprob_spike,comm_prec_att,comm_prec_def,comm_sd_attandcomm_sd_def).
To better capture draw occurrences, the package also allows for the fit of a diagonal-inflated bivariate Poisson and a zero-inflated Skellam model. The diagonal-inflated bivariate Poisson model (Karlis & Ntzoufras, 2003) is a mixture of a bivariate Poisson and a distribution over the draws:
\[P(Y^H_n = x, Y^A_n = y) = (1 - p) \, \mathsf{BivPoisson}(x, y | \lambda_{1n}, \lambda_{2n}, \lambda_{3n}) + p \, \theta_x \, I(x = y),\]
where \(p\) is the probability of the inflation component (parameter prob_of_draws,
with a \(\mathrm{U}(0, 1)\) prior) and \(\theta_j\), \(j = 0, \ldots, 3\), is a discrete
distribution over the draws 0-0, 1-1, 2-2 and 3-3 (parameter draw_dist, with a uniform
Dirichlet prior); draws with more than three goals per team are not inflated.
See the vignette for further details.
Value
An object of class "stanFoot", which is a list containing:
-
fit: TheCmdStanFitobject returned bycmdstanr. -
data: The input data. -
stan_data: The data list passed to Stan. -
stan_code: The Stan code of the underline model. -
stan_args: The optionalcmdstanrparameters passed to (...). -
alg_method: The inference algorithm used to obtain the Bayesian estimates.
References
Baio, G. and Blangiardo, M. (2010). Bayesian hierarchical model for the prediction of football results. Journal of Applied Statistics 37(2), 253-264.
Dixon, M. J. and Coles, S. G. (1997). Modelling association football scores and inefficiencies in the football betting market. Journal of the Royal Statistical Society: Series C (Applied Statistics), 46(2), 265-280.
Egidi, L., Pauli, F., and Torelli, N. (2018). Combining historical data and bookmakers' odds in modelling football scores. Statistical Modelling, 18(5-6), 436-459.
Gelman, A. (2014). Stan goes to the World Cup. From "Statistical Modeling, Causal Inference, and Social Science" blog.
Hobbs, B. P., Carlin, B. P., Mandrekar, S. J. and Sargent, D. J. (2011). Hierarchical commensurate and power prior models for adaptive incorporation of historical information in clinical trials. Biometrics, 67(3), 1047-1056.
Koopman, S. J. and Lit, R. (2015). A dynamic bivariate Poisson model for analysing and forecasting match results in the English Premier League. Journal of the Royal Statistical Society: Series A (Statistics in Society), 178(1), 167-186.
Macrì Demartino, R., Egidi, L. and Torelli, N. (2024). Alternative ranking measures to predict international football results. Computational Statistics, 1-19.
Macrì Demartino, R., Egidi, L. and Torelli, N. (2026). Bayesian weighted discrete-time dynamic models for association football prediction. Journal of the Royal Statistical Society Series C: Applied Statistics, qlag032. doi:10.1093/jrsssc/qlag032
Karlis, D. and Ntzoufras, I. (2003). Analysis of sports data by using bivariate poisson models. Journal of the Royal Statistical Society: Series D (The Statistician) 52(3), 381-393.
Karlis, D. and Ntzoufras,I. (2009). Bayesian modelling of football outcomes: Using the Skellam's distribution for the goal difference. IMA Journal of Management Mathematics 20(2), 133-145.
Owen, A. (2011). Dynamic Bayesian forecasting models of football match outcomes with estimation of the evolution variance parameter. IMA Journal of Management Mathematics, 22(2), 99-113.
Examples
## Not run:
if (instantiate::stan_cmdstan_exists()) {
library(dplyr)
data("italy")
italy <- as_tibble(italy)
# Four seasons of the Italian Serie A, each split into two halves
# (periods 1-8): the summer break precedes periods 3, 5 and 7
italy_2018_2021 <- italy %>%
select(Season, home, visitor, hgoal, vgoal) %>%
filter(Season %in% c("2018", "2019", "2020", "2021")) %>%
group_by(Season) %>%
mutate(half = if_else(row_number() <= n() / 2, 1, 2)) %>%
ungroup() %>%
mutate(periods = 2 * (as.numeric(Season) - 2018) + half) %>%
select(periods,
home_team = home, away_team = visitor,
home_goals = hgoal, away_goals = vgoal
)
# Koopman & Lit (2015): variance inflation at the summer breaks
fit_kl <- stan_foot(
data = italy_2018_2021,
model = "biv_pois",
dynamic_type = "seasonal",
dynamic_par = list(kl_variance = TRUE, periods_per_season = 2),
iter_sampling = 1000,
chains = 4,
parallel_chains = 4
)
print(fit_kl, pars = c("sigma_att_kl", "sigma_def_kl", "sigma_break"))
# Weighted dynamic model with commensurate priors
# (Macrì Demartino et al., 2026)
fit_wdm <- stan_foot(
data = italy_2018_2021,
model = "double_pois",
dynamic_type = "seasonal",
dynamic_weight = TRUE,
dynamic_par = list(spike = normal(9, 1.5), slab = normal(0, 3)),
iter_sampling = 1000,
chains = 4,
parallel_chains = 4
)
print(fit_wdm, pars = "prob_spike")
}
## End(Not run)