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

Advanced Topics Assessment and Practice

What You'll Build

In this project, you will construct a comprehensive cricket performance prediction system that integrates data preprocessing, feature engineering, model training, and evaluation pipelines into a single cohesive workflow. The system processes real cricket statistics — including runs scored, wickets taken, strike rates, and economy figures — and applies scikit-learn pipelines, pandas transformations, and numpy vectorization to generate actionable insights.

The project combines three distinct machine learning approaches: supervised learning for predicting player batting averages, unsupervised clustering for grouping similar playing styles, and ensemble methods for forecasting match outcomes. You will also implement cross-validation strategies, hyperparameter tuning, and model persistence using joblib.

By completing this exercise, you will demonstrate mastery of the full machine learning workflow, from raw data ingestion through production-ready model deployment. Each component you build contributes directly to robust, domain-specific predictions, reinforcing how individual techniques combine into reliable end-to-end systems.

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

  • Intermediate Python proficiency: list comprehensions, dictionary operations, function definitions, class inheritance, and exception handling
  • Solid foundation in pandas for data manipulation: reading CSV files, filtering rows, grouping operations, and creating derived columns
  • Understanding of scikit-learn basics: train-test splits, model instantiation, fit-predict workflows, and common classifier/regressor types
  • NumPy array operations: vectorized calculations, reshaping, indexing, and mathematical transformations for numerical data
  • Basic statistics knowledge: mean, standard deviation, correlation, and understanding of cross-validation and overfitting concepts

Setup & Project Structure

Begin by initializing a structured project directory that clearly separates data, code modules, and outputs. Creating a virtual environment at this stage isolates dependencies and ensures reproducibility across different machines and development setups.

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 essential packages required for the project: pandas for data manipulation, scikit-learn for machine learning algorithms, numpy for numerical computing, matplotlib and seaborn for visualization, and joblib for model persistence. Organizing your project with dedicated folders for raw data, processed data, model artifacts, and utility scripts enables team collaboration, reduces debugging time, and positions the pipeline for production use.

Finally, create a requirements.txt file that documents all package versions. This allows other developers to replicate your exact environment precisely, which is a critical practice for maintaining consistency across development, testing, and production stages.

bash
# Create project structure for cricket ML system
mkdir -p cricket_prediction_system/{data,models,notebooks,src}
cd cricket_prediction_system

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

# Create requirements file
cat > requirements.txt << 'EOF'
pandas==2.0.3
scikit-learn==1.3.0
numpy==1.24.3
matplotlib==3.7.2
seaborn==0.12.2
joblib==1.3.1
ipython==8.14.0
EOF

# Install dependencies
pip install -r requirements.txt

# Create project structure files
touch src/__init__.py
touch src/data_processor.py
touch src/model_trainer.py
touch src/evaluator.py
touch notebooks/exploration.ipynb
echo '# Cricket Prediction System' > README.md

# Create data directory with placeholder
echo 'Raw cricket statistics files go here' > data/README.md

Step 1 — Foundation

The foundation step establishes the data ingestion and exploratory analysis framework for the entire system. You will create a CricketStatsDataset class responsible for loading historical player performance data, validating its structure, and performing initial quality checks on fields such as player names, runs scored, wickets taken, matches played, strike rates, and economy figures.

This phase involves calculating descriptive statistics, identifying missing values, detecting outliers using quartile-based methods, and generating summary reports. Thorough exploration helps you understand data distributions, correlations between features, and potential quality issues before any modeling begins.

Investing carefully in this foundational work prevents garbage-in-garbage-out scenarios and ensures that all subsequent models train on trustworthy, well-understood data. Skipping or rushing this step typically leads to subtle errors that are far more costly to diagnose later in the pipeline.

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_processor.py - Foundation: Data Loading and Exploration
import pandas as pd
import numpy as np
from typing import Tuple, Dict
import warnings
warnings.filterwarnings('ignore')

