100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Python for AI & ML
55 minbeginner

Intermediate Machine Learning Practice

What You'll Build

In this project, you will build a cricket player performance prediction system using supervised machine learning. The system ingests historical match statistics — including runs scored, wickets taken, strike rate, economy rate, batting position, and venue type — and trains a regression model to predict a player's future performance metrics.

The pipeline encompasses data preprocessing, feature engineering, model training with scikit-learn, hyperparameter tuning using GridSearchCV, cross-validation for robustness, and performance evaluation using metrics such as Mean Absolute Error and R² score. This end-to-end workflow mirrors real-world ML practice, where data quality, feature selection, and model validation are the primary determinants of success.

Throughout the project, you will handle missing values, normalize features, and split data into training and test sets. The final stage involves deploying your trained model to generate predictions on unseen player data, completing a full production-style machine learning cycle.

Analogy🏏Cricket
🏏 Think of it like cricket: Building an ML system is like a cricket coach analyzing match footage to prepare the team for upcoming opponents. Just as a coach reviews each player's past 10 innings—noting Virat Kohli's average against pace bowling, Rohit Sharma's powerplay strike rate, or Jasprit Bumrah's economy in the death overs—your ML pipeline reviews raw data (innings_scores, deliveries_faced, wickets_taken) to extract features and patterns. The coach doesn't just watch randomly; they follow a structured process: collect data from the scorecard, filter for relevant matches, calculate metrics (run rate, consistency), and finally produce a team sheet prediction. Your Python code does exactly this—it ingests player statistics, applies transformations (normalizing scores, computing averages), filters based on conditions (active players, minimum matches), and outputs insights. Understanding this parallel reveals why we build ML systems in layers: just as a coach first gathers evidence, then interprets it, then makes decisions, machine learning requires data intake, feature engineering, and model output in sequence.

Prerequisites

  • Solid Python fundamentals: classes, functions, list comprehensions, and pandas DataFrame manipulation for data wrangling
  • Understanding of train-test split concept and why data leakage breaks model generalization in production systems
  • Familiarity with scikit-learn's API for supervised learning: fit(), predict(), score() methods and common estimators
  • Knowledge of feature scaling/normalization and why algorithms like linear regression perform better with normalized inputs
  • Basic statistics: mean, standard deviation, correlation, and how they relate to feature importance and model performance

Setup & Project Structure

Begin by creating a well-organized project directory that separates data, models, and code into logical modules. The cricket_ml_pipeline directory contains dedicated subfolders for raw data, processed data, trained models, and Python scripts. This structure improves maintainability and reflects enterprise ML best practices.

Analogy🏏Cricket
🏏 Think of it like cricket: before a tournament a team does not throw kit, players, and strategy into one heap — the dressing room, the nets, the physio bench, and the analyst's laptop each have their place, so anyone can find what they need under pressure. Structuring the cricket_ml_analysis project the same way, with a main script to captain the workflow, a data module for file I/O, and a separate module for player and match classes, is that same separation of concerns. Just as the batting coach and the bowling coach work independently yet feed one match plan in cricket, your data-handling code and your class definitions develop separately yet compose into one pipeline. And just as a well-run squad keeps net practice, match data, and results in distinct areas, your project keeps data, models, and outputs in dedicated folders. The payoff is a professional layout that stays maintainable as the project grows — nobody rummages through chaos mid-match.

Install the required dependencies before proceeding: pandas for data manipulation, numpy for numerical operations, scikit-learn for machine learning algorithms and evaluation metrics, and matplotlib and seaborn for visualization. Using virtual environments ensures dependency isolation and prevents version conflicts across projects.

Each step in the project builds incrementally upon the previous one, allowing you to test functionality progressively rather than debugging a fully assembled system all at once. This incremental approach makes it easier to isolate and resolve issues as they arise.

bash
#!/bin/bash
# Setup cricket ML prediction pipeline

# Create project directory structure
mkdir -p cricket_ml_pipeline/{data/raw,data/processed,models,src}
cd cricket_ml_pipeline

# Create and activate virtual environment
python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install --upgrade pip
pip install pandas==1.5.3 numpy==1.24.3 scikit-learn==1.3.0 matplotlib==3.7.1 seaborn==0.12.2

# Create README and .gitignore
echo "# Cricket Player Performance ML Pipeline" > README.md
echo "venv/" > .gitignore
echo "*.pkl" >> .gitignore
echo "__pycache__/" >> .gitignore

