TunedThresholdClassifierCV: the good, the bad, the ugly


Welcome!

Every Monday, I’ll drop a no-fluff, straight-to-the-point tip on a data science skill, tool, or
method to help you stay sharp in the field. I hope you find it useful!


TunedThresholdClassifierCV: the good, the bad, the ugly

For years, scikit-learn classifiers made one decision for us: predict() converted probabilities into class labels using a de facto cut-off of 0.5. That number seems obvious from stats classes. But in practice, it’s rarely the right one.

A threshold is not a property of the data or the model. It is a decision. And the best decision depends on what we are trying to achieve.

Why adjust the threshold?

Suppose a model estimates that a patient has a 20% probability of disease. Should we classify the patient as positive?

If the next step is an inexpensive screening test, perhaps yes. If the next step is risky surgery, almost certainly not. Based on the same probability, we make different decisions based on the cost. In other words, the cost determines the threshold. Not the model.

The same applies when we optimise a classification metric. Precision, recall, F1-score, balanced accuracy and the entries of the confusion matrix all depend on the cut-off used to convert a continuous model output into a class label. Lowering the threshold usually identifies more positives, but it also creates more false positives. Raising it does the opposite.

There is no reason to expect 0.5 to produce the trade-off we want. In my experiments for Imbalanced Data: Myths, Mistakes and Modern Solutions, the thresholds that maximised balanced accuracy were sometimes close to 0.005. If I had used the de facto threshold of 0.5, I would have underestimated the models and, in some cases, selected the wrong one.

This also explains part of the apparent success of resampling and class weighting. Both can shift the decision boundary towards 0.5. But we can often reach the same operating point by training on the original distribution and adjusting the threshold.

For a long time, predict() meant 0.5

When using scikit-learn, we could always call predict_proba(), apply our own threshold and calculate the metric manually. I know I did this. But this separated the decision rule from the estimator and made the whole process inconvenient.

Scikit-learn changed that with TunedThresholdClassifierCV. The class wraps a binary classifier, evaluates candidate thresholds by cross-validation and selects the one that maximises the metric we provide. After fitting, predict() uses the tuned threshold instead of the de facto 0.5

TunedThresholdClassifierCV in action

Let’s take a look at how TunedThresholdClassifierCV works. We assume we have some data split over a training and a testing set. We train a gradient boosting classifier, and then we want to evaluate the balanced accuracy at the optimal cut-off point. We’d do the following:

from sklearn.ensemble import GradientBoostingClassifier

from sklearn.model_selection import TunedThresholdClassifierCV


model = TunedThresholdClassifierCV(

GradientBoostingClassifier(random_state=0),

scoring="balanced_accuracy",

cv=5,

)

model.fit(X_train, y_train)

print(model.best_threshold_)

y_pred = model.predict(X_test)


model.best_threshold_ will show you the value of the optimal cut-off, and model.predict() assigns observations to classes based on that value.

The model and its decision rule now are shipped together, and we can pass it to hyperparameter tuning or cross-validation functions as we would do for any model.

The good

TunedThresholdClassifierCV solves a real and surprisingly common problem. It makes threshold optimisation part of a scikit-learn workflow. It uses cross-validation by default, accepts scikit-learn scorers and custom business metrics, and exposes the selected value through best_threshold_.

Most importantly, it reminds us that model training and decision-making are different tasks. The classifier estimates a score or probability. The threshold turns that estimate into a class.

The bad

The fitted object returns one best threshold. What it does not give us directly is the dispersion of that threshold across different samples.

That matters because the optimum is an estimate. If we change the observations in a validation fold, the best threshold can change.

And the ugly

By default, TunedThresholdClassifierCV evaluates 100 equi-distant thresholds. That sounds generous until we work with highly imbalanced data.

With imbalance datasets, useful thresholds can accumulate in a tiny region near zero. A grid spread across the full range may then place too few candidates where the optimum actually lies, and we may miss it.

The good thing however, is that we can manually pass hand-picked intervals of thresholds to TunedThresholdClassifierCV to explore the areas we think are more valuable.

Before you go

There is one more thing to consider. TunedThresholdClassifierCV will optimise the metric we ask it to optimise. For a different metric, we’ll need a different threshold, guaranteed.

My recommendation

Use TunedThresholdClassifierCV. It is a welcome addition, and for most projects it is a much better starting point than predict() at 0.5.

If you liked this article, you’ll also enjoy my new book: Imbalanced Data: Myths, Mistakes and Modern Solutions.

I hope this information was useful!

Wishing you a successful week ahead - see you next Monday! 👋🏻

Sole


Ready to enhance your skills?

Our specializations, courses and books are here to assist you:


Hi…I’m Sole
I’m a Python developer, AI educator, and developer advocate. I’m the creator and maintainer of Feature-engine and the main instructor at Train in Data.
I share practical Python lessons, open-source insights, and ideas to help our community build better software and grow as developers and data scientists.

You are receiving this email because you subscribed to our newsletter, signed up on our website, purchased or downloaded any products from us.


Follow us on social media

Copyright (C) 2026 Train in Data. All rights reserved.


Unsubscribe · Preferences

Train in Data

by Soledad Galli, PhD | Data scientist | Python Developer | Best-selling instructor | Book author | 👉 www.trainindata.com

Read more from Train in Data
Why optimise for calibration if I can recalibrate later?

Welcome! Every Monday, I’ll drop a no-fluff, straight-to-the-point tip on a data science skill, tool, ormethod to help you stay sharp in the field. I hope you find it useful! Machine Learning Interpretability course has been updated! I’ve recently finished updating my Machine Learning Interpretability course. 🎉 I’ve refreshed the course notebooks, replacing outdated libraries with practical alternatives and adding fixes to keep examples running as Python evolves. You’ll explore the same...

Undersampling does not improve model performance

Welcome! Every Monday, I’ll drop a no-fluff, straight-to-the-point tip on a data science skill, tool, ormethod to help you stay sharp in the field. I hope you find it useful! My Hyperparameter Optimization Course Just Got a Major Update! I’ve released a completely updated version of my course, now called Master Hyperparameter Optimization for Tabular Learning. It focuses on optimizing today’s leading tabular models using the tools and techniques the machine-learning community relies on,...

Class imbalance makes metrics volatile

Welcome! Every Monday, I’ll drop a no-fluff, straight-to-the-point tip on a data science skill, tool, ormethod to help you stay sharp in the field. I hope you find it useful! Have you added it to your reading list yet? Imbalanced Data, Myths, Mistakes and Modern Solutions Imbalanced data isn't a problem. How you handle it is. My book, Imbalanced Data: Myths, Mistakes and Modern Solutions, cuts through the common misconceptions and brings together practical, evidence-based approaches you can...