Pythonholics Learning Hub

Learn Python, Machine Learning, and Scientific Computing Faster

Step-by-step tutorials, complete code examples, practical projects, and AFAP books for students, engineers, and researchers.

Python Basics

Start with clean beginner-friendly tutorials and build confidence through small examples.

Open path →

Machine Learning

Learn scikit-learn, classification, regression, metrics, and practical model workflows.

Open tutorials →

AFAP Book Series

Follow the As Fast As Possible book series for structured Python and ML learning.

View books →

Sunday, August 30, 2026

Cross-Validation in scikit-learn: Complete Guide

Cross-validation estimates how well a machine-learning model will perform on unseen data by training and evaluating it across several data splits. This guide explains K-fold, stratified, grouped, repeated, and time-series cross-validation in scikit-learn, with reproducible Python examples and practical rules for avoiding data leakage.

Why Use Cross-Validation?

A single train/test split can produce a misleading result. The measured score depends on which observations happen to enter the test set. An easy split may make a weak model look strong, while a difficult split may hide the value of a good model.

Cross-validation reduces this dependence on one split. In five-fold cross-validation, the data is divided into five parts. The model trains on four folds and validates on the remaining fold. This process repeats until every fold has served as validation data once.

  1. Divide the development data into k folds.
  2. Train on k − 1 folds.
  3. Evaluate on the remaining fold.
  4. Repeat until every fold has been used for validation.
  5. Report the mean score and its variability across folds.
Cross-validation does not replace a final test set. Use cross-validation to compare models, preprocessing, and hyperparameters. Keep an untouched test set for one final evaluation after all important decisions are complete.

Quick Regression Example with KFold

The example below evaluates Ridge regression on the diabetes dataset. Preprocessing is placed inside a scikit-learn Pipeline, ensuring that scaling is fitted separately within every training fold.

import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.linear_model import Ridge
from sklearn.model_selection import KFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_diabetes(return_X_y=True)

model = Pipeline([
    ("scale", StandardScaler()),
    ("ridge", Ridge(alpha=1.0))
])

cv = KFold(
    n_splits=5,
    shuffle=True,
    random_state=42
)

scores = cross_val_score(
    model,
    X,
    y,
    cv=cv,
    scoring="neg_mean_absolute_error"
)

mae = -scores

print("MAE by fold:", mae)
print("Mean MAE:", mae.mean())
print("MAE standard deviation:", mae.std())

With this configuration, the mean absolute error is approximately 44.24. Your goal is not to memorize that number, but to understand that every observation is evaluated by a model that was not trained on that observation.

Why Are Error Scores Negative?

Scikit-learn follows a “higher is better” convention for scorer objects. Losses such as MAE and mean squared error are therefore returned as negative values. Multiply the scores by −1 before reporting them as familiar positive errors.

rmse_scores = cross_val_score(
    model,
    X,
    y,
    cv=cv,
    scoring="neg_root_mean_squared_error"
)

rmse_scores = -rmse_scores
print("Mean RMSE:", rmse_scores.mean())

Classification with StratifiedKFold

Ordinary K-fold splitting does not explicitly preserve class proportions. For binary or multiclass classification, StratifiedKFold attempts to keep approximately the same class distribution in every fold.

from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_validate
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_breast_cancer(return_X_y=True)

model = Pipeline([
    ("scale", StandardScaler()),
    ("logistic", LogisticRegression(max_iter=2000))
])

cv = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=42
)

results = cross_validate(
    model,
    X,
    y,
    cv=cv,
    scoring=["accuracy", "balanced_accuracy", "roc_auc"],
    n_jobs=-1,
    return_train_score=True
)

for metric in ["accuracy", "balanced_accuracy", "roc_auc"]:
    values = results[f"test_{metric}"]
    print(metric, values.mean(), values.std())

cross_validate() is useful when you need several metrics, training scores, fit times, or scoring times. For an in-depth discussion of metric selection, read Accuracy vs Precision vs Recall vs F1 vs ROC AUC.