class CricketStatsDataset:
    """
    Foundation class for loading and exploring cricket player statistics.
    Handles data validation, quality checks, and initial exploratory analysis.
    """
    
    def __init__(self, filepath: str):
        """
        Initialize dataset loader with file path.
        
        Args:
            filepath: Path to CSV containing cricket statistics
        """
        self.filepath = filepath
        self.df = None
        self.quality_report = {}
        
    def load_data(self) -> pd.DataFrame:
        """
        Load cricket statistics from CSV file.
        Validates that required columns exist.
        """
        try:
            self.df = pd.read_csv(self.filepath)
            print(f"✓ Loaded {len(self.df)} player records from {self.filepath}")
            return self.df
        except FileNotFoundError:
            print(f"✗ File not found: {self.filepath}")
            raise
    
    def validate_structure(self) -> Dict[str, bool]:
        """
        Validate that dataset contains required cricket statistics columns.
        Returns validation status for each required field.
        """
        required_columns = {
            'player_name': 'Player identifier',
            'matches_played': 'Games in sample',
            'runs_scored': 'Total career runs',
            'wickets_taken': 'Bowling dismissals',
            'batting_average': 'Runs per dismissal',
            'strike_rate': 'Runs per 100 balls',
            'economy_rate': 'Runs conceded per over'
        }
        
        validation = {}
        for col, description in required_columns.items():
            if col in self.df.columns:
                validation[col] = True
                print(f"✓ {col}: {description}")
            else:
                validation[col] = False
                print(f"✗ Missing {col}: {description}")
        
        return validation
    
    def check_data_quality(self) -> Dict:
        """
        Perform comprehensive data quality checks including:
        - Missing values detection
        - Outlier identification using IQR method
        - Duplicate records
        - Invalid numeric ranges
        """
        quality = {
            'total_records': len(self.df),
            'missing_values': self.df.isnull().sum().to_dict(),
            'duplicate_players': self.df.duplicated(subset=['player_name']).sum(),
            'outliers': {},
            'data_type_issues': []
        }
        
        # Check for negative or zero values where they shouldn't exist
        for col in ['matches_played', 'runs_scored', 'wickets_taken']:
            if col in self.df.columns:
                invalid = (self.df[col] < 0).sum()
                if invalid > 0:
                    quality['data_type_issues'].append(
                        f"{col}: {invalid} negative values found"
                    )
        
        # Identify outliers using IQR method for numeric columns
        numeric_cols = ['batting_average', 'strike_rate', 'economy_rate']
        for col in numeric_cols:
            if col in self.df.columns:
                Q1 = self.df[col].quantile(0.25)
                Q3 = self.df[col].quantile(0.75)
                IQR = Q3 - Q1
                outliers = ((self.df[col] < Q1 - 1.5*IQR) | 
                           (self.df[col] > Q3 + 1.5*IQR)).sum()
                quality['outliers'][col] = outliers
        
        self.quality_report = quality
        return quality
    
    def generate_summary_statistics(self) -> Dict:
        """
        Generate descriptive statistics for cricket metrics.
        Returns mean, median, std, min, max for key performance indicators.
        """
        summary = {}
        stat_cols = ['matches_played', 'runs_scored', 'batting_average', 
                     'strike_rate', 'economy_rate']
        
        for col in stat_cols:
            if col in self.df.columns:
                summary[col] = {
                    'mean': round(self.df[col].mean(), 3),
                    'median': round(self.df[col].median(), 3),
                    'std': round(self.df[col].std(), 3),
                    'min': round(self.df[col].min(), 3),
                    'max': round(self.df[col].max(), 3)
                }
        
        return summary
    
    def explore(self) -> Tuple[Dict, Dict]:
        """
        Run complete exploratory analysis pipeline.
        Returns quality report and summary statistics.
        """
        print("\n=== CRICKET DATASET FOUNDATION ANALYSIS ===")
        self.load_data()
        print("\n--- STRUCTURE VALIDATION ---")
        self.validate_structure()
        print("\n--- DATA QUALITY CHECK ---")
        quality = self.check_data_quality()
        for key, value in quality.items():
            print(f"{key}: {value}")
        print("\n--- SUMMARY STATISTICS ---")
        summary = self.generate_summary_statistics()
        for metric, stats in summary.items():
            print(f"{metric}: mean={stats['mean']}, median={stats['median']}, "
                  f"std={stats['std']}")
        
        return quality, summary


# Example usage demonstrating foundation work
if __name__ == "__main__":
    # Create sample cricket dataset
    sample_data = {
        'player_name': ['Virat Kohli', 'Rohit Sharma', 'Jasprit Bumrah', 
                       'Rishabh Pant', 'Mohammed Shami'],
        'matches_played': [120, 135, 95, 85, 72],
        'runs_scored': [7000, 6800, 450, 3200, 1850],
        'wickets_taken': [0, 0, 180, 5, 185],
        'batting_average': [58.3, 50.2, 22.5, 37.6, 26.4],
        'strike_rate': [95.2, 87.5, np.nan, 88.3, 85.1],
        'economy_rate': [np.nan, np.nan, 6.8, np.nan, 7.1]
    }
    
    df_sample = pd.DataFrame(sample_data)
    df_sample.to_csv('data/cricket_sample.csv', index=False)
    
    # Initialize and explore
    dataset = CricketStatsDataset('data/cricket_sample.csv')
    quality_report, summary_stats = dataset.explore()

Step 2 — Core Logic

The core logic phase implements the complete machine learning pipeline, encompassing feature engineering, model training, and cross-validation. You will create a PerformancePredictor class that transforms raw cricket statistics into predictive features, handles categorical encoding, normalizes numerical values using StandardScaler, and manages train-test splits.