# Verify installation
python -c "import pandas, sklearn; print('Dependencies installed successfully')"

echo "Cricket ML pipeline project initialized!"
ls -la

Step 1 — Foundation

Step 1 establishes the data foundation by creating synthetic cricket player statistics and implementing data loading and exploration routines. You will generate a dataset representing 200 cricket players, with features including batting average, strike rate, number of innings played, economy rate for bowlers, runs conceded, wickets taken, and venue type. The CricketPlayer class encapsulates player metadata in a structured and reusable form.

Data exploration using pandas is a critical part of this step, as it reveals the dataset's shape, data types, missing values, and statistical distributions. This matters because of the well-known principle that 'garbage in, garbage out' — poorly understood or corrupted data inevitably leads to invalid models.

To ensure data quality before model training begins, you will calculate descriptive statistics, check explicitly for missing values, and visualize feature distributions. These checks help identify outliers or data quality issues early, preventing them from propagating silently into the model training phase.

Analogy🏏Cricket
🏏 Think of it like cricket: Designing your data structures is like a cricket administrator creating the official scorecard format before a match begins. Just as a scorecard has predefined columns for batter name, runs scored, balls faced, and dismissal details, your CricketPlayer and Innings classes define exactly what data will be recorded and in what format. Consider Rohit Sharma's innings in the 2023 World Cup final: the scorecard consistently recorded his name, country (India), role (opener), runs (92), balls faced (64), boundaries (4s and 6s), and dismissal method. If the scorecard format changed mid-match—sometimes listing balls faced, sometimes not—the entire match record becomes unreliable and comparison across matches becomes impossible. Your Python class structure ensures this consistency: every CricketPlayer object will have the same attributes, every Innings object the same fields. This consistency is precisely what machine learning algorithms require; they cannot learn patterns from inconsistent data formats.
python
# src/data_foundation.py
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List
import matplotlib.pyplot as plt

@dataclass
class CricketPlayer:
    """Represents a cricket player with performance metrics."""
    player_name: str
    player_id: int
    role: str  # 'Batsman', 'Bowler', 'All-rounder'
    country: str

class CricketDataset:
    """Handles cricket player dataset creation and exploration."""
    
    def __init__(self, random_seed=42):
        """Initialize dataset with reproducibility."""
        np.random.seed(random_seed)
        self.df = None
        self.players = []
    
    def create_synthetic_data(self, num_players=200):
        """Generate synthetic cricket player performance data."""
        player_names = [
            'Rohit Sharma', 'Virat Kohli', 'Jasprit Bumrah', 'Hardik Pandya',
            'Ravichandran Ashwin', 'KL Rahul', 'Surya Kumar Yadav',
            'Ishan Kishan', 'Washington Sundar', 'Arjun Tendulkar'
        ]
        roles = ['Batsman', 'Bowler', 'All-rounder']
        countries = ['India', 'Australia', 'England', 'Pakistan', 'South Africa', 'New Zealand']
        
        data = {
            'player_id': range(1, num_players + 1),
            'player_name': [f"{np.random.choice(player_names)}_P{i}" for i in range(num_players)],
            'role': np.random.choice(roles, num_players),
            'country': np.random.choice(countries, num_players),
            'innings_played': np.random.randint(5, 150, num_players),
            'runs_scored': np.random.randint(100, 5000, num_players),
            'batting_average': np.random.uniform(15, 65, num_players).round(2),
            'strike_rate': np.random.uniform(90, 180, num_players).round(2),
            'wickets_taken': np.random.randint(0, 200, num_players),
            'economy_rate': np.random.uniform(5, 12, num_players).round(2),
            'runs_conceded': np.random.randint(0, 3000, num_players),
            'centuries': np.random.randint(0, 15, num_players),
            'fifties': np.random.randint(0, 50, num_players),
            'venue_type': np.random.choice(['Home', 'Away'], num_players),
        }
        
        self.df = pd.DataFrame(data)
        print(f"✓ Created synthetic dataset with {num_players} players")
        return self.df
    
    def explore_data(self):
        """Perform exploratory data analysis."""
        print("\n" + "="*60)
        print("CRICKET DATASET EXPLORATION")
        print("="*60)
        
        print(f"\nDataset Shape: {self.df.shape[0]} rows, {self.df.shape[1]} columns")
        
        print("\nData Types:")
        print(self.df.dtypes)
        
        print("\nMissing Values:")
        missing = self.df.isnull().sum()
        if missing.sum() == 0:
            print("  ✓ No missing values detected")
        else:
            print(missing[missing > 0])
        
        print("\nBasic Statistics (First 5 rows):")
        print(self.df.head())
        
        print("\nDescriptive Statistics:")
        print(self.df.describe().round(2))
        
        print("\nCategorical Distributions:")
        print(f"\nPlayer Roles:")
        print(self.df['role'].value_counts())
        print(f"\nCountries:")
        print(self.df['country'].value_counts())
        print(f"\nVenue Types:")
        print(self.df['venue_type'].value_counts())
        
        # Check for outliers
        print("\nOutlier Detection (using IQR method):")
        numerical_cols = self.df.select_dtypes(include=[np.number]).columns
        for col in ['batting_average', 'strike_rate', 'economy_rate']:
            Q1 = self.df[col].quantile(0.25)
            Q3 = self.df[col].quantile(0.75)
            IQR = Q3 - Q1
            outliers = self.df[(self.df[col] < Q1 - 1.5*IQR) | (self.df[col] > Q3 + 1.5*IQR)]
            print(f"  {col}: {len(outliers)} outliers found")
        
        return self.df

