From 2cc22371654e91156385afa6d93c3d2ec1d08ce7 Mon Sep 17 00:00:00 2001 From: Alejandro Moreo Date: Fri, 10 Jul 2026 16:14:45 +0200 Subject: [PATCH] EDy and EDx as separate methods with shared backbone functionals --- .../_modules/quapy/method/aggregative.html | 113 +++++++++++++++++- .../quapy/method/non_aggregative.html | 77 ++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index 09ac4fa..abfac5c 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -388,6 +388,7 @@ from quapy.classification.svmperf import SVMperf from quapy.data import LabelledCollection from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._energy import _EnergyDistanceCore from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, @@ -2279,6 +2280,115 @@ +
+[docs] +class EDy(_EnergyDistanceCore, AggregativeSoftQuantifier): + """ + Energy Distance y (EDy), a posterior-space distribution-matching quantifier + based on energy distance. + + The method represents each class by the posterior-probability vectors + produced by a probabilistic classifier on validation data, and estimates the + test prevalence vector by matching the test posterior distribution against + the class-conditional validation distributions through an energy-distance + objective solved as a quadratic program. The method is therefore another + instance of the general mixture-matching view of quantification, but it + operates directly on posterior vectors rather than on histogram summaries. + + This implementation works for binary and multiclass single-label + quantification and relies on the optional ``quadprog`` dependency. It was + adapted to QuaPy's current aggregative API from the original implementation + available in `quantificationlib <https://github.com/AICGijon/quantificationlib>`_, + and now shares its numerical core with the classifier-free + :class:`quapy.method.non_aggregative.EDx` variant. + + The current implementation follows the energy-distance formulation discussed + in: + + * Alberto Castaño, Laura Morán-Fernández, Jaime Alonso, + Verónica Bolón-Canedo, Amparo Alonso-Betanzos, and Juan José del Coz. + *An analysis of quantification methods based on matching distributions*. + * Hideko Kawakubo, Marthinus Christoffel du Plessis, and Masashi Sugiyama + (2016). *Computationally efficient class-prior estimation under class + balance change using energy distance*. IEICE Transactions on Information + and Systems, 99(1):176-186. + + :param classifier: a scikit-learn ``BaseEstimator``, or ``None`` to use + ``qp.environ['DEFAULT_CLS']`` + :param fit_classifier: whether to train the learner (default ``True``). + Set to ``False`` if the learner has already been trained outside the + quantifier + :param val_split: specification of the data used for generating validation + posterior probabilities. This can be an integer (default ``5``) for + k-fold cross-validation, a float in ``(0, 1)`` for a held-out split, + or a tuple ``(X, y)`` with explicit validation data + :param distance: distance used to compare posterior vectors. Valid string + aliases are ``'manhattan'`` (default) and ``'euclidean'``; a custom + callable compatible with pairwise-distance signatures can also be used + :param n_jobs: number of parallel workers (default ``None``, meaning the + value is taken from the environment) + """ + + def __init__( + self, + classifier: BaseEstimator = None, + fit_classifier: bool = True, + val_split=5, + distance: Union[str, Callable] = 'manhattan', + n_jobs=None, + ): + super().__init__(classifier, fit_classifier, val_split) + self.distance = distance + self.n_jobs = qp._get_njobs(n_jobs) + self.train_n_cls_i_ = None + self.train_distrib_ = None + self.K_ = None + self.G_ = None + self.C_ = None + self.b_ = None + self.a_ = None + + def _check_init_parameters(self): + self._check_ed_init_parameters() + +
+[docs] + def aggregation_fit(self, classif_predictions, labels): + """ + Estimate the class-conditional posterior distributions on validation + data and pre-compute the quadratic-program parameters that depend only + on the training side. + + In EDy, the validation posteriors are not discretized into histograms. + Instead, each class is represented by the cloud of posterior vectors + observed for that class, and these clouds are then compared through the + selected pairwise distance. + + :param classif_predictions: posterior probabilities returned by the + classifier on validation data + :param labels: true labels associated to each posterior vector + """ + posteriors = np.asarray(classif_predictions, dtype=float) + labels = np.asarray(labels) + train_distrib = [posteriors[labels == class_] for class_ in self.classes_] + return self._fit_energy_model(train_distrib)
+ + +
+[docs] + def aggregate(self, posteriors: np.ndarray): + """Estimate the prevalence vector for a test sample. + + :param posteriors: posterior probabilities returned by the classifier + for the instances in the test sample + :return: a prevalence vector of shape ``(n_classes,)`` + """ + posteriors = np.asarray(posteriors, dtype=float) + return self._predict_energy(posteriors)
+
+ + + # --------------------------------------------------------------- # imports # --------------------------------------------------------------- @@ -2297,9 +2407,6 @@ KDEyHD = _kdey.KDEyHD KDEyCS = _kdey.KDEyCS -from . import _edy - -EDy = _edy.EDy # --------------------------------------------------------------- # aliases diff --git a/docs/build/html/_modules/quapy/method/non_aggregative.html b/docs/build/html/_modules/quapy/method/non_aggregative.html index 813b2c7..f403508 100644 --- a/docs/build/html/_modules/quapy/method/non_aggregative.html +++ b/docs/build/html/_modules/quapy/method/non_aggregative.html @@ -382,9 +382,11 @@ from quapy.functional import get_divergence from quapy.method.base import BaseQuantifier, BinaryQuantifier from quapy.method._helper import _labels_to_indices +from quapy.method._energy import _EnergyDistanceCore import quapy.functional as F from scipy.optimize import lsq_linear from scipy import sparse +import quapy as qp
@@ -553,6 +555,79 @@ +
+[docs] +class EDx(_EnergyDistanceCore, BaseQuantifier): + """ + Energy Distance x (EDx), a covariate-space distribution-matching + quantifier based on energy distance. + + EDx is the classifier-free counterpart of :class:`quapy.method.aggregative.EDy`. + Instead of representing each class through posterior-probability vectors, it + represents each class by the cloud of raw feature vectors observed in the + training set and estimates the test prevalence vector by solving the same + energy-distance quadratic program directly in feature space. + + This implementation works for binary and multiclass single-label + quantification and relies on the optional ``quadprog`` dependency. The + current QuaPy adaptation shares its numerical core with EDy and keeps + credit to the original implementation available in + `quantificationlib <https://github.com/AICGijon/quantificationlib>`_. + + The formulation follows the same references as EDy, namely: + + * Alberto Castaño, Laura Morán-Fernández, Jaime Alonso, + Verónica Bolón-Canedo, Amparo Alonso-Betanzos, and Juan José del Coz. + *An analysis of quantification methods based on matching distributions*. + * Hideko Kawakubo, Marthinus Christoffel du Plessis, and Masashi Sugiyama + (2016). *Computationally efficient class-prior estimation under class + balance change using energy distance*. IEICE Transactions on Information + and Systems, 99(1):176-186. + + :param distance: distance used to compare feature vectors. Valid string + aliases are ``'manhattan'`` (default) and ``'euclidean'``; a custom + callable compatible with pairwise-distance signatures can also be used + :param n_jobs: number of parallel workers (default ``None``, meaning the + value is taken from the environment) + """ + + def __init__(self, distance: Union[str, Callable] = 'manhattan', n_jobs=None): + self.distance = distance + self.n_jobs = qp._get_njobs(n_jobs) + self.classes_ = None + self.n_features_in_ = None + self.train_distrib_ = None + self.train_n_cls_i_ = None + self.K_ = None + self.G_ = None + self.C_ = None + self.b_ = None + self.a_ = None + +
+[docs] + def fit(self, X, y): + """Fit class-conditional feature-space distributions from training data.""" + self._check_ed_init_parameters() + labels = np.asarray(y) + self.classes_ = np.unique(labels) + self.n_features_in_ = X.shape[1] + train_distrib = [X[labels == class_] for class_ in self.classes_] + return self._fit_energy_model(train_distrib)
+ + +
+[docs] + def predict(self, X): + """Estimate class prevalences for a test sample of raw instances.""" + assert X.shape[1] == self.n_features_in_, ( + f'wrong shape; expected {self.n_features_in_}, found {X.shape[1]}' + ) + return self._predict_energy(X)
+
+ + +
[docs] class ReadMe(BaseQuantifier, WithConfidenceABC): @@ -741,8 +816,10 @@ # aliases #--------------------------------------------------------------- + HDx = DMx.HDx DistributionMatchingX = DMx +EnergyDistanceX = EDx HellingerDistanceX = HDx