This phase requires implementing multiple models — Linear Regression, Random Forest, and Gradient Boosting — using scikit-learn, then training them on cricket performance data and evaluating them with appropriate metrics such as RMSE for regression tasks and ROC-AUC for classification tasks. Applying cross-validation at this stage provides a reliable estimate of real-world performance and helps detect overfitting before deployment.

Together, these steps demonstrate your ability to build production-grade machine learning systems that generalize beyond training data. The result is a prediction engine capable of providing reliable forecasts for new player statistics encountered after the model has been trained.

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_trainer.py - Core Logic: Feature Engineering and Model Training
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split, cross_val_score, KFold
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
import warnings
warnings.filterwarnings('ignore')

class PerformancePredictor:
    """
    Core ML pipeline for cricket performance prediction.
    Handles feature engineering, model training, and cross-validation.
    """
    
    def __init__(self, df: pd.DataFrame, target_column: str = 'batting_average'):
        """
        Initialize predictor with cricket statistics dataframe.
        
        Args:
            df: DataFrame with cricket player statistics
            target_column: Column to predict (default: batting_average)
        """
        self.df = df.copy()
        self.target_column = target_column
        self.X = None
        self.y = None
        self.X_train = None
        self.X_test = None
        self.y_train = None
        self.y_test = None
        self.models = {}
        self.cv_scores = {}
        self.predictions = {}
        
    def engineer_features(self) -> pd.DataFrame:
        """
        Create derived features from raw cricket statistics.
        Generates interaction terms and performance ratios.
        
        Returns:
            DataFrame with engineered features
        """
        df_engineered = self.df.copy()
        
        # Handle missing values - critical for real data
        df_engineered['strike_rate'].fillna(df_engineered['strike_rate'].median(), 
                                            inplace=True)
        df_engineered['economy_rate'].fillna(df_engineered['economy_rate'].median(), 
                                             inplace=True)
        
        # Feature 1: Runs per match efficiency
        df_engineered['runs_per_match'] = (
            df_engineered['runs_scored'] / df_engineered['matches_played']
        ).round(2)
        
        # Feature 2: Bowling efficiency (wickets per match)
        df_engineered['wickets_per_match'] = (
            df_engineered['wickets_taken'] / df_engineered['matches_played']
        ).round(2)
        
        # Feature 3: Consistency index (inverse of coefficient of variation concept)
        # Higher values indicate stable performance
        df_engineered['performance_stability'] = (
            df_engineered['batting_average'] / (df_engineered['strike_rate'] / 100 + 0.001)
        ).round(2)
        
        # Feature 4: Overall impact score (combines batting and bowling contributions)
        df_engineered['impact_score'] = (
            (df_engineered['batting_average'] * 0.6) + 
            (df_engineered['wickets_per_match'] * 50 * 0.4)
        ).round(2)
        
        # Feature 5: Experience indicator
        df_engineered['experience_level'] = pd.cut(
            df_engineered['matches_played'],
            bins=[0, 50, 100, 200],
            labels=['Emerging', 'Established', 'Veteran'],
            ordered=True
        )
        
        # Convert categorical to numeric
        experience_mapping = {'Emerging': 1, 'Established': 2, 'Veteran': 3}
        df_engineered['experience_numeric'] = (
            df_engineered['experience_level'].map(experience_mapping)
        )
        
        self.df_engineered = df_engineered
        print(f"✓ Generated {df_engineered.shape[1] - self.df.shape[1]} engineered features")
        print(f"  New feature columns: runs_per_match, wickets_per_match, "
              f"performance_stability, impact_score, experience_numeric")
        
        return df_engineered
    
    def prepare_data(self, test_size: float = 0.2, 
                    random_state: int = 42) -> Tuple:
        """
        Prepare feature matrix and target vector for modeling.
        Perform train-test split with stratification by experience level.
        
        Args:
            test_size: Proportion of data for testing
            random_state: Seed for reproducibility
            
        Returns:
            Tuple of (X_train, X_test, y_train, y_test)
        """
        # Select feature columns for modeling
        feature_columns = [
            'matches_played', 'runs_scored', 'wickets_taken',
            'strike_rate', 'economy_rate', 'runs_per_match',
            'wickets_per_match', 'performance_stability', 
            'impact_score', 'experience_numeric'
        ]
        
        # Filter to available columns
        available_features = [col for col in feature_columns 
                             if col in self.df_engineered.columns]
        
        self.X = self.df_engineered[available_features]
        self.y = self.df_engineered[self.target_column]
        
        # Remove any rows with NaN in features or target
        valid_idx = ~(self.X.isnull().any(axis=1) | self.y.isnull())
        self.X = self.X[valid_idx]
        self.y = self.y[valid_idx]
        
        # Train-test split
        self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(
            self.X, self.y, test_size=test_size, random_state=random_state
        )
        
        print(f"✓ Data preparation complete")
        print(f"  Training samples: {len(self.X_train)}, Testing samples: {len(self.X_test)}")
        print(f"  Feature count: {self.X.shape[1]}")
        print(f"  Target column: {self.target_column}")
        
        return self.X_train, self.X_test, self.y_train, self.y_test
    
    def build_pipeline(self, model_type: str = 'random_forest') -> Pipeline:
        """
        Build scikit-learn pipeline with scaling and modeling.
        
        Args:
            model_type: Type of model ('linear', 'random_forest', 'gradient_boost')
            
        Returns:
            Fitted pipeline object
        """
        scaler = StandardScaler()
        
        if model_type == 'linear':
            model = LinearRegression()
        elif model_type == 'random_forest':
            model = RandomForestRegressor(
                n_estimators=100,
                max_depth=10,
                min_samples_split=5,
                random_state=42,
                n_jobs=-1
            )
        elif model_type == 'gradient_boost':
            model = GradientBoostingRegressor(
                n_estimators=100,
                learning_rate=0.1,
                max_depth=5,
                random_state=42
            )
        else:
            raise ValueError(f"Unknown model type: {model_type}")
        
        pipeline = Pipeline([
            ('scaler', scaler),
            ('model', model)
        ])
        
        return pipeline
    
    def train_models(self) -> Dict:
        """
        Train multiple models and store them.
        Evaluates on both training and test sets.
        
        Returns:
            Dictionary with training results
        """
        model_configs = ['linear', 'random_forest', 'gradient_boost']
        results = {}
        
        for model_type in model_configs:
            print(f"\n--- Training {model_type.upper()} Model ---")
            
            pipeline = self.build_pipeline(model_type)
            pipeline.fit(self.X_train, self.y_train)
            self.models[model_type] = pipeline
            
            # Predictions on both sets
            y_train_pred = pipeline.predict(self.X_train)
            y_test_pred = pipeline.predict(self.X_test)
            self.predictions[model_type] = {
                'train': y_train_pred,
                'test': y_test_pred
            }
            
            # Evaluation metrics
            train_rmse = np.sqrt(mean_squared_error(self.y_train, y_train_pred))
            test_rmse = np.sqrt(mean_squared_error(self.y_test, y_test_pred))
            train_r2 = r2_score(self.y_train, y_train_pred)
            test_r2 = r2_score(self.y_test, y_test_pred)
            
            results[model_type] = {
                'train_rmse': round(train_rmse, 4),
                'test_rmse': round(test_rmse, 4),
                'train_r2': round(train_r2, 4),
                'test_r2': round(test_r2, 4)
            }
            
            print(f"Train RMSE: {train_rmse:.4f}, Test RMSE: {test_rmse:.4f}")
            print(f"Train R²: {train_r2:.4f}, Test R²: {test_r2:.4f}")
        
        return results
    
    def cross_validate(self, n_splits: int = 5) -> Dict:
        """
        Perform k-fold cross-validation on all models.
        Provides robust estimate of generalization performance.
        
        Args:
            n_splits: Number of folds for cross-validation
            
        Returns:
            Dictionary with cross-validation scores
        """
        kfold = KFold(n_splits=n_splits, shuffle=True, random_state=42)
        cv_results = {}
        
        for model_type in ['linear', 'random_forest', 'gradient_boost']:
            pipeline = self.build_pipeline(model_type)
            
            # Negative MSE scoring (sklearn convention)
            scores = -cross_val_score(
                pipeline, self.X, self.y, 
                cv=kfold, 
                scoring='neg_mean_squared_error',
                n_jobs=-1
            )
            rmse_scores = np.sqrt(scores)
            
            self.cv_scores[model_type] = {
                'rmse_scores': rmse_scores.round(4),
                'mean_rmse': round(rmse_scores.mean(), 4),
                'std_rmse': round(rmse_scores.std(), 4)
            }
            
            cv_results[model_type] = self.cv_scores[model_type]
            print(f"{model_type.upper()}: Mean CV RMSE = {rmse_scores.mean():.4f} "
                  f"(+/- {rmse_scores.std():.4f})")
        
        return cv_results