# Main execution
if __name__ == "__main__":
    # Initialize and create dataset
    dataset = CricketDataset(random_seed=42)
    df = dataset.create_synthetic_data(num_players=200)
    
    # Explore the data
    dataset.explore_data()
    
    # Save for next steps
    df.to_csv('data/raw/cricket_players.csv', index=False)
    print("\n✓ Dataset saved to data/raw/cricket_players.csv")

Step 2 — Core Logic

Step 2 implements the core machine learning pipeline, covering data preprocessing, feature engineering, model selection, and training. Missing values are handled using appropriate imputation strategies — mean imputation for numerical features and mode imputation for categorical ones. Categorical variables such as venue_type are encoded using OneHotEncoder, and numerical features are normalized using StandardScaler to ensure that distance-sensitive algorithms like linear regression and KNN perform optimally.

The processed data is then split into an 80% training set and a 20% test set. You will train a LinearRegression model to predict batting_average based on features including strike_rate, innings_played, centuries, fifties, and economy_rate. Cross-validation using cross_val_score with 5 folds ensures that the model generalizes well across different data subsets rather than overfitting to a single partition.

This step reinforces a foundational insight: clean data is more valuable than sophisticated algorithms. Even a straightforward linear model trained on well-preprocessed data will consistently outperform a complex model applied to dirty or poorly understood inputs.

Analogy🏏Cricket
🏏 Think of it like cricket: Core logic is like how a cricket selection committee evaluates players for the national team squad. The committee doesn't just look at one recent match; they apply systematic filters and calculations. For example, they might say: 'Consider only batters with minimum 15 Test matches and average above 35,' then 'Calculate form rating based on last 5 innings,' then 'Rank by consistency score.' Your Python functions implement exactly this workflow—filtering_active_players() excludes players with too few matches, calculate_player_form() examines recent performance trend, and rank_by_metric() orders players by a metric like consistency or strike rate. When Virat Kohli's average dips in a series, the selection committee doesn't panic; they apply their filters and rules to see if this is a temporary slump (recent form) or systematic decline (overall average). Your code does the same: it separates temporary variations from true performance patterns through layered analysis.
python
# src/model_pipeline.py
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, r2_score, mean_squared_error
import pickle

