Class Weights Do Not Improve Model Performance


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!


My New Book is Out!

Imbalanced Data, Myths, Mistakes and Modern Solutions

Most advice about imbalanced data is incomplete, or simply wrong. This book challenges common assumptions and provides practical, evidence-based guidance for tackling class imbalance and building better machine learning models.

Class Weights Do Not Improve Model Performance

Here’s a pattern I see all the time.

A practitioner trains a model on an imbalanced dataset and obtains a ROC-AUC of 0.670. They then set class_weight="balanced", train the model again, and obtain a ROC-AUC of 0.675.

The second number is higher, so the conclusion seems obvious: class weighting improved the model.

Except we have ignored an important question:

" How precise are those ROC-AUC estimates?"

 

A performance metric calculated on one test set is only an estimate. If the test set contained a slightly different collection of observations, we would obtain a slightly different value.

Therefore, the fact that one ROC-AUC is numerically higher than another does not necessarily mean that one model is genuinely better.

A difference is not necessarily an improvement

Imagine that we obtain the following results:

  • No class weights: ROC-AUC = 0.670
  • class_weight="balanced": ROC-AUC = 0.675

The observed improvement is 0.005.

If we report only those two numbers, class weighting wins.

But what if the uncertainty around that difference stretches is around 0.04?

Then, those values look more like the following:

  • No class weights: ROC-AUC = 0.666 - 0.674
  • class_weight="balanced": ROC-AUC = 0.671 - 0.679

Now we can see that the performance intervals overlap substantially. Therefore, the difference in performance is, most likely, not statistically significant. In lay terms: both models perform the same.

Show me the data

Let’s look at this with an example.

The following example creates an imbalanced dataset and trains two logistic regression models. Everything is identical except that one model uses class_weight="balanced".

First, we evaluate both models once on the test set:

import numpy as np

import matplotlib.pyplot as plt

​
​

from sklearn.datasets import make_classification

from sklearn.linear_model import LogisticRegression

from sklearn.metrics import roc_auc_score

from sklearn.model_selection import train_test_split

​
​

# Create an imbalanced dataset

X, y = make_classification(

n_samples=2200,

n_features=12,

n_informative=5,

n_redundant=3,

weights=[0.93, 0.07],

class_sep=0.75,

flip_y=0.03,

random_state=3,

)

​
​

X_train, X_test, y_train, y_test = train_test_split(

X,

y,

test_size=0.25,

stratify=y,

random_state=11,

)

​
​

models = {

"No weights": LogisticRegression(max_iter=2000),

"Balanced": LogisticRegression(

max_iter=2000,

class_weight="balanced",

),

}

​
​

probabilities = {}

​
​

for name, model in models.items():

model.fit(X_train, y_train)

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

​
​

single_scores = {

name: roc_auc_score(y_test, probs)

for name, probs in probabilities.items()

}

​
​

print(single_scores)

The results are approximately:

No weights: 0.670

Balanced: 0.675

If we stopped here, we might conclude that class weighting worked.

Let’s not stop here.

Estimating the uncertainty

We can bootstrap the test set to see how much the ROC-AUC changes when the evaluation sample changes.

Importantly, we sample the same observations for both models during each bootstrap iteration. This gives us a paired comparison: for every sample, we calculate not only the two ROC-AUC values, but also their difference.

rng = np.random.default_rng(0)

n_bootstraps = 2000

​
​

bootstrap_scores = {

"No weights": [],

"Balanced": [],

}

bootstrap_differences = []

​
​

for _ in range(n_bootstraps):

idx = rng.integers(0, len(y_test), len(y_test))

​
​

# ROC-AUC requires both classes

if len(np.unique(y_test[idx])) < 2:

continue

​
​

auc_no_weights = roc_auc_score(

y_test[idx],

probabilities["No weights"][idx],

)

auc_balanced = roc_auc_score(

y_test[idx],

probabilities["Balanced"][idx],

)

​
​

bootstrap_scores["No weights"].append(auc_no_weights)

bootstrap_scores["Balanced"].append(auc_balanced)

bootstrap_differences.append(

auc_balanced - auc_no_weights

)

We can now plot the ROC-AUC estimates and their 95% bootstrap intervals. The second panel shows the paired difference directly.

names = list(models)

means = [

np.mean(bootstrap_scores[name])

for name in names

]

​
​

intervals = [

np.percentile(bootstrap_scores[name], [2.5, 97.5])

for name in names

]

​
​

errors = np.array([

[mean - low, high - mean]

for mean, (low, high) in zip(means, intervals)

]).T

plt.figure(figsize=(5, 4))

plt.errorbar(

names,

means,

yerr=errors,

fmt="o",

capsize=6,

color="#c44e7b",

)

​
​

plt.ylabel("ROC-AUC")

plt.title("Performance with 95% bootstrap intervals")

plt.grid(axis="y", alpha=0.25)

plt.tight_layout()

plt.show()

The weighted model still has a slightly higher point estimate. But the confidence intervals overlap:

So the correct conclusion is not that class weighting improved performance.

The correct conclusion is that this experiment does not provide convincing evidence that either model performs better.

This is not an isolated result

For my book, Imbalanced Data: Myths, Mistakes and Modern Solutions, I investigated this question across 37 benchmark datasets.

I trained random forests and modern implementations of gradient boosting machines on the original class distribution. I then trained otherwise identical models using class weights derived from the imbalance ratio.

Across the datasets, the ROC-AUC of the weighted models remained essentially unchanged. The observed differences fell within the estimated uncertainty: class weighting did not make the models inherently better at separating positive from negative observations.

But class weighting did change something.

It distorted the estimated probabilities.

Class weights alter the effective class distribution seen during training. Consequently, the resulting probabilities are calibrated to the weighted distribution rather than the original one. In my experiments, this was reflected in higher Brier scores for the weighted models.

The same classification performance could be achieved by training on the original distribution and adjusting the decision threshold. Class weighting mostly shifted the decision boundary towards 0.5; it did not add new discriminatory information.

Conclusion

Never conclude that a modelling intervention worked because one metric increased on one evaluation sample.

Estimate the uncertainty around the comparison using cross-validation or bootstrapping. Ideally, examine the paired differences, not just two isolated error bars.

If the intervals overlap, the evidence does not establish an improvement.

And remember that class_weight="balanced" is not harmless. It may leave ranking performance unchanged while making the predicted probabilities harder to interpret.

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
Job descriptions rarely tell the whole story

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! New set of focused ML interpretability courses now on Udemy I know many of you already use Udemy, so this October I released 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 Interpretable Machine Learning:...

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