1. Introduction: The Foundational Probability Surface of Football
In quantitative association football trading, all major betting propositions—including the 1X2 moneyline, Over/Under total goals lines, Asian Handicaps, Both Teams To Score (BTTS), and exact winning margins—are not independent statistical phenomena. Rather, they are ancillary derivatives of a single underlying stochastic distribution: the joint probability surface of exact match scorelines.
Recreational sports bettors frequently treat different markets as disconnected events, betting on "Over 2.5 Goals" with one sportsbook, "Arsenal to Win" with another, and a "Both Teams To Score" prop elsewhere. In doing so, they expose themselves to synthetic bookmaker margins stacked across multiple derivative lines. Professional quantitative syndicates operate from the opposite paradigm: they construct a unified, bivariate probability grid—the Correct Score Matrix—and derive fair odds for all related proposition markets by integrating over subsets of matrix cells.
- Home Win (1):
- Draw (X):
- Away Win (2):
- Over 2.5 Goals:
- BTTS (Yes):
This technical guide provides a rigorous blueprint for constructing the Correct Score Matrix from Poisson goal expectancies ( and ). We analyze probability mass truncation thresholds, detail the exact cell summation partitions for ancillary markets, integrate the Dixon-Coles low-score correction, work through a matrix example, and provide Python code.
2. Bivariate Grid Construction: The Probability Matrix
Let an econometric model establish the expected goal intensities for Home Team () and Away Team (). Under the classical independent Poisson hypothesis, the probability of the home team scoring exactly goals and the visiting team scoring exactly goals is given by the product of their marginal probability mass functions:
Matrix Dimensionality and Truncation Thresholds
While the discrete Poisson distribution has an infinite support (), practical computing requires truncating the grid at an upper bound . What is the optimal dimension for an institutional grid?
How much a grid (scores from up to , 49 cells) captures depends on the expectancies: 99.9% of the probability mass for , 99.1% for , but only 95.2% for . Choose the grid using an explicit omitted-tail tolerance for the stated parameters. Normalizing a truncated grid conditions on that grid; it does not recover the omitted scorelines. Extend the grid and report the remaining tail.
3. Deriving Ancillary Betting Markets via Matrix Partitions
The supreme operational power of the Correct Score Matrix is that any market proposition corresponds to a geometric partition of the grid:
1. The 1X2 Match Outcome Market
The 49 scoreline cells are cleanly partitioned into three mutually exclusive regions relative to the main diagonal:
2. Over/Under Total Goals Lines
Total goal propositions evaluate whether the aggregate match score exceeds a specific half-goal threshold . The probability of the Under is the sum of cells along the anti-diagonals where :
3. Both Teams To Score (BTTS)
The "BTTS: No" proposition represents the event that at least one team fails to score ( or ). This corresponds to the union of the first row and the first column of the matrix:
4. Asian Handicaps
For any Asian Handicap spread , the probability of the home team covering is the sum of cells satisfying :
4. Incorporating the Dixon-Coles Low-Score Correction
Dixon and Coles (1997) found that independent Poisson models underestimate and draws and overestimate and scores. To correct this bias, the four low-scoring cells are multiplied by Dixon and Coles' (1997) adjustment factor :
Where is fitted on historical matches and is usually small and negative (the code below uses as an example). Applying the correction shifts probability mass from and scorelines into and draws, which raises the draw probability slightly.
5. Worked Step-by-Step Example: Manchester City vs. Newcastle
Consider a hypothetical Premier League fixture with expected goal intensities of (Manchester City at home) and (Newcastle away).
The Core Probability Grid (Subset)
| Man City \ Newcastle | 0 Goals () | 1 Goal () | 2 Goals () | 3 Goals () |
|---|---|---|---|---|
| 0 Goals () | 0-0: 4.98% | 0-1: 4.23% | 0-2: 1.80% | 0-3: 0.51% |
| 1 Goal () | 1-0: 10.70% | 1-1: 9.10% | 1-2: 3.87% | 1-3: 1.10% |
| 2 Goals () | 2-0: 11.51% | 2-1: 9.78% | 2-2: 4.16% | 2-3: 1.18% |
| 3 Goals () | 3-0: 8.25% | 3-1: 7.01% | 3-2: 2.98% | 3-3: 0.84% |
Derived Market Probabilities and Fair Odds
The 1X2 figures sum over all scorelines; in this match the grid holds 99.3% of the probability mass.
- Home Win ():
- Draw ():
- Away Win ():
- Over 2.5 Goals:
- BTTS (Yes):
6. Python Implementation: Full Correct Score Grid Generator
Below is a Python script that generates the complete grid, applies the Dixon-Coles adjustment, and extracts fair prices for all ancillary derivative markets.
# correct_score_matrix_engine.py
import numpy as np
from scipy.stats import poisson
def generate_correct_score_matrix(lambda_home, mu_away, rho=-0.11, max_goals=6):
# Generates a calibrated 7x7 correct score probability matrix.
grid_size = max_goals + 1
matrix = np.zeros((grid_size, grid_size), dtype=np.float64)
# Compute raw independent Poisson probabilities
for i in range(grid_size):
p_home = poisson.pmf(i, lambda_home)
for j in range(grid_size):
p_away = poisson.pmf(j, mu_away)
matrix[i, j] = p_home * p_away
# Apply Dixon-Coles low-score correction tau
tau_00 = 1.0 - lambda_home * mu_away * rho
tau_01 = 1.0 + lambda_home * rho
tau_10 = 1.0 + mu_away * rho
tau_11 = 1.0 - rho
matrix[0, 0] *= tau_00
matrix[0, 1] *= tau_01
matrix[1, 0] *= tau_10
matrix[1, 1] *= tau_11
# Normalize matrix to 1.0 to absorb truncation error
matrix /= np.sum(matrix)
# Derive Ancillary Markets
p_home_win = np.sum(np.tril(matrix, -1))
p_draw = np.sum(np.diag(matrix))
p_away_win = np.sum(np.triu(matrix, 1))
# Over / Under 2.5
under_25_mask = np.array([[i + j <= 2 for j in range(grid_size)] for i in range(grid_size)])
p_under_25 = np.sum(matrix[under_25_mask])
p_over_25 = 1.0 - p_under_25
# Both Teams To Score (BTTS)
p_btts_yes = np.sum(matrix[1:, 1:])
p_btts_no = 1.0 - p_btts_yes
return {
'matrix': matrix,
'home_win_prob': p_home_win,
'draw_prob': p_draw,
'away_win_prob': p_away_win,
'over_25_prob': p_over_25,
'under_25_prob': p_under_25,
'btts_yes_prob': p_btts_yes,
'fair_1x2_odds': (1.0 / p_home_win, 1.0 / p_draw, 1.0 / p_away_win),
'fair_ou25_odds': (1.0 / p_over_25, 1.0 / p_under_25),
'fair_btts_odds': (1.0 / p_btts_yes, 1.0 / p_btts_no)
}
if __name__ == '__main__':
res = generate_correct_score_matrix(2.15, 0.85)
print("Fair 1X2 Odds:", np.round(res['fair_1x2_odds'], 2))
print("Fair Over/Under 2.5:", np.round(res['fair_ou25_odds'], 2))
print("Fair BTTS Odds:", np.round(res['fair_btts_odds'], 2))
7. Numerical Stability and Normalization
When calculating joint Poisson matrices, floating-point precision matters for extreme scorelines such as 6-5 or 5-4. Production code accumulates logarithms of the probability mass functions, , and exponentiates back into probability space to avoid underflow. Dividing the whole grid by its cumulative sum makes the total probability exactly 1, so the 1X2, totals, BTTS and handicap prices derived from it stay consistent with each other.