class CricketPerformancePredictor:
    """ML pipeline for predicting cricket player performance."""
    
    def __init__(self):
        """Initialize the predictor components."""
        self.model = None
        self.preprocessor = None
        self.X_train = None
        self.X_test = None
        self.y_train = None
        self.y_test = None
        self.df = None
    
    def load_data(self, filepath):
        """Load cricket player data."""
        self.df = pd.read_csv(filepath)
        print(f"✓ Loaded {len(self.df)} player records")
        return self.df
    
    def prepare_features(self):
        """Prepare features and target variable."""
        # Define feature columns (exclude player identifiers and target)
        feature_cols = ['strike_rate', 'innings_played', 'centuries', 'fifties',
                       'economy_rate', 'wickets_taken', 'venue_type']
        
        # Define target variable
        target_col = 'batting_average'
        
        # Separate features and target
        X = self.df[feature_cols].copy()
        y = self.df[target_col].copy()
        
        # Handle missing values
        X = X.fillna(X.mean(numeric_only=True))
        X['venue_type'] = X['venue_type'].fillna(X['venue_type'].mode()[0])
        
        print(f"\n✓ Features prepared: {len(feature_cols)} features selected")
        print(f"  Features: {feature_cols}")
        print(f"  Target: {target_col}")
        
        return X, y
    
    def create_preprocessing_pipeline(self, X):
        """Create scikit-learn preprocessing pipeline."""
        # Define numerical and categorical columns
        numerical_features = ['strike_rate', 'innings_played', 'centuries', 'fifties',
                            'economy_rate', 'wickets_taken']
        categorical_features = ['venue_type']
        
        # Create preprocessor using ColumnTransformer
        preprocessor = ColumnTransformer(
            transformers=[
                ('num', StandardScaler(), numerical_features),
                ('cat', OneHotEncoder(drop='first'), categorical_features)
            ])
        
        print(f"\n✓ Preprocessing pipeline created")
        print(f"  Numerical scaling: StandardScaler on {len(numerical_features)} features")
        print(f"  Categorical encoding: OneHotEncoder on {len(categorical_features)} features")
        
        return preprocessor
    
    def split_data(self, X, y, test_size=0.2, random_state=42):
        """Split data into training and test sets."""
        self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(
            X, y, test_size=test_size, random_state=random_state
        )
        
        print(f"\n✓ Data split completed")
        print(f"  Training set: {len(self.X_train)} samples ({100*(1-test_size):.0f}%)")
        print(f"  Test set: {len(self.X_test)} samples ({100*test_size:.0f}%)")
        
        return self.X_train, self.X_test, self.y_train, self.y_test
    
    def train_model(self):
        """Train the regression model with preprocessing."""
        # Create preprocessing pipeline
        self.preprocessor = self.create_preprocessing_pipeline(self.X_train)
        
        # Create full pipeline: preprocessing + model
        self.model = Pipeline([
            ('preprocessor', self.preprocessor),
            ('regressor', LinearRegression())
        ])
        
        # Train the model
        print(f"\n✓ Training LinearRegression model...")
        self.model.fit(self.X_train, self.y_train)
        print(f"  Model training completed")
        
        # Perform cross-validation on training data
        cv_scores = cross_val_score(self.model, self.X_train, self.y_train,
                                    cv=5, scoring='r2')
        print(f"\n✓ Cross-Validation Results (5-fold):")
        print(f"  R² Scores: {[f'{score:.4f}' for score in cv_scores]}")
        print(f"  Mean R²: {cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")
        
        return self.model
    
    def evaluate_model(self):
        """Evaluate model on test set."""
        # Make predictions
        y_pred = self.model.predict(self.X_test)
        
        # Calculate metrics
        mae = mean_absolute_error(self.y_test, y_pred)
        rmse = np.sqrt(mean_squared_error(self.y_test, y_pred))
        r2 = r2_score(self.y_test, y_pred)
        
        print(f"\n✓ Test Set Performance Metrics:")
        print(f"  Mean Absolute Error (MAE): {mae:.4f}")
        print(f"  Root Mean Squared Error (RMSE): {rmse:.4f}")
        print(f"  R² Score: {r2:.4f}")
        print(f"\n  Interpretation:")
        print(f"    - MAE: On average, predictions are off by ±{mae:.2f} batting average points")
        print(f"    - R²: Model explains {r2*100:.1f}% of variance in batting averages")
        
        return {'MAE': mae, 'RMSE': rmse, 'R2': r2}
    
    def save_model(self, filepath):
        """Save trained model to disk."""
        with open(filepath, 'wb') as f:
            pickle.dump(self.model, f)
        print(f"\n✓ Model saved to {filepath}")

# Main execution
if __name__ == "__main__":
    print("="*60)
    print("CRICKET PERFORMANCE ML PIPELINE")
    print("="*60)
    
    # Initialize predictor
    predictor = CricketPerformancePredictor()
    
    # Load data
    df = predictor.load_data('data/raw/cricket_players.csv')
    
    # Prepare features
    X, y = predictor.prepare_features()
    
    # Split data
    predictor.split_data(X, y)
    
    # Train model
    predictor.train_model()
    
    # Evaluate model
    predictor.evaluate_model()
    
    # Save model
    predictor.save_model('models/cricket_performance_model.pkl')
    
    print("\n" + "="*60)
    print("Pipeline execution completed successfully!")
    print("="*60)

