

NRL Margin Model: H2H and Line Predictions
The margin model is built to answer a harder question than simply "who wins?"
It tries to estimate how much better one team is than the other on the day. That makes it useful for three different things at once:
predicting the winner
estimating the likely margin
finding value against betting linesThe model starts with historical match data, team performance trends, market information, try-timing patterns, and a set of leakage-safe rolling match stat features. The goal is to create a fair estimate of the home team's expected margin.
Why Margin Matters
Predicting a winner is useful, but margin is more informative.
A team projected to win by 1 point and a team projected to win by 18 points are both "tips", but they are very different predictions. Margin gives the model a richer target. It can be converted into:
head-to-head win probability
line cover probability
expected betting edgeThat is why the model is trained as a regression model. It predicts a number, not just a class.
The target is:
margin = team score - opponent scoreThe training set is restricted to home-team rows for deployment and backtesting, so the model keeps a consistent convention:
margin = home team marginThat consistency matters. It avoids accidentally mixing home and away perspectives when evaluating betting lines.
What Data Goes Into The Model
The model begins by fetching historical match data from the NRL database. Each match appears from a team perspective, with stats for the team and its opponent.
It then adds several groups of features.
The first group is basic match and team context:
home ground indicator
days since last game
recent home/away win percentage
year
era flag
ground conditionsThe days_since_last feature captures short turnarounds and rest advantages. It is capped at 10 days so long breaks do not dominate the signal.
The side_win_pct_30 feature looks at a team's recent win rate in the same home/away context. If a team has been strong at home, or poor away, the model can pick that up without manually hard-coding home-ground assumptions.
Ground conditions are mapped into an ordinal scale. Better, drier conditions generally support cleaner, faster football, while wet or heavy conditions can suppress scoring and change how teams win.
Adjusting For The Modern Era
NRL scoring changed materially around the six-again rule period. The model explicitly marks the post-June 2020 era:
era_flag = match_date >= "2020-06-01"The script also analyses pre- and post-six-again differences in totals, run metres, tackles, errors, offloads, line breaks, and margins.
Some historical columns are era-adjusted so older matches are scaled closer to modern scoring conditions. This is important because a 2016 game and a 2024 game do not always mean the same thing statistically. The rules, pace, and scoring environment changed.
The aim is not to erase history. It is to stop older matches from teaching the model the wrong baseline for the current game.
Expected Points
One of the most important engineered features is expected points.
The model trains a separate expected-points model using team performance stats such as:
run metres
missed tackles
kicking metres
home/away
opponent run metres
opponent missed tackles
opponent kicking metresThis expected-points model estimates how many points a team "should" have scored from the underlying match profile.
Once each team has expected points, the model creates:
expected_margin = expected_points - opponent_expected_pointsThis gives the margin model a clean team-strength feature based on underlying performance rather than just final scores.
Try Timing
The model also includes try-timing features. It uses helper functions to add information about when teams tend to score and concede tries.
The main features include:
average try time scored
standard deviation of try time scored
average try time conceded
standard deviation of try time concededThis gives the model a sense of game shape.
Some teams start fast. Some fade late. Some concede early but recover. Some produce more volatile scoring patterns. Try timing is not the whole model, but it adds context that raw scorelines miss.
Fantasy Strength Signal
The model also brings in fantasy-derived team strength:
starting13_avg_last10_fantasy_pointsThis is a practical proxy for player quality and team-list strength. Fantasy scoring captures involvement: tackles, metres, attacking stats, work rate, and role. When a team's starting 13 fantasy profile drops sharply, that can be a sign of missing players, weakened team lists, or role disruption.
The model can flag those week-to-week drops:
FANTASY_DROP_THRESHOLD_PCT = 0.10The current setup does not apply this filter to training by default, but it can apply it to betting selections. In plain English: the model can still learn from those games, but it can choose not to bet into situations where team-list quality has shifted sharply and the signal may be unstable.
Rolling Form Features
The margin model relies heavily on rolling features. These are historical form indicators calculated before the current game.
The configured rolling windows are:
games = [3, 8, "STD"]
h2h_windows = [5]
aggregates = ["mean"]The rolling input features include:
opponent rating
expected margin
average try time scored
try time scored volatility
average try time conceded
try time conceded volatilityThe model then creates team-vs-opponent differences. That is important because rugby league is relative. A team's expected margin is useful, but the difference between the two teams is usually more useful.
The Model Family
The model currently uses a combination of linear and random forest models. taking in all the variables discussed above
backtest_model_name = ["linear", "rf"]
ensemble_mode = "agreement"Agreement mode is conservative. It only treats a betting edge seriously when both selected models point the same way. That sacrifices volume, but it reduces low-quality signals.
Time-Aware Validation
The model does not randomly shuffle matches into train and test folds. That would leak future games into the past. Instead, it uses time-aware cross-validation:
TimeSeriesSplit(n_splits=3)The folds are built at match level, ordered by date, so earlier matches are used to predict later matches.
Each fold reports match-level:
RMSE
MAE
R²
baseline MAEThe baseline is just assuming the home team will win The key question is not just "does the model look good?" It is:
does the model beat a naive baseline?That keeps the evaluation honest.
From Margin To Probability
Once the model predicts a margin, it converts that margin into probabilities using a normal distribution.
First, it estimates the model's residual volatility:
sigma = standard deviation of post-2020 residualsThe post-2020 focus matters because scoring has increased significantly in the six-again era, meaning standard deviation is higher.
Then the model estimates line-cover probability:
P(home covers) = Φ((predicted_margin + line) / sigma)Where Φ is the normal cumulative distribution function.
For head-to-head:
P(home wins) = Φ(predicted_margin / sigma)This is a clean way to turn a margin estimate into a probability. If the model thinks the home team is 10 points better and residual uncertainty is low, the home win probability is high. If the model thinks the teams are nearly even, the probability stays near 50%.