if __name__ == "__main__":
    # Load sample data
    df_sample = pd.read_csv('data/cricket_sample.csv')
    
    # Initialize predictor
    predictor = PerformancePredictor(df_sample, target_column='batting_average')
    
    # Execute pipeline
    print("\n=== CRICKET ML PIPELINE: CORE LOGIC ===")
    predictor.engineer_features()
    predictor.prepare_data()
    results = predictor.train_models()
    cv_results = predictor.cross_validate(n_splits=3)

Step 3 — Integration & Enhancement

The integration phase brings all previously built components together into a unified, production-ready system. You will create an EnsemblePredictor that combines individual model predictions through weighted averaging or stacking, and implement model persistence using joblib to enable deployment across environments.

This phase also involves developing a prediction service that accepts new player statistics and returns forecasts accompanied by confidence intervals, as well as adding visualization utilities for performance analysis and generating feature importance reports to identify which cricket metrics most strongly drive predictions.

To ensure the system is robust in production, you will implement error handling and logging to support issue diagnosis after deployment. The phase concludes with a final assessment report that compares all models and recommends the best approach for deployment, demonstrating system design thinking that extends well beyond individual machine learning algorithms.

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/evaluator.py - Integration & Enhancement: Ensemble, Persistence, and Deployment
import pandas as pd
import numpy as np
from sklearn.ensemble import VotingRegressor, StackingRegressor
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import joblib
import warnings
warnings.filterwarnings('ignore')

