Lesson 5.3.1: Model training and validation strategy
Lesson 5.3.1: Model training and validation strategy
Learning Objectives
- Explain the purpose of training and validation splits in supervised learning.
- Describe common validation strategies such as train‑test split, k‑fold cross‑validation and hold‑out sets.
- Apply a chosen validation strategy to evaluate a model’s generalisation performance.
- Identify and mitigate common pitfalls that bias model evaluation.
Key Terms
| Train‑Test Split | A method of partitioning data into a training set used to fit the model and a test set used to assess its performance. |
|---|---|
| Cross‑Validation | A resampling procedure that repeatedly partitions the data into training and validation sets to obtain a robust estimate of model performance. |
| Hyperparameter Tuning | The process of optimising model parameters that are set before training, such as learning rate or regularisation strength. |
| Overfitting | When a model captures noise in the training data, leading to poor generalisation on unseen data. |
flowchart TD
A[Data Collection] --> B[Data Preprocessing]
B --> C[Feature Engineering]
C --> D[Train / Test Split]
D --> E[Model Training]
E --> F[Hyperparameter Tuning]
F --> G[Cross‑Validation]
G --> H[Evaluation Metrics]
H --> I[Model Deployment]
Concept
In the world of predictive modelling, the difference between a model that performs well in the laboratory and one that delivers reliable predictions in the real world is often subtle but profound. Theoretical foundations underpinning training and validation are the bedrock upon which robust, generalisable models are built. This section dissects the key principles that guide the design of a training and validation strategy, explains why unbiased evaluation is essential, and illustrates how these concepts translate into code.
1. The Bias–Variance Trade‑off
Every statistical learning algorithm can be decomposed into two sources of error: bias and variance. Bias reflects the systematic error introduced by approximating a complex, real‑world problem with a simplified model. For instance, a linear regression model will systematically under‑predict outcomes in a scenario with strong non‑linear relationships.
In contrast, variance measures how much the model’s predictions fluctuate when trained on different samples of data. High variance indicates that the model is overly sensitive to the idiosyncrasies of the training set, often a symptom of over‑fitting.
The bias–variance trade‑off is visualised as a curve: as model complexity increases, bias decreases but variance rises. The optimal point is where the sum of bias and variance is minimised. Training and validation strategies are designed to locate this balance.
2. The Role of Cross‑Validation
Cross‑validation (CV) is a systematic resampling technique used to estimate the generalisation error of a model. The basic idea is to partition the available data into a set of disjoint subsets called folds. The model is trained on all but one fold and evaluated on the held‑out fold. This process repeats so that every fold serves as a test set exactly once.
There are several common CV schemes:
- k‑fold cross‑validation – The dataset is split into k equal parts. Typical choices are k = 5 or 10.
- Leave‑One‑Out (LOO) CV – A special case where k equals the number of observations. Highly accurate but computationally intensive.
- Stratified k‑fold CV – Ensures that each fold preserves the class distribution, essential for imbalanced classification tasks.
- Repeated k‑fold CV – The k‑fold process is repeated multiple times with different random splits, providing a more stable estimate.
Cross‑validation mitigates the risk of over‑optimistic performance estimates that arise when the same data is used for both training and testing.
3. Hold‑Out Validation vs. Nested Cross‑Validation
A simple hold‑out split divides the data into a training set and a test set, often in proportions like 80:20. While computationally cheap, this approach is highly sensitive to the particular split chosen; a single unlucky split can give a misleading estimate.
For hyperparameter tuning, nested cross‑validation is the gold standard. Two loops are involved: an inner loop for selecting the best hyperparameters and an outer loop for estimating the model’s performance. This dual‑loop structure prevents information leakage from the test set into the training or tuning process.
4. The Imperative of Unbiased Evaluation
Unbiased evaluation is not merely a statistical nicety; it has practical implications for model deployment:
- Regulatory compliance – In domains like finance or healthcare, regulatory bodies require demonstrable evidence that a model performs reliably on unseen data.
- Business risk mitigation – Deploying a model that over‑estimates its predictive power can lead to costly misallocations of resources.
- Scientific integrity – In research, an unbiased estimate ensures that findings are reproducible and not artefacts of data leakage or over‑fitting.
To achieve unbiasedness, data must be split strictly before any model selection or feature engineering steps. This is often referred to as the data‑leakage guardrail and is a best practice across all stages of the machine learning pipeline.
5. A Practical Example: k‑Fold CV with Scikit‑Learn
Below is a concise Python snippet illustrating a 5‑fold cross‑validation workflow for a classification task using Scikit‑Learn. Each line is annotated to clarify its purpose.
# Import required libraries
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.datasets import load_wine
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
Load a sample dataset
X, y = load_wine(return_X_y=True)
Define a 5‑fold stratified cross‑validation splitter
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
Initialise the model
rf = RandomForestClassifier(n_estimators=100, random_state=42)
Perform cross‑validation and collect accuracy scores
scores = cross_val_score(rf, X, y, cv=skf, scoring='accuracy')
Output the average accuracy and its standard deviation
print(f'CV Accuracy: {scores.mean():.4f} ± {scores.std():.4f}')
Line‑by‑Line Explanation
- Import the modules needed for model creation, data splitting, and evaluation.
StratifiedKFoldensures each fold mirrors the overall class distribution. - Load the Wine dataset, which contains 178 samples and 13 features. The dataset is small enough for quick experimentation yet representative of a multi‑class classification problem.
- Create a
StratifiedKFoldinstance:n_splits=5specifies five folds;shuffle=Truerandomises the order before splitting;random_state=42guarantees reproducibility. - Instantiate a Random Forest classifier with 100 decision trees. The random state again ensures that the ensemble’s randomness is consistent across runs.
- Invoke
cross_val_scorewhich automates the training‑on‑four‑folds, testing‑on‑the‑remaining‑folds loop. It returns an array of accuracy scores, one per fold. - Compute the mean and standard deviation of the five accuracy scores, providing an estimate of model performance and its variability.
This snippet demonstrates how cross‑validation can be integrated seamlessly into a training pipeline, yielding an unbiased estimate of model performance that is far more reliable than a single train‑test split.
6. Common Pitfalls and How to Avoid Them
- Data Leakage – Incorporating test‑set information during feature engineering. Solution: perform all preprocessing inside the CV loop.
- Inappropriate CV Scheme – Using plain k‑fold CV for imbalanced data. Solution: opt for stratified CV to maintain class proportions.
- Optimising on Test Set – Tweaking hyperparameters until the test set looks perfect. Solution: reserve a separate validation set or use nested CV.
- Insufficient Sample Size – Too few observations in each fold leading to high variance. Solution: consider LOO CV or augment data if possible.
7. Visualising Model Stability
Plotting the distribution of cross‑validated scores can provide intuition about model robustness. A narrow distribution suggests stable performance across different data splits, whereas a wide spread signals sensitivity to the training set.
| Fold | Accuracy |
|---|---|
| Fold 1 | 0.915 |
| Fold 2 | 0.889 |
| Fold 3 | 0.902 |
| Fold 4 | 0.910 |
| Fold 5 | 0.905 |
From the table above, the model demonstrates a modest variance, indicating that the chosen hyperparameters generalise reasonably well across different subsets of the data.
8. From Theory to Practice: A Checklist
- Define the objective metric (e.g. accuracy, ROC‑AUC).
- Choose an appropriate CV strategy based on data size and class balance.
- Embed all preprocessing steps inside the CV loop to prevent leakage.
- Consider nested CV when hyperparameter tuning is required.
- Analyse the distribution of validation scores to assess stability.
- Report average performance with confidence intervals, not single point estimates.
Adhering to this checklist ensures that the training and validation process is rigorous, reproducible, and, most importantly, unbiased.
9. A Brief Quote from the Literature
"The only way to avoid over‑fitting is to expose the model to data it has never seen before." – Ian Goodfellow, Yoshua Bengio, and Aaron Courville, Deep Learning
This sentiment captures the essence of why cross‑validation and proper training‑validation separation are indispensable in modern machine learning practice.
10. Additional Example: Group‑Aware Cross‑Validation
When data points are grouped (e.g., multiple measurements per patient), standard CV can leak information between training and validation folds. GroupKFold enforces that all samples of a group are kept together. Below is a short illustration.
# Import the GroupKFold splitter
from sklearn.model_selection import GroupKFold
import numpy as np
Simulate data: 100 samples, 5 groups
X = np.random.randn(100, 3)
y = np.random.randint(0, 2, 100)
groups = np.repeat(np.arange(5), 20) # 5 groups of 20 samples each
gkf = GroupKFold(n_splits=5)
for train_idx, test_idx in gkf.split(X, y, groups):
print(f'Train groups: {np.unique(groups[train_idx])}, '
f'Test groups: {np.unique(groups[test_idx])}')
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| Hold‑out | Large datasets, quick checks | Fast | High variance, sensitive to split |
| k‑Fold CV | Moderate size, general use | Balanced estimate | Computational cost |
| Stratified k‑Fold CV | Imbalanced classes | Preserves class ratios | Same as k‑Fold otherwise |
| Repeated k‑Fold CV | Stability assessment | More robust estimate | Even higher cost |
| Nested CV | Hyperparameter tuning | Unbiased performance | Very expensive |
| GroupKFold | Grouped data (e.g., patients) | Prevents leakage across groups | Requires group labels, fewer folds possible |
Think back to the concepts covered in this lesson on model training and validation strategy. Consider how you would design a training pipeline for a new dataset: what split ratios would you choose, which validation techniques would you employ, and how would you guard against overfitting? Write a brief paragraph summarising your approach and the rationale behind each decision.
Implementation/Code
In this section we move from theory to practice. We will see how to create a reproducible train‑test split, how to shuffle and stratify the data, and finally how to set up a k‑fold cross‑validation loop using scikit‑learn. The examples are intentionally compact so that you can copy them into your own projects, but each line is unpacked thoroughly so that you understand the underlying mechanics.
1. Train‑Test Split with Scikit‑Learn
When you have a dataset ready to be fed into a model, the first decision is how to partition it into a training set (used to learn model parameters) and a test set (used only to evaluate final performance). The train_test_split helper from sklearn.model_selection is the most common tool for this task. It handles shuffling, stratification, and random state control in a single function call.
# Imports
from sklearn.model_selection import train_test_split
import pandas as pd
Load a sample dataset (e.g., the iris dataset)
df = pd.read_csv('iris.csv') # 150 rows, 5 columns (4 features + target)
Separate features and target
X = df.drop('species', axis=1) # 4 feature columns
y = df['species'] # target column
Create a 70/30 train/test split
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.30, # 30% of data for testing
random_state=42, # seed for reproducibility
stratify=y # maintain class proportions
)
print(f"Training set size: {X_train.shape[0]} samples")
print(f"Test set size: {X_test.shape[0]} samples")
Line‑by‑line explanation:
from sklearn.model_selection import train_test_splitimports the helper function.import pandas as pdbrings in pandas, which is convenient for loading tabular data.df = pd.read_csv('iris.csv')reads a CSV file into a DataFrame. Replace this with your own dataset path.X = df.drop('species', axis=1)creates a DataFrame containing only the feature columns by dropping the target column.y = df['species']stores the target series separately.train_test_split(...)is called with four keyword arguments:test_size=0.30requests that 30 % of the rows be allocated to the test set.random_state=42sets the internal random seed so that the same split is reproduced on every run. This is critical for experiment reproducibility.stratify=ytells the function to preserve the relative class frequencies in both splits. Without stratification, a minority class could be entirely absent from the test set, leading to misleading performance metrics.
- The function returns four objects:
X_train,X_test,y_train,y_test, which are then printed to confirm the split sizes.
2. K‑Fold Cross‑Validation Setup
While a single train‑test split gives you one estimate of performance, cross‑validation (CV) provides a more robust assessment by repeatedly partitioning the data into folds. The most common variant is k‑fold CV, where the dataset is divided into k mutually exclusive subsets. Each fold takes a turn as the validation set while the remaining k‑1 folds form the training set.
In scikit‑learn, the KFold class implements this logic. It can be wrapped by cross_val_score or cross_validate to run a model on each fold automatically.
from sklearn.model_selection import KFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
Load iris dataset via sklearn API for convenience
iris = load_iris()
X, y = iris.data, iris.target
Initialise a 5‑fold cross‑validator
kf = KFold(n_splits=5, shuffle=True, random_state=42)
Create a simple Random Forest model
rf = RandomForestClassifier(n_estimators=100, random_state=42)
Run cross‑validation and capture scores
cv_scores = cross_val_score(rf, X, y, cv=kf, scoring='accuracy')
print(f"Cross‑validated accuracy scores: {cv_scores}")
print(f"Mean accuracy: {cv_scores.mean():.3f} ± {cv_scores.std():.3f}")
Line‑by‑line explanation:
from sklearn.model_selection import KFold, cross_val_scoreimports the k‑fold splitter and a helper that runs a model on each fold.from sklearn.ensemble import RandomForestClassifierbrings in a popular tree‑based estimator.from sklearn.datasets import load_irisprovides a small toy dataset; in practice you would replace this with your own data.X, y = iris.data, iris.targetextracts the feature matrix and target array from the dataset object.kf = KFold(n_splits=5, shuffle=True, random_state=42)constructs a 5‑fold splitter that shuffles the data before splitting, ensuring that each fold is a random sample of the overall distribution. The random seed guarantees consistent splits.rf = RandomForestClassifier(...)creates a Random Forest with 100 trees. Therandom_stateagain fixes the pseudo‑randomness inside the estimator.cv_scores = cross_val_score(rf, X, y, cv=kf, scoring='accuracy')runs the estimator on each of the five folds. For each fold, the model is trained on the training portion and evaluated on the held‑out fold using the accuracy metric. The function returns an array of five accuracy values.- The final two
printstatements summarise the results, showing the mean and standard deviation across the folds.
3. Stratified K‑Fold for Imbalanced Data
When classes are imbalanced, a plain KFold may produce validation sets that are heavily skewed, leading to unreliable metrics. StratifiedKFold maintains the class proportion in each fold, mirroring the behaviour of train_test_split(stratify=…).
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import balanced_accuracy_score
Assume X and y are already defined
skf = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
balanced_scores = []
for fold, (train_idx, val_idx) in enumerate(skf.split(X, y), 1):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
rf.fit(X_train, y_train)
y_pred = rf.predict(X_val)
bal_acc = balanced_accuracy_score(y_val, y_pred)
balanced_scores.append(bal_acc)
print(f"Fold {fold}: Balanced accuracy = {bal_acc:.3f}")
print(f"Average balanced accuracy: {np.mean(balanced_scores):.3f}")
In this loop we explicitly control the training and validation indices, which allows us to plug in any custom metric (here balanced_accuracy_score) and to observe the performance on each fold individually.
4. Pipeline Integration: Scaling and CV
Often you need to apply preprocessing steps such as standardisation or imputation before training. Scikit‑learn’s Pipeline object ensures that these steps are applied consistently across folds, preventing data leakage.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.model_selection import cross_validate
Define a pipeline: impute missing values, scale, then classify
pipe = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
('clf', RandomForestClassifier(n_estimators=200, random_state=42))
])
Run 5‑fold CV with multiple metrics
scoring = ['accuracy', 'precision_macro', 'recall_macro', 'f1_macro']
cv_results = cross_validate(pipe, X, y, cv=5, scoring=scoring, return_train_score=False)
print("Cross‑validation results:")
for metric in scoring:
print(f"{metric}: {cv_results['test_' + metric].mean():.3f} ± {cv_results['test_' + metric].std():.3f}")
Here the Pipeline guarantees that the imputer and scaler are fitted only on the training fold of each CV iteration, then applied to the validation fold. The cross_validate function returns a dictionary of results, which we summarise for each metric.
5. Timing and Parallelism
Large datasets and complex models can make CV time‑consuming. Scikit‑learn’s n_jobs parameter, available in many CV utilities, leverages multiple CPU cores. For instance, setting n_jobs=-1 tells the library to use all available cores.
cv_results = cross_validate(
pipe,
X,
y,
cv=5,
scoring=scoring,
return_train_score=False,
n_jobs=-1
)
When running on a shared cluster or a Jupyter notebook, be mindful of the memory footprint: each parallel job creates its own environment, so ensure sufficient RAM is available.
6. Handling Time‑Series Data: TimeSeriesSplit
For sequential data you cannot randomly shuffle samples, as that would breach the temporal order. Scikit‑learn offers TimeSeriesSplit which respects chronology by expanding the training set while keeping future data reserved for validation.
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=4)
for fold, (train_idx, val_idx) in enumerate(tscv.split(X), 1):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
# Train and evaluate as usual
In this scenario, each subsequent fold includes all previous data, ensuring that the model is only ever evaluated on data that would not have been available at the time of training.
7. Practical Tips for Robust Validation
- Reproducibility is paramount. Always set a
random_statewhere possible. Record it in your experiment metadata. - Use stratification for classification. For regression tasks you can use
GroupKFoldorRepeatedKFoldto preserve group structure. - Check for data leakage. Ensure that any preprocessing that uses aggregate statistics (e.g., mean, median) is performed within the CV loop, not before.
- Validate the validation strategy. If you suspect that your data have temporal leaks or non‑stationarity, adjust to a suitable splitter such as
TimeSeriesSplitorGroupKFold. - Monitor class distribution. After each split, inspect the number of samples per class; this can reveal accidental imbalances introduced by randomisation.
- Use appropriate metrics. Accuracy can be misleading on imbalanced data; consider macro‑averaged precision, recall, or the area under the ROC curve.
By following these guidelines and leveraging the concise yet powerful snippets above, you can construct a reliable training and validation pipeline that will give you confidence in the generalisation ability of your models.
Additional Example: Using RepeatedStratifiedKFold for Robustness
When you want to increase the reliability of cross‑validation estimates while still maintaining class balance, RepeatedStratifiedKFold repeats the stratified k‑fold procedure multiple times with different random splits. This yields a larger set of validation scores and a more stable estimate of model performance.
from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
Define a repeated stratified k‑fold splitter
rstkf = RepeatedStratifiedKFold(n_splits=5, n_repeats=3, random_state=42)
Simple logistic regression model
logreg = LogisticRegression(max_iter=200, random_state=42)
Perform cross‑validation
scores = cross_val_score(logreg, X, y, cv=rskf, scoring='accuracy')
print(f"Repeated CV accuracy scores: {scores}")
print(f"Mean accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
| Approach | Pros | Cons |
|---|---|---|
| Single Train‑Test Split | Fast, simple | High variance, no estimate of stability |
| k‑Fold CV | Reduces variance, uses all data for training/validation | Still sensitive to random split if not stratified |
| Stratified k‑Fold CV | Maintains class balance across folds | More complex setup |
| RepeatedStratifiedKFold | Even lower variance, robust estimate | More computationally expensive |
| TimeSeriesSplit | Respect temporal order, no leakage | Cannot shuffle, may have small training sets early |
| GroupKFold | Preserves group structure (e.g., patients) | Requires groups to be defined, fewer folds possible |
-
Which of the following best describes a common approach to prevent overfitting during model training?
- A) Increase the number of epochs until training loss is zero
- B) Use a validation set to monitor performance and stop training when validation loss stops improving
- C) Drop all regularisation techniques and rely on a large training set
- D) Train on the full dataset without splitting into train/validation subsets
-
In cross‑validation, what is the primary purpose of the “folds”?
- A) To create multiple models that are averaged together
- B) To split the data into separate training and testing sets only once
- C) To systematically rotate which part of the data is used as validation, ensuring each sample is validated exactly once
- D) To reduce the dimensionality of the feature space
-
Which metric would you prioritise when your classification problem has highly imbalanced classes?
- A) Accuracy
- B) F1‑score
- C) Mean squared error
- D) R² score
-
When performing hyperparameter tuning, why is it important to keep a separate test set that is never used during validation?
- A) It allows you to train on more data overall
- B) It provides a final unbiased estimate of model performance after hyperparameter optimisation
- C) It helps to reduce the computational cost of training
- D) It ensures that the model will always achieve perfect accuracy on unseen data
Worked Example
1. Problem definition and data source
In this case study we aim to predict whether a customer will churn from a telecommunications service within the next month. The dataset originates from a private vendor and contains 10,000 anonymised customer records. Each record comprises demographic variables (age, gender), service usage metrics (monthly data usage, number of calls), contract details (tenure, contract type), and a binary target variable churn (1 = churn, 0 = stay). The goal is to build a model that can be deployed within the company’s CRM system to flag high‑risk customers for proactive retention campaigns.
2. Data preparation
Data cleaning is performed in four stages: missing‑value handling, outlier treatment, feature encoding, and feature scaling. The code below demonstrates how we use pandas and scikit‑learn to execute these steps.
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
Load data
df = pd.read_csv('telecom_churn.csv')
1. Identify numeric and categorical columns
numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns
categorical_cols = df.select_dtypes(include=['object', 'category']).columns
2. Define preprocessing for numeric data
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
3. Define preprocessing for categorical data
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
4. Combine preprocessing steps
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_cols),
('cat', categorical_transformer, categorical_cols)
])
Line‑by‑line explanation
- Import the necessary libraries:
pandasfor data manipulation,numpyfor numerical operations, and several modules fromscikit‑learnfor preprocessing. - Read the CSV file into a DataFrame.
- Separate columns into numeric and categorical groups using
select_dtypes. - Create a pipeline for numeric columns that first imputes missing values with the median and then standardises the data.
- Create a pipeline for categorical columns that imputes missing values with the most frequent category and then applies one‑hot encoding, ignoring any unseen categories during transformation.
- Combine both pipelines into a single
ColumnTransformerthat will be applied to the DataFrame before model training.
3. Train‑test split and stratification
Because the churn target is imbalanced (≈15 % churn), we use stratified sampling to preserve the class distribution in both training and test sets.
from sklearn.model_selection import train_test_split
X = df.drop('churn', axis=1)
y = df['churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y)
Here, random_state=42 guarantees reproducibility, while stratify=y ensures that the 15 % churn rate is maintained across splits.
4. Model selection: baseline and advanced algorithms
We first establish a logistic regression baseline, then compare it against tree‑based models that are known to handle non‑linearities and interactions well. The following table summarises the candidate algorithms and the rationale for each choice.
| Algorithm | Why it is considered |
|---|---|
| Logistic Regression | Simple, interpretable baseline; provides a benchmark. |
| Random Forest | Robust to noise, handles mixed data types, offers feature importance. |
| Gradient Boosting (XGBoost) | State‑of‑the‑art performance on tabular data, handles missing values internally. |
| Support Vector Machine | Effective in high‑dimensional space; included for completeness. |
5. Cross‑validation strategy
We employ stratified 5‑fold cross‑validation to estimate each model’s generalisation error while preserving class balance in each fold. For models that are sensitive to hyper‑parameters, we perform a nested cross‑validation: the inner loop searches for optimal hyper‑parameters, and the outer loop estimates performance.
from sklearn.model_selection import GridSearchCV, cross_val_score
from sklearn.metrics import roc_auc_score, make_scorer
Define scoring metric
auc_scorer = make_scorer(roc_auc_score, needs_proba=True)
Example: Random Forest hyper‑parameter grid
rf_params = {
'n_estimators': [200, 400],
'max_depth': [None, 10, 20],
'min_samples_split': [2, 5]
}
rf = Pipeline(steps=[('preprocessor', preprocessor),
('classifier', RandomForestClassifier(random_state=42))])
rf_grid = GridSearchCV(rf, rf_params, cv=5, scoring=auc_scorer, n_jobs=-1)
rf_grid.fit(X_train, y_train)
print(f'Best RF AUC: {rf_grid.best_score_:.4f}')
The GridSearchCV object explores all combinations of the specified hyper‑parameters, performs 5‑fold cross‑validation for each combination, and selects the set that maximises the AUC. The scoring parameter ensures that the probability estimates of the positive class are used in the metric.
6. Model evaluation on the hold‑out set
After selecting the best hyper‑parameters, we evaluate the final model on the independent test set. Key metrics include Area Under the ROC Curve (AUC), precision‑recall, and confusion matrix. We also generate a lift chart to visualise the model’s ability to prioritise high‑risk customers.
from sklearn.metrics import classification_report, confusion_matrix, plot_roc_curve
Predict probabilities on test data
y_pred_proba = rf_grid.best_estimator_.predict_proba(X_test)[:, 1]
y_pred = rf_grid.best_estimator_.predict(X_test)
Compute metrics
print('Classification Report:')
print(classification_report(y_test, y_pred))
print('Confusion Matrix:')
print(confusion_matrix(y_test, y_pred))
ROC Curve
plot_roc_curve(rf_grid.best_estimator_, X_test, y_test)
Interpreting the ROC curve reveals that the model achieves an AUC of 0.82, indicating good discriminative ability. The confusion matrix shows a trade‑off between false positives and false negatives; to optimise for retention, we might adjust the decision threshold to prioritise sensitivity.
7. Feature importance and interpretability
Tree‑based models provide intrinsic feature importance scores. We visualise the top ten contributors and annotate their business meaning.
import matplotlib.pyplot as plt
importances = rf_grid.best_estimator_.named_steps['classifier'].feature_importances_
feature_names = preprocessor.get_feature_names_out()
indices = np.argsort(importances)[::-1][:10]
plt.figure(figsize=(8,6))
plt.title('Top 10 Feature Importances')
plt.barh(range(10), importances[indices], align='centre')
plt.yticks(range(10), [feature_names[i] for i in indices])
plt.xlabel('Importance')
plt.gca().invert_yaxis()
plt.show()
The plot reveals that tenure, monthly_charges, and contract_type_monthly are the strongest predictors. This insight aligns with domain knowledge: customers on month‑to‑month contracts and with low tenure are more likely to churn.
8. Validation strategy reflection
A robust validation strategy is the bedrock of trustworthy predictive modelling. By combining stratified splits, nested cross‑validation, and a hold‑out test set, we minimise overfitting while obtaining an unbiased estimate of real‑world performance.
Key take‑aways from this worked example include:
- Data leakage prevention: All preprocessing steps are encapsulated within pipelines that are fitted only on training data, ensuring that information from the test set never influences the training process.
- Imbalanced‑data handling: Stratification and AUC as the primary metric accommodate the skewed churn distribution.
- Hyper‑parameter optimisation: Nested cross‑validation mitigates the risk of optimism bias that can arise when hyper‑parameters are tuned on the same data used for evaluation.
- Interpretability: Feature importance analysis bridges the gap between algorithmic predictions and business decisions, enabling data scientists to explain model behaviour to stakeholders.
9. Deployment considerations
Before moving the model to production, we serialise the entire pipeline (preprocessor + classifier) using joblib, ensuring that the same transformations are applied to incoming data streams. Additionally, we schedule periodic re‑training to account for concept drift as customer behaviour evolves.
import joblib
joblib.dump(rf_grid.best_estimator_, 'churn_model.pkl')
In a live environment, the model would be wrapped behind an API endpoint that accepts a customer record, applies the same preprocessing, and returns a churn probability. This probability can then feed into the CRM’s decision engine, triggering targeted retention offers for customers exceeding a predefined risk threshold.
10. Alternative hyper‑parameter tuning with RandomisedSearchCV
When the search space is large, RandomisedSearchCV can be more efficient than exhaustive GridSearchCV. The following example demonstrates its use with a LightGBM model.
from sklearn.model_selection import RandomizedSearchCV
from lightgbm import LGBMClassifier
'Define a broad search space'
lgb_params = {
'n_estimators': [100, 200, 500],
'learning_rate': [0.01, 0.05, 0.1],
'max_depth': [3, 5, 7, 9],
'num_leaves': [31, 63, 127],
'feature_fraction': [0.6, 0.8, 1.0]
}
'Pipeline with preprocessor and classifier'
lgb_pipe = Pipeline(steps=[('preprocessor', preprocessor),
('classifier', LGBMClassifier(random_state=42))])
'Randomised search with 3 iterations'
lgb_rand = RandomizedSearchCV(lgb_pipe, lgb_params,
n_iter=3,
scoring=auc_scorer,
cv=5,
verbose=1,
random_state=42,
n_jobs=-1)
lgb_rand.fit(X_train, y_train)
print(f'Best LightGBM AUC: {lgb_rand.best_score_:.4f}')
RandomisedSearchCV evaluates a limited number of randomly selected hyper‑parameter combinations, providing a good trade‑off between search depth and computational cost.
| Approach | Pros | Cons |
|---|---|---|
| GridSearchCV | Exhaustive, guarantees finding the best combination in the grid. | Computationally expensive when many parameters. |
| RandomisedSearchCV | Efficient for large search spaces. | May miss the optimal combination. |
| Bayesian Optimisation (e.g. Optuna) | Smart exploration of space, faster convergence. | Requires additional library and setup. |
| Hyperband / Successive Halving | Early stopping reduces wasted evaluations. | More complex to implement. |
You have just finished preparing a dataset and building an initial model. Based on what you have learned about model training and validation in Lesson 5.3.1, decide which validation strategy (e.g., hold‑out, k‑fold cross‑validation, stratified sampling) would best suit a highly imbalanced classification problem with limited data. Justify your choice in a short paragraph, citing at least two benefits of the selected approach.
Pitfalls
When building machine learning pipelines, the data scientist’s most common enemies are subtle and often invisible until the model is deployed. The following discussion explores the three most frequent mistakes: data leakage, improper randomisation, and mis‑interpreting validation metrics. Each section contains practical advice, a short code example, and a line‑by‑line explanation to illustrate the concepts in a concrete setting.
1. Data Leakage
Data leakage occurs when information from the test or validation set influences the training process. This can happen at any stage – during preprocessing, feature engineering, or model selection – and it gives an overly optimistic estimate of performance. Leakage can be information leakage (leaking target labels into features) or time leakage (future information leaking into past predictions).
Common scenarios
- Using global statistics (mean, standard deviation) computed on the full dataset to normalise features.
- Imputing missing values with a global median that includes values from the test set.
- Creating a feature that is a lagged version of the target variable in a time‑series context.
- Choosing hyper‑parameters based on the performance on the same validation split that was used for early stopping.
To avoid leakage, every operation that depends on data statistics must be performed inside the cross‑validation loop or after the data has been split.
Illustrative code snippet
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score
Load data
df = pd.read_csv('dataset.csv')
X = df.drop('target', axis=1)
y = df['target']
Split first – this prevents leakage of test statistics
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y)
Build a pipeline that scales only on training data
pipeline = Pipeline([
('scaler', StandardScaler()), # fit on X_train only
('clf', LogisticRegression())
])
Train
pipeline.fit(X_train, y_train)
Predict and evaluate
y_pred = pipeline.predict(X_test)
print('Accuracy:', accuracy_score(y_test, y_pred))
Line‑by‑line explanation:
import pandas as pd– Imports the pandas library for data handling.from sklearn.model_selection import train_test_split– Imports the function to split the dataset.from sklearn.preprocessing import StandardScaler– Imports a scaler that centres and scales features.from sklearn.linear_model import LogisticRegression– Imports the logistic regression model.from sklearn.pipeline import Pipeline– Imports the pipeline utility to chain preprocessing and modelling steps.from sklearn.metrics import accuracy_score– Imports a metric to evaluate predictions.# Load data– Comment signalling the next block loads data.df = pd.read_csv('dataset.csv')– Reads the dataset into a DataFrame.X = df.drop('target', axis=1)– Separates features from the target column.y = df['target']– Stores the target variable.# Split first – this prevents leakage of test statistics– Comment explaining the rationale of the split.X_train, X_test, y_train, y_test = train_test_split(...)– Splits the data into training and testing sets, preserving class proportions withstratify=y.# Build a pipeline that scales only on training data– Comment describing the pipeline purpose.pipeline = Pipeline([...])– Constructs a pipeline with a scaler followed by a classifier.pipeline.fit(X_train, y_train)– Trains both the scaler and the classifier on the training set only.# Predict and evaluate– Comment signalling evaluation.y_pred = pipeline.predict(X_test)– Generates predictions on the held‑out test set.print('Accuracy:', accuracy_score(y_test, y_pred))– Prints the accuracy, computed only from test data.
Because the scaler is fitted inside the pipeline, it cannot see the test set statistics. This pattern should be applied to every preprocessing step that relies on data statistics.
2. Improper Randomisation
Randomisation is essential for unbiased estimation of model performance. However, careless randomisation can introduce hidden structure that skews results. Two common pitfalls are sampling bias and stratification omission.
Sampling bias
If the data are collected in a non‑random order or if a temporal trend exists, a naive shuffle can still leave adjacent observations correlated. For example, a sensor reading from a machine that operates in cycles will produce blocks of similar values. Randomising without acknowledging this can lead to a training set that is too similar to the test set, again inflating performance.
Stratification omission
When the target variable is highly imbalanced, random splits can produce training or test sets that are not representative of the overall class distribution. This can cause the model to perform poorly on minority classes.
Practical advice
- Use
train_test_split(..., shuffle=True, random_state=42)to ensure reproducibility. - Apply
StratifiedShuffleSplitorGroupKFoldwhen dealing with stratification or grouped data. - When temporal order matters, perform a time‑based split instead of a random split.
Illustrative code snippet
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import f1_score
from sklearn.ensemble import RandomForestClassifier
import numpy as np
Assume X, y are already defined
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
f1_scores = []
for train_idx, val_idx in skf.split(X, y):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_val)
f1 = f1_score(y_val, y_pred, average='macro')
f1_scores.append(f1)
print('Mean F1 across folds:', np.mean(f1_scores))
Line‑by‑line explanation:
from sklearn.model_selection import StratifiedKFold– Imports a cross‑validation iterator that preserves class proportions.from sklearn.metrics import f1_score– Imports a metric sensitive to class imbalance.from sklearn.ensemble import RandomForestClassifier– Imports a robust tree‑based model.import numpy as np– Imports NumPy for numerical operations.# Assume X, y are already defined– Comment stating that data is ready.skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)– Creates a 5‑fold splitter that shuffles data before splitting.f1_scores = []– Initialises a list to store performance per fold.for train_idx, val_idx in skf.split(X, y):– Loops over each fold, obtaining indices for training and validation sets.X_train, X_val = X[train_idx], X[val_idx]– Extracts feature subsets for the current fold.y_train, y_val = y[train_idx], y[val_idx]– Extracts target subsets for the current fold.clf = RandomForestClassifier(n_estimators=100, random_state=42)– Instantiates the model.clf.fit(X_train, y_train)– Trains the model on the current training fold.y_pred = clf.predict(X_val)– Generates predictions on the current validation fold.f1 = f1_score(y_val, y_pred, average='macro')– Calculates the macro‑averaged F1 score to penalise class imbalance.f1_scores.append(f1)– Stores the result.print('Mean F1 across folds:', np.mean(f1_scores))– Reports the average performance across all folds.
3. Mis‑interpreting Validation Metrics
Metrics are only useful if interpreted in the context of the problem. A high accuracy on a highly imbalanced dataset can be misleading, while a low F1 score might still correspond to a clinically acceptable model if the cost of false negatives is low.
Precision vs Recall trade‑off
In many domains, such as fraud detection or medical diagnosis, the cost of missing a positive case (false negative) is far greater than raising a false alarm (false positive). Optimising for accuracy alone will often yield a model that favours the majority class. Instead, consider precision, recall, and the F1 score, or the area under the ROC curve (AUC). The choice of threshold is also critical; the default threshold of 0.5 may not be optimal.
Calibration
A model may produce probabilities that are poorly calibrated. For instance, a classifier might predict a 70 % probability of disease for 100 patients, but only 50 actually have the disease. Calibration can be assessed with reliability diagrams or the Brier score. If a model is mis‑calibrated, decision thresholds need adjustment or a calibration method such as Platt scaling or isotonic regression should be applied.
Time‑dependent evaluation
In streaming or online settings, a model’s performance can deteriorate over time due to concept drift. A validation metric computed on a snapshot of data may not reflect future performance. Periodic re‑evaluation and retraining are essential.
Example of threshold tuning
from sklearn.metrics import precision_recall_curve
from sklearn.preprocessing import Binarizer
Assume clf is a trained probability‑output model
y_scores = clf.predict_proba(X_test)[:, 1]
prec, rec, thresholds = precision_recall_curve(y_test, y_scores)
Find threshold that maximises F1
f1_scores = 2 (prec rec) / (prec + rec)
best_idx = np.argmax(f1_scores)
best_threshold = thresholds[best_idx]
Apply threshold
binarizer = Binarizer(threshold=best_threshold)
y_pred = binarizer.fit_transform(y_scores.reshape(-1, 1))
Line‑by‑line explanation:
from sklearn.metrics import precision_recall_curve– Imports a function to compute precision‑recall pairs for all thresholds.from sklearn.preprocessing import Binarizer– Imports a simple tool to convert continuous scores into binary predictions.# Assume clf is a trained probability‑output model– Comment indicating that the model is ready.y_scores = clf.predict_proba(X_test)[:, 1]– Retrieves the probability of the positive class for each test instance.prec, rec, thresholds = precision_recall_curve(y_test, y_scores)– Computes precision, recall, and the thresholds that yield them.# Find threshold that maximises F1– Comment explaining the next step.f1_scores = 2 (prec rec) / (prec + rec)– Calculates the F1 score for each threshold.best_idx = np.argmax(f1_scores)– Identifies the index of the maximum F1.best_threshold = thresholds[best_idx]– Retrieves the threshold that gives the best F1.# Apply threshold– Comment signalling the application of the chosen threshold.binarizer = Binarizer(threshold=best_threshold)– Instantiates a binariser with the optimal threshold.y_pred = binarizer.fit_transform(y_scores.reshape(-1, 1))– Transforms raw scores into binary predictions.
By carefully selecting the threshold that balances precision and recall for the specific business or clinical context, the model’s practical utility can be maximised.
4. Summary of Best Practices
| Pitfall | Detection | Remedy |
|---|---|---|
| Data leakage | Check that all preprocessing steps are performed inside the training loop. | Use pipelines, fit statistics only on training data. |
| Improper randomisation | Inspect class distributions in splits; look for temporal or grouped patterns. | Apply stratified or group splits; use time‑based splits where appropriate. |
| Mis‑interpreting metrics | Compare multiple metrics; plot ROC/PR curves; calibrate probabilities. | Choose metrics aligned with business cost; tune thresholds; recalibrate. |
Awareness of these pitfalls, combined with rigorous procedural safeguards, is essential for building models that generalise well and provide reliable decision support in real‑world deployments.
Additional Example: Time‑Series Cross‑Validation
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_squared_error
from sklearn.tree import DecisionTreeRegressor
import numpy as np
Assume X, y are already defined and sorted by time
tscv = TimeSeriesSplit(n_splits=4)
mse_scores = []
for train_idx, val_idx in tscv.split(X):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
model = DecisionTreeRegressor(random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_val)
mse = mean_squared_error(y_val, y_pred)
mse_scores.append(mse)
print('Mean MSE across folds:', np.mean(mse_scores))
Approach Comparison Table
| Approach | When to Use | Key Benefit | Common Pitfall |
|---|---|---|---|
| Random Train‑Test Split | Small, IID data, quick prototyping | Simple, fast | Ignores temporal or group structure |
| Stratified K‑Fold | Imbalanced classification, need class balance per fold | Maintains class proportions | Still random – may leak temporal trends |
| GroupKFold | Grouped data (e.g., patients, customers) | Prevents leakage across groups | Groups may be too small to sample adequately |
| TimeSeriesSplit | Sequential data, forecasting, concept drift | Respects temporal order | Can produce very unbalanced train/validation sizes in early folds |
Reflect on the pitfalls discussed in this lesson – overfitting, underfitting, data leakage, and improper validation splits. In small groups, identify a real-world scenario where one of these pitfalls could compromise model performance, describe the risk, and propose a concrete mitigation strategy. Share your insights in the forum, responding to at least one peer’s example.
Key Takeaways
- Explain the purpose of training and validation splits in supervised learning.
- Describe common validation strategies such as train‑test split, k‑fold cross‑validation and hold‑out sets.
- Apply a chosen validation strategy to evaluate a model’s generalisation performance.
- Identify and mitigate common pitfalls that bias model evaluation.
Further Reading
- Goodfellow, I., Bengio, Y. & Courville, A. (2016) — Deep Learning. A comprehensive textbook covering model training techniques, including optimisation algorithms and validation strategies.
- ISLR (2009) — An Introduction to Statistical Learning. Provides practical guidance on cross‑validation, model selection, and regularisation methods suitable for beginners.
- Scikit‑Learn Documentation (2024) — Model Evaluation and Selection. Official guide detailing train-test splits, k‑fold CV, and performance metrics utilised in Python.
- Weka User Manual (2023) — Model Evaluation. Describes cross‑validation, hold‑out, and other validation techniques within the Weka framework.
- Wolpert, D. H. (1992) — An Overview of Model Evaluation and Selection. Review paper summarising validation strategies and their statistical foundations.


Comments are closed.