100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Machine Learning with Scikit-learn
45 minintermediate

Phase 1 — Regression: Predicting Match Scores

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.

Analogy🏏Cricket
🏏 Think of it like cricket: at the 10-over mark in an IPL match you can see the current run rate, wickets lost, powerplay runs, and venue. A regression model is your digital version of the grizzled assistant coach who says 'Given where we are, they will end up around 165-170.' You want to quantify that intuition with an RMSE low enough for the coach to trust.

Step 0 — Load Shared Data and Select Features

python
# ── 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

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

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

python
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

python
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

python
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`.

Analogy🏏Cricket
🏏 Think of it like cricket: this checklist is the umpires' final confirmation before you move to the next innings. Just as you'd never declare an innings sound without comparing at least two batting approaches, you confirm two or more regression models compared on RMSE, MAE and R². Just as a single good over proves little and you judge a bowler across a full spell, five-fold cross-validation RMSE per model checks the result holds across repeated tests. Just as a coach studies the wagon-wheel for a systematic weakness — always edging outside off, a residual plot must show no systematic bias, residuals centred on zero with even spread. Just as you'd identify which shots actually built the total, a permutation importance chart names the top features. And just as you'd test a new batting plan in the nets before the match, the predict_final_score function must run on at least two hypothetical scenarios. Ticking every box is what certifies Phase 1 as genuinely complete rather than merely attempted, so Phase 2 builds on solid ground.
  • 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.
Lesson 32 of 35
0% complete