Choosing the Correct Cross-Validation Splitter

SplitterUse it whenMain protection
KFoldRegression or approximately independent observationsEvery sample is validated once
StratifiedKFoldBinary or multiclass classificationPreserves class proportions
RepeatedKFoldYou want a more stable estimate across multiple random partitionsReduces dependence on one fold assignment
RepeatedStratifiedKFoldClassification with repeated partitionsRepetition plus class balance
GroupKFoldMultiple rows belong to the same person, machine, site, or experimentKeeps groups from crossing train and validation sets
StratifiedGroupKFoldGrouped classification with imbalanced classesNon-overlapping groups plus approximate class balance
TimeSeriesSplitChronologically ordered observationsPrevents training on future data and testing on the past
LeaveOneOutVery small datasets where computational cost is acceptableUses all but one sample for each training run

Cross-Validation and Data Leakage

Cross-validation is only trustworthy when every learned preprocessing step is fitted inside each training fold. Leakage occurs if information from a validation fold influences scaling, imputation, feature selection, encoding, PCA, resampling, or target-based transformations.

Wrong approach

# Do not scale the full dataset before cross-validation.
X_scaled = StandardScaler().fit_transform(X)

scores = cross_val_score(
    LogisticRegression(),
    X_scaled,
    y,
    cv=5
)

Correct approach

model = Pipeline([
    ("scale", StandardScaler()),
    ("logistic", LogisticRegression(max_iter=2000))
])

scores = cross_val_score(
    model,
    X,
    y,
    cv=cv,
    scoring="roc_auc"
)
Rule: if a step learns anything from the data, put it inside the pipeline before cross-validation. This applies even when the transformation appears simple.

Grouped Cross-Validation

Random splitting is invalid when several rows come from the same entity. For example, repeated measurements from one patient, turbine, building, or manufacturing batch are correlated. If one entity appears in both training and validation data, performance can look unrealistically high.

import numpy as np
from sklearn.model_selection import GroupKFold

X = np.arange(200).reshape(100, 2)
y = np.arange(100) % 2

# Ten measurements for each of ten subjects.
groups = np.repeat(np.arange(10), 10)

cv = GroupKFold(n_splits=5)

for fold, (train_idx, valid_idx) in enumerate(
    cv.split(X, y, groups=groups),
    start=1
):
    train_groups = set(groups[train_idx])
    valid_groups = set(groups[valid_idx])

    print(f"Fold {fold}")
    print("Shared groups:", train_groups & valid_groups)

The set of shared groups should be empty in every fold. For grouped classification where class balance also matters, consider StratifiedGroupKFold.

Time-Series Cross-Validation

Random K-fold cross-validation usually breaks chronology. It can train on future observations and validate on earlier observations, which creates temporal leakage. TimeSeriesSplit uses earlier data for training and later data for validation.

import numpy as np
from sklearn.model_selection import TimeSeriesSplit

X = np.arange(40).reshape(20, 2)

cv = TimeSeriesSplit(
    n_splits=4,
    gap=1
)

for fold, (train_idx, valid_idx) in enumerate(
    cv.split(X),
    start=1
):
    print(
        f"Fold {fold}:",
        f"train {train_idx[0]}-{train_idx[-1]}",
        f"validation {valid_idx[0]}-{valid_idx[-1]}"
    )

The optional gap excludes observations between the end of training and the start of validation. This is useful when features or labels are influenced by nearby time periods.

Out-of-Fold Predictions with cross_val_predict

cross_val_predict() returns one prediction for every observation from a model that did not train on that observation. These out-of-fold predictions are valuable for diagnostic plots, confusion matrices, and stacking.

from sklearn.metrics import confusion_matrix
from sklearn.model_selection import cross_val_predict

y_pred_oof = cross_val_predict(
    model,
    X,
    y,
    cv=cv,
    n_jobs=-1
)

cm = confusion_matrix(y, y_pred_oof)
print(cm)