class EnsemblePredictor:
    """
    Production-grade ensemble system combining multiple models.
    Handles persistence, deployment, and performance reporting.
    """
    
    def __init__(self, individual_models: Dict, X_train, y_train, X_test, y_test):
        """
        Initialize ensemble with trained individual models.
        
        Args:
            individual_models: Dict of {name: trained_pipeline}
            X_train, y_train: Training features and target
            X_test, y_test: Test features and target
        """
        self.individual_models = individual_models
        self.X_train = X_train
        self.y_train = y_train
        self.X_test = X_test
        self.y_test = y_test
        self.ensemble_model = None
        self.ensemble_weights = {}
        self.feature_importance_global = {}
        
    def calculate_model_weights(self) -> Dict:
        """
        Calculate weights for ensemble based on individual model performance.
        Higher R² scores receive higher weights.
        
        Returns:
            Dictionary of {model_name: weight}
        """
        from sklearn.metrics import r2_score
        
        r2_scores = {}
        for name, model in self.individual_models.items():
            y_pred = model.predict(self.X_test)
            r2 = r2_score(self.y_test, y_pred)
            r2_scores[name] = max(r2, 0)  # Ensure non-negative
        
        # Normalize to sum to 1 (probability distribution)
        total = sum(r2_scores.values())
        if total > 0:
            self.ensemble_weights = {k: v/total for k, v in r2_scores.items()}
        else:
            # Fallback to equal weights
            n = len(r2_scores)
            self.ensemble_weights = {k: 1/n for k in r2_scores.keys()}
        
        print("\n--- ENSEMBLE MODEL WEIGHTS (Performance-Based) ---")
        for model, weight in sorted(self.ensemble_weights.items(), 
                                    key=lambda x: x[1], reverse=True):
            print(f"{model}: {weight:.4f} ({weight*100:.2f}%)")
        
        return self.ensemble_weights
    
    def build_weighted_ensemble(self):
        """
        Build voting regressor with weighted individual models.
        Voting ensembles provide robust predictions from multiple experts.
        """
        estimators = list(self.individual_models.items())
        weights = [self.ensemble_weights.get(name, 1.0) 
                  for name, _ in estimators]
        
        self.ensemble_model = VotingRegressor(
            estimators=estimators,
            weights=weights
        )
        
        print("\n✓ Weighted voting ensemble created")
        print(f"  Combining {len(self.individual_models)} models with calculated weights")
        
    def extract_feature_importance(self):
        """
        Extract feature importance from tree-based models in ensemble.
        Aggregates importance across Random Forest and Gradient Boosting.
        """
        importance_scores = {}
        feature_names = self.X_train.columns.tolist()
        
        for model_name, model in self.individual_models.items():
            if hasattr(model, 'named_steps'):
                if 'model' in model.named_steps:
                    base_model = model.named_steps['model']
                    if hasattr(base_model, 'feature_importances_'):
                        importances = base_model.feature_importances_
                        for feat, imp in zip(feature_names, importances):
                            if feat not in importance_scores:
                                importance_scores[feat] = []
                            importance_scores[feat].append(imp)
        
        # Average importance across models
        self.feature_importance_global = {
            feat: np.mean(scores) 
            for feat, scores in importance_scores.items()
        }
        
        # Normalize to sum to 1
        total = sum(self.feature_importance_global.values())
        if total > 0:
            self.feature_importance_global = {
                feat: score/total 
                for feat, score in self.feature_importance_global.items()
            }
        
        # Sort by importance
        sorted_importance = sorted(self.feature_importance_global.items(),
                                  key=lambda x: x[1], reverse=True)
        
        print("\n--- TOP PREDICTIVE FEATURES ---")
        for feat, imp in sorted_importance[:5]:
            print(f"{feat}: {imp:.4f} ({imp*100:.2f}%)")
        
        return self.feature_importance_global
    
    def save_models(self, model_dir: str = 'models/'):
        """
        Persist trained models to disk for production deployment.
        Uses joblib format for efficient serialization.
        
        Args:
            model_dir: Directory to save model files
        """
        import os
        os.makedirs(model_dir, exist_ok=True)
        
        # Save individual models
        for name, model in self.individual_models.items():
            filepath = f"{model_dir}{name}_model.pkl"
            joblib.dump(model, filepath)
            print(f"✓ Saved {name} to {filepath}")
        
        # Save ensemble
        if self.ensemble_model:
            joblib.dump(self.ensemble_model, f"{model_dir}ensemble_model.pkl")
            print(f"✓ Saved ensemble to {model_dir}ensemble_model.pkl")
        
        # Save metadata
        metadata = {
            'feature_names': self.X_train.columns.tolist(),
            'ensemble_weights': self.ensemble_weights,
            'feature_importance': self.feature_importance_global
        }
        joblib.dump(metadata, f"{model_dir}metadata.pkl")
        print(f"✓ Saved metadata to {model_dir}metadata.pkl")
    
    def load_models(self, model_dir: str = 'models/'):
        """
        Load persisted models from disk for inference.
        
        Args:
            model_dir: Directory containing model files
        """
        self.ensemble_model = joblib.load(f"{model_dir}ensemble_model.pkl")
        metadata = joblib.load(f"{model_dir}metadata.pkl")
        self.feature_names = metadata['feature_names']
        print(f"✓ Models loaded from {model_dir}")
    
    def predict_with_confidence(self, player_stats: Dict) -> Dict:
        """
        Make prediction on new cricket player stats with confidence metrics.
        
        Args:
            player_stats: Dictionary with features for new player
            Example: {'matches_played': 50, 'runs_scored': 2000, ...}
            
        Returns:
            Dictionary with prediction, confidence interval, and model agreement
        """
        # Prepare input
        X_new = pd.DataFrame([player_stats])
        X_new = X_new[self.X_train.columns]  # Ensure correct feature order
        
        # Get predictions from individual models
        individual_predictions = {}
        for name, model in self.individual_models.items():
            pred = model.predict(X_new)[0]
            individual_predictions[name] = round(pred, 3)
        
        # Ensemble prediction
        if self.ensemble_model:
            ensemble_pred = self.ensemble_model.predict(X_new)[0]
        else:
            # Fallback: manual weighted average
            ensemble_pred = sum(
                individual_predictions[name] * 
                self.ensemble_weights.get(name, 0)
                for name in individual_predictions.keys()
            )
        
        # Confidence interval (based on prediction spread)
        pred_values = list(individual_predictions.values())
        pred_std = np.std(pred_values)
        confidence_lower = round(ensemble_pred - 1.96 * pred_std, 3)
        confidence_upper = round(ensemble_pred + 1.96 * pred_std, 3)
        
        # Model agreement metric (lower std = higher agreement)
        agreement = round(100 * (1 - min(pred_std / (ensemble_pred + 0.001), 1)), 2)
        
        return {
            'player_data': player_stats,
            'ensemble_prediction': round(ensemble_pred, 3),
            'confidence_interval': (confidence_lower, confidence_upper),
            'model_agreement': f"{agreement}%",
            'individual_predictions': individual_predictions,
            'prediction_spread': round(pred_std, 3)
        }
    
    def generate_assessment_report(self) -> str:
        """
        Generate comprehensive assessment report for model selection.
        Compares ensemble vs individual models and provides recommendations.
        
        Returns:
            Formatted report string
        """
        from sklearn.metrics import r2_score, mean_squared_error
        
        report = "\n" + "="*60
        report += "\nCRICKET ML SYSTEM: FINAL ASSESSMENT REPORT"
        report += "\n" + "="*60
        
        report += "\n\n[INDIVIDUAL MODEL PERFORMANCE]"
        for name, model in self.individual_models.items():
            y_pred = model.predict(self.X_test)
            rmse = np.sqrt(mean_squared_error(self.y_test, y_pred))
            r2 = r2_score(self.y_test, y_pred)
            report += f"\n{name.upper()}"
            report += f"\n  RMSE: {rmse:.4f}"
            report += f"\n  R²: {r2:.4f}"
            report += f"\n  Weight in Ensemble: {self.ensemble_weights.get(name, 0):.2%}"
        
        report += "\n\n[ENSEMBLE MODEL PERFORMANCE]"
        if self.ensemble_model:
            y_ensemble_pred = self.ensemble_model.predict(self.X_test)
            ensemble_rmse = np.sqrt(mean_squared_error(self.y_test, y_ensemble_pred))
            ensemble_r2 = r2_score(self.y_test, y_ensemble_pred)
            report += f"\nWeighted Voting Ensemble"
            report += f"\n  RMSE: {ensemble_rmse:.4f}"
            report += f"\n  R²: {ensemble_r2:.4f}"
        
        report += "\n\n[DEPLOYMENT RECOMMENDATION]"
        report += "\n✓ Use ensemble model for production predictions"
        report += "\n  Reason: Combines strengths of multiple algorithms"
        report += "\n  Benefit: Robust to individual model limitations"
        report += "\n✓ Models saved to /models/ directory"
        report += "\n  Usage: Load with joblib.load('models/ensemble_model.pkl')"
        
        report += "\n\n[KEY FEATURES DRIVING PREDICTIONS]"
        for feat, imp in sorted(self.feature_importance_global.items(),
                               key=lambda x: x[1], reverse=True)[:5]:
            report += f"\n  {feat}: {imp*100:.2f}%"
        
        report += "\n" + "="*60 + "\n"
        return report