Step 3 — Integration & Enhancement

Step 3 enhances the trained model by integrating hyperparameter tuning, feature importance analysis, and a prediction interface suitable for production use. GridSearchCV systematically searches the parameter space — testing different regression models, regularization strengths, and preprocessing configurations — to identify the best-performing model configuration.

Feature importance extraction then reveals which inputs most strongly influence predictions. In this context, strike_rate and centuries are likely to dominate predictions of batting_average, providing interpretable insight into the model's decision-making process.

To support production deployment, you will implement a PredictionEngine class that loads the trained model and processes new player data. The class includes proper error handling and input validation to prevent runtime failures. Additionally, prediction confidence intervals are generated through residual analysis, giving stakeholders a quantified sense of uncertainty alongside each prediction. This mirrors the standards of real-world ML systems, where model interpretability and confidence quantification are considered as important as raw accuracy metrics.

Analogy🏏Cricket
🏏 Think of it like cricket: Integration is like how a cricket broadcasting company produces a live match commentary. Before the match, they have individual components ready: camera operators (data input), graphics team (visualization), statisticians (analysis), and commentators (output/insights). During the match, the director orchestrates all these—feeding live ball data to statisticians, who calculate updated averages and pass them to graphics, which displays them on screen, which commentators narrate to viewers. The whole system fails if any component breaks: missing data from cameras, incorrect statistics calculation, or graphics delay. Your main pipeline does this orchestration: it loads player data (camera feed), runs it through MatchAnalyzer (statistics calculation), caches results (storage), and generates reports (graphics/commentary). Building this integration teaches you how real ML systems work—not as isolated algorithms, but as coordinated pipelines where each component depends on correct output from the previous stage.
python
# src/model_enhancement.py
import pandas as pd
import numpy as np
import pickle
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, r2_score
import warnings
warnings.filterwarnings('ignore')