For binary or multiclass interpretation and visualization, continue with the Confusion Matrix in Python complete guide.

Be careful: use a classification pipeline and a classification splitter with the classification example above. The variables model, X, y, and cv should refer to the same classification workflow.

How Many Folds Should You Use?

ChoiceAdvantageTrade-off
5 foldsGood general-purpose balance of stability and speedEach model trains on 80% of the data
10 foldsMore training data in each runApproximately twice as many fits as five-fold CV
Repeated 5-foldMeasures sensitivity to several random partitionsCan become computationally expensive
Leave-one-outMaximum training data per splitVery expensive and often high-variance

Five-fold cross-validation is a strong default for many tabular machine-learning problems. Increase the number of folds only when the dataset, computational budget, and evaluation objective justify it.

Reproducibility and Shuffling

For independent tabular observations, using shuffle=True prevents fold quality from depending on the original row order. Set random_state to an integer so the split can be reproduced.

cv = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=42
)

Do not shuffle time-series data. For grouped data, the group constraint matters more than random row order.

Parallel Cross-Validation

Many scikit-learn validation functions accept n_jobs. Setting n_jobs=-1 allows all available CPU cores to process independent fits, subject to memory limits and the estimator's own parallelism.

results = cross_validate(
    model,
    X,
    y,
    cv=cv,
    scoring=["accuracy", "roc_auc"],
    n_jobs=-1
)

Avoid uncontrolled nested parallelism. If both cross-validation and the estimator use every CPU core, the machine may create too many workers. In that case, parallelize one level and limit the other.

Cross-Validation for Hyperparameter Tuning

GridSearchCV and RandomizedSearchCV evaluate candidate hyperparameters using cross-validation. Pass the correct splitter to the cv parameter and keep preprocessing inside the searched pipeline.

from sklearn.model_selection import GridSearchCV

search = GridSearchCV(
    estimator=model,
    param_grid={
        "logistic__C": [0.01, 0.1, 1.0, 10.0]
    },
    scoring="roc_auc",
    cv=cv,
    n_jobs=-1
)

search.fit(X, y)

print("Best parameters:", search.best_params_)
print("Best CV score:", search.best_score_)

When you report an unbiased estimate after intensive tuning, use a separate test set or nested cross-validation. The inner loop chooses hyperparameters; the outer loop estimates performance.

Common Cross-Validation Mistakes

  1. Preprocessing the full dataset before splitting. This leaks validation information.
  2. Using KFold for time series. Random or ordinary folds can train on the future.
  3. Splitting repeated entities across folds. Use grouped validation.
  4. Optimizing on the final test set. The test set must remain untouched.
  5. Using the wrong metric. Accuracy may be misleading for imbalanced classes.
  6. Reporting only the mean. Include standard deviation or fold-level results.
  7. Ignoring failed fits. Investigate warnings and use error_score="raise" during debugging.
  8. Comparing models on different folds. Use the same splitter and random seed for fair comparison.
  9. Oversampling before cross-validation. Resampling must happen inside each training fold.
  10. Creating too much parallelism. Coordinate n_jobs across validation and estimator levels.

Practical Checklist

  • Identify whether observations are independent, grouped, or time-ordered.
  • Select the splitter that matches the data-generating process.
  • Place all learned preprocessing inside a pipeline.
  • Choose metrics before comparing models.
  • Use identical folds for fair model comparison.
  • Report mean, variability, and individual fold results.
  • Reserve a final test set whenever possible.
  • Record the splitter, fold count, seed, scoring, and library version.

Frequently Asked Questions

Is five-fold cross-validation enough?

It is a strong default for many tabular problems. Repeated validation can improve stability when the dataset is small or results are sensitive to the split.

Should I stratify regression data?

Standard StratifiedKFold is designed for class labels. For regression, use KFold unless the data requires grouped or temporal splitting.

Can cross-validation overfit?

Yes. Repeatedly choosing models and hyperparameters based on the same cross-validation results can overfit the validation process. Use a final test set or nested cross-validation for a less biased estimate.