if __name__ == "__main__":
    print("Integration module loaded. Use with trained models from Step 2.")

Step 4 — Testing & Verification

The testing phase validates that the complete system functions correctly end-to-end when processing real cricket data. You will run the integrated pipeline on a fresh dataset, verify that all components interact as expected, and generate predictions on new player statistics to confirm the system's output quality.

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.

Validation tests during this phase check that feature engineering produces the expected number of output columns, that model persistence saves and loads artifacts without corruption, and that ensemble predictions remain within reasonable numerical ranges. You will also generate summary reports comparing ensemble predictions against ground truth values.

Documenting any failures, edge cases — such as missing features or unusual player statistics — and corresponding correction strategies is an essential part of this phase. This documentation confirms production readiness and provides a reference for maintaining the system after it has been deployed.

bash
# Complete testing and verification script
# Run this after completing Steps 1-3

#!/bin/bash

# Activate virtual environment
source venv/bin/activate

# Run complete pipeline with sample cricket data
echo "==============================================="
echo "CRICKET ML SYSTEM - COMPLETE PIPELINE TEST"
echo "==============================================="
echo ""

# Test Step 1: Foundation
echo "[STEP 1] FOUNDATION - Data Loading and Exploration"
echo "Run: python -c "
echo "from src.data_processor import CricketStatsDataset"
echo "dataset = CricketStatsDataset('data/cricket_sample.csv')"
echo "quality, summary = dataset.explore()"
echo ""
echo "Expected output:"
echo "✓ Loaded X player records"
echo "✓ Column validation results"
echo "Missing values: {...}"
echo "Outliers identified: {...}"
echo ""

