Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
[0.11.0] - 2026-10-17¶
Three hand-rolled pieces are replaced. Dykstra, the Euclidean slack
projection, two-stage and the OvR baseline use scipy’s isotonic regression;
the nearly-isotonic prox is an exact O(n) algorithm; and calibrate_soft is a
quadratic program instead of an ADMM. Answers are unchanged or more accurate,
and every affected path is faster. Read Removed before upgrading.
Changed¶
calibrate_softis one quadratic program solved by Clarabel, ascalibrate_qpis, instead of an ADMM. At N = 300 and 1000 the ADMM stopped at its iteration cap up to 2e-2 above the optimum; the QP solves every case tested, matches an independent solver, and is 25-90x faster (0.29 s against 11.9 s at N = 1000).tolis now the solver tolerance (default 1e-10), and the result reportsobjectivein place ofobjective_valuesandfinal_change.prox_near_isotonicis exact: dynamic programming along the chain in O(n), as Johnson’s (2013) fused-lasso algorithm with the clip interval [-lam, 0]. It replaces an inner ADMM, is 16x faster at n = 1000, and matches an independent solver.Isotonic regression is
scipy.optimize.isotonic_regression, in place of three pure-Python copies, one of which had mishandled weights in 0.9. The KL slack projection keeps its own log-weight pooling, because its weights underflow as plain floats, andprox_kl_near_isotonicis still an iterative ADMM. Dykstra is 32x faster (100 iterations at N = 2000: 0.05 s against 1.60 s) with outputs unchanged to 1e-15.scipy>=1.12is now required.The package description and keywords no longer mention ADMM.
Fixed¶
project_near_isotonic_euclideanwith non-uniformweightsand asum_targetshifted every entry equally, which is optimal only for uniform weights; it now raises. No solver passes weights, so none was affected.
Removed¶
calibrate_soft’srhoandmax_iters, which no longer apply.calibrate_dykstra(rtol=...), a merge tolerance in the old pure-Python PAV that scipy’s has no equivalent for; its default, 0, is what every path used.prox_near_isotonic’s ADMM settings (rho,max_iters,abstol,reltol,return_info), andreturn_infoonprox_near_isotonic_with_sum.
[0.10.0] - 2026-10-16¶
Every solver is now tested against one that shares no code with it (cvxpy,
scipy, scikit-learn or IPF). That found calibrate_kl, calibrate_kl_soft,
calibrate_soft, calibrate_ovr_isotonic and ties="group" returning wrong
answers. Several functions that duplicated others or did nothing are removed;
read Removed before upgrading.
Fixed¶
ties="group"returned garbage fromcalibrate_dykstra. The weighted isotonic step expanded each pooled block by its weight instead of its element count, writing past the end of the output. It now matches an independent QP solution.calibrate_ovr_isotonicwas off by up to 0.31 whenever scores tied, from the same weighted isotonic bug. It now matches scikit-learn’sIsotonicRegressionexactly. Labels outside[0, J), or not real numbers, now raise; they produced NaN or were silently cast.calibrate_klstopped short of the optimum it claimed: on a 30-row problem its KL was 1.5443 against a true constrained optimum of 1.5345. It is now solved exactly as an exponential-cone program (Clarabel). It solves the rank-penalised program at a weight certified large enough to leave no violations, because posing the isotonic constraints directly stalls the interior-point solver.calibrate_kl_softnever converged, and above λ ≈ 10 a larger penalty gave more rank violation (#37). It is now the same exponential-cone program, exact at every λ, socalibrate_kl_paretotraces a monotone frontier.The KL solvers judge convergence on the returned matrix (row and column errors at most 1e-7), not on the solver’s status, so a stall retried at a looser tolerance cannot report constraints it misses.
calibrate_softstopped far from its optimum wheneverlam_r > 0: its rank step was ten subgradient iterations. Atlam_r = 1it reported convergence at an objective of 0.548 against a true 0.290. Its ADMM variant also weighted the marginal penalty bylam_m / N. It is now one ADMM with the exact proximal operator of the rank penalty, stops on primal and dual residuals, and matches an independent solver to 1e-7.calibrate_ipfreportedconverged=Truewhen its columns missedM. Convergence now means the returned matrix’s column error is belowtol.nearlywith any mode other than"epsilon"was silently treated as strict isotonic; it now raises.calibrate_kl_paretoreturned an empty frontier on malformed input instead of raising.Rank-violation diagnostics sorted tied scores with an unstable sort while the constraints used a stable one, so ties could read as violations. With the new certificate that made
calibrate_klreject feasible problems with tied scores. Every diagnostic now uses the stable order.project_near_kl_isotonicreturned a feasible point that was not the KL projection whenevereps_slack > 0: it pooled without the weights the slack substitution introduces. It now matches an independent solver. Combiningsum_targetwith non-uniform weights, which rescaling cannot make exact, now raises.classwise_ecereturned the sum of the per-class ECEs instead of their mean, so it was too large by a factor of the number of classes. It now follows Kull et al. (2019), eq. 4.auc_deltasbroke ties between scores by row order, so the AUC of a calibrated column with flat runs depended on how the rows happened to be sorted. Tied scores now share their average rank, as in the standard Mann-Whitney AUC.informativeness_ratiowarned about an empty mean when every column’s ratio was infinite, that is, each original column was flat and its calibrated column was not;mean_column_ratiois now NaN in that case.Every docstring example runs and records its output, and the docs build runs them again. The quick start’s targets did not sum to the number of rows, so it could not converge; several examples used targets the input already met.
Changed¶
calibrate_softis slower: its rank step is an exact prox, itself solved iteratively, about 1.5 s at N=30.The KL solvers preserve rank to solver precision (violations at most 1e-8), not exactly;
calibrate_qpstill drives them to zero.The row-simplex projection is vectorised in numpy, about 5x faster than the Numba version was and 80x faster than the old pure-Python fallback.
The documentation’s changelog page includes this file instead of a stale copy that ended at 0.8.0.
Removed¶
calibrate_admm,ADMMResultandcalibrate(method="admm")(#39). Its returned matrix was alwayscalibrate_dykstra’s, recomputed fromP, and itsnearly={"mode": "lambda"}option never reached the output. Penalised rank violations arecalibrate_soft.calibrate_soft_admm, merged intocalibrate_soft.Positional
rho,max_iters,tolandverboseincalibrate_soft; they are keyword-only, so old positional calls fail loudly rather than being misread now thatrhocomes first.The
max_itersandepsarguments of the KL solvers, which no longer iterate.The
[performance]extra, Numba, theuse_jitargument, and two compiled helpers nothing called.nllandbrier. They duplicatedsklearn.metrics.log_lossandsklearn.metrics.brier_score_loss(..., scale_by_half=False), which give the same numbers.classwise_ece(balanced=...). It weighted bins, not classes, so it did not do what it said.
[0.9.1] - 2026-10-14¶
Fixed¶
calibrate_dykstra’s final polishing step ignored the caller’srow_atolandcol_atoland used its own defaults, so it could stop short of the tolerances the caller asked for.
Changed¶
Package metadata: project URLs now point at the hyphenated repository name, the license is declared as an SPDX expression, and the package ships a
py.typedmarker.The documentation examples are scripts run by sphinx-gallery on every docs build, replacing four notebooks that did not run. The installation page no longer lists
[examples],[testing]and[all]extras, which never existed; the one extra is[performance].
[0.9.0] - 2026-07-31¶
An audit against an independent convex solver. The core mathematics held; four things around it did not, and the practical ceiling that stopped the package being usable at scale is gone.
Added¶
calibrate()— the recommended entry point, dispatching onmethod("auto","qp","dykstra","admm"). Existing functions are unchanged.calibrate_qp()— solves the projection exactly as a sparse quadratic program via Clarabel. Faster than alternating projections at every size measured: 4x atN=10, 13x atN=25, 2230x atN=50, and it reaches sizes the projection method cannot (N=1600in 1.4 s;N=400previously did not converge in 60,000 iterations).The lesson is about the algorithm class, not the problem. Dykstra and OSQP are both first-order and these constraint sets meet at a shallow angle; OSQP is worse than Dykstra here, returning 592 rank violations at
N=400while reporting that it ran. An interior-point method is indifferent to that geometry.Its solver tolerance defaults to
1e-10, tighter than Clarabel’s own: at the looser default the result carries small isotonic violations — 204 atN=400, 1655 atN=1600, each between 1e-9 and 1e-7. Rank preservation is this package’s guarantee, and one that holds only to 1e-7 is a different guarantee. Tightening costs 0.24 s against 0.19 s.is_feasible()— an exported feasibility predicate with absolute tolerances.
Fixed¶
Every feasibility check silently ignored its stated tolerance.
np.allclose(x, target, atol=...)keepsrtol=1e-5unless told otherwise, so a statedatol=1e-12on row sums behaved as1e-5, and the column check — measured againstM_j, which grows withN— became looser as the problem got larger.converged=Truewas returned withmax_row_error=1.0e-05.The ADMM
Q-update was misderived. It pulled every element toward the row and column targets, when those target the row and column sums. Each row was driven towardJ*(1 + M_j)/2instead of 1, for anyrho, socalibrate_admmfailed on problems as small asN=25. Replaced with the exact minimiser, which closes in the row sums, column sums and grand total — no linear system, stillO(NJ), verified against a direct solve to 4.5e-14.calibrate_admmcould return a rank-violating matrix silently. When its final projection failed it caught the error, warned only atverbose=True(defaultFalse), and returned its raw iterate — 27 rank violations on a 25-row problem, with an objective below the true optimum because the point was infeasible. It now raises, and the projection gets a budget proportional toNrather than a flat 1500.The documented behaviour on infeasible targets was wrong. The README promised convergence to “the closest point satisfying both sets of constraints”; no such point exists when the sets are disjoint, and the code raises. Feasibility requires
sum(M) == Nexactly — the warning fired only above 10% ofN, so a 2% mismatch warned about nothing and then failed hard. It now fires at floating-point slack and says how to fix it.
Changed¶
calibrate_ovr_isotonicis documented as the baseline it is. Its row normalisation inverts 57% of adjacent within-class pairs on columns that are perfectly isotonic beforehand — it does not preserve rank, despite living in a package named for it. Tied scores are now pooled before interpolation, so the result no longer depends on row order.Convergence tolerances are absolute and achievable (
1e-8). Alternating projections end each sweep on a column projection, which perturbs the row sums, so no finite iterate satisfies both constraint sets exactly.README.mdanddocs/source/theory.mdrecord the measured iteration scaling, the interior-point finding, and the exact-feasibility rule — including that whensum(M) == Nthe intersection is never empty, sinceQ[i,j] = M[j]/Nalways satisfies it, so failure is conditioning rather than infeasibility.
Dependencies¶
Added
scipyandclarabel(Apache-2.0, ~2.5 MB). Thedykstrapath remains numpy-only.
[0.7.1] - 2024-12-21¶
Changed¶
Removed tqdm dependency
Eliminated optional import patterns for cleaner dependency management
Removed
progress_barparameter fromcalibrate_dykstraandcalibrate_admmfunctionsUpdated examples and documentation to remove progress bar references
Enhanced docstrings
Converted all docstrings to Google style format for consistency
Added comprehensive examples, args, and returns documentation
Improved API documentation across all public functions
Python modernization
Refactored input validation to use Python 3.11+ match statements
Updated validation patterns for better readability and maintainability
Removed¶
Breaking Change:
progress_barparameter no longer supportedRemoved tqdm from both performance and dev dependency groups
[0.7.0] - 2024-12-13¶
Added¶
Performance optimizations with Numba JIT compilation
JIT-compiled versions of performance-critical functions (
project_row_simplex,isotonic_regression)1.5-3x speedup on moderate-sized problems when Numba is available
Graceful fallback to pure Python when Numba is not installed
New
use_jitparameter forcalibrate_dykstraandcalibrate_admm(defaults to True)
Progress bar support for long-running calibrations
Optional tqdm progress bars via
progress_barparameterWorks with both Dykstra and ADMM algorithms
Graceful fallback when tqdm is not installed
Modern Python 3.11+ features
Added
slots=Trueto dataclasses for better memory efficiencyUse pathlib instead of os.path for file operations
Updated to use importlib.metadata for version handling
Changed¶
Dependency management improvements
Added optional performance dependencies:
numba>=0.56,tqdm>=4.65Configured deptry for proper dependency tracking
Updated dev dependency groups to include performance deps
Code quality enhancements
Fixed all linting issues for consistent code formatting
Improved numerical tolerance handling in tests
Enhanced error messages and validation
Fixed¶
Improved numerical stability in edge cases
Better handling of optional dependencies with proper fallbacks
Fixed test tolerances to account for expected JIT floating-point differences
[0.6.0] - Previous Release¶
Basic rank-preserving calibration functionality
Dykstra’s alternating projections algorithm
ADMM optimization algorithm
Nearly isotonic constraints (epsilon-slack and lambda-penalty)
Comprehensive test suite and documentation