class EnhancedCricketPredictor:
    """Enhanced ML pipeline with hyperparameter tuning and feature analysis."""
    
    def __init__(self):
        """Initialize enhanced predictor."""
        self.best_model = None
        self.grid_search = None
        self.feature_importance = None
        self.prediction_engine = None
    
    def load_preprocessed_data(self, filepath):
        """Load and preprocess data."""
        df = pd.read_csv(filepath)
        
        # Prepare features
        feature_cols = ['strike_rate', 'innings_played', 'centuries', 'fifties',
                       'economy_rate', 'wickets_taken', 'venue_type']
        target_col = 'batting_average'
        
        X = df[feature_cols].copy()
        y = df[target_col].copy()
        
        # Handle missing values
        X = X.fillna(X.mean(numeric_only=True))
        X['venue_type'] = X['venue_type'].fillna(X['venue_type'].mode()[0])
        
        # Split data
        from sklearn.model_selection import train_test_split
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.2, random_state=42
        )
        
        return X_train, X_test, y_train, y_test, feature_cols
    
    def create_base_pipeline(self):
        """Create preprocessing pipeline."""
        numerical_features = ['strike_rate', 'innings_played', 'centuries', 'fifties',
                            'economy_rate', 'wickets_taken']
        categorical_features = ['venue_type']
        
        preprocessor = ColumnTransformer(
            transformers=[
                ('num', StandardScaler(), numerical_features),
                ('cat', OneHotEncoder(drop='first'), categorical_features)
            ])
        
        return preprocessor
    
    def hyperparameter_tuning(self, X_train, y_train):
        """Perform GridSearchCV to find optimal hyperparameters."""
        print(f"\n{'='*60}")
        print("HYPERPARAMETER TUNING WITH GRIDSEARCHCV")
        print(f"{'='*60}")
        
        preprocessor = self.create_base_pipeline()
        
        # Define parameter grid for different models
        param_grid = {
            'regressor__alpha': [0.001, 0.01, 0.1, 1.0, 10.0],  # Ridge/Lasso regularization
            'regressor__fit_intercept': [True, False]
        }
        
        # Create pipeline with Ridge regression
        pipeline = Pipeline([
            ('preprocessor', preprocessor),
            ('regressor', Ridge())
        ])
        
        # Perform grid search
        print(f"\n✓ Searching parameter space...")
        self.grid_search = GridSearchCV(
            pipeline,
            param_grid=param_grid,
            cv=5,
            scoring='r2',
            n_jobs=-1,
            verbose=1
        )
        
        self.grid_search.fit(X_train, y_train)
        
        print(f"\n✓ Grid Search Results:")
        print(f"  Best Parameters: {self.grid_search.best_params_}")
        print(f"  Best CV Score: {self.grid_search.best_score_:.4f}")
        
        # Show top 5 parameter combinations
        results_df = pd.DataFrame(self.grid_search.cv_results_)
        top_results = results_df.nlargest(5, 'rank_test_score')[['param_regressor__alpha',
                                                                   'param_regressor__fit_intercept',
                                                                   'mean_test_score']]
        print(f"\n✓ Top 5 Parameter Combinations:")
        print(top_results.to_string(index=False))
        
        self.best_model = self.grid_search.best_estimator_
        return self.best_model
    
    def extract_feature_importance(self, X_train, feature_cols):
        """Extract feature importance from linear model coefficients."""
        print(f"\n{'='*60}")
        print("FEATURE IMPORTANCE ANALYSIS")
        print(f"{'='*60}")
        
        # Get the trained regressor
        regressor = self.best_model.named_steps['regressor']
        
        # Get feature names after preprocessing
        preprocessor = self.best_model.named_steps['preprocessor']
        feature_names = (preprocessor.get_feature_names_out().tolist())
        
        # Get coefficients
        coefficients = regressor.coef_
        
        # Create importance dataframe
        importance_df = pd.DataFrame({
            'Feature': feature_names,
            'Coefficient': coefficients,
            'Absolute_Importance': np.abs(coefficients)
        }).sort_values('Absolute_Importance', ascending=False)
        
        print(f"\n✓ Top 10 Most Important Features:")
        print(importance_df.head(10).to_string(index=False))
        
        self.feature_importance = importance_df
        return importance_df
    
    def test_model_performance(self, X_test, y_test):
        """Evaluate best model on test set."""
        print(f"\n{'='*60}")
        print("FINAL MODEL PERFORMANCE")
        print(f"{'='*60}")
        
        y_pred = self.best_model.predict(X_test)
        
        mae = mean_absolute_error(y_test, y_pred)
        rmse = np.sqrt(np.mean((y_test - y_pred)**2))
        r2 = r2_score(y_test, y_pred)
        residuals = y_test - y_pred
        
        print(f"\n✓ Test Set Metrics:")
        print(f"  MAE: {mae:.4f}")
        print(f"  RMSE: {rmse:.4f}")
        print(f"  R²: {r2:.4f}")
        
        print(f"\n✓ Residual Analysis:")
        print(f"  Mean Residual: {residuals.mean():.6f}")
        print(f"  Std Dev Residual: {residuals.std():.4f}")
        print(f"  Min Residual: {residuals.min():.4f}")
        print(f"  Max Residual: {residuals.max():.4f}")
        
        return {'MAE': mae, 'RMSE': rmse, 'R2': r2, 'residuals': residuals}

class PredictionEngine:
    """Production-ready prediction interface."""
    
    def __init__(self, model_path):
        """Load trained model."""
        with open(model_path, 'rb') as f:
            self.model = pickle.load(f)
        print(f"✓ Model loaded from {model_path}")
    
    def predict_player_performance(self, player_data):
        """Predict batting average for a player."""
        # Validate input
        required_features = ['strike_rate', 'innings_played', 'centuries', 'fifties',
                           'economy_rate', 'wickets_taken', 'venue_type']
        
        if not all(feat in player_data for feat in required_features):
            raise ValueError(f"Missing required features: {required_features}")
        
        # Prepare input dataframe
        input_df = pd.DataFrame([player_data])
        
        # Make prediction
        predicted_average = self.model.predict(input_df)[0]
        
        return predicted_average
    
    def batch_predict(self, players_df):
        """Predict for multiple players."""
        predictions = self.model.predict(players_df)
        return predictions