# Test Step 2: Core Logic  
echo "[STEP 2] CORE LOGIC - Feature Engineering and Model Training"
echo "Expected outputs:"
echo "✓ Generated 4 engineered features"
echo "✓ Data preparation complete - X training samples, Y testing samples"
echo "Training results for:"
echo "  - Linear Regression model"
echo "  - Random Forest model"
echo "  - Gradient Boosting model"
echo "Cross-validation scores with mean RMSE and standard deviation"
echo ""

# Test Step 3: Integration
echo "[STEP 3] INTEGRATION - Ensemble and Deployment"
echo "Expected outputs:"
echo "✓ Ensemble model weights calculated"
echo "✓ Feature importance extracted"
echo "✓ Models saved to models/ directory:"
echo "  - linear_model.pkl"
echo "  - random_forest_model.pkl"
echo "  - gradient_boost_model.pkl"
echo "  - ensemble_model.pkl"
echo "  - metadata.pkl"
echo ""

# Test Step 4: Verification
echo "[STEP 4] VERIFICATION - Sample Predictions"
echo ""
echo "Example: Predicting batting average for new player"
echo "Input player statistics:"
echo "  matches_played: 45"
echo "  runs_scored: 1800"
echo "  wickets_taken: 0"
echo "  strike_rate: 92.5"
echo "  economy_rate: 0 (batsman)"
echo ""
echo "Expected output:"
echo "  Ensemble Prediction: ~38.5"
echo "  Confidence Interval: (35.2, 41.8)"
echo "  Model Agreement: 92%"
echo "  Individual Predictions:"
echo "    linear: 37.8"
echo "    random_forest: 38.9"
echo "    gradient_boost: 39.2"
echo ""
echo "This indicates strong model consensus on prediction."
echo ""

# Run actual Python test
echo "Running complete integration test..."
echo ""

python3 << 'EOF'
import pandas as pd
import numpy as np
from src.data_processor import CricketStatsDataset
from src.model_trainer import PerformancePredictor
from src.evaluator import EnsemblePredictor
import warnings
warnings.filterwarnings('ignore')

# Load data
print("[TEST] Loading cricket sample data...")
df = pd.read_csv('data/cricket_sample.csv')
print(f"✓ Loaded {len(df)} players\n")

# Step 1: Foundation
print("[TEST] Step 1 - Foundation\n" + "-"*40)
dataset = CricketStatsDataset('data/cricket_sample.csv')
quality, summary = dataset.explore()

# Step 2: Core Logic
print("\n[TEST] Step 2 - Core Logic\n" + "-"*40)
predictor = PerformancePredictor(df, target_column='batting_average')
predictor.engineer_features()
predictor.prepare_data(test_size=0.3)
training_results = predictor.train_models()
cv_results = predictor.cross_validate(n_splits=3)

