Short Side logo
Short Side
HomeFantasyLineupsBettingStatsNRL AIArticlesAbout
HomeFantasyLineupsBettingStatsNRL AIArticlesAbout
Back to articles
NRL Margin Model: H2H and Line Predictions header 1
NRL Margin Model: H2H and Line Predictions header 2
Short Side · May 3, 2026

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 lines

The 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 edge

That 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 score

The training set is restricted to home-team rows for deployment and backtesting, so the model keeps a consistent convention:

margin = home team margin

That 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 conditions

The 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:

python
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 metres

This 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_points

This 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 conceded

This 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_points

This 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:

python
FANTASY_DROP_THRESHOLD_PCT = 0.10

The 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:

python
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 volatility

The 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

python
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:

python
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 MAE

The 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:

python
sigma = standard deviation of post-2020 residuals

The 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%.