Walk-Forward Validation¶
Summary¶
Walk-forward validation (also called walk-forward optimization or rolling forward) is the gold standard for validating sports betting models. Unlike k-fold cross-validation which randomly splits data, walk-forward validation respects temporal order: the model is trained on historical data, then tested on future data that was not available at training time.
The process: train on period 1 → test on period 2 → expand training window to include period 2 → test on period 3 → repeat. This mimics real deployment where today's model is built on all historical data and used to predict tomorrow's games.
Walk-forward validation prevents look-ahead bias (using future information to make predictions) and gives an honest estimate of how the model would have performed in real-time. The client's spec explicitly requires walk-forward validation across 2010, 2014, 2018, and 2022 World Cup group stages — specifically because k-fold cross-validation is inappropriate for time-series betting data.
Key Concepts¶
- Expanding window: Training set grows over time (all history to T1, all history to T2, etc.). Most common for betting models.
- Rolling window: Training set stays fixed size (last N games), slides forward. Better when team characteristics change significantly.
- Purged cross-validation: Removes a buffer zone between training and test sets to prevent information leakage from near-future data influencing training features.
- Look-ahead bias: Using information that wouldn't have been available at prediction time. Walk-forward prevents this by design.
- Out-of-sample testing: Data the model has never seen during training. Walk-forward ensures every test period is out-of-sample.
- Walk-forward optimization: Iterating through the data to find optimal parameters that work across all walk-forward periods.
Process¶
Period 1 (2010 WC) → train on 2006-2010 → test on 2010 WC
Period 2 (2014 WC) → train on 2006-2014 → test on 2014 WC
Period 3 (2018 WC) → train on 2006-2018 → test on 2018 WC
Period 4 (2022 WC) → train on 2006-2022 → test on 2022 WC
For each period:
1. Train model on all data up to tournament start
2. Generate predictions for all matches
3. Compare to actual outcomes
4. Record metrics: ROI, CLV, Brier score, hit rate
Python Implementation¶
import pandas as pd
import numpy as np
from datetime import datetime
def walk_forward_validate(df, train_start, test_start, test_end, features, target, model_class, params={}):
"""
Perform single walk-forward validation fold.
Args:
df: full historical DataFrame
train_start: start date for training
test_start: start date for test period
test_end: end date for test period
features: list of feature column names
target: target column name
model_class: sklearn-compatible model class
params: model hyperparameters
Returns:
DataFrame of predictions for test period
"""
train_df = df[(df['date'] >= train_start) & (df['date'] < test_start)]
test_df = df[(df['date'] >= test_start) & (df['date'] <= test_end)]
if len(train_df) < 50 or len(test_df) < 5:
return None
X_train = train_df[features]
y_train = train_df[target]
X_test = test_df[features]
model = model_class(**params)
model.fit(X_train, y_train)
predictions = test_df.copy()
predictions['pred_prob'] = model.predict_proba(X_test)[:, 1]
predictions['fold'] = f"{test_start.date()}_{test_end.date()}"
return predictions
def run_walk_forward(df, tournaments, features, target, model_class, params={}):
"""
Run walk-forward validation across multiple tournaments.
tournaments: list of (train_cutoff, test_start, test_end) tuples
"""
all_results = []
for train_end, test_start, test_end in tournaments:
result = walk_forward_validate(
df,
train_start=df['date'].min(), # expanding window
test_start=test_start,
test_end=test_end,
features=features,
target=target,
model_class=model_class,
params=params
)
if result is not None:
all_results.append(result)
combined = pd.concat(all_results)
# Aggregate metrics
metrics = {
'total_bets': len(combined),
'roi': (combined['pnl'].sum() / combined['stake'].sum()) if 'pnl' in combined.columns else None,
'brier_score': ((combined['pred_prob'] - combined['outcome'])**2).mean(),
'clv_mean': combined['clv'].mean() if 'clv' in combined.columns else None,
'hit_rate': combined['correct'].mean() if 'correct' in combined.columns else None
}
return combined, metrics
# Tournament schedule for walk-forward
world_cup_tournaments = [
# (train_end, test_start, test_end)
('2010-06-10', '2010-06-11', '2010-07-11'), # 2010 WC
('2014-06-12', '2014-06-13', '2014-07-13'), # 2014 WC
('2018-06-14', '2018-06-15', '2018-07-15'), # 2018 WC
('2022-11-20', '2022-11-21', '2022-12-18'), # 2022 WC
]
Notes¶
- Walk-forward is computationally expensive but gives the most honest validation picture for betting models
- Key metric: does the model beat the closing line (positive CLV) consistently across all four World Cups?
- The client specifically tests for walk-forward validation (not k-fold), so this is a hard requirement for the MVP
- For World Cup specifically: with only 64 matches per tournament, each fold has small sample — aggregate across all 4 tournaments for statistical power
- The expanding window approach is appropriate for World Cup since team strength accumulates over years