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
AI-generated pull requests won't land you that data science job

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! Three proof copies of my books looking for a new home While I was working on making the physical copies of my books available through different providers, I ended up with three proof copies. They are not perfect: one has the "not for sale" band, and one of the feature selection copies has a few typos here and there....

How to Choose the Right Hyperparameter Search

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 new focused machine learning courses are now on Udemy I know many of you already use—and love—Udemy, so I’m back with a new series of short, focused courses designed to help you learn one valuable machine learning skill at a time. The Complete SHAP and Shapley Values Masterclass Master Hyperparameter Tuning with Grid...

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...