Thanks to visit codestin.com
Credit goes to github.com

Skip to content

FIX always use logits in CalibratedClassifierCV - #34313

Open
ogrisel wants to merge 51 commits into
scikit-learn:mainfrom
ogrisel:fix-always-use-logits-in-CalibratedClassifierCV
Open

FIX always use logits in CalibratedClassifierCV#34313
ogrisel wants to merge 51 commits into
scikit-learn:mainfrom
ogrisel:fix-always-use-logits-in-CalibratedClassifierCV

Conversation

@ogrisel

@ogrisel ogrisel commented Jun 16, 2026

Copy link
Copy Markdown
Member

Fixes: #34312

Note: this draft PR was started a long time ago. Work is needed to realign it with the current main branch.

@antoinebaker

Copy link
Copy Markdown
Contributor

I agree, we should definitively use the logits for sigmoid recalibration / Platt scaling.

@ogrisel Could you grant me rights to push to your PR ? I'll try to merge main and update this draft PR.

@ogrisel

ogrisel commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

@antoinebaker you were already registered as a collaborator on my scikit-learn fork :)

Please go ahead with the merge and update of this PR.

@antoinebaker antoinebaker moved this to In progress in Labs Jun 19, 2026
Comment thread sklearn/calibration.py Outdated
@antoinebaker

Copy link
Copy Markdown
Contributor

Hi @ogrisel and @virchan, the PR is currently passing the CI.

However I would like to refactor the code, in line with your code comments @ogrisel and your comment @virchan. I would greatly appreciate your feedback (see Comments section) to discuss future implementations !

With the help of cursor, here a summary of the current logic of _enforce_logits (which role is to preprocess the predictions as logits before feeding them to the calibrators).

predict_proba

Inputs are always probabilities p_k = P(y=k).

Calibration method Target logit space Transformation
sigmoid / isotonic OvR (“Bernoulli”) logits z_k = logit(p_k)
temperature Multinomial logits z_k = log(p_k) - log(p_mean)

decision_function

Inputs are scores s whose meaning varies by estimator.

Calibration method Target logit space Current transformation
sigmoid / isotonic OvR (“Bernoulli”) logits Binary (1D): pass-through s (assumed positive-class log-odds). Multiclass (K>2): softmax(s) → p_k, then z_k = logit(p_k).
temperature Multinomial logits Pass-through s (assumed joint logits with p = softmax(s)).

Comments

For me, the handling of predict_proba is correct and solves #34312. The handling of decision_function is more debatable. It seems to always assume "Multinomial logits"-like scores, and in particular converts them to Bernoulli logits for the sigmoid/isotonic calibration method.

Here are some implementation options I'd like to discuss:

  1. get rid of the logit_preprocessing parameter. Instead we always decide based on the calibration method (sigmoid vs softmax) and response_method_name (predict_proba vs decision_function)
  2. only convert predict_proba for now, let decision_function passthrough. This would make the code much easier to understand. This would already fixes # 34312.
  3. try to determine (for an estimator) if the decision_function scores are "morally" more Bernoulli vs Multinomial logits and convert based on that. But this seems difficult to do in general.
  4. always convert the decision_function scores to the target logit space, even if they are in the correct logit space to start with. Currently done for the sigmoid method, but could be done for the temperature method as well.

@ogrisel

ogrisel commented Jun 25, 2026

Copy link
Copy Markdown
Member Author
  1. get rid of the logit_preprocessing parameter. Instead we always decide based on the calibration method (sigmoid vs softmax) and response_method_name (predict_proba vs decision_function)

I think I originally introduced the logit_preprocessing parameter in order to be able to conduct an empirical evaluation similarly to what was done in the CalArena paper. This evaluation was added at the end of examples/calibration/plot_calibration_multiclass.py but it wasn't meant to stay after the review of the PR. Since the results of the CalArena paper are clear, I think we can indeed drop this parameter.

  1. only convert predict_proba for now, let decision_function passthrough. This would make the code much easier to understand. This would already fixes # 34312.

I agree.

Comment thread examples/calibration/plot_calibration_curve.py Outdated
@ogrisel

ogrisel commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

@antoinebaker Let's first add temperature scaling to examples/calibration/plot_calibration_multiclass.py and then check then results collected at the end of this notebook that "softmax" logits preprocessing is always better than (or very similar to) sigmoid logits preprocessing for temperature scaling.

Let's also check that "sigmoid" logits preprocessing is always better or similar to "softmax" logits preprocessing for the two OvR methods.

If all goes according to our plan, we can hard code the logits preprocessing logic and remove the empirical evaluation of the logits_processing parameter from that example.