Can I use cross-validation with Random Forest?

Yes. The same principles apply. Compare ensemble methods under identical folds using the workflow in Random Forest vs Gradient Boosting in Python.

Confusion Matrix in Python: Complete Guide

A confusion matrix shows exactly how a classification model is right and wrong. Instead of reducing performance to one score, it counts correct predictions and each type of mistake. In this guide, you will build, plot, normalize, and interpret confusion matrices in Python with scikit-learn.

What Is a Confusion Matrix?

A confusion matrix compares the true class of every observation with the class predicted by a model. Rows represent actual classes and columns represent predicted classes in scikit-learn. Correct predictions appear on the main diagonal, while errors appear outside that diagonal.

Predicted NegativePredicted Positive
Actual NegativeTrue Negative (TN)False Positive (FP)
Actual PositiveFalse Negative (FN)True Positive (TP)
  • True negative: the observation is negative and the model predicts negative.
  • False positive: the observation is negative but the model predicts positive.
  • False negative: the observation is positive but the model predicts negative.
  • True positive: the observation is positive and the model predicts positive.
Important: the meaning of positive and negative depends on your problem. In fraud detection, positive may mean fraud. In medical screening, positive may mean that a condition is present. Define the positive class before interpreting the matrix.

Complete Python Example

The following reproducible example trains logistic regression on scikit-learn's breast cancer dataset. Scaling and the classifier are placed in a scikit-learn Pipeline so preprocessing is learned only from the training data.

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix

X, y = load_breast_cancer(return_X_y=True)

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.20,
    stratify=y,
    random_state=42
)

model = Pipeline([
    ("scale", StandardScaler()),
    ("model", LogisticRegression(max_iter=2000))
])

model.fit(X_train, y_train)
y_pred = model.predict(X_test)

cm = confusion_matrix(y_test, y_pred)
print(cm)

With the specified random seed, this example produces:

[[41  1]
 [ 1 71]]

The model correctly classified 41 negative examples and 71 positive examples. It produced one false positive and one false negative. Keep the class order in mind: by default, scikit-learn sorts labels and uses that order for both rows and columns.

Extract TN, FP, FN, and TP

For a binary 2 × 2 matrix, use ravel() to unpack the four counts:

tn, fp, fn, tp = cm.ravel()

print("True negatives:", tn)
print("False positives:", fp)
print("False negatives:", fn)
print("True positives:", tp)

This shortcut is for binary classification. A multiclass matrix contains more than four cells, so it cannot be unpacked into only TN, FP, FN, and TP.

Plot a Confusion Matrix

ConfusionMatrixDisplay.from_predictions() creates a labeled Matplotlib visualization directly from the true and predicted targets:

import matplotlib.pyplot as plt
from sklearn.metrics import ConfusionMatrixDisplay

ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred,
    display_labels=["malignant", "benign"],
    cmap="Blues",
    values_format="d"
)

plt.title("Confusion Matrix")
plt.tight_layout()
plt.show()

A strong model has large values on the diagonal and small values outside it. However, the cost of each off-diagonal error may be very different. One false negative can matter more than many false positives in a high-risk screening problem.

Normalize the Matrix

Raw counts are useful, but they can be misleading when classes contain very different numbers of observations. Row normalization shows the proportion of each actual class assigned to every predicted class.

ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred,
    display_labels=["malignant", "benign"],
    normalize="true",
    cmap="Blues",
    values_format=".2f"
)

plt.title("Normalized Confusion Matrix")
plt.tight_layout()
plt.show()
normalize valueMeaningUseful for
NoneRaw countsUnderstanding the number of cases
"true"Normalize each actual-class rowComparing class-specific recall
"pred"Normalize each predicted-class columnUnderstanding prediction composition
"all"Divide by the entire sample countViewing each cell's share of all cases

Calculate Accuracy, Precision, Recall, and F1

The four matrix counts generate several familiar classification metrics:

