Dixon–Coles Correction

Summary

The Dixon–Coles model is a statistical framework for modeling association football (soccer) match outcomes. It was introduced by Mark Dixon and Sam Coles in their 1997 paper "Modelling Association Football Scores and Inefficiency in the Football Betting Market." The core innovation is an extension of the independent Poisson model that addresses the systematic underestimation of low-scoring draws (particularly 0-0, 1-0, 0-1, and 1-1 scorelines).

In standard independent Poisson models, goals scored by each team are modeled as independent random variables. However, this assumption is violated in practice because: (a) teams playing defensively after taking a lead suppress the opponent's goal probability, (b) 0-0 draws are more common than the model predicts, and (c) 1-1 scorelines have a similar bias. Dixon and Coles introduced a dependency correction (rho/τ parameter) that adjusts the joint probability of low-scoring outcomes.

Key Concepts

  • Independence violation: Standard Poisson assumes goals are independent. In reality, a team winning 1-0 plays more defensively, affecting the away team's goal probability.
  • Tau parameter (ρ): The Dixon–Coles correction factor for low-scoring matches. It adjusts P(0,0), P(1,0), P(0,1), P(1,1) simultaneously to correct the underestimation.
  • Bivariate Poisson: The mathematical foundation — a Poisson distribution with correlation between the two variables. When correlation = 0, it reduces to independent Poisson.
  • Time-weighting: Dixon and Coles also proposed weighting recent matches more heavily than distant ones, since team form changes over a season.
  • DC for betting markets: Particularly important for Asian handicap and correct score markets where specific low-scoring outcomes matter.

Formulas

Independent Poisson probability:
$$P(h, a) = \frac{\lambda_h^h e^{-\lambda_h}}{h!} \times \frac{\lambda_a^a e^{-\lambda_a}}{a!}$$

Dixon–Coles adjustment for low-scoring matches:
The joint probability for (h,a) goals is adjusted by multiplying by a correction factor τ(h,a) for specific low-scoring combinations. The correction is typically:

$$\tau(0,0) = 1 - \rho$$
$$\tau(1,0) = 1 - \rho$$
$$\tau(0,1) = 1 - \rho$$
$$\tau(1,1) = 1 + \rho$$
$$\tau(h,a) = 1 \quad \text{for all other combinations}$$

Where ρ is estimated from the data (can be positive or negative). A negative ρ indicates the model underestimates low-scoring draws.

Time-weighted DC:
$$w_{ij} = \exp(-\delta \times (t_{now} - t_{game}))$$
Where δ is a decay parameter (typically 0.001–0.005 per day), giving more weight to recent matches when estimating attack/defense parameters.

Python Implementation

import numpy as np
from scipy.stats import poisson
from scipy.optimize import minimize

def dixon_coles_log_likelihood(params, goals_home, goals_away, times=None, rho=0.0):
    """
    Compute negative log-likelihood for Dixon-Coles model.
    params: [attack_home, defense_home, attack_away, defense_away]
    """
    lambda_home = params[0] - params[1]  # attack - defense
    lambda_away = params[2] - params[3]

    ll = 0
    for i in range(len(goals_home)):
        h, a = goals_home[i], goals_away[i]
        # Base Poisson
        p_base = poisson.pmf(h, lambda_home) * poisson.pmf(a, lambda_away)
        # DC correction for low scores
        if h <= 1 and a <= 1:
            correction = 1 - rho
        elif h == 1 and a == 1:
            correction = 1 + rho
        else:
            correction = 1
        ll += np.log(p_base * correction)

    return -ll

def fit_dixon_coles(goals_home, goals_away, times=None):
    """
    Fit Dixon-Coles model to match data.
    Returns: attack, defense parameters, rho
    """
    def objective(params):
        return dixon_coles_log_likelihood(
            params[:4], goals_home, goals_away, times, params[4]
        )

    # Initial guess from independent Poisson
    avg_home = np.mean(goals_home)
    avg_away = np.mean(goals_away)
    init = [avg_home, 0, avg_away, 0, -0.1]  # [attack_h, def_h, attack_a, def_a, rho]

    result = minimize(objective, init, method='L-BFGS-B',
                     bounds=[(0.1, 5), (-2, 2), (0.1, 5), (-2, 2), (-0.5, 0.5)])
    return result.x

def dc_probabilities(lambda_home, lambda_away, rho, max_goals=6):
    """Generate scoreline probabilities with Dixon-Coles correction."""
    matrix = np.zeros((max_goals + 1, max_goals + 1))
    for h in range(max_goals + 1):
        for a in range(max_goals + 1):
            p_base = poisson.pmf(h, lambda_home) * poisson.pmf(a, lambda_away)
            if h <= 1 and a <= 1 and not (h == 1 and a == 1):
                correction = 1 - rho
            elif h == 1 and a == 1:
                correction = 1 + rho
            else:
                correction = 1
            matrix[h, a] = p_base * correction
    return matrix / matrix.sum()  # normalize

Notes

  • The rho parameter is typically estimated via maximum likelihood alongside the attack/defense parameters
  • For World Cup modeling: with only 3 group stage games per team, rho estimation is noisy — using a prior from broader football data is advisable
  • Dixon and Coles also recommended time-weighting to account for form changes during a season, which is particularly relevant for long tournaments
  • The DC correction has the largest effect on correct score and Asian handicap markets, less on 1X2 main lines
  • Some implementations use a different correction formula — the one above is the most common simplified version