Comment thread examples/calibration/plot_calibration_multiclass.py Outdated
Comment thread doc/modules/calibration.rst Outdated
implements :term:`predict_proba`, :math:`f_i = \text{logit}(\hat{p}_i)` is the
logit of the predicted probability; otherwise, :math:`f_i` is the score from
:term:`decision_function`. :math:`A` and :math:`B` are real numbers to be
determined when fitting the regressor via maximum likelihood.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"fitting the regressor" is a bit weird as we speak of classifiers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 7ebe1a8

Comment thread doc/modules/calibration.rst Outdated
Comment thread doc/whats_new/upcoming_changes/sklearn.calibration/34313.fix.rst Outdated
Comment thread doc/whats_new/upcoming_changes/sklearn.calibration/34313.fix.rst Outdated
Comment thread sklearn/tests/test_calibration.py Outdated
Comment thread sklearn/tests/test_calibration.py Outdated
Comment thread sklearn/tests/test_calibration.py
Comment thread sklearn/tests/test_calibration.py Outdated
Comment thread sklearn/calibration.py
_CLASSIFIER_RESPONSE_METHODS = ("predict_proba", "decision_function")


def _ensure_logits(predictions, response_method_name, method):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have _convert_to_logits in this file. Why a new function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ensure_logits and _convert_to_logits both do some sort of "logit preprocessing", but in a different way, I tried to merge them at some point but the resulting code was too messy.

_ensure_logits is only used inside CalibratedClassifierCV while _convert_to_logits is only used inside _TemperatureScaling.

antoinebaker and others added 2 commits July 28, 2026 16:32
@github-actions github-actions Bot added the CI:Linter failure The linter CI is failing on this PR label Jul 28, 2026
@github-actions github-actions Bot removed the CI:Linter failure The linter CI is failing on this PR label Jul 28, 2026
@antoinebaker antoinebaker moved this from In progress to PR waiting for reviews in Labs Jul 29, 2026

@virchan virchan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both calibration.py and test_calibration.py LGTM. I will review the examples later.

@ogrisel

ogrisel commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Merging main to re-trigger an HTML rendering of the doc changed in this PR.

@ogrisel ogrisel left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did another pass of review and tried to mark as resolved all previous comments by @lorentzenchr on outdated sections of the diff that seem to have been actually addressed by subsequent commits. This was quite painful because I think the comment navigation of the diff view of github seems to be badly broken on this PR...

From my point of view this seems good to go.

Comment thread doc/modules/calibration.rst Outdated
@ogrisel ogrisel added the CUDA CI label Sep 1, 2026
@github-actions github-actions Bot removed the CUDA CI label Sep 1, 2026

@lorentzenchr lorentzenchr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further partial review. What remains is reviewing the actual fix 😏

Comment on lines +134 to +136
# indicated by the diagonal calibration curve. This is expected since we
# adjusted the regularization parameter to minimize the :ref:`log_loss` via
# internal cross-validation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected

Is it? I would put it less strongly.

Comment on lines +139 to +141
# by default because of the redundant features which violate the assumption of
# feature-independence and result in an **overly confident** classifier, which
# is indicated by the typical transposed-sigmoid calibration curve.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you split the sentence? It is too long and complex.

#
# Post-hoc calibration of the predicted probabilities of
# :class:`~sklearn.naive_bayes.GaussianNB` with :ref:`isotonic` or
# :ref:`Sigmoid regression <sigmoid_regressor>` can fix this issue as can be

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# :ref:`Sigmoid regression <sigmoid_regressor>` can fix this issue as can be
# :ref:`Sigmoid regression <sigmoid_regressor> fix this issue to a good extent as can be

# because of the limited amount of training and calibration data (1,000 samples
# for both).
#
# Below we will make a quantitative analysis considering several classification

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Below we will make a quantitative analysis considering several classification
# Below we make a quantitative analysis considering several classification

Same as section dataset.

Comment on lines +196 to +197
# Post-hoc calibration improves the :ref:`brier_score_loss` (a metric composed
# of calibration term and refinement term) and :ref:`log_loss` as expected

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both metrics, Brier score and log loss, are composed of a calibration (reliability) and refinement (resolution) term.

# According to the Brier score, the calibrated classifier is also slightly
# better than the original model.
#
# Be aware that an improvement in log-loss or Brier score on a test set is not

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Be aware that an improvement in log-loss or Brier score on a test set is not
# Be aware that an improvement in log loss or Brier score on a test set is not

Comment on lines +246 to +247
# always guaranteed since the calibration set is finite and sometimes the base
# classifier is already well calibrated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just state the fact and remove the "because".
Or we add that calibration might decrease the resolution/discrimination term of the loss.


# %%
#
# Let's now do the same for various classifiers with different mis-calibration

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Let's now do the same for various classifiers with different mis-calibration
# Let's now repeat the above for various classifiers with different mis-calibration

