Utilities for computing optimal classification cutoffs for binary and multiclass classification
Project description
Optimal Classification Cut-Offs
Probabilistic classifiers output per-class probabilities, and fixed cutoffs such as 0.5 rarely maximize metrics like accuracy or the F\ :sub:1 score.
This package provides utilities to select optimal probability cutoffs for both binary and multiclass classification.
For multiclass problems, the package uses a One-vs-Rest strategy to optimize per-class thresholds independently.
Optimization methods include brute-force search, numerical techniques, and gradient-based approaches.
Quick start
Binary Classification
from optimal_cutoffs import ThresholdOptimizer
# true binary labels and predicted probabilities
y_true = [0, 1, 1, 0, 1]
y_prob = [0.2, 0.8, 0.7, 0.3, 0.9]
# Auto-selection (uses sort_scan for piecewise metrics like F1)
optimizer = ThresholdOptimizer(objective="f1", method="auto")
optimizer.fit(y_true, y_prob)
y_pred = optimizer.predict(y_prob)
# Explicit sort_scan for maximum performance on piecewise metrics
optimizer_fast = ThresholdOptimizer(objective="f1", method="sort_scan")
optimizer_fast.fit(y_true, y_prob)
y_pred_fast = optimizer_fast.predict(y_prob)
Multiclass Classification
import numpy as np
from optimal_cutoffs import ThresholdOptimizer
# true multiclass labels and predicted probability matrix
y_true = [0, 1, 2, 0, 1]
y_prob = np.array([
[0.7, 0.2, 0.1], # probabilities for classes 0, 1, 2
[0.1, 0.8, 0.1],
[0.1, 0.1, 0.8],
[0.6, 0.3, 0.1],
[0.2, 0.7, 0.1],
])
# Standard One-vs-Rest optimization
optimizer = ThresholdOptimizer(objective="f1", method="auto")
optimizer.fit(y_true, y_prob)
y_pred = optimizer.predict(y_prob)
# For imbalanced datasets, try coordinate ascent
optimizer_coord = ThresholdOptimizer(objective="f1", method="coord_ascent")
optimizer_coord.fit(y_true, y_prob)
y_pred_coord = optimizer_coord.predict(y_prob) # often better macro-F1
Understanding Piecewise-Constant Metrics
Classification metrics like F1 score, accuracy, precision, and recall are piecewise-constant functions with respect to the decision threshold. This means they only change values when the threshold crosses one of the unique predicted probabilities—they remain constant between these "breakpoints."
Why Continuous Optimizers Can Miss the Maximum
Standard optimization methods like scipy.optimize.minimize_scalar assume smooth functions and use gradient-based techniques. However, piecewise-constant functions have:
- Zero gradients everywhere except at breakpoints (where they're undefined)
- Flat regions that provide no directional information to guide optimization
- Step discontinuities that can trap optimizers in suboptimal regions
The figure below illustrates this phenomenon:
F1 score only changes at unique probability values (red dots). Continuous optimizers may converge anywhere within the flat regions, potentially missing the true optimum.
Our Solution: Optimized Algorithms for Piecewise Metrics
This library addresses the piecewise-constant challenge through multiple specialized algorithms:
-
Sort-and-Scan (
method="sort_scan"): Our fastest exact algorithm for piecewise-constant metrics in binary classification. Uses O(n log n) sorting with vectorized confusion matrix computation, providing 50-100x performance improvements while guaranteeing the global optimum. -
Smart Brute Force (
method="smart_brute"): Fallback algorithm that evaluates all unique probability values as threshold candidates. Still significantly faster than naive approaches through optimized implementations. -
Coordinate Ascent (
method="coord_ascent"): Specialized multiclass optimizer that maintains single-label consistency through coupled threshold optimization. Particularly effective for imbalanced datasets where it often improves macro-F1 compared to independent One-vs-Rest optimization. -
Auto Selection (
method="auto"): Intelligently selects the best algorithm based on your data and metric characteristics. -
Fallback Mechanisms: Including
scipy.optimize.minimize_scalarwith unique probability evaluation for non-piecewise metrics.
For detailed mathematical explanation and interactive visualizations, see our theoretical documentation.
When to Use Calibration
Threshold optimization and probability calibration serve different purposes and are complementary:
Use Calibration When:
- You need reliable probability estimates (e.g., "this prediction has 70% confidence")
- Comparing models based on probability quality
- Converting arbitrary scores to meaningful probabilities
Use Threshold Optimization When:
- Maximizing specific classification metrics (F1, precision, recall)
- Making binary decisions for deployment
- Handling imbalanced datasets (where 0.5 threshold is suboptimal)
Best Practice: Use Both Together
from sklearn.calibration import CalibratedClassifierCV
from optimal_cutoffs import ThresholdOptimizer
# 1. Train and calibrate your classifier
calibrated_model = CalibratedClassifierCV(base_model, cv=3)
calibrated_model.fit(X_train, y_train)
y_prob_cal = calibrated_model.predict_proba(X_val)[:, 1]
# 2. Optimize threshold on calibrated probabilities
optimizer = ThresholdOptimizer(objective="f1")
optimizer.fit(y_val, y_prob_cal)
# 3. Use both for final predictions
y_prob_test = calibrated_model.predict_proba(X_test)[:, 1] # Calibrated probabilities
y_pred_test = optimizer.predict(y_prob_test) # Optimized decisions
Key insight: Calibration improves probability quality; threshold optimization maximizes classification metrics. Using both gives you reliable probabilities and optimal decisions.
For more details on calibration methods (Platt scaling, isotonic regression) and when to use them, see our full documentation.
Advanced Methods and Future Enhancements
Expected F-beta optimization via Dinkelbach method: For scenarios requiring optimization of expected F-beta under calibrated probabilities, the Dinkelbach fractional programming method provides an ultra-fast exact solution. This leverages the F1 threshold identity that states the optimal threshold equals the ratio of false negatives to false positives at optimality. This mathematically elegant approach can be significantly faster than iterative optimization for calibrated probability distributions. Implementation of this method is planned for a future release, building on the theoretical foundation established in the threshold optimization literature.
API
get_confusion_matrix(true_labs, pred_prob, threshold)
- Purpose: Compute confusion-matrix counts for a threshold.
- Args: arrays of true binary labels and probabilities, plus the decision threshold.
- Returns:
(tp, tn, fp, fn)counts.
get_multiclass_confusion_matrix(true_labs, pred_prob, thresholds)
- Purpose: Compute per-class confusion-matrix counts for multiclass classification using One-vs-Rest.
- Args: true class labels (0, 1, 2, ...), probability matrix (n_samples, n_classes), and per-class thresholds.
- Returns: List of per-class
(tp, tn, fp, fn)tuples.
register_metric(name=None, func=None)
- Purpose: Add a metric function to the global registry.
- Args: optional metric name and callable; can also be used as a decorator.
- Returns: the registered function or decorator.
register_metrics(metrics)
- Purpose: Register multiple metric functions at once.
- Args: dictionary mapping names to callables.
- Returns:
None.
multiclass_metric(confusion_matrices, metric_name, average="macro")
- Purpose: Compute multiclass metrics from per-class confusion matrices.
- Args: list of confusion matrices, metric name, averaging strategy ("macro", "micro", "weighted").
- Returns: aggregated metric score.
get_probability(true_labs, pred_prob, objective='accuracy', verbose=False)
- Purpose: Brute-force search for the threshold that maximizes accuracy or F\ :sub:
1using scipy.optimize.brute. - Args: true labels, predicted probabilities, objective ("accuracy" or "f1"), and verbosity flag.
- Returns: optimal threshold.
get_optimal_threshold(true_labs, pred_prob, metric='f1', method='auto', sample_weight=None, comparison='>')
- Purpose: Optimize any registered metric using different strategies. Automatically detects binary vs multiclass inputs.
- Args: true labels (binary or multiclass), probabilities (1D for binary, 2D for multiclass), metric name, optimization method, optional sample weights, and comparison operator.
- Methods:
"auto": Automatically selects the best method based on metric characteristics (default)"sort_scan": Exact O(n log n) algorithm for piecewise-constant metrics in binary classification. Provides 50-100x speedup while guaranteeing global optimum"smart_brute": Evaluates all unique probabilities (fallback when sort_scan unavailable)"minimize": Uses scipy.optimize.minimize_scalar with fallback evaluation"gradient": Simple gradient ascent
- Comparison:
">"(exclusive, default) or">="(inclusive) for handling tied probabilities. The sort-and-scan algorithm uses midpoint thresholds to make this robust either way. - Returns: optimal threshold (float for binary, array for multiclass).
get_optimal_multiclass_thresholds(true_labs, pred_prob, metric='f1', method='auto', average='macro', sample_weight=None, comparison='>')
- Purpose: Find optimal per-class thresholds for multiclass classification.
- Args: true class labels, probability matrix (n_samples, n_classes), metric name, optimization method, averaging strategy, optional sample weights, and comparison operator.
- Methods: Same as
get_optimal_thresholdplus:"coord_ascent": Coordinate ascent for coupled multiclass optimization (single-label consistent). Iteratively optimizes per-class thresholds while maintaining single-label predictions via argmax(P - tau). Often improves macro-F1 on imbalanced datasets compared to independent One-vs-Rest optimization
- Strategies: "macro"/"weighted" (One-vs-Rest independent), "micro" (joint optimization), "coord_ascent" (coupled optimization)
- Returns: array of optimal thresholds, one per class.
cv_threshold_optimization(true_labs, pred_prob, metric='f1', method='auto', cv=5, random_state=None)
- Purpose: Estimate thresholds via cross-validation and report per-fold scores.
- Args: Same parameters as
get_optimal_threshold, plus cross-validation folds and random state. - Returns: arrays of thresholds and scores.
nested_cv_threshold_optimization(true_labs, pred_prob, metric='f1', method='auto', inner_cv=5, outer_cv=5, random_state=None)
- Purpose: Perform nested cross-validation for threshold estimation and unbiased performance evaluation.
- Returns: arrays of outer-fold thresholds and scores.
ThresholdOptimizer(objective='accuracy', verbose=False, method='auto', comparison='>')
- Purpose: High-level wrapper with
fit/predictmethods using scikit-learn style API. Supports both binary and multiclass classification. - Args: objective metric name (e.g., "accuracy", "f1", "precision", "recall"), verbosity flag, optimization method, and comparison operator.
- Methods: All methods from
get_optimal_thresholdandget_optimal_multiclass_thresholds, including"sort_scan"for binary and"coord_ascent"for multiclass. - Comparison:
">"(exclusive) or">="(inclusive) for threshold comparison behavior. - Returns: fitted instance with
threshold_attribute (float for binary, array for multiclass). Thepredictmethod returns boolean predictions for binary, class indices for multiclass.
Examples
- Basic binary usage
- Advanced binary usage with sklearn
- Multiclass classification
- Cross-validation and gradient methods
Authors
Suriyan Laohaprapanon and Gaurav Sood
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file optimal_classification_cutoffs-0.2.1.tar.gz.
File metadata
- Download URL: optimal_classification_cutoffs-0.2.1.tar.gz
- Upload date:
- Size: 77.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08662847378987980788dc37f4d3682ffc001a11b7e55a8c53782836e893404c
|
|
| MD5 |
e9f821564a1fcc25c4be0fd627459ed3
|
|
| BLAKE2b-256 |
559b9a60d8c13e29b5dd41f57534ab578efbf70e2282d5de4833a0c542c3ea68
|
Provenance
The following attestation bundles were made for optimal_classification_cutoffs-0.2.1.tar.gz:
Publisher:
python-publish.yml on finite-sample/optimal_classification_cutoffs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
optimal_classification_cutoffs-0.2.1.tar.gz -
Subject digest:
08662847378987980788dc37f4d3682ffc001a11b7e55a8c53782836e893404c - Sigstore transparency entry: 558813907
- Sigstore integration time:
-
Permalink:
finite-sample/optimal_classification_cutoffs@e478698a4b5679bdb6a98c56dab48458b2ca0fbb -
Branch / Tag:
refs/heads/master - Owner: https://github.com/finite-sample
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@e478698a4b5679bdb6a98c56dab48458b2ca0fbb -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file optimal_classification_cutoffs-0.2.1-py3-none-any.whl.
File metadata
- Download URL: optimal_classification_cutoffs-0.2.1-py3-none-any.whl
- Upload date:
- Size: 36.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2af8d7ef271f4cdeea94f518e7d566d5453a8e1f4967fcaf12e14751044c821
|
|
| MD5 |
9ffc13b3913c72f2943cca276280ab5c
|
|
| BLAKE2b-256 |
b8564cbbc3c4a0610c72dd64563b6cbb898e3a004d2c56eeabca7ca5b6581883
|
Provenance
The following attestation bundles were made for optimal_classification_cutoffs-0.2.1-py3-none-any.whl:
Publisher:
python-publish.yml on finite-sample/optimal_classification_cutoffs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
optimal_classification_cutoffs-0.2.1-py3-none-any.whl -
Subject digest:
c2af8d7ef271f4cdeea94f518e7d566d5453a8e1f4967fcaf12e14751044c821 - Sigstore transparency entry: 558813972
- Sigstore integration time:
-
Permalink:
finite-sample/optimal_classification_cutoffs@e478698a4b5679bdb6a98c56dab48458b2ca0fbb -
Branch / Tag:
refs/heads/master - Owner: https://github.com/finite-sample
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@e478698a4b5679bdb6a98c56dab48458b2ca0fbb -
Trigger Event:
workflow_dispatch
-
Statement type: