From 1612b5124c31af65a79b64f14a6a939b269bf94a Mon Sep 17 00:00:00 2001 From: Mirko Bunse Date: Wed, 16 Jul 2025 14:51:47 +0200 Subject: [PATCH 1/2] Adapt the qunfold wrapper for composable methods to the changes between version 1.4 and the upcoming version 1.5 --- .github/workflows/ci.yml | 4 +- quapy/method/composable.py | 120 ++++++++++++++++++++++++------------ quapy/tests/test_methods.py | 10 +-- 3 files changed, 88 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17a6c39..fe752d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel - python -m pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" + python -m pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@main" python -m pip install -e .[bayes,tests] - name: Test with unittest run: python -m unittest @@ -47,7 +47,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel "jax[cpu]" - python -m pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" + python -m pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@main" python -m pip install -e .[neural,docs] - name: Build documentation run: sphinx-build -M html docs/source docs/build diff --git a/quapy/method/composable.py b/quapy/method/composable.py index 5d40aad..d4a7956 100644 --- a/quapy/method/composable.py +++ b/quapy/method/composable.py @@ -1,19 +1,26 @@ """This module allows the composition of quantification methods from loss functions and feature transformations. This functionality is realized through an integration of the qunfold package: https://github.com/mirkobunse/qunfold.""" -_import_error_message = """qunfold, the back-end of quapy.method.composable, is not properly installed. +from dataclasses import dataclass +from .base import BaseQuantifier + +# what to display when an ImportError is thrown +_IMPORT_ERROR_MESSAGE = """qunfold, the back-end of quapy.method.composable, is not properly installed. To fix this error, call: pip install --upgrade pip setuptools wheel pip install "jax[cpu]" - pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" + pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.5" """ +# try to import members of qunfold as members of this module try: import qunfold - from qunfold.quapy import QuaPyWrapper + from qunfold.base import BaseMixin + from qunfold.methods import AbstractMethod from qunfold.sklearn import CVClassifier from qunfold import ( + LinearMethod, # methods LeastSquaresLoss, # losses BlobelLoss, EnergyLoss, @@ -21,46 +28,81 @@ try: CombinedLoss, TikhonovRegularization, TikhonovRegularized, - ClassTransformer, # transformers - HistogramTransformer, - DistanceTransformer, - KernelTransformer, - EnergyKernelTransformer, - LaplacianKernelTransformer, - GaussianKernelTransformer, - GaussianRFFKernelTransformer, + ClassRepresentation, # representations + HistogramRepresentation, + DistanceRepresentation, + KernelRepresentation, + EnergyKernelRepresentation, + LaplacianKernelRepresentation, + GaussianKernelRepresentation, + GaussianRFFKernelRepresentation, ) - - __all__ = [ # control public members, e.g., for auto-documentation in sphinx; omit QuaPyWrapper - "ComposableQuantifier", - "CVClassifier", - "LeastSquaresLoss", - "BlobelLoss", - "EnergyLoss", - "HellingerSurrogateLoss", - "CombinedLoss", - "TikhonovRegularization", - "TikhonovRegularized", - "ClassTransformer", - "HistogramTransformer", - "DistanceTransformer", - "KernelTransformer", - "EnergyKernelTransformer", - "LaplacianKernelTransformer", - "GaussianKernelTransformer", - "GaussianRFFKernelTransformer", - ] except ImportError as e: - raise ImportError(_import_error_message) from e + raise ImportError(_IMPORT_ERROR_MESSAGE) from e -def ComposableQuantifier(loss, transformer, **kwargs): +__all__ = [ # control public members, e.g., for auto-documentation in sphinx + "QUnfoldWrapper", + "ComposableQuantifier", + "CVClassifier", + "LeastSquaresLoss", + "BlobelLoss", + "EnergyLoss", + "HellingerSurrogateLoss", + "CombinedLoss", + "TikhonovRegularization", + "TikhonovRegularized", + "ClassRepresentation", + "HistogramRepresentation", + "DistanceRepresentation", + "KernelRepresentation", + "EnergyKernelRepresentation", + "LaplacianKernelRepresentation", + "GaussianKernelRepresentation", + "GaussianRFFKernelRepresentation", +] + +@dataclass +class QUnfoldWrapper(BaseQuantifier,BaseMixin): + """A thin wrapper for using qunfold methods in QuaPy. + + Args: + _method: An instance of `qunfold.methods.AbstractMethod` to wrap. + + Examples: + Here, we wrap an instance of ACC to perform a grid search with QuaPy. + + >>> from qunfold import ACC + >>> qunfold_method = QUnfoldWrapper(ACC(RandomForestClassifier(obb_score=True))) + >>> quapy.model_selection.GridSearchQ( + >>> model = qunfold_method, + >>> param_grid = { # try both splitting criteria + >>> "representation__classifier__estimator__criterion": ["gini", "entropy"], + >>> }, + >>> # ... + >>> ) + """ + _method: AbstractMethod + def fit(self, data): # data is a qp.LabelledCollection + self._method.fit(*data.Xy, data.n_classes) + return self + def quantify(self, X): + return self._method.predict(X) + def set_params(self, **params): + self._method.set_params(**params) + return self + def get_params(self, deep=True): + return self._method.get_params(deep) + def __str__(self): + return self._method.__str__() + +def ComposableQuantifier(loss, representation, **kwargs): """A generic quantification / unfolding method that solves a linear system of equations. This class represents any quantifier that can be described in terms of a loss function, a feature transformation, and a regularization term. In this implementation, the loss is minimized through unconstrained second-order minimization. Valid probability estimates are ensured through a soft-max trick by Bunse (2022). Args: loss: An instance of a loss class from `quapy.methods.composable`. - transformer: An instance of a transformer class from `quapy.methods.composable`. + representation: An instance of a representation class from `quapy.methods.composable`. solver (optional): The `method` argument in `scipy.optimize.minimize`. Defaults to `"trust-ncg"`. solver_options (optional): The `options` argument in `scipy.optimize.minimize`. Defaults to `{"gtol": 1e-8, "maxiter": 1000}`. seed (optional): A random number generator seed from which a numpy RandomState is created. Defaults to `None`. @@ -72,12 +114,12 @@ def ComposableQuantifier(loss, transformer, **kwargs): >>> ComposableQuantifier, >>> TikhonovRegularized, >>> LeastSquaresLoss, - >>> ClassTransformer, + >>> ClassRepresentation, >>> ) >>> from sklearn.ensemble import RandomForestClassifier >>> o_acc = ComposableQuantifier( >>> TikhonovRegularized(LeastSquaresLoss(), 0.01), - >>> ClassTransformer(RandomForestClassifier(oob_score=True)) + >>> ClassRepresentation(RandomForestClassifier(oob_score=True)) >>> ) Here, we perform hyper-parameter optimization with the ordinal ACC. @@ -85,7 +127,7 @@ def ComposableQuantifier(loss, transformer, **kwargs): >>> quapy.model_selection.GridSearchQ( >>> model = o_acc, >>> param_grid = { # try both splitting criteria - >>> "transformer__classifier__estimator__criterion": ["gini", "entropy"], + >>> "representation__classifier__estimator__criterion": ["gini", "entropy"], >>> }, >>> # ... >>> ) @@ -96,7 +138,7 @@ def ComposableQuantifier(loss, transformer, **kwargs): >>> from sklearn.linear_model import LogisticRegression >>> acc_lr = ComposableQuantifier( >>> LeastSquaresLoss(), - >>> ClassTransformer(CVClassifier(LogisticRegression(), 10)) + >>> ClassRepresentation(CVClassifier(LogisticRegression(), 10)) >>> ) """ - return QuaPyWrapper(qunfold.GenericMethod(loss, transformer, **kwargs)) + return QUnfoldWrapper(LinearMethod(loss, representation, **kwargs)) diff --git a/quapy/tests/test_methods.py b/quapy/tests/test_methods.py index cf5bf39..18e7f52 100644 --- a/quapy/tests/test_methods.py +++ b/quapy/tests/test_methods.py @@ -14,20 +14,20 @@ from quapy.method.composable import ( ComposableQuantifier, LeastSquaresLoss, HellingerSurrogateLoss, - ClassTransformer, - HistogramTransformer, + ClassRepresentation, + HistogramRepresentation, CVClassifier, ) COMPOSABLE_METHODS = [ ComposableQuantifier( # ACC LeastSquaresLoss(), - ClassTransformer(CVClassifier(LogisticRegression())) + ClassRepresentation(CVClassifier(LogisticRegression())) ), ComposableQuantifier( # HDy HellingerSurrogateLoss(), - HistogramTransformer( + HistogramRepresentation( 3, # 3 bins per class - preprocessor = ClassTransformer(CVClassifier(LogisticRegression())) + preprocessor = ClassRepresentation(CVClassifier(LogisticRegression())) ) ), ] From 208599003d1542c0cd0b47335723f0b46d446c44 Mon Sep 17 00:00:00 2001 From: Mirko Bunse Date: Thu, 17 Jul 2025 10:56:50 +0200 Subject: [PATCH 2/2] Reflect the adaptation of the qunfold wrapper also in the documentation --- docs/source/manuals/methods.md | 29 ++++++++++++++++++----------- examples/14.composable_methods.py | 26 +++++++++++++------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/docs/source/manuals/methods.md b/docs/source/manuals/methods.md index 1a9a2dc..9a02179 100644 --- a/docs/source/manuals/methods.md +++ b/docs/source/manuals/methods.md @@ -447,7 +447,7 @@ The [](quapy.method.composable) module allows the composition of quantification ```sh pip install --upgrade pip setuptools wheel pip install "jax[cpu]" -pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" +pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.5" ``` ### Basics @@ -455,9 +455,16 @@ pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" The composition of a method is implemented through the [](quapy.method.composable.ComposableQuantifier) class. Its documentation also features an example to get you started in composing your own methods. ```python +from quapy.method.composable import ( + ComposableQuantifier, + TikhonovRegularized, + LeastSquaresLoss, + ClassRepresentation, +) + ComposableQuantifier( # ordinal ACC, as proposed by Bunse et al., 2022 - TikhonovRegularized(LeastSquaresLoss(), 0.01), - ClassTransformer(RandomForestClassifier(oob_score=True)) + TikhonovRegularized(LeastSquaresLoss(), 0.01), + ClassRepresentation(RandomForestClassifier(oob_score=True)) ) ``` @@ -484,16 +491,16 @@ You can use the [](quapy.method.composable.CombinedLoss) to create arbitrary, we ### Feature transformations -- [](quapy.method.composable.ClassTransformer) -- [](quapy.method.composable.DistanceTransformer) -- [](quapy.method.composable.HistogramTransformer) -- [](quapy.method.composable.EnergyKernelTransformer) -- [](quapy.method.composable.GaussianKernelTransformer) -- [](quapy.method.composable.LaplacianKernelTransformer) -- [](quapy.method.composable.GaussianRFFKernelTransformer) +- [](quapy.method.composable.ClassRepresentation) +- [](quapy.method.composable.DistanceRepresentation) +- [](quapy.method.composable.HistogramRepresentation) +- [](quapy.method.composable.EnergyKernelRepresentation) +- [](quapy.method.composable.GaussianKernelRepresentation) +- [](quapy.method.composable.LaplacianKernelRepresentation) +- [](quapy.method.composable.GaussianRFFKernelRepresentation) ```{hint} -The [](quapy.method.composable.ClassTransformer) requires the classifier to have a property `oob_score==True` and to produce a property `oob_decision_function` during fitting. In [scikit-learn](https://scikit-learn.org/), this requirement is fulfilled by any bagging classifier, such as random forests. Any other classifier needs to be cross-validated through the [](quapy.method.composable.CVClassifier). +The [](quapy.method.composable.ClassRepresentation) requires the classifier to have a property `oob_score==True` and to produce a property `oob_decision_function` during fitting. In [scikit-learn](https://scikit-learn.org/), this requirement is fulfilled by any bagging classifier, such as random forests. Any other classifier needs to be cross-validated through the [](quapy.method.composable.CVClassifier). ``` diff --git a/examples/14.composable_methods.py b/examples/14.composable_methods.py index 5ffcb94..e8340d4 100644 --- a/examples/14.composable_methods.py +++ b/examples/14.composable_methods.py @@ -1,6 +1,6 @@ """ This example illustrates the composition of quantification methods from -arbitrary loss functions and feature transformations. It will extend the basic +arbitrary loss functions and feature representations. It will extend the basic example on the usage of quapy with this composition. This example requires the installation of qunfold, the back-end of QuaPy's @@ -8,7 +8,7 @@ composition module: pip install --upgrade pip setuptools wheel pip install "jax[cpu]" - pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" + pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.5" """ import numpy as np @@ -24,20 +24,20 @@ data = qp.data.preprocessing.text2tfidf( training, testing = data.train_test # We start by recovering PACC from its building blocks, a LeastSquaresLoss and -# a probabilistic ClassTransformer. A 5-fold cross-validation is implemented +# a probabilistic ClassRepresentation. A 5-fold cross-validation is implemented # through a CVClassifier. from quapy.method.composable import ( ComposableQuantifier, LeastSquaresLoss, - ClassTransformer, + ClassRepresentation, CVClassifier, ) from sklearn.linear_model import LogisticRegression pacc = ComposableQuantifier( LeastSquaresLoss(), - ClassTransformer( + ClassRepresentation( CVClassifier(LogisticRegression(random_state=0), 5), is_probabilistic = True ), @@ -63,7 +63,7 @@ from quapy.method.composable import HellingerSurrogateLoss model = ComposableQuantifier( HellingerSurrogateLoss(), # the loss is different from before - ClassTransformer( # we use the same transformer + ClassRepresentation( # we use the same representation CVClassifier(LogisticRegression(random_state=0), 5), is_probabilistic = True ), @@ -79,7 +79,7 @@ absolute_errors = qp.evaluation.evaluate( print(f"MAE = {np.mean(absolute_errors):.4f}+-{np.std(absolute_errors):.4f}") # In general, any composed method solves a linear system of equations by -# minimizing the loss after transforming the data. Methods of this kind include +# minimizing the loss after representing the data. Methods of this kind include # ACC, PACC, HDx, HDy, and many other well-known methods, as well as an # unlimited number of re-combinations of their building blocks. @@ -93,18 +93,18 @@ from quapy.method.composable import CombinedLoss model = ComposableQuantifier( CombinedLoss(HellingerSurrogateLoss(), LeastSquaresLoss()), - ClassTransformer( + ClassRepresentation( CVClassifier(LogisticRegression(random_state=0), 5), is_probabilistic = True ), ) -from qunfold.quapy import QuaPyWrapper -from qunfold import GenericMethod +from quapy.method.composable import QUnfoldWrapper +from qunfold import LinearMethod -model = QuaPyWrapper(GenericMethod( +model = QUnfoldWrapper(LinearMethod( CombinedLoss(HellingerSurrogateLoss(), LeastSquaresLoss()), - ClassTransformer( + ClassRepresentation( CVClassifier(LogisticRegression(random_state=0), 5), is_probabilistic = True ), @@ -115,7 +115,7 @@ model = QuaPyWrapper(GenericMethod( param_grid = { "loss__weights": [ (w, 1-w) for w in [.1, .5, .9] ], - "transformer__classifier__estimator__C": [1e-1, 1e1], + "representation__classifier__estimator__C": [1e-1, 1e1], } grid_search = qp.model_selection.GridSearchQ(