Phase 1 — Regression: Predicting Match Scores
Your first task for the franchise analytics team: build a model that predicts the final score a batting team will post, given match conditions observable at the halfway point (10 overs). The franchise head coach wants to use this during live matches to guide defensive field placements and bowling strategy in the second half of the innings.
Step 0 — Load Shared Data and Select Features
# ── Paste the shared data generator from Lesson 31 here first ────────────────
# Then continue below
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import Ridge
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
import matplotlib.pyplot as plt
# Feature sets
numeric_features = ['powerplay_runs','powerplay_wickets','run_rate_10',
'wickets_at_10','top_order_avg','toss_winner_bats',
'team_batting_rank']
categorical_features = ['venue']
target = 'final_score'
X = matches[numeric_features + categorical_features]
y = matches[target]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42)
print(f"Train: {X_train.shape} | Test: {X_test.shape}")
print(f"Target range: {y.min()}–{y.max()} runs | Mean: {y.mean():.1f}")
Step 1 — Preprocessing Pipeline with ColumnTransformer
# Preprocessing: scale numerics, one-hot encode venue
preprocessor = ColumnTransformer(transformers=[
('num', StandardScaler(), numeric_features),
('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features),
])
# Quick sanity check on the transformer
X_proc = preprocessor.fit_transform(X_train)
print(f"Processed feature matrix shape: {X_proc.shape}")
print(f" ({len(numeric_features)} numeric + {X_proc.shape[1]-len(numeric_features)} one-hot encoded venue dummies)")
Step 2 — Train and Evaluate Two Regressors
# Build pipelines for each model
models = {
'Ridge Regression': Pipeline([
('pre', preprocessor),
('reg', Ridge(alpha=1.0))
]),
'Random Forest': Pipeline([
('pre', preprocessor),
('reg', RandomForestRegressor(n_estimators=200, max_depth=8,
random_state=42, n_jobs=-1))
]),
'Gradient Boosting': Pipeline([
('pre', preprocessor),
('reg', GradientBoostingRegressor(n_estimators=200, learning_rate=0.05,
max_depth=4, random_state=42))
]),
}
results = {}
for name, pipe in models.items():
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
rmse = mean_squared_error(y_test, y_pred, squared=False)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
cv_rmse = (-cross_val_score(pipe, X_train, y_train,
cv=5, scoring='neg_root_mean_squared_error')).mean()
results[name] = {'RMSE': rmse, 'MAE': mae, 'R²': r2, 'CV-RMSE': cv_rmse}
print(f"{name:22s} RMSE={rmse:.2f} MAE={mae:.2f} R²={r2:.4f} CV-RMSE={cv_rmse:.2f}")
best_name = min(results, key=lambda k: results[k]['RMSE'])
print(f"\n✅ Best model: {best_name}")
Step 3 — Residual Analysis
best_pipe = models[best_name]
y_pred_best = best_pipe.predict(X_test)
residuals = y_test.values - y_pred_best
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Predicted vs actual
axes[0].scatter(y_test, y_pred_best, alpha=0.5, s=20)
lo, hi = y_test.min()-5, y_test.max()+5
axes[0].plot([lo, hi], [lo, hi], 'r--')
axes[0].set(xlabel='Actual Score', ylabel='Predicted Score',
title='Predicted vs Actual')
# Residuals vs predicted
axes[1].scatter(y_pred_best, residuals, alpha=0.5, s=20)
axes[1].axhline(0, color='red', ls='--')
axes[1].set(xlabel='Predicted', ylabel='Residual',
title='Residuals vs Predicted')
# Residual histogram
axes[2].hist(residuals, bins=30, edgecolor='white')
axes[2].axvline(0, color='red', ls='--')
axes[2].set(xlabel='Residual (runs)', ylabel='Count',
title=f'Residual Distribution (σ={residuals.std():.1f} runs)')
plt.suptitle(f'Residual Analysis — {best_name}', fontsize=13)
plt.tight_layout(); plt.show()
Step 4 — Feature Importance
from sklearn.inspection import permutation_importance
# Permutation importance works for any model
perm_result = permutation_importance(
best_pipe, X_test, y_test,
n_repeats=15, random_state=42, scoring='neg_root_mean_squared_error'
)
# Feature names after preprocessing
num_names = numeric_features
cat_names = (best_pipe.named_steps['pre']
.named_transformers_['cat']
.get_feature_names_out(categorical_features).tolist())
all_feature_names = num_names + cat_names
# Sort by mean importance
imp_mean = perm_result.importances_mean
imp_std = perm_result.importances_std
sorted_idx = np.argsort(imp_mean)[::-1][:10] # top 10
plt.figure(figsize=(10, 4))
plt.barh(
[all_feature_names[i] for i in sorted_idx[::-1]],
imp_mean[sorted_idx[::-1]],
xerr=imp_std[sorted_idx[::-1]],
color='steelblue', alpha=0.8
)
plt.xlabel('Increase in RMSE when feature permuted')
plt.title(f'Permutation Importance — {best_name}')
plt.tight_layout(); plt.show()
print("Top 5 features:")
for i in sorted_idx[:5]:
print(f" {all_feature_names[i]:25s} {imp_mean[i]:.3f} ± {imp_std[i]:.3f}")
Step 5 — Live Score Predictor Function
def predict_final_score(pipe, powerplay_runs, powerplay_wickets,
run_rate_10, wickets_at_10, top_order_avg,
toss_winner_bats, team_batting_rank, venue):
"""Predict final T20 score from 10-over match state."""
row = pd.DataFrame([{
'powerplay_runs' : powerplay_runs,
'powerplay_wickets': powerplay_wickets,
'run_rate_10' : run_rate_10,
'wickets_at_10' : wickets_at_10,
'top_order_avg' : top_order_avg,
'toss_winner_bats' : int(toss_winner_bats),
'team_batting_rank': team_batting_rank,
'venue' : venue,
}])
pred = pipe.predict(row)[0]
return round(pred, 1)
# Example: MI at Wankhede, 10-over snapshot
score = predict_final_score(
best_pipe,
powerplay_runs=62, powerplay_wickets=1,
run_rate_10=9.8, wickets_at_10=2,
top_order_avg=38, toss_winner_bats=True,
team_batting_rank=2, venue='Wankhede'
)
print(f"Predicted final score: {score} runs")
Phase 1 Deliverables Checklist
Before moving to Phase 2, confirm: (1) At least two regression models trained and compared on RMSE, MAE, and R². (2) 5-fold cross-validation RMSE reported for each model. (3) Residual plot showing no systematic bias (residuals roughly centred on 0 with constant spread). (4) Permutation importance chart identifying top features. (5) The `predict_final_score` function tested on at least two hypothetical match states. If your best model's test RMSE exceeds 20 runs, revisit feature selection and try increasing `n_estimators` or tuning `max_depth`.
- Always wrap preprocessing and modelling in a single Pipeline to prevent data leakage between train and test sets.
- Use ColumnTransformer to apply different preprocessing steps (scaling vs one-hot encoding) to different feature subsets.
- Compare at least two regression algorithms; use 5-fold CV RMSE as the primary selection metric, not just holdout RMSE.
- Residual analysis is mandatory: check for systematic patterns (heteroscedasticity, bias at extremes) before declaring a model production-ready.
- Permutation importance is model-agnostic and works for any Pipeline — it is safer than tree-based feature importance for correlated features.
- Wrap the final model in a prediction function with named parameters to simulate how the analytics tool will be used in production.