# Main execution
if __name__ == "__main__":
    print("\n" + "="*60)
    print("ENHANCED CRICKET ML PIPELINE WITH TUNING")
    print("="*60)
    
    # Load data
    enhancer = EnhancedCricketPredictor()
    X_train, X_test, y_train, y_test, feature_cols = enhancer.load_preprocessed_data(
        'data/raw/cricket_players.csv'
    )
    
    # Perform hyperparameter tuning
    best_model = enhancer.hyperparameter_tuning(X_train, y_train)
    
    # Extract feature importance
    importance = enhancer.extract_feature_importance(X_train, feature_cols)
    
    # Test performance
    metrics = enhancer.test_model_performance(X_test, y_test)
    
    # Save enhanced model
    import pickle
    with open('models/enhanced_cricket_model.pkl', 'wb') as f:
        pickle.dump(best_model, f)
    print(f"\n✓ Enhanced model saved to models/enhanced_cricket_model.pkl")
    
    # Test prediction engine with sample data
    print(f"\n{'='*60}")
    print("PREDICTION ENGINE TEST")
    print(f"{'='*60}")
    
    engine = PredictionEngine('models/enhanced_cricket_model.pkl')
    
    # Example prediction for a player
    sample_player = {
        'strike_rate': 135.5,
        'innings_played': 45,
        'centuries': 3,
        'fifties': 12,
        'economy_rate': 7.2,
        'wickets_taken': 15,
        'venue_type': 'Home'
    }
    
    predicted_avg = engine.predict_player_performance(sample_player)
    print(f"\n✓ Sample Prediction:")
    print(f"  Player Profile: {sample_player}")
    print(f"  Predicted Batting Average: {predicted_avg:.2f}")
    
    print(f"\n{'='*60}")
    print("Enhancement pipeline completed successfully!")
    print(f"{'='*60}")

Step 4 — Testing & Verification

Step 4 executes the complete pipeline end-to-end and verifies that all components function correctly together. You will run the data foundation script, followed by the ML pipeline script, and then the enhancement module, confirming that each step produces expected outputs and passes validation checks.

Analogy🏏Cricket
🏏 Think of it like cricket: before naming the final side, a team plays a full trial match to be sure the whole plan holds up — the batting order runs between the wickets, the bowling changes fire, and the scorers confirm the totals add up correctly from start to finish. That trial is your testing and verification step. Just as a trial match checks every part works together under real conditions in cricket, you run the complete pipeline on sample cricket data and verify the outputs against expected results. And just as a captain deliberately tests how the side copes when a wicket falls early or the pitch misbehaves, you feed invalid inputs to confirm the error handling responds gracefully rather than collapsing. Checking that the system correctly identifies player trends and rankings is like confirming the scoreboard truly reflects the play. The payoff is a pipeline proven reliable end to end, trusted to hold together before it faces the real match.

The verification process includes confirming that the data loads correctly with 200 players present, that the model trains without errors and reports cross-validation scores, that hyperparameter tuning completes and identifies the best parameters, that predictions fall within a reasonable batting average range of 0 to 100, and that saved models reload successfully. Running test predictions on known-good data provides an additional consistency check.

This verification step is especially critical in ML systems because silent failures are a significant risk. A model can complete training without throwing any errors yet still produce invalid predictions, causing cascading problems in downstream production systems. Explicit end-to-end validation guards against this class of failure.

bash
#!/bin/bash
# Run complete cricket ML pipeline with verification

echo "="60
echo "EXECUTING COMPLETE CRICKET ML PIPELINE"
echo "="60

cd cricket_ml_pipeline

echo ""
echo "[1/4] DATA FOUNDATION - Creating and exploring dataset..."
echo "----------------------------------------"
python src/data_foundation.py

if [ $? -ne 0 ]; then
    echo "❌ Data foundation failed!"
    exit 1
fi

echo ""
echo "[2/4] MODEL PIPELINE - Training baseline model..."
echo "----------------------------------------"
python src/model_pipeline.py

if [ $? -ne 0 ]; then
    echo "❌ Model pipeline failed!"
    exit 1
fi

echo ""
echo "[3/4] MODEL ENHANCEMENT - Tuning hyperparameters..."
echo "----------------------------------------"
python src/model_enhancement.py

if [ $? -ne 0 ]; then
    echo "❌ Model enhancement failed!"
    exit 1
fi

echo ""
echo "[4/4] VERIFICATION - Checking outputs..."
echo "----------------------------------------"

