Feature Store Catalogue

This document explains where feature definitions live, how dbt models feed the Python models, which features are leakage-safe, and how to add a new feature safely.

Where Feature Definitions Live

Source Location Layer
dbt ML models analytics/dbt_nrl/models/ml/schema.yml ML Features
dbt Silver models analytics/dbt_nrl/models/silver/schema.yml Silver (Staged)
dbt Gold models analytics/dbt_nrl/models/gold/schema.yml Gold (Modelled)
Python backtest nrl-bet-advisor/data/backtest.py Python (Runtime)
Python prediction nrl-bet-advisor/data/prediction.py Python (Runtime)
Python lineup nrl-bet-advisor/data/lineup.py Python (Runtime)

The interactive Feature Store page at /features renders this metadata live from the YAML and Python source files.


How dbt Models Feed Python Models

Raw bucket objects (DO Spaces)
       │
       ▼
Silver layer (dbt)         ← staged Parquet via DuckDB
  stg_nrl_draw
  stg_nrl_ladder
  stg_nrl_team_lists
       │
       ▼
ML feature layer (dbt)     ← point-in-time safe features
  ml_nrl_team_form_features
  ml_nrl_ladder_features
  ml_nrl_match_training_examples
       │
       ▼
Gold layer (dbt)            ← model-ready analytics tables
  gold_nrl_draw_latest
  gold_nrl_stats_latest
       │
       ▼
Python prediction engine    ← runtime inference
  data/prediction.py        ← predict_round()
  data/backtest.py          ← walk-forward backtest
  data/lineup.py            ← team list features

Training uses ml_nrl_match_training_examples from DuckDB. Prediction uses runtime lookups via predict_round() which queries DuckDB and the Odds API.


Feature Groups

Team Form Features

Rolling statistics computed from completed matches before the fixture kickoff (point-in-time safe).

Key features:
- rolling_wins_l5 — wins in last 5 games
- rolling_total_points_l5 — average total points scored last 5 games
- home_ground_advantage — home-field win rate
- rest_days — days since last match

Source: ml_nrl_team_form_features (dbt ML layer)

Ladder Features

Derived from the most recent ladder snapshot with round < fixture round.

Key features:
- ladder_position — current ladder position (1 = top)
- ladder_points — competition points
- win_rate — wins / games played
- points_diff — for/against differential

Source: ml_nrl_ladder_features (dbt ML layer)

Weather Features

Weather conditions at match venue, provided as defaults with zero-fill when not scraped.

Key features (with defaults):
- temperature — °C at kickoff (default: 18.0)
- rainfall_mm — precipitation in mm (default: 0.0)
- wind_speed_kmh — wind speed (default: 10.0)
- is_wet_track — boolean flag for wet conditions (default: 0)

Source: WEATHER_FEATURES in data/backtest.py

Lineup / Player Features

Delta features comparing team list composition between rounds.

Key features:
- delta_true_new_player_rate — rate of debut players relative to opponent
- delta_transferred_player_rate — transferred player delta
- lineup_overall_delta — overall lineup strength delta
- lineup_spine_delta — halfback/hooker/fullback stability delta

Source: data/lineup.pyLINEUP_FEATURES in data/backtest.py

Odds / Market Features

Bookmaker-derived implied probabilities and market signals.

Key features:
- close_home_implied_prob — normalised implied home win probability at closing
- open_home_implied_prob — opening implied home win probability
- market_move — closing minus opening implied probability (steam signal)
- close_home_line — closing home handicap line
- close_total_points — closing over/under line
- elo_home_prob — Elo-based home win probability

Source: MARKET_FEATURES + MARKET_MODEL_FEATURES in data/backtest.py

Target / Result Fields

These fields are not used as model inputs. They are observed outcomes used for training labels and post-hoc evaluation.

Field Description
home_margin home_score - away_score (training target for margin model)
home_score Final home team score
away_score Final away team score
result Match outcome label (home win / draw / away win)

Leakage risk: Never include score-derived features as model inputs. Only include them as y labels.

Paper Trading / CLV Fields

Diagnostic-only fields used to evaluate whether recommendations had positive Closing Line Value.

Field Description
stake Flat stake allocated by edge policy
model_ev Expected value at time of recommendation
taken_price Odds at time of recommendation
clv Closing Line Value (closing odds - taken odds)
settlement WON / LOST / PUSH / NO BET

These are diagnostics only — do not use clv or settlement as model inputs.


Leakage-Safe Feature Checklist

line-cover-goal-v1 research features

The opening-line cover study is evaluated by scripts/report_line_cover_goal.py.
Its model features are built from the opening line and prior-only backtest
state: open_home_line, absolute line, favourite-side indicator, de-vigged
opening H2H probability, their interaction, prior-only ELO difference, rolling
margin and win rate, points for/against, rest, finals, neutral venue, prior
match counts, and explicit history/form/ELO observation flags. Current team
lists, future weather, closing prices, movement, and result fields are
excluded. The favourite indicator is retained as a registered diagnostic
feature even though it is algebraically implied by the line sign; no other
deterministic duplicates are added.

The round-level evaluator uses gold_nrl_draw_latest.round when the historical
row maps to the draw; unmatched legacy rows use a documented calendar-week
proxy and are counted separately in the evidence ledger. Hyperparameters are
selected from pre-season history, while each candidate is refit only after a
completed evaluation round.

Before adding a new feature, verify:

  1. Point-in-time safe — the feature value is derived only from data available before the fixture kickoff timestamp.
  2. No score leakage — does not use home_score, away_score, home_margin, or any result field.
  3. No CLV/settlement leakage — does not use closing odds, CLV, or outcome labels.
  4. Deduplication safe — if from a rolling window, the window excludes the current match.
  5. NaN-handled — missing values have a sensible default (zero-fill for lineup, median for form features).

How to Add a New Feature Safely

1. Define the feature in dbt (preferred)

Create or extend an ML-layer model in analytics/dbt_nrl/models/ml/:

-- Example: ml_nrl_home_crowd_features.sql
SELECT
    match_id,
    season,
    round,
    kickoff_at_utc,
    team_key,
    is_home,
    home_crowd_avg_l5  -- rolling 5-game average home crowd
FROM ...
WHERE kickoff_at_utc < {{ fixture_kickoff }}  -- point-in-time filter

Add to schema.yml:

- name: ml_nrl_home_crowd_features
  description: Rolling home crowd features at match-team grain.
  columns:
    - name: home_crowd_avg_l5
      description: Rolling 5-game average home crowd, PIT-safe.
      tests:
        - not_null

2. Add to Python feature lists

In data/backtest.py:

CORE_FEATURES = [
    ...
    "home_crowd_avg_l5",  # add here
]

In data/prediction.py, ensure the feature is populated in base_feats (with a sensible default):

base_feats = {
    ...
    "home_crowd_avg_l5": form.get("home_crowd_avg_l5") or 20000.0,
}

3. Rebuild and validate

cd analytics/dbt_nrl
dbt run --select ml_nrl_home_crowd_features

Then run the backtest to verify the feature improves AUC:

cd nrl-bet-advisor
python -m data.backtest

4. Update this catalogue

Add the new feature to the relevant group section above and document:
- Source table or function
- Calculation summary
- Known caveats or leakage risks


Known Caveats

Feature Caveat
crowd_home_pct Requires manual Excel update; 0 real training samples until R13+ 2026
weather_* Defaults to moderate dry conditions when not scraped
lineup_* Zero-filled when team list not available (e.g., early in round)
market_move Currently set to 0.0 (no separate open price from odds API)
elo_home_prob Regresses to mean at season boundary; early-season predictions less reliable