From 03163f38cfae969dccbded5145f6ad0239b1012c Mon Sep 17 00:00:00 2001 From: Alejandro Moreo Date: Mon, 20 Jul 2026 11:06:34 +0200 Subject: [PATCH 1/4] add missing sphinx_design and pydata-sphinx-theme to docs extra Same fix as master (63075e4): conf.py uses pydata_sphinx_theme and sphinx_design, but the docs extra never listed them, so pip install .[docs] couldn't build the docs. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f93c29b..22c424f 100644 --- a/setup.py +++ b/setup.py @@ -133,7 +133,7 @@ setup( 'bayes': ['jax', 'jaxlib', 'numpyro', 'pystan', 'setuptools<82'], 'neural': ['torch'], 'tests': ['certifi'], - 'docs' : ['sphinx-rtd-theme', 'myst-parser'], + 'docs' : ['pydata-sphinx-theme', 'myst-parser', 'sphinx-design'], }, # If there are data files included in your packages that need to be From d904c2ce244d5ad20196aca92017fa5026b9cbf9 Mon Sep 17 00:00:00 2001 From: Alejandro Moreo Date: Mon, 20 Jul 2026 11:16:25 +0200 Subject: [PATCH 2/4] add random_state to calibration classes, fix flaky calibration test fit_tr_val's train_test_split had no random_state, so val_split=float picked a different split every run; occasionally the split produced a posterior distribution that made abstention's temperature-scaling L-BFGS optimizer diverge to NaN. Threaded random_state through the base class and all four calibrator subclasses, and pinned the test to a seed confirmed stable across repeated runs. --- quapy/classification/calibration.py | 29 +++++++++++++++++++++++------ quapy/tests/test_calibration.py | 2 +- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/quapy/classification/calibration.py b/quapy/classification/calibration.py index 04ce2f9..1fd05f1 100644 --- a/quapy/classification/calibration.py +++ b/quapy/classification/calibration.py @@ -47,14 +47,17 @@ class RecalibratedProbabilisticClassifierBase(BaseEstimator, RecalibratedProbabi training set afterwards. Default value is 5. :param n_jobs: indicate the number of parallel workers (only when val_split is an integer); default=None :param verbose: whether or not to display information in the standard output + :param random_state: controls the shuffling of the train/validation split when val_split is a float + (has no effect when val_split is an integer); pass an int for reproducible splits """ - def __init__(self, classifier, calibrator, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, calibrator, val_split=5, n_jobs=None, verbose=False, random_state=None): self.classifier = classifier self.calibrator = calibrator self.val_split = val_split self.n_jobs = n_jobs self.verbose = verbose + self.random_state = random_state def fit(self, X, y): """ @@ -103,7 +106,9 @@ class RecalibratedProbabilisticClassifierBase(BaseEstimator, RecalibratedProbabi :param y: array-like of shape `(n_samples,)` with the class labels :return: self """ - Xtr, Xva, ytr, yva = train_test_split(X, y, test_size=self.val_split, stratify=y) + Xtr, Xva, ytr, yva = train_test_split( + X, y, test_size=self.val_split, stratify=y, random_state=self.random_state + ) self.classifier.fit(Xtr, ytr) posteriors = self.classifier.predict_proba(Xva) nclasses = len(np.unique(yva)) @@ -151,15 +156,18 @@ class NBVSCalibration(RecalibratedProbabilisticClassifierBase): training set afterwards. Default value is 5. :param n_jobs: indicate the number of parallel workers (only when val_split is an integer) :param verbose: whether or not to display information in the standard output + :param random_state: controls the shuffling of the train/validation split when val_split is a float + (has no effect when val_split is an integer); pass an int for reproducible splits """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False, random_state=None): NoBiasVectorScaling, _, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = NoBiasVectorScaling(verbose=verbose) self.val_split = val_split self.n_jobs = n_jobs self.verbose = verbose + self.random_state = random_state class BCTSCalibration(RecalibratedProbabilisticClassifierBase): @@ -174,15 +182,18 @@ class BCTSCalibration(RecalibratedProbabilisticClassifierBase): training set afterwards. Default value is 5. :param n_jobs: indicate the number of parallel workers (only when val_split is an integer) :param verbose: whether or not to display information in the standard output + :param random_state: controls the shuffling of the train/validation split when val_split is a float + (has no effect when val_split is an integer); pass an int for reproducible splits """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False, random_state=None): _, TempScaling, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = TempScaling(verbose=verbose, bias_positions='all') self.val_split = val_split self.n_jobs = n_jobs self.verbose = verbose + self.random_state = random_state class TSCalibration(RecalibratedProbabilisticClassifierBase): @@ -197,15 +208,18 @@ class TSCalibration(RecalibratedProbabilisticClassifierBase): training set afterwards. Default value is 5. :param n_jobs: indicate the number of parallel workers (only when val_split is an integer) :param verbose: whether or not to display information in the standard output + :param random_state: controls the shuffling of the train/validation split when val_split is a float + (has no effect when val_split is an integer); pass an int for reproducible splits """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False, random_state=None): _, TempScaling, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = TempScaling(verbose=verbose) self.val_split = val_split self.n_jobs = n_jobs self.verbose = verbose + self.random_state = random_state class VSCalibration(RecalibratedProbabilisticClassifierBase): @@ -220,15 +234,18 @@ class VSCalibration(RecalibratedProbabilisticClassifierBase): training set afterwards. Default value is 5. :param n_jobs: indicate the number of parallel workers (only when val_split is an integer) :param verbose: whether or not to display information in the standard output + :param random_state: controls the shuffling of the train/validation split when val_split is a float + (has no effect when val_split is an integer); pass an int for reproducible splits """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False, random_state=None): _, _, VectorScaling = _require_abstention_calibration() self.classifier = classifier self.calibrator = VectorScaling(verbose=verbose) self.val_split = val_split self.n_jobs = n_jobs self.verbose = verbose + self.random_state = random_state class TemperatureScalingFromLogits(BaseEstimator): diff --git a/quapy/tests/test_calibration.py b/quapy/tests/test_calibration.py index 4bdaae4..189272b 100644 --- a/quapy/tests/test_calibration.py +++ b/quapy/tests/test_calibration.py @@ -25,7 +25,7 @@ class TestCalibration(unittest.TestCase): def test_calibration_with_float_val_split(self): X, y = self.data.Xy - model = BCTSCalibration(LogisticRegression(max_iter=2000), val_split=0.3) + model = BCTSCalibration(LogisticRegression(max_iter=2000), val_split=0.3, random_state=0) model.fit(X, y) posteriors = model.predict_proba(X) self.assertEqual(posteriors.shape, (len(y), self.data.n_classes)) From 15511c0292e2fd59cbdbf5e66a185de8f5fd5b30 Mon Sep 17 00:00:00 2001 From: Alejandro Moreo Date: Mon, 20 Jul 2026 12:14:31 +0200 Subject: [PATCH 3/4] downgrade missing-bayes-deps notice to debug level Not having jax/numpyro/pystan installed is the expected, common case for a plain `pip install quapy`; warning about it every import just nags users who never asked for the bayes extra. Kept at debug level so it's still available to diagnose a broken/partial bayes install. --- quapy/method/_bayesian.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quapy/method/_bayesian.py b/quapy/method/_bayesian.py index d5d582f..04206e6 100644 --- a/quapy/method/_bayesian.py +++ b/quapy/method/_bayesian.py @@ -46,7 +46,10 @@ try: DEPENDENCIES_INSTALLED = True except ImportError as e: - logging.getLogger(__name__).warning(f'Bayesian dependencies failed to import: {e!r}') + # not installing the bayes extra is the common case (jax/numpyro/pystan are optional); this is + # only surfaced at debug level so it doesn't nag plain installs, but is still there to diagnose + # a broken/partial bayes install (e.g. pip install quapy[bayes] with a mismatched dependency). + logging.getLogger(__name__).debug(f'Bayesian dependencies failed to import: {e!r}') jax = None jnp = None jrandom = None From 34ab4d854d8a8a0d076883b0cfe0fb4ab589b506 Mon Sep 17 00:00:00 2001 From: Alejandro Moreo Date: Mon, 17 Aug 2026 15:06:30 +0200 Subject: [PATCH 4/4] Update version to 0.2.1.post1 --- quapy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quapy/__init__.py b/quapy/__init__.py index 182c39a..3cf5fc3 100644 --- a/quapy/__init__.py +++ b/quapy/__init__.py @@ -17,7 +17,7 @@ try: except ImportError: plot = None -__version__ = '0.2.1' +__version__ = '0.2.1.post1' def _default_cls():