# Step 3: Integration & Ensemble
print("\n[TEST] Step 3 - Integration\n" + "-"*40)
ensemble = EnsemblePredictor(
    predictor.models,
    predictor.X_train,
    predictor.y_train,
    predictor.X_test,
    predictor.y_test
)
ensemble.calculate_model_weights()
ensemble.build_weighted_ensemble()
ensemble.extract_feature_importance()
ensemble.save_models('models/')

# Step 4: Verification
print("\n[TEST] Step 4 - Verification & Predictions\n" + "-"*40)
print("\nTest Case 1: Virat Kohli-like player")
kohli_stats = {
    'matches_played': 120,
    'runs_scored': 7000,
    'wickets_taken': 0,
    'strike_rate': 95.2,
    'economy_rate': 0.0,
    'runs_per_match': 58.3,
    'wickets_per_match': 0.0,
    'performance_stability': 61.3,
    'impact_score': 35.0,
    'experience_numeric': 3
}
pred1 = ensemble.predict_with_confidence(kohli_stats)
print(f"Prediction: {pred1['ensemble_prediction']}")
print(f"Confidence Interval: {pred1['confidence_interval']}")
print(f"Model Agreement: {pred1['model_agreement']}")

print("\nTest Case 2: Emerging player")
emerging_stats = {
    'matches_played': 20,
    'runs_scored': 400,
    'wickets_taken': 0,
    'strike_rate': 85.0,
    'economy_rate': 0.0,
    'runs_per_match': 20.0,
    'wickets_per_match': 0.0,
    'performance_stability': 23.5,
    'impact_score': 12.0,
    'experience_numeric': 1
}
pred2 = ensemble.predict_with_confidence(emerging_stats)
print(f"Prediction: {pred2['ensemble_prediction']}")
print(f"Confidence Interval: {pred2['confidence_interval']}")
print(f"Model Agreement: {pred2['model_agreement']}")

# Generate assessment
print("\n[ASSESSMENT]\n" + "="*60)
report = ensemble.generate_assessment_report()
print(report)

print("\n✓ ALL TESTS COMPLETED SUCCESSFULLY")
print("✓ System ready for production deployment")
EOF

echo ""
echo "==============================================="
echo "TEST SUMMARY"
echo "==============================================="
echo "✓ Step 1 (Foundation): Data validation passed"
echo "✓ Step 2 (Core Logic): Models trained and cross-validated"
echo "✓ Step 3 (Integration): Ensemble created and models persisted"
echo "✓ Step 4 (Verification): Predictions generated and verified"
echo ""
echo "All pipeline components working correctly."
echo "Ready for production deployment."

deactivate

Warning: The most common error when integrating Steps 2 and 3 is mismatched feature column ordering. When you save models in Step 3 and reload them in Step 4, the input DataFrame columns must be in exactly the same order as training data. If you later add or remove features, the loaded model will fail silently—producing incorrect predictions. Solution: Always save feature column names in metadata.pkl and explicitly reorder input features using X_new = X_new[saved_feature_names] before prediction. Additionally, watch for missing value handling inconsistencies: if your training pipeline filled NaN values with median, your new data must use the same median value (saved during training), not recalculate it from test data.

Extension Challenge: Implement a real-time prediction API using Flask that exposes your ensemble model as a REST endpoint. Create a /predict endpoint accepting JSON with player statistics, returning ensemble predictions with confidence intervals. Add a /batch_predict endpoint for processing multiple players simultaneously. Implement caching using Redis for repeated predictions (same player data). Add monitoring that logs prediction requests and accuracy metrics. Deploy this API using Docker containerization so others can easily run your cricket prediction system. Bonus: Create a web dashboard visualizing feature importance, model performance comparisons, and historical predictions—making your AI/ML work accessible to non-technical cricket analysts.

  • Foundation work (Step 1) validates data quality and identifies issues before modeling, preventing garbage-in-garbage-out failures in production systems.
  • Feature engineering (Step 2) creates domain-specific insights from raw statistics—like impact_score combining batting and bowling—that raw models cannot discover automatically.
  • Ensemble methods combine multiple model strengths (linear regression's interpretability with tree models' nonlinearity) producing more robust predictions than any individual algorithm.
  • Cross-validation estimates real-world performance on unseen data, preventing misleading accuracy metrics from simple train-test splits that overestimate generalization.
  • Model persistence with joblib enables deployment and sharing—loading pre-trained models costs milliseconds versus retraining which takes hours, enabling production APIs.
  • Feature importance analysis reveals which cricket metrics actually drive predictions, enabling stakeholder understanding and business decision-making beyond black-box accuracy numbers.
Lesson 30 of 35
0% complete