Accuracy = (TP + TN) / (TP + TN + FP + FN)

Precision = TP / (TP + FP)

Recall = TP / (TP + FN)

F1 score = 2 × Precision × Recall / (Precision + Recall)

from sklearn.metrics import (
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    classification_report
)

print("Accuracy:", accuracy_score(y_test, y_pred))
print("Precision:", precision_score(y_test, y_pred))
print("Recall:", recall_score(y_test, y_pred))
print("F1:", f1_score(y_test, y_pred))
print(classification_report(y_test, y_pred))

For a deeper comparison of these scores, thresholds, and imbalanced-data behavior, read Accuracy vs Precision vs Recall vs F1 vs ROC AUC.

Confusion Matrix for Multiclass Classification

In multiclass classification, the matrix has one row and one column for every class. The diagonal still contains correct predictions. Each off-diagonal cell shows one specific confusion, such as actual class A predicted as class B.

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

labels = ["cat", "dog", "rabbit"]
y_true = ["cat", "cat", "dog", "dog", "rabbit", "rabbit"]
y_pred = ["cat", "dog", "dog", "dog", "rabbit", "cat"]

cm = confusion_matrix(y_true, y_pred, labels=labels)
print(cm)

ConfusionMatrixDisplay.from_predictions(
    y_true,
    y_pred,
    labels=labels,
    display_labels=labels,
    cmap="Purples"
)

Pass an explicit labels order when a stable row and column order matters. This is particularly helpful when comparing matrices across several models or data splits.

How the Decision Threshold Changes the Matrix

Many binary classifiers first produce a probability or decision score and then convert it into a class. A threshold of 0.50 is common, but it is not automatically the best business threshold.

y_score = model.predict_proba(X_test)[:, 1]

threshold = 0.35
y_pred_custom = (y_score >= threshold).astype(int)

print(confusion_matrix(y_test, y_pred_custom))

Lowering the threshold usually predicts more positives. This can reduce false negatives and increase recall, but may also create more false positives and lower precision. Choose the threshold using validation data and a clearly defined cost for each error type.

Practical rule: never tune a decision threshold on the final test set. Select the model and threshold using training and validation data, then use the untouched test set once for final evaluation.

Common Confusion Matrix Mistakes

  1. Reversing axes: confirm whether rows are actual or predicted. In scikit-learn, rows are true classes and columns are predicted classes.
  2. Assuming class 1 is always the important class: inspect the target definition and the label order.
  3. Reporting only accuracy: two models with equal accuracy can make very different and differently costly errors.
  4. Ignoring class imbalance: include a normalized matrix and class-specific precision and recall.
  5. Evaluating training data: use held-out data or out-of-fold predictions.
  6. Tuning on the test set: this leaks information and makes reported performance too optimistic.
  7. Comparing raw counts across different sample sizes: compare normalized matrices or rates.

Which Errors Should You Minimize?

SituationOften more costlyMetric to watch
Disease screeningFalse negativeRecall / sensitivity
Spam filter for important mailFalse positivePrecision or specificity
Fraud detectionDepends on fraud loss and investigation costPrecision-recall trade-off
Balanced classes with similar error costsBothAccuracy and macro metrics

The correct target is determined by the real-world cost of a mistake, not by a universal rule. Translate each matrix cell into an operational consequence before selecting a model.

Frequently Asked Questions

Is a confusion matrix only for binary classification?

No. It supports binary and multiclass classification. Multilabel problems can use a separate 2 × 2 matrix for each label with multilabel_confusion_matrix.

Why does my confusion matrix look good while the model is poor?

Raw counts can hide poor minority-class performance. Normalize the matrix, inspect per-class recall and precision, and compare against a simple baseline.

Should I use training or test predictions?

Use unseen validation or test predictions for evaluation. A training-set matrix mainly tells you how well the model fits data it has already seen.

Can I compare confusion matrices from different models?

Yes, provided they use the same evaluation observations, class order, and decision rules. Normalized matrices make comparisons easier when sample counts differ.