Start with two estimated scoring rates
A Poisson score calculator takes an expected goal count for each side and turns those two numbers into a distribution over scorelines. The useful output is the whole distribution: a single most likely score conceals how much probability sits elsewhere. This tutorial builds the calculation and reports its omitted tail, so a small score grid cannot quietly claim to contain every possible result.
Here lambda means a model rate, not the API expected_goals field. For a simple historical baseline, select finished matches in a defined competition and training period, reject missing scores, and estimate home and away goal averages. Keep the selected match IDs and cutoff. A rate of 1.4 is an illustrative assumption; it is not a live forecast or evidence about any particular team.
Build the matrix without losing probability
For a nonnegative rate lambda, the chance of k goals is exp(-lambda) times lambda to the power k divided by k factorial. Under an independence assumption, multiply the home and away probabilities to obtain a score cell. The assumption is convenient, but match state, red cards and tactical interactions can make real scores dependent. Treat it as a baseline to evaluate, not an established description of every fixture.
A displayed grid needs a finite maximum. Stopping at six goals discards a tail; dividing every visible cell by their sum hides that omission and changes the interpretation. The code keeps unconditional probabilities, reports the mass outside the grid, and checks symmetry for equal rates. A zero rate is valid and must place all of that side's probability on zero goals.
Run the reproducible example
This Python 3 example uses deliberately synthetic inputs, not a downloaded match or a forecast for a real fixture. Save it as example.py and run python3 example.py. It uses only the standard library and its assertions check the behavior discussed here. Replace the fixture inputs with validated API records when connecting your application; demo identifiers are not production lookup values.
from math import exp, factorial
def goals(rate, maximum=8):
if rate < 0 or maximum < 0:
raise ValueError("nonnegative inputs required")
return [exp(-rate) * rate**k / factorial(k) for k in range(maximum+1)]
def matrix(home_rate, away_rate, maximum=8):
home, away = goals(home_rate, maximum), goals(away_rate, maximum)
return [[h*a for a in away] for h in home]
cells = matrix(1.4, 1.1)
home = sum(p for h,row in enumerate(cells) for a,p in enumerate(row) if h>a)
draw = sum(cells[i][i] for i in range(len(cells)))
away = sum(p for h,row in enumerate(cells) for a,p in enumerate(row) if h<a)
tail = 1 - home - draw - away
assert 0 <= tail < 0.0001
assert matrix(0, 0)[0][0] == 1
symmetric = matrix(1.2, 1.2)
assert all(abs(symmetric[h][a]-symmetric[a][h]) < 1e-12
for h in range(9) for a in range(9))
print(f"home={home:.4f} draw={draw:.4f} away={away:.4f} tail={tail:.6f}")
Read the result
The program prints probabilities for the three outcomes and a small positive omitted tail. They sum to one when that tail is included. The assertions also prove the degenerate zero-rate case and symmetry under equal rates; they do not measure forecasting skill on real football results.
Connect historical results to the calculation
Use GET /football/matches with competition_id, season_id and status=finished, plus date filters when required. Follow its documented pagination and read home_team.score and away_team.score only when both are non-null. Resolve the competition and season first rather than inventing IDs. A full season average and a team's recent scoring rate are different models; label which one you used and how many observations support it.
Do not use matches played after the forecast cutoff to calculate the rates. Persist the training window, the chosen rates, the matrix maximum and the model version alongside each estimate. That record lets you reproduce the forecast after a result correction or a change in your method. Stats API supplies the selected football inputs; this program supplies the probabilities.
Decide whether the model is useful
Evaluate on later matches, compare against a simple baseline, and inspect how performance changes when the rate estimates change. A small sample can move a team's average sharply, so showing two decimal places does not establish certainty. Extending the grid reduces truncation error; it does not repair a badly estimated scoring rate or a wrong independence assumption.
For an interface, display a few likely cells, home/draw/away probabilities and the omitted tail together. Explain that a 1-0 mode is merely the largest cell, not a confident promise of 1-0. Keep results and estimates visually distinct, and never substitute the model score for an unavailable API result.
Connect this to your project
Try different rates in the Poisson tool before integrating historical inputs. Then keep this calculation as a small independently tested module in your application. Use the prediction workflow guide to plan evidence retention and later-match evaluation before showing estimates to customers.