Adapt the qunfold wrapper for composable methods to the changes between version 1.4 and the upcoming version 1.5

This commit is contained in:
Mirko Bunse 2025-07-16 14:51:47 +02:00
parent 3129187df8
commit 1612b5124c
3 changed files with 88 additions and 46 deletions

View File

@ -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

View File

@ -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,17 +28,20 @@ try:
CombinedLoss,
TikhonovRegularization,
TikhonovRegularized,
ClassTransformer, # transformers
HistogramTransformer,
DistanceTransformer,
KernelTransformer,
EnergyKernelTransformer,
LaplacianKernelTransformer,
GaussianKernelTransformer,
GaussianRFFKernelTransformer,
ClassRepresentation, # representations
HistogramRepresentation,
DistanceRepresentation,
KernelRepresentation,
EnergyKernelRepresentation,
LaplacianKernelRepresentation,
GaussianKernelRepresentation,
GaussianRFFKernelRepresentation,
)
except ImportError as e:
raise ImportError(_IMPORT_ERROR_MESSAGE) from e
__all__ = [ # control public members, e.g., for auto-documentation in sphinx; omit QuaPyWrapper
__all__ = [ # control public members, e.g., for auto-documentation in sphinx
"QUnfoldWrapper",
"ComposableQuantifier",
"CVClassifier",
"LeastSquaresLoss",
@ -41,26 +51,58 @@ try:
"CombinedLoss",
"TikhonovRegularization",
"TikhonovRegularized",
"ClassTransformer",
"HistogramTransformer",
"DistanceTransformer",
"KernelTransformer",
"EnergyKernelTransformer",
"LaplacianKernelTransformer",
"GaussianKernelTransformer",
"GaussianRFFKernelTransformer",
]
except ImportError as e:
raise ImportError(_import_error_message) from e
"ClassRepresentation",
"HistogramRepresentation",
"DistanceRepresentation",
"KernelRepresentation",
"EnergyKernelRepresentation",
"LaplacianKernelRepresentation",
"GaussianKernelRepresentation",
"GaussianRFFKernelRepresentation",
]
def ComposableQuantifier(loss, transformer, **kwargs):
@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))

View File

@ -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()))
)
),
]