Comment on lines +396 to +397
# - Some estimators such as highly regularized polynomial classifiers and
# shallow tree-based models tend to be under-confident by default and each

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# - Some estimators such as highly regularized polynomial classifiers and
# shallow tree-based models tend to be under-confident by default and each
# - Some estimators such as the highly regularized polynomial classifier and
# the shallow tree-based model tend to be under-confident by default and each

Similar below.

Comment on lines +425 to +427
# - The temperature scaling method applies a single global rescaling of the
# multinomial logits. This induces a smooth mapping that preserves the
# ranking of the predicted classes while adjusting their confidence.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not so much the multinomial logits, it is that it minimized the multinomial log loss (instead of ovr binary log loss + normalization).

@lorentzenchr lorentzenchr changed the title Fix always use logits in calibrated classifier cv FIX always use logits in CalibratedClassifierCV Sep 11, 2026

@lorentzenchr lorentzenchr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining part of the review.

Comment on lines +128 to +129
# TODO: once we have a calibration loss, use it instead of the
# brier score to check recalibration.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is absolute not related to the fix of this PR. But ok, let's keep it.

# Check probabilities sum to 1
assert_allclose(np.sum(probas, axis=1), np.ones(len(X_test)))
y_pred_cal = cal_clf.predict_proba(X_test)
assert_allclose(np.sum(y_pred_cal, axis=1), np.ones(len(X_test)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert_allclose(np.sum(y_pred_cal, axis=1), np.ones(len(X_test)))
assert_allclose(np.sum(y_pred_cal, axis=1), 1)

Comment on lines +291 to +293
labels = np.arange(n_classes)
bs_uncal = brier_score_loss(y_test, y_pred_uncal, labels=labels)
bs_cal = brier_score_loss(y_test, y_pred_cal, labels=labels)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
labels = np.arange(n_classes)
bs_uncal = brier_score_loss(y_test, y_pred_uncal, labels=labels)
bs_cal = brier_score_loss(y_test, y_pred_cal, labels=labels)
bs_uncal = brier_score_loss(y_test, y_pred_uncal, labels=cal_clf.classes_)
bs_cal = brier_score_loss(y_test, y_pred_cal, labels=cal_clf.classes_)

This is cleaner and more secure.

Comment on lines +336 to +338
All calibration methods are expected to improve the log-loss of the base
classifiers given enough calibration data because the log-loss is a
strictly proper scoring rule.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% convinced of the premise. A proper scoring rule is decomposed as:
loss = mis-calibration - discrimination + entropy. Entropy is constant (given by data only, not the model). If we change the predictions, we change the mis-calibration term, but also the discrimination term.

def test_ensure_logits_decision_function(method, predictions):
# Apart from reshaping, this is a passthrough.
logits = _ensure_logits(predictions, "decision_function", method)
assert_allclose(logits.ravel(), predictions.ravel())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the ravel needed?

Comment thread sklearn/calibration.py

When the response method is ``predict_proba``:

- For ``method='sigmoid'``, Bernoulli logits are computed per class

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- For ``method='sigmoid'``, Bernoulli logits are computed per class
- For ``method='sigmoid'``, logits are computed per class

Comment thread sklearn/calibration.py
Comment on lines +130 to +131
eps = xp.finfo(predictions.dtype).eps
eps_ = xp.asarray(eps, dtype=predictions.dtype, device=device_)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
eps = xp.finfo(predictions.dtype).eps
eps_ = xp.asarray(eps, dtype=predictions.dtype, device=device_)
eps = xp.asarray(xp.finfo(predictions.dtype).eps, dtype=predictions.dtype, device=device_)

Or just renaming eps_ to eps.

Comment thread sklearn/calibration.py
return xp.reshape(predictions, (-1, 1))
return predictions

if method == "isotonic":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if method == "isotonic":
elif method == "isotonic":

and so on. Not really needed, but a bit cleaner, imo.

Comment thread sklearn/calibration.py
Comment on lines +153 to +156
raise ValueError(
f"Unknown calibration method: {method}. "
"Expected 'sigmoid', 'isotonic', or 'temperature'."
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise ValueError(
f"Unknown calibration method: {method}. "
"Expected 'sigmoid', 'isotonic', or 'temperature'."
)
else:
raise ValueError(
f"Unknown calibration method: {method}. "
"Expected 'sigmoid', 'isotonic', or 'temperature'."
)

Comment thread sklearn/calibration.py
)


def _to_calibration_logits(predictions, *, response_method_name, method, classes=None):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is only used once: Do we needed it or better put the code in place?

@lorentzenchr

Copy link
Copy Markdown
Member

For a bugfix this is quite a large PR. I understand that examples are improved, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CalibratedClassifierCV(method="sigmoid") should always take the raw logits as inputs instead of the predicted probabilities

5 participants