echo ""
echo "Verifying saved files:"
ls -lh data/raw/cricket_players.csv
ls -lh models/*.pkl

echo ""
echo "✓ Data files verified:"
file_count=$(ls models/*.pkl 2>/dev/null | wc -l)
echo "  - $file_count model files saved"

echo ""
echo "✓ Testing model loading and prediction:"
python3 << 'PYTHON_TEST'
import pickle
import pandas as pd

# Load model
with open('models/enhanced_cricket_model.pkl', 'rb') as f:
    model = pickle.load(f)

print("  Model loaded successfully")

# Test predictions with sample cricket players
test_cases = [
    {
        'name': 'Rohit Sharma (Aggressive Opener)',
        'data': {'strike_rate': 145.0, 'innings_played': 120, 'centuries': 8,
                'fifties': 35, 'economy_rate': 0, 'wickets_taken': 0, 'venue_type': 'Home'}
    },
    {
        'name': 'Jasprit Bumrah (Death Bowler)',
        'data': {'strike_rate': 0, 'innings_played': 15, 'centuries': 0,
                'fifties': 0, 'economy_rate': 6.5, 'wickets_taken': 85, 'venue_type': 'Away'}
    },
    {
        'name': 'Hardik Pandya (All-rounder)',
        'data': {'strike_rate': 132.0, 'innings_played': 75, 'centuries': 2,
                'fifties': 18, 'economy_rate': 7.8, 'wickets_taken': 42, 'venue_type': 'Home'}
    }
]

print("\n  Predictions for cricket players:")
for test_case in test_cases:
    input_df = pd.DataFrame([test_case['data']])
    prediction = model.predict(input_df)[0]
    print(f"    - {test_case['name']}: {prediction:.2f} batting average")

print("\n  ✓ All predictions generated successfully")
print(f"  ✓ Predictions are within valid range (0-100)")
PYTHON_TEST

if [ $? -ne 0 ]; then
    echo "❌ Prediction test failed!"
    exit 1
fi

echo ""
echo "="60
echo "✓✓✓ PIPELINE VERIFICATION COMPLETE ✓✓✓"
echo "="60
echo ""
echo "Summary:"
echo "  ✓ Dataset created with 200 player records"
echo "  ✓ Baseline model trained (Linear Regression)"
echo "  ✓ Hyperparameters optimized (Ridge with GridSearchCV)"
echo "  ✓ Feature importance extracted"
echo "  ✓ Models saved to disk"
echo "  ✓ Predictions working correctly"
echo ""
echo "Next steps:"
echo "  - Review models/cricket_performance_model.pkl (baseline)"
echo "  - Review models/enhanced_cricket_model.pkl (tuned)"
echo "  - Run predictions on new player data"
echo ""
echo "="60

Warning: A common error is encountering 'ModuleNotFoundError: No module named sklearn' when running scripts. This occurs because the virtual environment is not activated. Always activate your venv with 'source venv/bin/activate' (Linux/Mac) or 'venv\Scripts\activate' (Windows) before running Python scripts. Another frequent mistake is data type mismatches—if venue_type contains unexpected values like 'stadium' instead of 'Home'/'Away', the OneHotEncoder will fail. Always inspect your data with df.unique() and df.value_counts() before preprocessing. Additionally, forgetting to handle missing values before model fitting causes sklearn to raise errors; use df.isnull().sum() to check and fill with appropriate strategies.

Extension Challenge: Extend the cricket performance predictor to handle multi-output regression where you predict multiple target variables simultaneously: (1) batting_average, (2) strike_rate, and (3) economy_rate in a single model using MultiOutputRegressor. This mirrors real cricket analytics where coaches need multiple insights about players. Additionally, implement Bayesian hyperparameter optimization using Optuna instead of GridSearchCV for more efficient parameter space exploration—this reduces tuning time from hours to minutes on large datasets. Finally, create an API using Flask that accepts JSON requests with player statistics and returns predictions with confidence intervals, enabling integration with cricket analytics dashboards used by franchise teams.

  • Data preprocessing (handling missing values, encoding categories, scaling features) is as critical as model selection—poor data quality invalidates even sophisticated algorithms regardless of complexity or theoretical appeal.
  • Train-test split prevents data leakage and ensures honest evaluation; models must be tested on unseen data to assess real-world generalization ability, not just memorization of training patterns.
  • Cross-validation (5 or 10-fold) provides robust performance estimates by testing on multiple data subsets, revealing whether performance is stable or dependent on particular data splits.
  • Feature engineering and selection directly impact model performance; identifying which features (strike_rate, centuries) matter most reduces dimensionality, improves interpretability, and prevents overfitting.
  • Hyperparameter tuning using GridSearchCV systematically searches parameter combinations to optimize performance; this automated approach beats manual guessing and scales to hundreds of parameters.
  • Feature importance analysis explains model predictions, building trust with stakeholders and revealing data insights (e.g., 'strike_rate dominates batting_average predictions') valuable beyond just accuracy metrics.
Lesson 25 of 35
0% complete