diff --git a/docs/build/html/_modules/index.html b/docs/build/html/_modules/index.html index 10f0751..8a6f07e 100644 --- a/docs/build/html/_modules/index.html +++ b/docs/build/html/_modules/index.html @@ -29,7 +29,7 @@ - + @@ -383,6 +383,7 @@
  • quapy.method._threshold_optim
  • quapy.method.aggregative
  • quapy.method.base
  • +
  • quapy.method.composable
  • quapy.method.confidence
  • quapy.method.meta
  • quapy.method.non_aggregative
  • @@ -390,7 +391,9 @@
  • quapy.plot
  • quapy.protocol
  • quapy.util
  • -
  • sklearn.utils._metadata_requests
  • +
  • qunfold.methods.linear.losses
  • +
  • qunfold.methods.linear.representations
  • +
  • qunfold.sklearn
  • diff --git a/docs/build/html/_modules/quapy/classification/calibration.html b/docs/build/html/_modules/quapy/classification/calibration.html index 1eac806..3224d17 100644 --- a/docs/build/html/_modules/quapy/classification/calibration.html +++ b/docs/build/html/_modules/quapy/classification/calibration.html @@ -1,86 +1,382 @@ - - - - - - quapy.classification.calibration — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.classification.calibration — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    + +

    Source code for quapy.classification.calibration

    +from copy import deepcopy
    +
    +from sklearn.base import BaseEstimator, clone
    +from sklearn.model_selection import cross_val_predict, train_test_split
    +from sklearn.preprocessing import LabelEncoder
    +from sklearn.utils.validation import check_X_y
    +import numpy as np
     
     
     # Wrappers of calibration defined by Alexandari et al. in paper <http://proceedings.mlr.press/v119/alexandari20a.html>
    @@ -88,20 +384,20 @@
     # see https://github.com/kundajelab/abstention
     
     
    -def _require_abstention_calibration():
    +def _require_abstention_calibration():
         try:
    -        from abstention.calibration import NoBiasVectorScaling, TempScaling, VectorScaling
    +        from abstention.calibration import NoBiasVectorScaling, TempScaling, VectorScaling
         except ImportError as exc:
             raise ImportError(
                 "Calibration methods in quapy.classification.calibration require the optional "
                 "'abstention' package."
    -        ) from exc
    +        ) from exc
         return NoBiasVectorScaling, TempScaling, VectorScaling
     
     
     
    [docs] -class RecalibratedProbabilisticClassifier: +class RecalibratedProbabilisticClassifier: """ Abstract class for (re)calibration method from `abstention.calibration`, as defined in `Alexandari, A., Kundaje, A., & Shrikumar, A. (2020, November). Maximum likelihood with bias-corrected calibration @@ -114,7 +410,7 @@
    [docs] -class RecalibratedProbabilisticClassifierBase(BaseEstimator, RecalibratedProbabilisticClassifier): +class RecalibratedProbabilisticClassifierBase(BaseEstimator, RecalibratedProbabilisticClassifier): """ Applies a (re)calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_. @@ -130,7 +426,7 @@ :param verbose: whether or not to display information in the standard output """ - 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): self.classifier = classifier self.calibrator = calibrator self.val_split = val_split @@ -139,7 +435,7 @@
    [docs] - def fit(self, X, y): + def fit(self, X, y): """ Fits the calibration for the probabilistic classifier. @@ -160,7 +456,7 @@
    [docs] - def fit_cv(self, X, y): + def fit_cv(self, X, y): """ Fits the calibration in a cross-validation manner, i.e., it generates posterior probabilities for all training instances via cross-validation, and then retrains the classifier on all training instances. @@ -181,7 +477,7 @@
    [docs] - def fit_tr_val(self, X, y): + def fit_tr_val(self, X, y): """ Fits the calibration in a train/val-split manner, i.e.t, it partitions the training instances into a training and a validation set, and then uses the training samples to learn classifier which is then used @@ -202,7 +498,7 @@
    [docs] - def predict(self, X): + def predict(self, X): """ Predicts class labels for the data instances in `X` @@ -214,7 +510,7 @@
    [docs] - def predict_proba(self, X): + def predict_proba(self, X): """ Generates posterior probabilities for the data instances in `X` @@ -226,7 +522,7 @@ @property - def classes_(self): + def classes_(self): """ Returns the classes on which the classifier has been trained on @@ -238,7 +534,7 @@
    [docs] -class NBVSCalibration(RecalibratedProbabilisticClassifierBase): +class NBVSCalibration(RecalibratedProbabilisticClassifierBase): """ Applies the No-Bias Vector Scaling (NBVS) calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_: @@ -252,7 +548,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): NoBiasVectorScaling, _, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = NoBiasVectorScaling(verbose=verbose) @@ -264,7 +560,7 @@
    [docs] -class BCTSCalibration(RecalibratedProbabilisticClassifierBase): +class BCTSCalibration(RecalibratedProbabilisticClassifierBase): """ Applies the Bias-Corrected Temperature Scaling (BCTS) calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_: @@ -278,7 +574,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): _, TempScaling, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = TempScaling(verbose=verbose, bias_positions='all') @@ -290,7 +586,7 @@
    [docs] -class TSCalibration(RecalibratedProbabilisticClassifierBase): +class TSCalibration(RecalibratedProbabilisticClassifierBase): """ Applies the Temperature Scaling (TS) calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_: @@ -304,7 +600,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): _, TempScaling, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = TempScaling(verbose=verbose) @@ -316,7 +612,7 @@
    [docs] -class VSCalibration(RecalibratedProbabilisticClassifierBase): +class VSCalibration(RecalibratedProbabilisticClassifierBase): """ Applies the Vector Scaling (VS) calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_: @@ -330,7 +626,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): _, _, VectorScaling = _require_abstention_calibration() self.classifier = classifier self.calibrator = VectorScaling(verbose=verbose) @@ -342,7 +638,7 @@
    [docs] -class TemperatureScalingFromLogits(BaseEstimator): +class TemperatureScalingFromLogits(BaseEstimator): """ Calibrates a matrix of logits by learning a temperature-scaling mapping with the calibration methods from `abstention.calibration`. @@ -357,13 +653,13 @@ information """ - def __init__(self, bias_corrected=False, verbose=False): + def __init__(self, bias_corrected=False, verbose=False): self.bias_corrected = bias_corrected self.verbose = verbose
    [docs] - def fit(self, X, y): + def fit(self, X, y): """ Fits the logits calibrator. @@ -398,7 +694,7 @@
    [docs] - def predict_proba(self, X): + def predict_proba(self, X): """ Converts logits into calibrated posterior probabilities. @@ -412,7 +708,7 @@
    [docs] - def predict(self, X): + def predict(self, X): """ Predicts class labels after calibration. @@ -427,31 +723,75 @@
    -
    + + + + + + +
    + +
    +
    +
    + +
    + + + +
    -
    - -
    - -
    -

    © Copyright 2024, Alejandro Moreo.

    +
    + +
    + + +
    + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + -
    - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/classification/methods.html b/docs/build/html/_modules/quapy/classification/methods.html index d921a91..08e9987 100644 --- a/docs/build/html/_modules/quapy/classification/methods.html +++ b/docs/build/html/_modules/quapy/classification/methods.html @@ -1,88 +1,384 @@ - - - - - - quapy.classification.methods — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.classification.methods — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    +

    Source code for quapy.classification.methods

    -import numpy as np
    -from sklearn.base import BaseEstimator
    -from sklearn.decomposition import TruncatedSVD
    -from sklearn.linear_model import LogisticRegression
    +import numpy as np
    +from sklearn.base import BaseEstimator
    +from sklearn.decomposition import TruncatedSVD
    +from sklearn.linear_model import LogisticRegression
     
     
     
    [docs] -class LowRankLogisticRegression(BaseEstimator): +class LowRankLogisticRegression(BaseEstimator): """ An example of a classification method (i.e., an object that implements `fit`, `predict`, and `predict_proba`) that also generates embedded inputs (i.e., that implements `transform`), as those required for @@ -96,13 +392,13 @@ `Logistic Regression <https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html>`__ classifier """ - def __init__(self, n_components=100, **kwargs): + def __init__(self, n_components=100, **kwargs): self.n_components = n_components self.classifier = LogisticRegression(**kwargs)
    [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator. @@ -115,7 +411,7 @@
    [docs] - def set_params(self, **params): + def set_params(self, **params): """ Set the parameters of this estimator. @@ -132,7 +428,7 @@
    [docs] - def fit(self, X, y): + def fit(self, X, y): """ Fit the model according to the given training data. The fit consists of fitting `TruncatedSVD` and then `LogisticRegression` on the low-rank representation. @@ -153,7 +449,7 @@
    [docs] - def predict(self, X): + def predict(self, X): """ Predicts labels for the instances `X` embedded into the low-rank space. @@ -167,7 +463,7 @@
    [docs] - def predict_proba(self, X): + def predict_proba(self, X): """ Predicts posterior probabilities for the instances `X` embedded into the low-rank space. @@ -180,7 +476,7 @@
    [docs] - def transform(self, X): + def transform(self, X): """ Returns the low-rank approximation of `X` with `n_components` dimensions, or `X` unaltered if `n_components` >= `X.shape[1]`. @@ -197,7 +493,7 @@
    [docs] -class MockClassifierFromPosteriors(BaseEstimator): +class MockClassifierFromPosteriors(BaseEstimator): """ Mock classifier that bypasses classifier training when the input instances are already posterior probabilities produced by a pretrained probabilistic @@ -208,50 +504,94 @@
    [docs] - def fit(self, X, y): + def fit(self, X, y): self.classes_ = np.sort(np.unique(y)) return self
    [docs] - def predict(self, X): + def predict(self, X): return np.argmax(X, axis=1)
    [docs] - def predict_proba(self, X): + def predict_proba(self, X): return X
    -
    + + + + + + +
    + +
    +
    +
    + +
    + + + +
    -
    - -
    - -
    -

    © Copyright 2024, Alejandro Moreo.

    +
    + +
    + + +
    + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + -
    - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/classification/neural.html b/docs/build/html/_modules/quapy/classification/neural.html index e5788fa..871d391 100644 --- a/docs/build/html/_modules/quapy/classification/neural.html +++ b/docs/build/html/_modules/quapy/classification/neural.html @@ -29,7 +29,7 @@ - + @@ -370,26 +370,27 @@

    Source code for quapy.classification.neural

    -import os
    -from abc import ABCMeta, abstractmethod
    -from pathlib import Path
    +import logging
    +import os
    +from abc import ABCMeta, abstractmethod
    +from pathlib import Path
     
    -import numpy as np
    -import torch
    -import torch.nn as nn
    -import torch.nn.functional as F
    -from sklearn.metrics import accuracy_score, f1_score
    -from torch.nn.utils.rnn import pad_sequence
    -from tqdm import tqdm
    +import numpy as np
    +import torch
    +import torch.nn as nn
    +import torch.nn.functional as F
    +from sklearn.metrics import accuracy_score, f1_score
    +from torch.nn.utils.rnn import pad_sequence
    +from tqdm import tqdm
     
    -import quapy as qp
    -from quapy.data import LabelledCollection
    -from quapy.util import EarlyStop
    +import quapy as qp
    +from quapy.data import LabelledCollection
    +from quapy.util import EarlyStop
     
     
     
    [docs] -class NeuralClassifierTrainer: +class NeuralClassifierTrainer: """ Trains a neural network for text classification. @@ -407,7 +408,7 @@ according to the evaluation in the held-out validation split (default '../checkpoint/classifier_net.dat') """ - def __init__(self, + def __init__(self, net: 'TextClassifierNet', lr=1e-3, weight_decay=0, @@ -416,7 +417,7 @@ batch_size=64, batch_size_test=512, padding_length=300, - device='cuda', + device='cpu', checkpointpath='../checkpoint/classifier_net.dat'): super().__init__() @@ -437,12 +438,12 @@ self.learner_hyperparams = self.net.get_params() self.checkpointpath = checkpointpath - print(f'[NeuralNetwork running on {device}]') + logging.getLogger(__name__).info(f'NeuralNetwork running on {device}') os.makedirs(Path(checkpointpath).parent, exist_ok=True)
    [docs] - def reset_net_params(self, vocab_size, n_classes): + def reset_net_params(self, vocab_size, n_classes): """Reinitialize the network parameters :param vocab_size: the size of the vocabulary @@ -455,7 +456,7 @@
    [docs] - def get_params(self): + def get_params(self): """Get hyper-parameters for this estimator :return: a dictionary with parameter names mapped to their values @@ -465,7 +466,7 @@
    [docs] - def set_params(self, **params): + def set_params(self, **params): """Set the parameters of this trainer and the learner it is training. In this current version, parameter names for the trainer and learner should be disjoint. @@ -491,14 +492,14 @@ @property - def device(self): + def device(self): """ Gets the device in which the network is allocated :return: device """ return next(self.net.parameters()).device - def _train_epoch(self, data, status, pbar, epoch): + def _train_epoch(self, data, status, pbar, epoch): self.net.train() criterion = torch.nn.CrossEntropyLoss() losses, predictions, true_labels = [], [], [] @@ -518,7 +519,7 @@ status["f1"] = f1_score(true_labels, predictions, average='macro') self.__update_progress_bar(pbar, epoch) - def _test_epoch(self, data, status, pbar, epoch): + def _test_epoch(self, data, status, pbar, epoch): self.net.eval() criterion = torch.nn.CrossEntropyLoss() losses, predictions, true_labels = [], [], [] @@ -536,7 +537,7 @@ status["f1"] = f1_score(true_labels, predictions, average='macro') self.__update_progress_bar(pbar, epoch) - def __update_progress_bar(self, pbar, epoch): + def __update_progress_bar(self, pbar, epoch): pbar.set_description(f'[{self.net.__class__.__name__}] training epoch={epoch} ' f'tr-loss={self.status["tr"]["loss"]:.5f} ' f'tr-acc={100 * self.status["tr"]["acc"]:.2f}% ' @@ -548,7 +549,7 @@
    [docs] - def fit(self, instances, labels, val_split=0.3): + def fit(self, instances, labels, val_split=0.3): """ Fits the model according to the given training data. @@ -583,21 +584,22 @@ if self.early_stop.IMPROVED: torch.save(self.net.state_dict(), checkpoint) elif self.early_stop.STOP: - print(f'training ended by patience exhasted; loading best model parameters in {checkpoint} ' - f'for epoch {self.early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {self.early_stop.best_epoch}') self.net.load_state_dict(torch.load(checkpoint)) break - print('performing one training pass over the validation set...') + logging.getLogger(__name__).info('performing one training pass over the validation set...') self._train_epoch(valid_generator, self.status['tr'], pbar, epoch=0) - print('[done]') + logging.getLogger(__name__).info('done') return self
    [docs] - def predict(self, instances): + def predict(self, instances): """ Predicts labels for the instances @@ -610,7 +612,7 @@
    [docs] - def predict_proba(self, instances): + def predict_proba(self, instances): """ Predicts posterior probabilities for the instances @@ -629,7 +631,7 @@
    [docs] - def transform(self, instances): + def transform(self, instances): """ Returns the embeddings of the instances @@ -651,7 +653,7 @@
    [docs] -class TorchDataset(torch.utils.data.Dataset): +class TorchDataset(torch.utils.data.Dataset): """ Transforms labelled instances into a Torch's :class:`torch.utils.data.DataLoader` object @@ -659,19 +661,19 @@ :param labels: array-like of shape `(n_samples, n_classes)` with the class labels """ - def __init__(self, instances, labels=None): + def __init__(self, instances, labels=None): self.instances = instances self.labels = labels - def __len__(self): + def __len__(self): return len(self.instances) - def __getitem__(self, index): + def __getitem__(self, index): return {'doc': self.instances[index], 'label': self.labels[index] if self.labels is not None else None}
    [docs] - def asDataloader(self, batch_size, shuffle, pad_length, device): + def asDataloader(self, batch_size, shuffle, pad_length, device): """ Converts the labelled collection into a Torch DataLoader with dynamic padding for the batch @@ -684,7 +686,7 @@ :param device: whether to allocate tensors in cpu or in cuda :return: a :class:`torch.utils.data.DataLoader` object """ - def collate(batch): + def collate(batch): data = [torch.LongTensor(item['doc'][:pad_length]) for item in batch] data = pad_sequence(data, batch_first=True, padding_value=qp.environ['PAD_INDEX']).to(device) targets = [item['label'] for item in batch] @@ -702,7 +704,7 @@
    [docs] -class TextClassifierNet(torch.nn.Module, metaclass=ABCMeta): +class TextClassifierNet(torch.nn.Module, metaclass=ABCMeta): """ Abstract Text classifier (`torch.nn.Module`) """ @@ -710,7 +712,7 @@
    [docs] @abstractmethod - def document_embedding(self, x): + def document_embedding(self, x): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -725,7 +727,7 @@
    [docs] - def forward(self, x): + def forward(self, x): """Performs the forward pass. :param x: a batch of instances, typically generated by a torch's `DataLoader` @@ -739,7 +741,7 @@
    [docs] - def dimensions(self): + def dimensions(self): """Gets the number of dimensions of the embedding space :return: integer @@ -749,7 +751,7 @@
    [docs] - def predict_proba(self, x): + def predict_proba(self, x): """ Predicts posterior probabilities for the instances in `x` @@ -764,7 +766,7 @@
    [docs] - def xavier_uniform(self): + def xavier_uniform(self): """ Performs Xavier initialization of the network parameters """ @@ -776,7 +778,7 @@
    [docs] @abstractmethod - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -786,7 +788,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary @@ -798,7 +800,7 @@
    [docs] -class LSTMnet(TextClassifierNet): +class LSTMnet(TextClassifierNet): """ An implementation of :class:`quapy.classification.neural.TextClassifierNet` based on Long Short Term Memory networks. @@ -812,7 +814,7 @@ :param drop_p: drop probability for dropout (default 0.5) """ - def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, lstm_class_nlayers=1, + def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, lstm_class_nlayers=1, drop_p=0.5): super().__init__() @@ -834,7 +836,7 @@ self.doc_embedder = torch.nn.Linear(hidden_size, self.dim) self.output = torch.nn.Linear(self.dim, n_classes) - def __init_hidden(self, set_size): + def __init_hidden(self, set_size): opt = self.hyperparams var_hidden = torch.zeros(opt['lstm_class_nlayers'], set_size, opt['hidden_size']) var_cell = torch.zeros(opt['lstm_class_nlayers'], set_size, opt['hidden_size']) @@ -844,7 +846,7 @@
    [docs] - def document_embedding(self, x): + def document_embedding(self, x): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -863,7 +865,7 @@
    [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -873,7 +875,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary @@ -885,7 +887,7 @@
    [docs] -class CNNnet(TextClassifierNet): +class CNNnet(TextClassifierNet): """ An implementation of :class:`quapy.classification.neural.TextClassifierNet` based on Convolutional Neural Networks. @@ -902,7 +904,7 @@ :param drop_p: drop probability for dropout (default 0.5) """ - def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, + def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, kernel_heights=[3, 5, 7], stride=1, padding=0, drop_p=0.5): super(CNNnet, self).__init__() @@ -927,7 +929,7 @@ self.doc_embedder = torch.nn.Linear(len(kernel_heights) * hidden_size, self.dim) self.output = nn.Linear(self.dim, n_classes) - def __conv_block(self, input, conv_layer): + def __conv_block(self, input, conv_layer): conv_out = conv_layer(input) # conv_out.size() = (batch_size, out_channels, dim, 1) activation = F.relu(conv_out.squeeze(3)) # activation.size() = (batch_size, out_channels, dim1) max_out = F.max_pool1d(activation, activation.size()[2]).squeeze(2) # maxpool_out.size() = (batch_size, out_channels) @@ -935,7 +937,7 @@
    [docs] - def document_embedding(self, input): + def document_embedding(self, input): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -960,7 +962,7 @@
    [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -970,7 +972,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary diff --git a/docs/build/html/_modules/quapy/classification/svmperf.html b/docs/build/html/_modules/quapy/classification/svmperf.html index 9b60c1a..e855546 100644 --- a/docs/build/html/_modules/quapy/classification/svmperf.html +++ b/docs/build/html/_modules/quapy/classification/svmperf.html @@ -1,94 +1,391 @@ - - - - - - quapy.classification.svmperf — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.classification.svmperf — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    +

    Source code for quapy.classification.svmperf

    -import random
    -import shutil
    -import subprocess
    -import tempfile
    -from os import remove, makedirs
    -from os.path import join, exists
    -from subprocess import PIPE, STDOUT
    -import numpy as np
    -from sklearn.base import BaseEstimator, ClassifierMixin
    -from sklearn.datasets import dump_svmlight_file
    +import logging
    +import random
    +import shutil
    +import subprocess
    +import tempfile
    +from os import remove, makedirs
    +from os.path import join, exists
    +from subprocess import PIPE, STDOUT
    +import numpy as np
    +from sklearn.base import BaseEstimator, ClassifierMixin
    +from sklearn.datasets import dump_svmlight_file
     
     
     
    [docs] -class SVMperf(BaseEstimator, ClassifierMixin): +class SVMperf(BaseEstimator, ClassifierMixin): """A wrapper for the `SVM-perf package <https://www.cs.cornell.edu/people/tj/svm_light/svm_perf.html>`__ by Thorsten Joachims. When using losses for quantification, the source code has to be patched. See the `installation documentation <https://hlt-isti.github.io/QuaPy/build/html/Installation.html#svm-perf-with-quantification-oriented-losses>`__ @@ -110,7 +407,7 @@ # losses with their respective codes in svm_perf implementation valid_losses = {'01':0, 'f1':1, 'kld':12, 'nkld':13, 'q':22, 'qacc':23, 'qf1':24, 'qgm':25, 'mae':26, 'mrae':27} - def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): + def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): assert exists(svmperf_base), \ (f'path {svmperf_base} does not seem to point to a valid path;' f'did you install svm-perf? ' @@ -123,7 +420,7 @@
    [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the SVM for the multivariate performance loss @@ -146,8 +443,7 @@ # this would allow to run parallel instances of predict random_code = 'svmperfprocess'+'-'.join(str(local_random.randint(0, 1000000)) for _ in range(5)) if self.host_folder is None: - # tmp dir are removed after the fit terminates in multiprocessing... - self.tmpdir = tempfile.TemporaryDirectory(suffix=random_code).name + self.tmpdir = join(tempfile.gettempdir(), random_code) else: self.tmpdir = join(self.host_folder, '.' + random_code) makedirs(self.tmpdir, exist_ok=True) @@ -159,21 +455,21 @@ cmd = ' '.join([self.svmperf_learn, self.c_cmd, self.loss_cmd, traindat, self.model]) if self.verbose: - print('[Running]', cmd) - p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) + logging.getLogger(__name__).info(f'[Running] {cmd}') + p = subprocess.run(cmd.split(), stdout=PIPE, stderr=PIPE) if not exists(self.model): - print(p.stderr.decode('utf-8')) + logging.getLogger(__name__).error(p.stderr.decode('utf-8')) remove(traindat) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) return self
    [docs] - def predict(self, X): + def predict(self, X): """ Predicts labels for the instances `X` @@ -188,7 +484,7 @@
    [docs] - def decision_function(self, X, y=None): + def decision_function(self, X, y=None): """ Evaluate the decision function for the samples in `X`. @@ -211,11 +507,11 @@ cmd = ' '.join([self.svmperf_classify, testdat, self.model, predictions_path]) if self.verbose: - print('[Running]', cmd) + logging.getLogger(__name__).info(f'[Running] {cmd}') p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) scores = np.loadtxt(predictions_path) remove(testdat) @@ -224,38 +520,82 @@ return scores
    - def __del__(self): + def __del__(self): if hasattr(self, 'tmpdir'): shutil.rmtree(self.tmpdir, ignore_errors=True)
    -
    +
    + + + + + + + + + + + + - + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/base.html b/docs/build/html/_modules/quapy/data/base.html index 27f34de..87203ec 100644 --- a/docs/build/html/_modules/quapy/data/base.html +++ b/docs/build/html/_modules/quapy/data/base.html @@ -1,96 +1,392 @@ - - - - - - quapy.data.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    + +

    Source code for quapy.data.base

    +import itertools
    +from functools import cached_property
    +from typing import Iterable
    +
    +import numpy as np
    +from scipy.sparse import issparse
    +from scipy.sparse import vstack
    +from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold
    +from numpy.random import RandomState
    +from quapy.functional import strprev
    +from quapy.util import temp_seed
    +import quapy.functional as F
     
     
     
    [docs] -class LabelledCollection: +class LabelledCollection: """ A LabelledCollection is a set of objects each with a label attached to each of them. This class implements several sampling routines and other utilities. @@ -102,7 +398,7 @@ (i.e., a prevalence of 0) """ - def __init__(self, instances, labels, classes=None): + def __init__(self, instances, labels, classes=None): if issparse(instances): self.instances = instances elif isinstance(instances, list) and len(instances) > 0 and isinstance(instances[0], str): @@ -121,7 +417,7 @@ self._index = None @property - def index(self): + def index(self): if not hasattr(self, '_index') or self._index is None: self._index = {class_: np.arange(len(self))[self.labels == class_] for class_ in self.classes_} return self._index @@ -129,7 +425,7 @@
    [docs] @classmethod - def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): """ Loads a labelled set of data and convert it into a :class:`LabelledCollection` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of the reading functions @@ -146,7 +442,7 @@ return LabelledCollection(*loader_func(path, **loader_kwargs), classes)
    - def __len__(self): + def __len__(self): """ Returns the length of this collection (number of labelled instances) @@ -156,7 +452,7 @@
    [docs] - def prevalence(self): + def prevalence(self): """ Returns the prevalence, or relative frequency, of the classes in the codeframe. @@ -168,7 +464,7 @@
    [docs] - def counts(self): + def counts(self): """ Returns the number of instances for each of the classes in the codeframe. @@ -179,7 +475,7 @@ @property - def n_classes(self): + def n_classes(self): """ The number of classes @@ -188,7 +484,7 @@ return len(self.classes_) @property - def n_instances(self): + def n_instances(self): """ The number of instances @@ -197,7 +493,7 @@ return len(self.labels) @property - def binary(self): + def binary(self): """ Returns True if the number of classes is 2 @@ -207,7 +503,7 @@
    [docs] - def sampling_index(self, size, *prevs, shuffle=True, random_state=None): + def sampling_index(self, size, *prevs, shuffle=True, random_state=None): """ Returns an index to be used to extract a random sample of desired size and desired prevalence values. If the prevalence values are not specified, then returns the index of a uniform sampling. @@ -270,7 +566,7 @@
    [docs] - def uniform_sampling_index(self, size, random_state=None): + def uniform_sampling_index(self, size, random_state=None): """ Returns an index to be used to extract a uniform sample of desired size. The sampling is drawn with replacement. @@ -288,7 +584,7 @@
    [docs] - def sampling(self, size, *prevs, shuffle=True, random_state=None): + def sampling(self, size, *prevs, shuffle=True, random_state=None): """ Return a random sample (an instance of :class:`LabelledCollection`) of desired size and desired prevalence values. For each class, the sampling is drawn with replacement. @@ -308,7 +604,7 @@
    [docs] - def uniform_sampling(self, size, random_state=None): + def uniform_sampling(self, size, random_state=None): """ Returns a uniform sample (an instance of :class:`LabelledCollection`) of desired size. The sampling is drawn with replacement. @@ -323,7 +619,7 @@
    [docs] - def sampling_from_index(self, index): + def sampling_from_index(self, index): """ Returns an instance of :class:`LabelledCollection` whose elements are sampled from this collection using the index. @@ -338,7 +634,7 @@
    [docs] - def split_stratified(self, train_prop=0.6, random_state=None): + def split_stratified(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split with stratification from this collection, at desired proportion. @@ -360,7 +656,7 @@
    [docs] - def split_random(self, train_prop=0.6, random_state=None): + def split_random(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split randomly from this collection, at desired proportion. @@ -387,7 +683,7 @@ return training, test
    - def __add__(self, other): + def __add__(self, other): """ Returns a new :class:`LabelledCollection` as the union of this collection with another collection. Both labelled collections must have the same classes. @@ -403,7 +699,7 @@
    [docs] @classmethod - def join(cls, *args: Iterable['LabelledCollection']): + def join(cls, *args: Iterable['LabelledCollection']): """ Returns a new :class:`LabelledCollection` as the union of the collections given in input. @@ -439,12 +735,14 @@ else: raise NotImplementedError('unsupported operation for collection types') labels = np.concatenate([lc.labels for lc in args]) - classes = np.unique(labels).sort() + # union of each collection's own classes_, so a class declared but absent from + # this particular join (e.g. an empty fold) is preserved at zero prevalence + classes = np.unique(np.concatenate([lc.classes_ for lc in args])) return LabelledCollection(instances, labels, classes=classes)
    @property - def classes(self): + def classes(self): """ Gets an array-like with the classes used in this collection @@ -453,7 +751,7 @@ return self.classes_ @property - def Xy(self): + def Xy(self): """ Gets the instances and labels. This is useful when working with `sklearn` estimators, e.g.: @@ -464,7 +762,7 @@ return self.instances, self.labels @property - def Xp(self): + def Xp(self): """ Gets the instances and the true prevalence. This is useful when implementing evaluation protocols from a :class:`LabelledCollection` object. @@ -474,7 +772,7 @@ return self.instances, self.prevalence() @property - def X(self): + def X(self): """ An alias to self.instances @@ -483,7 +781,7 @@ return self.instances @property - def y(self): + def y(self): """ An alias to self.labels @@ -492,7 +790,7 @@ return self.labels @property - def p(self): + def p(self): """ An alias to self.prevalence() @@ -503,7 +801,7 @@
    [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this collection. E.g.,: @@ -538,7 +836,7 @@
    [docs] - def kFCV(self, nfolds=5, nrepeats=1, random_state=None): + def kFCV(self, nfolds=5, nrepeats=1, random_state=None): """ Generator of stratified folds to be used in k-fold cross validation. @@ -554,7 +852,7 @@ yield train, test
    - def __repr__(self): + def __repr__(self): repr=f'<{self.n_instances} instances (dtype={type(self.instances[0])}), ' repr+=f'n_classes={self.n_classes} {self.classes_}, prevalence={F.strprev(self.prevalence())}>' return repr
    @@ -563,7 +861,7 @@
    [docs] -class Dataset: +class Dataset: """ Abstraction of training and test :class:`LabelledCollection` objects. @@ -573,7 +871,7 @@ :param name: a string representing the name of the dataset """ - def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): + def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): assert set(training.classes_) == set(test.classes_), 'incompatible labels in training and test collections' self.training = training self.test = test @@ -583,7 +881,7 @@
    [docs] @classmethod - def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): + def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): """ Generates a :class:`Dataset` from a stratified split of a :class:`LabelledCollection` instance. See :meth:`LabelledCollection.split_stratified` @@ -596,7 +894,7 @@ @property - def classes_(self): + def classes_(self): """ The classes according to which the training collection is labelled @@ -605,7 +903,7 @@ return self.training.classes_ @property - def n_classes(self): + def n_classes(self): """ The number of classes according to which the training collection is labelled @@ -614,7 +912,7 @@ return self.training.n_classes @property - def binary(self): + def binary(self): """ Returns True if the training collection is labelled according to two classes @@ -625,7 +923,7 @@
    [docs] @classmethod - def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): """ Loads a training and a test labelled set of data and convert it into a :class:`Dataset` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of @@ -647,7 +945,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ If the dataset is textual, and the vocabulary was indicated, returns the size of the vocabulary @@ -656,7 +954,7 @@ return len(self.vocabulary) @property - def train_test(self): + def train_test(self): """ Alias to `self.training` and `self.test` @@ -667,7 +965,7 @@
    [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this dataset. E.g.,: @@ -694,7 +992,7 @@
    [docs] @classmethod - def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): + def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): """ Generator of stratified folds to be used in k-fold cross validation. This function is only a wrapper around :meth:`LabelledCollection.kFCV` that returns :class:`Dataset` instances made of training and test folds. @@ -711,7 +1009,7 @@
    [docs] - def reduce(self, n_train=100, n_test=100, random_state=None): + def reduce(self, n_train=100, n_test=100, random_state=None): """ Reduce the number of instances in place for quick experiments. Preserves the prevalence of each set. @@ -732,36 +1030,80 @@ return self
    - def __repr__(self): + def __repr__(self): return f'training={self.training}; test={self.test}'
    -
    + + + + + + +
    + +
    +
    +
    + +
    + + + +
    -
    - -
    - -
    -

    © Copyright 2024, Alejandro Moreo.

    +
    + +
    + + +
    + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + -
    - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/datasets.html b/docs/build/html/_modules/quapy/data/datasets.html index 20d9588..cc6a18b 100644 --- a/docs/build/html/_modules/quapy/data/datasets.html +++ b/docs/build/html/_modules/quapy/data/datasets.html @@ -29,7 +29,7 @@ - + @@ -370,27 +370,28 @@

    Source code for quapy.data.datasets

    -import os
    -from contextlib import contextmanager
    -import zipfile
    -from os.path import join
    -import pandas as pd
    -from quapy.data.base import Dataset, LabelledCollection
    -from quapy.data.preprocessing import text2tfidf, reduce_columns
    -from quapy.data.preprocessing import standardize as standardizer
    -from quapy.data.reader import *
    -from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource
    -from sklearn.preprocessing import StandardScaler
    +import logging
    +import os
    +from contextlib import contextmanager
    +import zipfile
    +from os.path import join
    +import pandas as pd
    +from quapy.data.base import Dataset, LabelledCollection
    +from quapy.data.preprocessing import text2tfidf, reduce_columns
    +from quapy.data.preprocessing import standardize as standardizer
    +from quapy.data.reader import *
    +from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource
    +from sklearn.preprocessing import StandardScaler
     
     
    -def _fetch_ucirepo(*args, **kwargs):
    +def _fetch_ucirepo(*args, **kwargs):
         try:
    -        from ucimlrepo import fetch_ucirepo
    +        from ucimlrepo import fetch_ucirepo
         except ImportError as exc:
             raise ImportError(
                 "UCI dataset fetching requires the optional 'ucimlrepo' package. "
                 "Install it to use fetch_UCIBinaryDataset or fetch_UCIMulticlassDataset."
    -        ) from exc
    +        ) from exc
         return fetch_ucirepo(*args, **kwargs)
     
     
    @@ -497,7 +498,7 @@
     
     
    [docs] -def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Reviews dataset as a Dataset instance, as used in `Esuli, A., Moreo, A., and Sebastiani, F. "A recurrent neural network for sentiment quantification." @@ -547,7 +548,7 @@
    [docs] -def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Twitter dataset as a :class:`quapy.data.base.Dataset` instance, as used in: `Gao, W., Sebastiani, F.: From classification to quantification in tweet sentiment analysis. @@ -589,8 +590,9 @@ if dataset_name in {'semeval13', 'semeval14', 'semeval15'}: trainset_name = 'semeval' testset_name = 'semeval' if for_model_selection else dataset_name - print(f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " - f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") + logging.getLogger(__name__).info( + f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " + f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") else: if dataset_name == 'semeval' and for_model_selection==False: raise ValueError('dataset "semeval" can only be used for model selection. ' @@ -624,7 +626,7 @@
    [docs] -def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: +def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: """ Loads a UCI dataset as an instance of :class:`quapy.data.base.Dataset`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -658,7 +660,7 @@
    [docs] -def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI collection as an instance of :class:`quapy.data.base.LabelledCollection`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -851,7 +853,7 @@ file = join(data_home, "uci_datasets", dataset_group + ".pkl") @contextmanager - def download_tmp_file(url_group: str, filename: str): + def download_tmp_file(url_group: str, filename: str): """ Download a data file for a group of datasets temporarely. When used as a context, the file is removed once the context exits. @@ -869,7 +871,7 @@ finally: os.remove(data_path) - def download(id: int | None, group: str) -> dict: + def download(id: int | None, group: str) -> dict: """ Download the data to be pickled for a dataset group. Use the `fetch_ucirepo` api when possible. @@ -935,7 +937,7 @@ return data - def binarize_data(name, data: dict) -> LabelledCollection: + def binarize_data(name, data: dict) -> LabelledCollection: """ Filter and transform data to extract a binary dataset. @@ -980,7 +982,7 @@
    [docs] -def fetch_UCIMulticlassDataset( +def fetch_UCIMulticlassDataset( dataset_name, data_home=None, min_test_split=0.3, @@ -1043,7 +1045,7 @@
    [docs] -def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI multiclass collection as an instance of :class:`quapy.data.base.LabelledCollection`. @@ -1146,7 +1148,7 @@ file = join(data_home, 'uci_multiclass', dataset_name+'.pkl') - def dummify_categorical_features(df_features, dataset_id): + def dummify_categorical_features(df_features, dataset_id): categorical_features = { 158: ["S1", "C1", "S2", "C2", "S3", "C3", "S4", "C4", "S5", "C5"], # poker_hand } @@ -1160,7 +1162,7 @@ return X - def download(id, name): + def download(id, name): df = _fetch_ucirepo(id=id) X_df = dummify_categorical_features(df.data.features, id) @@ -1173,7 +1175,7 @@ y = np.searchsorted(classes, y) return LabelledCollection(X, y) - def filter_classes(data: LabelledCollection, min_class_support): + def filter_classes(data: LabelledCollection, min_class_support): if min_class_support is None or min_class_support == 0.: return data if isinstance(min_class_support, float): @@ -1207,18 +1209,18 @@ -def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): +def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): df[col] = df[col].apply(lambda x:repl[x]).astype(astype, copy=False) -def _array_replace(arr, repl={"yes": 1, "no": 0}): +def _array_replace(arr, repl={"yes": 1, "no": 0}): for k, v in repl.items(): arr[arr == k] = v
    [docs] -def fetch_lequa2022(task, data_home=None): +def fetch_lequa2022(task, data_home=None): """ Loads the official datasets provided for the `LeQua 2022 <https://lequa2022.github.io/index>`_ competition. In brief, there are 4 tasks (T1A, T1B, T2A, T2B) having to do with text quantification @@ -1244,7 +1246,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir + from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir assert task in LEQUA2022_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2022_TASKS}' @@ -1258,7 +1260,7 @@ lequa_dir = join(data_home, 'lequa2022') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1294,7 +1296,7 @@
    [docs] -def fetch_lequa2024(task, data_home=None, merge_T3=False): +def fetch_lequa2024(task, data_home=None, merge_T3=False): """ Loads the official datasets provided for the `LeQua 2024 <https://lequa2024.github.io/index>`_ competition. LeQua 2024 defines four tasks (T1, T2, T3, T4) related to the problem of quantification; @@ -1325,7 +1327,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir + from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir assert task in LEQUA2024_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2024_TASKS}' @@ -1344,7 +1346,7 @@ lequa_dir = join(data_home, 'lequa2024') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1384,7 +1386,7 @@
    [docs] -def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): +def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): """ Loads the IFCB dataset for quantification from `Zenodo <https://zenodo.org/records/10036244>`_ (for more information on this dataset, please follow the zenodo link). @@ -1409,7 +1411,7 @@ i.e., a sampling protocol that returns a series of samples labelled by prevalence. """ - from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split + from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split if data_home is None: data_home = get_quapy_home() @@ -1421,7 +1423,7 @@ ifcb_dir = join(data_home, 'ifcb') os.makedirs(ifcb_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(ifcb_dir, 'ifcb_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1466,7 +1468,7 @@ -def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: +def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: """ Loads a pre-generated embedding set (train, val, or test) of an image dataset from `Zenodo <https://zenodo.org/records/21131944>`_. @@ -1496,7 +1498,7 @@ trained_network = dataset_network[dataset_name] - def download_embedding_npz(dataset_name, trained_network, embedding): + def download_embedding_npz(dataset_name, trained_network, embedding): target_file = f'{dataset_name}_{trained_network}_{embedding}.npz' URL = f'https://zenodo.org/records/21131944/files/{target_file}' os.makedirs(join(data_home, 'image'), exist_ok=True) @@ -1512,14 +1514,12 @@ val = LabelledCollection(embedding_dict['val'], labels_dict['val'], classes=train.classes) test = LabelledCollection(embedding_dict['test'], labels_dict['test'], classes=train.classes) - print(f'{len(train)} | {len(val)} | {len(test)} | {train.X.shape[1]} | {train.n_classes} | {train.n_classes}') - return train, val, test
    [docs] -def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: +def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: """ Loads an image dataset with pre-generated embeddings. Available datasets include: @@ -1556,14 +1556,6 @@ -if __name__ == '__main__': - #train, val, test = _fetch_image_embedding_splits(dataset_name='mnist', embedding='logits') - #print(train) - #print(val) - #print(test) - - dataset = fetch_image_embeddings(dataset_name='svhn', embedding='features', heldout_only=True) - print(dataset)
    diff --git a/docs/build/html/_modules/quapy/data/reader.html b/docs/build/html/_modules/quapy/data/reader.html index 827d348..617442d 100644 --- a/docs/build/html/_modules/quapy/data/reader.html +++ b/docs/build/html/_modules/quapy/data/reader.html @@ -1,87 +1,385 @@ - - - - - - quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    +

    Source code for quapy.data.reader

    -import numpy as np
    -from scipy.sparse import dok_matrix
    -from tqdm import tqdm
    +import logging
    +
    +import numpy as np
    +from scipy.sparse import dok_matrix
    +from tqdm import tqdm
     
     
     
    [docs] -def from_text(path, encoding='utf-8', verbose=1, class2int=True): +def from_text(path, encoding='utf-8', verbose=1, class2int=True): """ Reads a labelled colletion of documents. File fomart <0 or 1>\t<document>\n @@ -108,14 +406,14 @@ all_sentences.append(sentence) all_labels.append(label) except ValueError: - print(f'format error in {line}') + logging.getLogger(__name__).warning(f'format error in {line}') return all_sentences, all_labels
    [docs] -def from_sparse(path): +def from_sparse(path): """ Reads a labelled collection of real-valued instances expressed in sparse format File format <-1 or 0 or 1>[\s col(int):val(float)]\n @@ -124,7 +422,7 @@ :return: a `csr_matrix` containing the instances (rows), and a ndarray containing the labels """ - def split_col_val(col_val): + def split_col_val(col_val): col, val = col_val.split(':') col, val = int(col) - 1, float(val) return col, val @@ -152,7 +450,7 @@
    [docs] -def from_csv(path, encoding='utf-8'): +def from_csv(path, encoding='utf-8'): """ Reads a csv file in which columns are separated by ','. File format <label>,<feat1>,<feat2>,...,<featn>\n @@ -175,7 +473,7 @@
    [docs] -def reindex_labels(y): +def reindex_labels(y): """ Re-indexes a list of labels as a list of indexes, and returns the classnames corresponding to the indexes. E.g.: @@ -198,7 +496,7 @@
    [docs] -def binarize(y, pos_class): +def binarize(y, pos_class): """ Binarizes a categorical array-like collection of labels towards the positive class `pos_class`. E.g.,: @@ -218,31 +516,75 @@
    -
    + + + + + + +
    + +
    +
    +
    + +
    + + + +
    -
    - -
    - -
    -

    © Copyright 2024, Alejandro Moreo.

    +
    + +
    + + +
    + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + -
    - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/error.html b/docs/build/html/_modules/quapy/error.html index 73a9a53..9b93ed4 100644 --- a/docs/build/html/_modules/quapy/error.html +++ b/docs/build/html/_modules/quapy/error.html @@ -1,89 +1,385 @@ - - - - - - quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    +

    Source code for quapy.error

     """Implementation of error measures used for quantification"""
     
    -import numpy as np
    -from sklearn.metrics import f1_score
    -import quapy as qp
    +import numpy as np
    +from sklearn.metrics import f1_score
    +import quapy as qp
     
     
     
    [docs] -def from_name(err_name): +def from_name(err_name): """Gets an error function from its name. E.g., `from_name("mae")` will return function :meth:`quapy.error.mae` @@ -98,7 +394,7 @@
    [docs] -def f1e(y_true, y_pred): +def f1e(y_true, y_pred): """F1 error: simply computes the error in terms of macro :math:`F_1`, i.e., :math:`1-F_1^M`, where :math:`F_1` is the harmonic mean of precision and recall, defined as :math:`\\frac{2tp}{2tp+fp+fn}`, with `tp`, `fp`, and `fn` standing @@ -116,7 +412,7 @@
    [docs] -def acce(y_true, y_pred): +def acce(y_true, y_pred): """Computes the error in terms of 1-accuracy. The accuracy is computed as :math:`\\frac{tp+tn}{tp+fp+fn+tn}`, with `tp`, `fp`, `fn`, and `tn` standing for true positives, false positives, false negatives, and true negatives, @@ -132,7 +428,7 @@
    [docs] -def mae(prevs_true, prevs_hat): +def mae(prevs_true, prevs_hat): """Computes the mean absolute error (see :meth:`quapy.error.ae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -146,7 +442,7 @@
    [docs] -def ae(prevs_true, prevs_hat): +def ae(prevs_true, prevs_hat): """Computes the absolute error between the two prevalence vectors. Absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`AE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}|\\hat{p}(y)-p(y)|`, @@ -165,7 +461,7 @@
    [docs] -def nae(prevs_true, prevs_hat): +def nae(prevs_true, prevs_hat): """Computes the normalized absolute error between the two prevalence vectors. Normalized absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`NAE(p,\\hat{p})=\\frac{AE(p,\\hat{p})}{z_{AE}}`, @@ -185,7 +481,7 @@
    [docs] -def mnae(prevs_true, prevs_hat): +def mnae(prevs_true, prevs_hat): """Computes the mean normalized absolute error (see :meth:`quapy.error.nae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -199,7 +495,7 @@
    [docs] -def mse(prevs_true, prevs_hat): +def mse(prevs_true, prevs_hat): """Computes the mean squared error (see :meth:`quapy.error.se`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the @@ -214,7 +510,7 @@
    [docs] -def se(prevs_true, prevs_hat): +def se(prevs_true, prevs_hat): """Computes the squared error between the two prevalence vectors. Squared error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`SE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}(\\hat{p}(y)-p(y))^2`, @@ -233,7 +529,7 @@
    [docs] -def sre(prevs_true, prevs_hat, prevs_train, eps=0.): +def sre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the squared ratio error between two prevalence vectors. The squared ratio error between prevalence vectors :math:`p` and @@ -269,7 +565,7 @@
    [docs] -def msre(prevs_true, prevs_hat, prevs_train, eps=0.): +def msre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the mean squared ratio error (see :meth:`quapy.error.sre`) across the sample pairs. @@ -285,7 +581,7 @@
    [docs] -def aitchisondist(prevs_true, prevs_hat): +def aitchisondist(prevs_true, prevs_hat): """ Computes the Aitchison distance between two prevalence vectors. The Aitchison distance between prevalence vectors :math:`p` and @@ -298,7 +594,7 @@ :param prevs_hat: array-like with the predicted prevalence values :return: Aitchison distance """ - from quapy.functional import CLRtransformation + from quapy.functional import CLRtransformation clr = CLRtransformation() return np.linalg.norm(clr(prevs_true) - clr(prevs_hat), axis=-1)
    @@ -307,7 +603,7 @@
    [docs] -def maitchisondist(prevs_true, prevs_hat): +def maitchisondist(prevs_true, prevs_hat): """ Computes the mean Aitchison distance (see :meth:`quapy.error.aitchisondist`) across the sample pairs, i.e., @@ -324,7 +620,7 @@
    [docs] -def mkld(prevs_true, prevs_hat, eps=None): +def mkld(prevs_true, prevs_hat, eps=None): """Computes the mean Kullback-Leibler divergence (see :meth:`quapy.error.kld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -345,7 +641,7 @@
    [docs] -def kld(prevs_true, prevs_hat, eps=None): +def kld(prevs_true, prevs_hat, eps=None): """Computes the Kullback-Leibler divergence between the two prevalence distributions. Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -371,7 +667,7 @@
    [docs] -def mnkld(prevs_true, prevs_hat, eps=None): +def mnkld(prevs_true, prevs_hat, eps=None): """Computes the mean Normalized Kullback-Leibler divergence (see :meth:`quapy.error.nkld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -391,7 +687,7 @@
    [docs] -def nkld(prevs_true, prevs_hat, eps=None): +def nkld(prevs_true, prevs_hat, eps=None): """Computes the Normalized Kullback-Leibler divergence between the two prevalence distributions. Normalized Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -415,7 +711,7 @@
    [docs] -def mrae(prevs_true, prevs_hat, eps=None): +def mrae(prevs_true, prevs_hat, eps=None): """Computes the mean relative absolute error (see :meth:`quapy.error.rae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -436,7 +732,7 @@
    [docs] -def rae(prevs_true, prevs_hat, eps=None): +def rae(prevs_true, prevs_hat, eps=None): """Computes the absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -462,7 +758,7 @@
    [docs] -def nrae(prevs_true, prevs_hat, eps=None): +def nrae(prevs_true, prevs_hat, eps=None): """Computes the normalized absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -490,7 +786,7 @@
    [docs] -def mnrae(prevs_true, prevs_hat, eps=None): +def mnrae(prevs_true, prevs_hat, eps=None): """Computes the mean normalized relative absolute error (see :meth:`quapy.error.nrae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -511,7 +807,7 @@
    [docs] -def nmd(prevs_true, prevs_hat): +def nmd(prevs_true, prevs_hat): """ Computes the Normalized Match Distance; which is the Normalized Distance multiplied by the factor `1/(n-1)` to guarantee the measure ranges between 0 (best prediction) and 1 (worst prediction). @@ -529,7 +825,7 @@
    [docs] -def bias_binary(prevs_true, prevs_hat): +def bias_binary(prevs_true, prevs_hat): """ Computes the (positive) bias in a binary problem. The bias is simply the difference between the predicted positive value and the true positive value, so that a positive such value indicates the @@ -549,7 +845,7 @@
    [docs] -def mean_bias_binary(prevs_true, prevs_hat): +def mean_bias_binary(prevs_true, prevs_hat): """ Computes the mean of the (positive) bias in a binary problem. :param prevs_true: array-like of shape `(n_classes,)` with the true prevalence values @@ -562,7 +858,7 @@
    [docs] -def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): +def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): """ Computes the Match Distance, under the assumption that the cost in mistaking class i with class i+1 is 1 in all cases. @@ -582,7 +878,7 @@
    [docs] -def smooth(prevs, eps): +def smooth(prevs, eps): """ Smooths a prevalence distribution with :math:`\\epsilon` (`eps`) as: :math:`\\underline{p}(y)=\\frac{\\epsilon+p(y)}{\\epsilon|\\mathcal{Y}|+ \\displaystyle\\sum_{y\\in \\mathcal{Y}}p(y)}` @@ -597,7 +893,7 @@ -def __check_eps(eps=None): +def __check_eps(eps=None): if eps is None: sample_size = qp.environ['SAMPLE_SIZE'] if sample_size is None: @@ -634,31 +930,75 @@ match_distance = md
    -
    + + + + + + +
    + +
    +
    +
    + +
    + + + +
    -
    - -
    - -
    -

    © Copyright 2024, Alejandro Moreo.

    +
    + +
    + + +
    + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + -
    - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/functional.html b/docs/build/html/_modules/quapy/functional.html index b7743b8..c275ca4 100644 --- a/docs/build/html/_modules/quapy/functional.html +++ b/docs/build/html/_modules/quapy/functional.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - Home + QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - Home -

    QuaPy

    @@ -131,7 +139,7 @@ @@ -183,6 +191,21 @@ + + @@ -229,7 +252,7 @@ @@ -272,6 +295,21 @@ + + @@ -332,17 +370,17 @@

    Source code for quapy.functional

    -import warnings
    -from abc import ABC, abstractmethod
    -from collections import defaultdict
    -from functools import lru_cache
    -from typing import Literal, Union, Callable
    -from numpy.typing import ArrayLike
    +import warnings
    +from abc import ABC, abstractmethod
    +from collections import defaultdict
    +from functools import lru_cache
    +from typing import Literal, Union, Callable
    +from numpy.typing import ArrayLike
     
    -import scipy
    -import numpy as np
    +import scipy
    +import numpy as np
     
    -import quapy as qp
    +import quapy as qp
     
     
     # ------------------------------------------------------------------------------------------
    @@ -351,7 +389,7 @@
     
     
    [docs] -def classes_from_labels(labels): +def classes_from_labels(labels): """ Obtains a np.ndarray with the (sorted) classes :param labels: array-like with the instances' labels @@ -365,7 +403,7 @@
    [docs] -def num_classes_from_labels(labels): +def num_classes_from_labels(labels): """ Obtains the number of classes from an array-like of instance's labels :param labels: array-like with the instances' labels @@ -380,7 +418,7 @@
    [docs] -def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: +def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: """ Computes the raw count values from a vector of labels. @@ -401,7 +439,7 @@
    [docs] -def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): +def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): """ Computes the prevalence values from a vector of labels. @@ -419,7 +457,7 @@
    [docs] -def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): +def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): """ Returns a vector of prevalence values from a matrix of posterior probabilities. @@ -443,7 +481,7 @@
    [docs] -def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: +def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: """ Computes the number of valid prevalence combinations in the n_classes-dimensional simplex if `n_prevpoints` equally distant prevalence values are generated and `n_repeats` repetitions are requested. @@ -472,7 +510,7 @@
    [docs] -def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: +def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: """ Searches for the largest number of (equidistant) prevalence points to define for each of the `n_classes` classes so that the number of valid prevalence values generated as combinations of prevalence points (points in a @@ -500,7 +538,7 @@
    [docs] -def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: +def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: """ Helper that, given a float representing the prevalence for the positive class, returns a np.ndarray of two values representing a binary distribution. @@ -522,7 +560,7 @@
    [docs] -def strprev(prevalences: ArrayLike, prec: int=3) -> str: +def strprev(prevalences: ArrayLike, prec: int=3) -> str: """ Returns a string representation for a prevalence vector. E.g., @@ -539,7 +577,7 @@
    [docs] -def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): +def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): """ Checks that `prevalences` is a valid prevalence vector, i.e., it contains values in [0,1] and the values sum up to 1. In other words, verifies that the `prevalences` vectors lies in the @@ -581,7 +619,7 @@
    [docs] -def uniform_prevalence(n_classes): +def uniform_prevalence(n_classes): """ Returns a vector representing the uniform distribution for `n_classes` @@ -597,7 +635,7 @@
    [docs] -def normalize_prevalence(prevalences: ArrayLike, method='l1'): +def normalize_prevalence(prevalences: ArrayLike, method='l1'): """ Normalizes a vector or matrix of prevalence values. The normalization consists of applying a L1 normalization in cases in which the prevalence values are not all-zeros, and to convert the prevalence values into `1/n_classes` in @@ -640,7 +678,7 @@
    [docs] -def l1_norm(prevalences: ArrayLike) -> np.ndarray: +def l1_norm(prevalences: ArrayLike) -> np.ndarray: """ Applies L1 normalization to the `unnormalized_arr` so that it becomes a valid prevalence vector. Zero vectors are mapped onto the uniform distribution. Raises an exception if @@ -666,7 +704,7 @@
    [docs] -def clip(prevalences: ArrayLike) -> np.ndarray: +def clip(prevalences: ArrayLike) -> np.ndarray: """ Clips the values in [0,1] and then applies the L1 normalization. @@ -681,7 +719,7 @@
    [docs] -def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: +def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: """Projects a point onto the probability simplex. The code is adapted from Mathieu Blondel's BSD-licensed @@ -709,7 +747,7 @@
    [docs] -def softmax(prevalences: ArrayLike) -> np.ndarray: +def softmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function to all vectors even if the original vectors were valid distributions. If you want to leave valid vectors untouched, use condsoftmax instead. @@ -724,7 +762,7 @@
    [docs] -def condsoftmax(prevalences: ArrayLike) -> np.ndarray: +def condsoftmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function only to vectors that do not represent valid distributions. @@ -749,7 +787,7 @@
    [docs] -def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: +def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: """ Computes the Hellingher Distance (HD) between (discretized) distributions `P` and `Q`. The HD for two discrete distributions of `k` bins is defined as: @@ -767,7 +805,7 @@
    [docs] -def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): +def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): """ Topsoe distance between two (discretized) distributions `P` and `Q`. The Topsoe distance for two discrete distributions of `k` bins is defined as: @@ -786,7 +824,7 @@
    [docs] -def get_divergence(divergence: Union[str, Callable]): +def get_divergence(divergence: Union[str, Callable]): """ Guarantees that the divergence received as argument is a function. That is, if this argument is already a callable, then it is returned, if it is instead a string, then tries to instantiate the corresponding @@ -815,7 +853,7 @@
    [docs] -def argmin_prevalence(loss: Callable, +def argmin_prevalence(loss: Callable, n_classes: int, method: Literal["optim_minimize", "linear_search", "ternary_search"]='optim_minimize'): """ @@ -842,7 +880,7 @@
    [docs] -def optim_minimize(loss: Callable, n_classes: int, return_loss=False): +def optim_minimize(loss: Callable, n_classes: int, return_loss=False): """ Searches for the optimal prevalence values, i.e., an `n_classes`-dimensional vector of the (`n_classes`-1)-simplex that yields the smallest lost. This optimization is carried out by means of a constrained search using scipy's @@ -854,7 +892,7 @@ :return: (ndarray) the best prevalence vector found or a tuple which also contains the value of the loss if return_loss=True """ - from scipy import optimize + from scipy import optimize # the initial point is set as the uniform distribution uniform_distribution = uniform_prevalence(n_classes=n_classes) @@ -873,7 +911,7 @@ + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - +
    +
    -
    -
    - -
    - + +
    + + + +
    + + + + + +
    + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/meta.html b/docs/build/html/_modules/quapy/method/meta.html index 8ff0a2a..2413195 100644 --- a/docs/build/html/_modules/quapy/method/meta.html +++ b/docs/build/html/_modules/quapy/method/meta.html @@ -1,97 +1,394 @@ - - - - - - quapy.method.meta — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.meta — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    + +

    Source code for quapy.method.meta

    +import itertools
    +import logging
    +from copy import deepcopy
    +from typing import Union, List
    +import numpy as np
    +from sklearn.linear_model import LogisticRegression
    +from sklearn.metrics import f1_score, make_scorer, accuracy_score
    +from sklearn.model_selection import GridSearchCV, cross_val_predict
    +from tqdm import tqdm
    +
    +import quapy as qp
    +from quapy import functional as F
    +from quapy.data import LabelledCollection
    +from quapy.model_selection import GridSearchQ
    +from quapy.method.base import BaseQuantifier, BinaryQuantifier
    +from quapy.method.aggregative import CC, ACC, PACC, HDy, EMQ, AggregativeQuantifier, AggregativeSoftQuantifier
     
     try:
    -    from . import _neural
    +    from . import _neural
     except ModuleNotFoundError:
         _neural = None
     
    @@ -102,83 +399,9 @@
         QuaNet = "QuaNet is not available due to missing torch package"
     
     
    -
    -[docs] -class MedianEstimator2(BinaryQuantifier): - """ - This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the - estimation returned by differently (hyper)parameterized base quantifiers. - The median of unit-vectors is only guaranteed to be a unit-vector for n=2 dimensions, - i.e., in cases of binary quantification. - - :param base_quantifier: the base, binary quantifier - :param random_state: a seed to be set before fitting any base quantifier (default None) - :param param_grid: the grid or parameters towards which the median will be computed - :param n_jobs: number of parllel workes - """ - def __init__(self, base_quantifier: BinaryQuantifier, param_grid: dict, random_state=None, n_jobs=None): - self.base_quantifier = base_quantifier - self.param_grid = param_grid - self.random_state = random_state - self.n_jobs = qp._get_njobs(n_jobs) - -
    -[docs] - def get_params(self, deep=True): - return self.base_quantifier.get_params(deep)
    - - -
    -[docs] - def set_params(self, **params): - self.base_quantifier.set_params(**params)
    - - - def _delayed_fit(self, args): - with qp.util.temp_seed(self.random_state): - params, X, y = args - model = deepcopy(self.base_quantifier) - model.set_params(**params) - model.fit(X, y) - return model - -
    -[docs] - def fit(self, X, y): - self._check_binary(y, self.__class__.__name__) - - configs = qp.model_selection.expand_grid(self.param_grid) - self.models = qp.util.parallel( - self._delayed_fit, - ((params, X, y) for params in configs), - seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs - ) - return self
    - - - def _delayed_predict(self, args): - model, instances = args - return model.predict(instances) - -
    -[docs] - def predict(self, X): - prev_preds = qp.util.parallel( - self._delayed_predict, - ((model, X) for model in self.models), - seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs - ) - prev_preds = np.asarray(prev_preds) - return np.median(prev_preds, axis=0)
    -
    - - -
    [docs] -class MedianEstimator(BinaryQuantifier): +class MedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -190,7 +413,7 @@ :param param_grid: the grid or parameters towards which the median will be computed :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: BinaryQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: BinaryQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -198,17 +421,17 @@
    [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep)
    [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params)
    - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -218,7 +441,7 @@
    [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) configs = qp.model_selection.expand_grid(self.param_grid) @@ -232,13 +455,13 @@ return self
    - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances)
    [docs] - def predict(self, X): + def predict(self, X): prev_preds = qp.util.parallel( self._delayed_predict, ((model, X) for model in self.models), @@ -254,7 +477,7 @@
    [docs] -class Ensemble(BaseQuantifier): +class Ensemble(BaseQuantifier): VALID_POLICIES = {'ave', 'ptr', 'ds'} | qp.error.QUANTIFICATION_ERROR_NAMES """ @@ -294,7 +517,7 @@ :param verbose: set to True (default is False) to get some information in standard output """ - def __init__(self, + def __init__(self, quantifier: BaseQuantifier, size=50, red_size=25, @@ -319,13 +542,13 @@ self.verbose = verbose self.max_sample_size = max_sample_size - def _sout(self, msg): + def _sout(self, msg): if self.verbose: - print('[Ensemble]' + msg) + logging.getLogger(__name__).info('[Ensemble] ' + msg)
    [docs] - def fit(self, X, y): + def fit(self, X, y): data = LabelledCollection(X, y) @@ -366,7 +589,7 @@
    [docs] - def predict(self, X): + def predict(self, X): predictions = np.asarray( qp.util.parallel(_delayed_quantify, ((Qi, X) for Qi in self.ensemble), n_jobs=self.n_jobs) ) @@ -382,7 +605,7 @@
    [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): """ This function should not be used within :class:`quapy.model_selection.GridSearchQ` (is here for compatibility with the abstract class). @@ -401,7 +624,7 @@
    [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): """ This function should not be used within :class:`quapy.model_selection.GridSearchQ` (is here for compatibility with the abstract class). @@ -416,13 +639,13 @@ raise NotImplementedError()
    - def _accuracy_policy(self, error_name): + def _accuracy_policy(self, error_name): """ Selects the red_size best performant quantifiers in a static way (i.e., dropping all non-selected instances). For each model in the ensemble, the performance is measured in terms of _error_name_ on the quantification of the samples used for training the rest of the models in the ensemble. """ - from quapy.evaluation import evaluate_on_samples + from quapy.evaluation import evaluate_on_samples error = qp.error.from_name(error_name) tests = [m[3] for m in self.ensemble] scores = [] @@ -432,7 +655,7 @@ self.ensemble = _select_k(self.ensemble, order, k=self.red_size) - def _ptr_policy(self, predictions): + def _ptr_policy(self, predictions): """ Selects the predictions made by models that have been trained on samples with a prevalence that is most similar to a first approximation of the test prevalence as made by all models in the ensemble. @@ -443,7 +666,7 @@ order = np.argsort(ptr_differences) return _select_k(predictions, order, k=self.red_size) - def _ds_policy_get_posteriors(self, data: LabelledCollection): + def _ds_policy_get_posteriors(self, data: LabelledCollection): """ In the original article, there are some aspects regarding this method that are not mentioned. The paper says that the distribution of posterior probabilities from training and test examples is compared by means of the @@ -474,7 +697,7 @@ return posteriors, posteriors_generator - def _ds_policy(self, predictions, test): + def _ds_policy(self, predictions, test): test_posteriors = self.post_proba_fn(test) test_distribution = get_probability_distribution(test_posteriors) tr_distributions = [m[2] for m in self.ensemble] @@ -483,7 +706,7 @@ return _select_k(predictions, order, k=self.red_size) @property - def aggregative(self): + def aggregative(self): """ Indicates that the quantifier is not aggregative. @@ -492,7 +715,7 @@ return False @property - def probabilistic(self): + def probabilistic(self): """ Indicates that the quantifier is not probabilistic. @@ -504,7 +727,7 @@
    [docs] -def get_probability_distribution(posterior_probabilities, bins=8): +def get_probability_distribution(posterior_probabilities, bins=8): """ Gets a histogram out of the posterior probabilities (only for the binary case). @@ -519,14 +742,14 @@ -def _select_k(elements, order, k): +def _select_k(elements, order, k): return [elements[idx] for idx in order[:k]] -def _delayed_new_instance(args): +def _delayed_new_instance(args): base_quantifier, data, val_split, prev, posteriors, keep_samples, verbose, sample_size = args if verbose: - print(f'\tfit-start for prev {F.strprev(prev)}, sample_size={sample_size}') + logging.getLogger(__name__).info(f'fit-start for prev {F.strprev(prev)}, sample_size={sample_size}') model = deepcopy(base_quantifier) if val_split is not None: @@ -546,17 +769,17 @@ tr_distribution = get_probability_distribution(posteriors[sample_index]) if (posteriors is not None) else None if verbose: - print(f'\t--fit-ended for prev {F.strprev(prev)}') + logging.getLogger(__name__).info(f'fit-ended for prev {F.strprev(prev)}') return (model, tr_prevalence, tr_distribution, sample if keep_samples else None) -def _delayed_quantify(args): +def _delayed_quantify(args): quantifier, instances = args return quantifier[0].predict(instances) -def _draw_simplex(ndim, min_val, max_trials=100): +def _draw_simplex(ndim, min_val, max_trials=100): """ Returns a uniform sampling from the ndim-dimensional simplex but guarantees that all dimensions are >= min_class_prev (for min_val>0, this makes the sampling not truly uniform) @@ -581,7 +804,7 @@ f'>= {min_val} is unlikely (it failed after {max_trials} trials)') -def _instantiate_ensemble(classifier, base_quantifier_class, param_grid, optim, param_model_sel, **kwargs): +def _instantiate_ensemble(classifier, base_quantifier_class, param_grid, optim, param_model_sel, **kwargs): if optim is None: base_quantifier = base_quantifier_class(classifier) elif optim in qp.error.CLASSIFICATION_ERROR: @@ -600,7 +823,7 @@ return Ensemble(base_quantifier, **kwargs) -def _check_error(error): +def _check_error(error): if error is None: return None if error in qp.error.QUANTIFICATION_ERROR or error in qp.error.CLASSIFICATION_ERROR: @@ -614,7 +837,7 @@
    [docs] -def ensembleFactory(classifier, base_quantifier_class, param_grid=None, optim=None, param_model_sel: dict = None, +def ensembleFactory(classifier, base_quantifier_class, param_grid=None, optim=None, param_model_sel: dict = None, **kwargs): """ Ensemble factory. Provides a unified interface for instantiating ensembles that can be optimized (via model @@ -668,7 +891,7 @@
    [docs] -def ECC(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): +def ECC(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): """ Implements an ensemble of :class:`quapy.method.aggregative.CC` quantifiers, as used by `Pérez-Gállego et al., 2019 <https://www.sciencedirect.com/science/article/pii/S1566253517303652>`_. @@ -694,7 +917,7 @@
    [docs] -def EACC(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): +def EACC(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): """ Implements an ensemble of :class:`quapy.method.aggregative.ACC` quantifiers, as used by `Pérez-Gállego et al., 2019 <https://www.sciencedirect.com/science/article/pii/S1566253517303652>`_. @@ -720,7 +943,7 @@
    [docs] -def EPACC(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): +def EPACC(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): """ Implements an ensemble of :class:`quapy.method.aggregative.PACC` quantifiers. @@ -745,7 +968,7 @@
    [docs] -def EHDy(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): +def EHDy(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): """ Implements an ensemble of :class:`quapy.method.aggregative.HDy` quantifiers, as used by `Pérez-Gállego et al., 2019 <https://www.sciencedirect.com/science/article/pii/S1566253517303652>`_. @@ -771,7 +994,7 @@
    [docs] -def EEMQ(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): +def EEMQ(classifier, param_grid=None, optim=None, param_mod_sel=None, **kwargs): """ Implements an ensemble of :class:`quapy.method.aggregative.EMQ` quantifiers. @@ -796,7 +1019,7 @@
    [docs] -def merge(prev_predictions, merge_fun): +def merge(prev_predictions, merge_fun): prev_predictions = np.asarray(prev_predictions) if merge_fun == 'median': prevalences = np.median(prev_predictions, axis=0) @@ -811,11 +1034,11 @@
    [docs] -class SCMQ(AggregativeSoftQuantifier): +class SCMQ(AggregativeSoftQuantifier): MERGE_FUNCTIONS = ['median', 'mean'] - def __init__(self, classifier, quantifiers: List[AggregativeSoftQuantifier], merge_fun='median', val_split=5): + def __init__(self, classifier, quantifiers: List[AggregativeSoftQuantifier], merge_fun='median', val_split=5): self.classifier = classifier self.quantifiers = [deepcopy(q) for q in quantifiers] assert merge_fun in self.MERGE_FUNCTIONS, f'unknown {merge_fun=}, valid ones are {self.MERGE_FUNCTIONS}' @@ -824,7 +1047,7 @@
    [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): for quantifier in self.quantifiers: quantifier.classifier = self.classifier quantifier.aggregation_fit(classif_predictions, labels) @@ -833,7 +1056,7 @@
    [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prev_predictions = [] for quantifier_i in self.quantifiers: prevalence_i = quantifier_i.aggregate(classif_predictions) @@ -845,8 +1068,8 @@
    [docs] -class MCSQ(BaseQuantifier): - def __init__(self, classifiers, quantifier: AggregativeSoftQuantifier, merge_fun='median', val_split=5): +class MCSQ(BaseQuantifier): + def __init__(self, classifiers, quantifier: AggregativeSoftQuantifier, merge_fun='median', val_split=5): self.merge_fun = merge_fun self.val_split = val_split self.mcsqs = [] @@ -857,7 +1080,7 @@
    [docs] - def fit(self, data: LabelledCollection): + def fit(self, data: LabelledCollection): for q in self.mcsqs: q.fit(data, val_split=self.val_split) return self
    @@ -865,7 +1088,7 @@
    [docs] - def quantify(self, instances): + def quantify(self, instances): prev_predictions = [] for q in self.mcsqs: prevalence_i = q.quantify(instances) @@ -877,8 +1100,8 @@
    [docs] -class MCMQ(BaseQuantifier): - def __init__(self, classifiers, quantifiers: List[AggregativeSoftQuantifier], merge_fun='median', val_split=5): +class MCMQ(BaseQuantifier): + def __init__(self, classifiers, quantifiers: List[AggregativeSoftQuantifier], merge_fun='median', val_split=5): self.merge_fun = merge_fun self.scmqs = [] for classifier in classifiers: @@ -886,7 +1109,7 @@
    [docs] - def fit(self, data: LabelledCollection): + def fit(self, data: LabelledCollection): for q in self.scmqs: q.fit(data) return self
    @@ -894,7 +1117,7 @@
    [docs] - def quantify(self, instances): + def quantify(self, instances): prev_predictions = [] for q in self.scmqs: prevalence_i = q.quantify(instances) @@ -928,31 +1151,75 @@
    -
    +
    + + + + + + + + + + + + - + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/non_aggregative.html b/docs/build/html/_modules/quapy/method/non_aggregative.html index 11c56e1..5ff1429 100644 --- a/docs/build/html/_modules/quapy/method/non_aggregative.html +++ b/docs/build/html/_modules/quapy/method/non_aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - Home + QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - Home -

    QuaPy

    @@ -131,7 +139,7 @@ @@ -183,6 +191,21 @@ + + @@ -229,7 +252,7 @@ @@ -272,6 +295,21 @@ + + @@ -332,26 +370,26 @@

    Source code for quapy.method.non_aggregative

    -from itertools import product
    -from tqdm import tqdm
    -from typing import Union, Callable, Counter
    -import numpy as np
    -from sklearn.feature_extraction.text import CountVectorizer
    -from sklearn.utils import resample
    -from sklearn.preprocessing import normalize
    +from itertools import product
    +from tqdm import tqdm
    +from typing import Union, Callable, Counter
    +import numpy as np
    +from sklearn.feature_extraction.text import CountVectorizer
    +from sklearn.utils import resample
    +from sklearn.preprocessing import normalize
     
    -from quapy.method.confidence import WithConfidenceABC, ConfidenceRegionABC
    -from quapy.functional import get_divergence
    -from quapy.method.base import BaseQuantifier, BinaryQuantifier
    -from quapy.method._helper import _labels_to_indices
    -import quapy.functional as F
    -from scipy.optimize import lsq_linear
    -from scipy import sparse
    +from quapy.method.confidence import WithConfidenceABC, ConfidenceRegionABC
    +from quapy.functional import get_divergence
    +from quapy.method.base import BaseQuantifier, BinaryQuantifier
    +from quapy.method._helper import _labels_to_indices
    +import quapy.functional as F
    +from scipy.optimize import lsq_linear
    +from scipy import sparse
     
     
     
    [docs] -class MaximumLikelihoodPrevalenceEstimation(BaseQuantifier): +class MaximumLikelihoodPrevalenceEstimation(BaseQuantifier): """ The `Maximum Likelihood Prevalence Estimation` (MLPE) method is a lazy method that assumes there is no prior probability shift between training and test instances (put it other way, that the i.i.d. assumpion holds). @@ -360,12 +398,12 @@ any quantification method should beat. """ - def __init__(self): + def __init__(self): self._classes_ = None
    [docs] - def fit(self, X, y): + def fit(self, X, y): """ Computes the training prevalence and stores it. @@ -380,7 +418,7 @@
    [docs] - def predict(self, X): + def predict(self, X): """ Ignores the input instances and returns, as the class prevalence estimantes, the training prevalence. @@ -394,7 +432,7 @@
    [docs] -class DMx(BaseQuantifier): +class DMx(BaseQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of covariates. This implementation takes the number of bins, the divergence, and the possibility to work on CDF as hyperparameters. @@ -410,7 +448,7 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, nbins=8, divergence: Union[str, Callable]='HD', cdf=False, search='optim_minimize', n_jobs=None): + def __init__(self, nbins=8, divergence: Union[str, Callable]='HD', cdf=False, search='optim_minimize', n_jobs=None): self.nbins = nbins self.divergence = divergence self.cdf = cdf @@ -420,7 +458,7 @@
    [docs] @classmethod - def HDx(cls, n_jobs=None): + def HDx(cls, n_jobs=None): """ `Hellinger Distance x <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDx). HDx is a method for training binary quantifiers, that models quantification as the problem of @@ -436,7 +474,7 @@ :return: an instance of this class setup to mimick the performance of the HDx as originally proposed by González-Castro, Alaiz-Rodríguez, Alegre (2013) """ - from quapy.method.meta import MedianEstimator + from quapy.method.meta import MedianEstimator dmx = DMx(divergence='HD', cdf=False, search='linear_search') nbins = {'nbins': np.linspace(10, 110, 11, dtype=int)} @@ -444,7 +482,7 @@ return hdx
    - def __get_distributions(self, X): + def __get_distributions(self, X): histograms = [] for feat_idx in range(self.nfeats): feature = X[:, feat_idx] @@ -461,7 +499,7 @@
    [docs] - def fit(self, X, y): + def fit(self, X, y): """ Generates the validation distributions out of the training data (covariates). The validation distributions have shape `(n, nfeats, nbins)`, with `n` the number of classes, `nfeats` @@ -488,7 +526,7 @@
    [docs] - def predict(self, X): + def predict(self, X): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -504,7 +542,7 @@ test_distribution = self.__get_distributions(X) divergence = get_divergence(self.divergence) n_classes, n_feats, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes,-1)).reshape(n_feats, -1) divs = [divergence(test_distribution[feat], mixture_distribution[feat]) for feat in range(n_feats)] @@ -517,7 +555,7 @@
    [docs] -class ReadMe(BaseQuantifier, WithConfidenceABC): +class ReadMe(BaseQuantifier, WithConfidenceABC): """ ReadMe is a non-aggregative quantification system proposed by `Daniel Hopkins and Gary King, 2007. A method of automated nonparametric content analysis for @@ -558,7 +596,7 @@ MAX_FEATURES_FOR_EMPIRICAL_ESTIMATION = 25 PROBABILISTIC_MODELS = ["naive", "full"] - def __init__(self, + def __init__(self, prob_model="full", bootstrap_trials=300, bagging_trials=300, @@ -580,7 +618,7 @@
    [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_matrix(X) self.rng = np.random.default_rng(self.random_state) @@ -600,7 +638,7 @@
    [docs] - def predict_conf(self, X, confidence_level=0.95) -> (np.ndarray, ConfidenceRegionABC): + def predict_conf(self, X, confidence_level=0.95) -> (np.ndarray, ConfidenceRegionABC): self._check_matrix(X) n_features = X.shape[1] @@ -627,12 +665,12 @@
    [docs] - def predict(self, X): + def predict(self, X): prev_estim, _ = self.predict_conf(X) return prev_estim
    - def _quantify_iteration(self, Xtr, ytr, Xte): + def _quantify_iteration(self, Xtr, ytr, Xte): """Single ReadMe estimate.""" PX_given_Y = np.asarray([self._compute_P(Xtr[ytr == c]) for i,c in enumerate(self.classes_)]) PX = self._compute_P(Xte) @@ -641,17 +679,17 @@ pY = np.maximum(res.x, 0) return pY / pY.sum() - def _check_matrix(self, X): + def _check_matrix(self, X): """the "full" model requires estimating empirical distributions; due to the high computational cost, this function is only made available for binary matrices""" if self.prob_model == 'full' and not self._is_binary_matrix(X): raise ValueError('the empirical distribution can only be computed efficiently on binary matrices') - def _is_binary_matrix(self, X): + def _is_binary_matrix(self, X): data = X.data if sparse.issparse(X) else X return np.all((data == 0) | (data == 1)) - def _compute_P(self, X): + def _compute_P(self, X): if self.prob_model == 'naive': return self._multinomial_distribution(X) elif self.prob_model == 'full': @@ -659,7 +697,7 @@ else: raise ValueError(f'unknown {self.prob_model}; valid ones are {ReadMe.PROBABILISTIC_MODELS=}') - def _empirical_distribution(self, X): + def _empirical_distribution(self, X): if X.shape[1] > self.MAX_FEATURES_FOR_EMPIRICAL_ESTIMATION: raise ValueError(f'the empirical distribution can only be computed efficiently for dimensions ' @@ -677,14 +715,14 @@ return probs - def _multinomial_distribution(self, X): + def _multinomial_distribution(self, X): PX = np.asarray(X.sum(axis=0)) PX = normalize(PX, norm='l1', axis=1) return PX.ravel()
    -def _get_features_range(X): +def _get_features_range(X): feat_ranges = [] ncols = X.shape[1] for col_idx in range(ncols): diff --git a/docs/build/html/_modules/quapy/model_selection.html b/docs/build/html/_modules/quapy/model_selection.html index 461c068..e483552 100644 --- a/docs/build/html/_modules/quapy/model_selection.html +++ b/docs/build/html/_modules/quapy/model_selection.html @@ -1,101 +1,398 @@ - - - - - - quapy.model_selection — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.model_selection — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    +

    Source code for quapy.model_selection

    -import itertools
    -import signal
    -from copy import deepcopy
    -from enum import Enum
    -from typing import Union, Callable
    -from functools import wraps
    +import itertools
    +import logging
    +import signal
    +from copy import deepcopy
    +from enum import Enum
    +from typing import Union, Callable
    +from functools import wraps
     
    -import numpy as np
    -from sklearn import clone
    +import numpy as np
    +from sklearn import clone
     
    -import quapy as qp
    -from quapy import evaluation
    -from quapy.protocol import AbstractProtocol, OnLabelledCollectionProtocol
    -from quapy.data.base import LabelledCollection
    -from quapy.method.aggregative import BaseQuantifier, AggregativeQuantifier
    -from quapy.util import timeout
    -from time import time
    +import quapy as qp
    +from quapy import evaluation
    +from quapy.protocol import AbstractProtocol, OnLabelledCollectionProtocol
    +from quapy.data.base import LabelledCollection
    +from quapy.method.aggregative import BaseQuantifier, AggregativeQuantifier
    +from quapy.util import timeout
    +from time import time
     
     
     
    [docs] -class Status(Enum): +class Status(Enum): SUCCESS = 1 TIMEOUT = 2 INVALID = 3 @@ -105,28 +402,28 @@
    [docs] -class ConfigStatus: +class ConfigStatus: - def __init__(self, params, status, msg=''): + def __init__(self, params, status, msg=''): self.params = params self.status = status self.msg = msg - def __str__(self): + def __str__(self): return f':params:{self.params} :status:{self.status} ' + self.msg - def __repr__(self): + def __repr__(self): return str(self)
    [docs] - def success(self): + def success(self): return self.status == Status.SUCCESS
    [docs] - def failed(self): + def failed(self): return self.status != Status.SUCCESS
    @@ -134,7 +431,7 @@
    [docs] -class GridSearchQ(BaseQuantifier): +class GridSearchQ(BaseQuantifier): """Grid Search optimization targeting a quantification-oriented metric. Optimizes the hyperparameters of a quantification method, based on an evaluation method and on an evaluation @@ -157,7 +454,7 @@ :param verbose: set to True to get information through the stdout """ - def __init__(self, + def __init__(self, model: BaseQuantifier, param_grid: dict, protocol: AbstractProtocol, @@ -179,11 +476,11 @@ self.__check_error_measure(error) assert isinstance(protocol, AbstractProtocol), 'unknown protocol' - def _sout(self, msg): + def _sout(self, msg): if self.verbose: - print(f'[{self.__class__.__name__}:{self.model.__class__.__name__}]: {msg}') + logging.getLogger(__name__).info(f'[{self.__class__.__name__}:{self.model.__class__.__name__}]: {msg}') - def __check_error_measure(self, error): + def __check_error_measure(self, error): if error in qp.error.QUANTIFICATION_ERROR: self.error = error elif isinstance(error, str): @@ -194,10 +491,10 @@ raise ValueError(f'unexpected error type; must either be a callable function or a str representing\n' f'the name of an error function in {qp.error.QUANTIFICATION_ERROR_NAMES}') - def _prepare_classifier(self, cls_params): + def _prepare_classifier(self, cls_params): model = deepcopy(self.model) - def job(cls_params): + def job(cls_params): model.set_params(**cls_params) predictions = model.classifier_fit_predict(self._training_X, self._training_y) return predictions @@ -206,12 +503,12 @@ self._sout(f'[classifier fit] hyperparams={cls_params} [took {took:.3f}s]') return model, predictions, status, took - def _prepare_aggregation(self, args): + def _prepare_aggregation(self, args): model, predictions, cls_took, cls_params, q_params = args model = deepcopy(model) params = {**cls_params, **q_params} - def job(q_params): + def job(q_params): model.set_params(**q_params) P, y = predictions model.aggregation_fit(P, y) @@ -222,10 +519,10 @@ self._print_status(params, score, status, aggr_took) return model, params, score, status, (cls_took+aggr_took) - def _prepare_nonaggr_model(self, params): + def _prepare_nonaggr_model(self, params): model = deepcopy(self.model) - def job(params): + def job(params): model.set_params(**params) model.fit(self._training_X, self._training_y) score = evaluation.evaluate(model, protocol=self.protocol, error_metric=self.error) @@ -235,7 +532,7 @@ self._print_status(params, score, status, took) return model, params, score, status, took - def _break_down_fit(self): + def _break_down_fit(self): """ Decides whether to break down the fit phase in two (classifier-fit followed by aggregation-fit). In order to do so, some conditions should be met: a) the quantifier is of type aggregative, @@ -250,7 +547,7 @@ return False return True - def _compute_scores_aggregative(self, X, y): + def _compute_scores_aggregative(self, X, y): # break down the set of hyperparameters into two: classifier-specific, quantifier-specific cls_configs, q_configs = group_params(self.param_grid) @@ -287,7 +584,7 @@ return aggr_outs - def _compute_scores_nonaggregative(self, X, y): + def _compute_scores_nonaggregative(self, X, y): configs = expand_grid(self.param_grid) self._training_X = X self._training_y = y @@ -299,7 +596,7 @@ ) return scores - def _print_status(self, params, score, status, took): + def _print_status(self, params, score, status, took): if status.success(): self._sout(f'hyperparams=[{params}]\t got {self.error.__name__} = {score:.5f} [took {took:.3f}s]') else: @@ -307,7 +604,7 @@
    [docs] - def fit(self, X, y): + def fit(self, X, y): """ Learning routine. Fits methods with all combinations of hyperparameters and selects the one minimizing the error metric. @@ -378,7 +675,7 @@
    [docs] - def predict(self, X): + def predict(self, X): """Estimate class prevalence values using the best model found after calling the :meth:`fit` method. :param X: sample contanining the instances @@ -391,7 +688,7 @@
    [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): """Sets the hyper-parameters to explore. :param parameters: a dictionary with keys the parameter names and values the list of values to explore @@ -401,7 +698,7 @@
    [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): """Returns the dictionary of hyper-parameters to explore (`param_grid`) :param deep: Unused @@ -412,7 +709,7 @@
    [docs] - def best_model(self): + def best_model(self): """ Returns the best model found after calling the :meth:`fit` method, i.e., the one trained on the combination of hyper-parameters that minimized the error function. @@ -424,7 +721,7 @@ raise ValueError('best_model called before fit')
    - def _error_handler(self, func, params): + def _error_handler(self, func, params): """ Endorses one job with two returned values: the status, and the time of execution @@ -437,7 +734,7 @@ output = None - def _handle(status, exception): + def _handle(status, exception): if self.raise_errors: raise exception else: @@ -465,7 +762,7 @@
    [docs] -def cross_val_predict(quantifier: BaseQuantifier, data: LabelledCollection, nfolds=3, random_state=0): +def cross_val_predict(quantifier: BaseQuantifier, data: LabelledCollection, nfolds=3, random_state=0): """ Akin to `scikit-learn's cross_val_predict <https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.cross_val_predict.html>`_ but for quantification. @@ -491,7 +788,7 @@
    [docs] -def expand_grid(param_grid: dict): +def expand_grid(param_grid: dict): """ Expands a param_grid dictionary as a list of configurations. Example: @@ -513,7 +810,7 @@
    [docs] -def group_params(param_grid: dict): +def group_params(param_grid: dict): """ Partitions a param_grid dictionary as two lists of configurations, one for the classifier-specific hyper-parameters, and another for que quantifier-specific hyper-parameters @@ -537,31 +834,75 @@
    -
    +
    + + + + + + + + + + + + - + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/plot.html b/docs/build/html/_modules/quapy/plot.html index 86b9d5a..16f5407 100644 --- a/docs/build/html/_modules/quapy/plot.html +++ b/docs/build/html/_modules/quapy/plot.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - Home + QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - Home -

    QuaPy

    @@ -131,7 +139,7 @@ @@ -183,6 +191,21 @@ + + @@ -229,7 +252,7 @@ @@ -272,6 +295,21 @@ + + @@ -332,19 +370,19 @@

    Source code for quapy.plot

    -from collections import defaultdict
    -import math
    +from collections import defaultdict
    +import math
     
    -from matplotlib import cm
    -import matplotlib.colors as mcolors
    -import matplotlib.pyplot as plt
    -from matplotlib.colors import LinearSegmentedColormap, ListedColormap
    -from matplotlib.pyplot import get_cmap
    -from matplotlib.ticker import ScalarFormatter
    -import numpy as np
    -from scipy.stats import ttest_ind_from_stats
    +from matplotlib import cm
    +import matplotlib.colors as mcolors
    +import matplotlib.pyplot as plt
    +from matplotlib.colors import LinearSegmentedColormap, ListedColormap
    +from matplotlib.pyplot import get_cmap
    +from matplotlib.ticker import ScalarFormatter
    +import numpy as np
    +from scipy.stats import ttest_ind_from_stats
     
    -import quapy as qp
    +import quapy as qp
     
     plt.rcParams['figure.figsize'] = [10, 6]
     plt.rcParams['figure.dpi'] = 200
    @@ -353,7 +391,7 @@
     
     
    [docs] -def binary_diagonal(method_names, true_prevs, estim_prevs, pos_class=1, title=None, show_std=True, legend=True, +def binary_diagonal(method_names, true_prevs, estim_prevs, pos_class=1, title=None, show_std=True, legend=True, train_prev=None, savepath=None, method_order=None): """ The diagonal plot displays the predicted prevalence values (along the y-axis) as a function of the true prevalence @@ -433,7 +471,7 @@
    [docs] -def binary_bias_global(method_names, true_prevs, estim_prevs, pos_class=1, title=None, savepath=None): +def binary_bias_global(method_names, true_prevs, estim_prevs, pos_class=1, title=None, savepath=None): """ Box-plots displaying the global bias (i.e., signed error computed as the estimated value minus the true value) for each quantification method with respect to a given positive class. @@ -479,7 +517,7 @@
    [docs] -def binary_bias_bins(method_names, true_prevs, estim_prevs, pos_class=1, title=None, nbins=5, colormap=cm.tab10, +def binary_bias_bins(method_names, true_prevs, estim_prevs, pos_class=1, title=None, nbins=5, colormap=cm.tab10, vertical_xticks=False, legend=True, savepath=None): """ Box-plots displaying the local bias (i.e., signed error computed as the estimated value minus the true value) @@ -505,7 +543,7 @@ :param savepath: path where to save the plot. If not indicated (as default), the plot is shown. :return: returns (fig, ax) matplotlib objects for eventual customisation """ - from pylab import boxplot, plot, setp + from pylab import boxplot, plot, setp fig, ax = plt.subplots() ax.grid() @@ -581,7 +619,7 @@
    [docs] -def error_by_drift(method_names, true_prevs, estim_prevs, tr_prevs, +def error_by_drift(method_names, true_prevs, estim_prevs, tr_prevs, n_bins=20, error_name='ae', show_std=False, show_density=True, show_legend=True, @@ -717,7 +755,7 @@
    [docs] -def brokenbar_supremacy_by_drift(method_names, true_prevs, estim_prevs, tr_prevs, +def brokenbar_supremacy_by_drift(method_names, true_prevs, estim_prevs, tr_prevs, n_bins=20, binning='isomerous', x_error='ae', y_error='ae', ttest_alpha=0.005, tail_density_threshold=0.005, method_order=None, @@ -831,7 +869,6 @@ best_bucket_methods.append(method_order[method_index]) best_methods.append(best_bucket_methods) salient_methods.update(best_bucket_methods) - print(best_bucket_methods) if binning=='isomerous': fig, axes = plt.subplots(2, 1, gridspec_kw={'height_ratios': [0.2, 1]}, figsize=(20, len(salient_methods))) @@ -914,7 +951,7 @@
    [docs] -def plot_simplex( +def plot_simplex( point_layers=None, region_layers=None, density_function=None, @@ -1032,7 +1069,7 @@ -def _merge(method_names, true_prevs, estim_prevs): +def _merge(method_names, true_prevs, estim_prevs): ndims = true_prevs[0].shape[1] data = defaultdict(lambda: {'true': np.empty(shape=(0, ndims)), 'estim': np.empty(shape=(0, ndims))}) method_order=[] @@ -1046,14 +1083,14 @@ return method_order, true_prevs_, estim_prevs_ -def _set_colors(ax, n_methods): +def _set_colors(ax, n_methods): NUM_COLORS = n_methods if NUM_COLORS>10: cm = plt.get_cmap('tab20') ax.set_prop_cycle(color=[cm(1. * i / NUM_COLORS) for i in range(NUM_COLORS)]) -def _save_or_show(savepath): +def _save_or_show(savepath): # if savepath is specified, then saves the plot in that path; otherwise the plot is shown if savepath is not None: qp.util.create_parent_dir(savepath) @@ -1063,7 +1100,7 @@ plt.show() -def _join_data_by_drift(method_names, true_prevs, estim_prevs, tr_prevs, x_error, y_error, method_order): +def _join_data_by_drift(method_names, true_prevs, estim_prevs, tr_prevs, x_error, y_error, method_order): data = defaultdict(lambda: {'x': np.empty(shape=(0)), 'y': np.empty(shape=(0))}) if method_order is None: @@ -1084,7 +1121,7 @@ return data -def _simplex_to_cartesian(prevalences): +def _simplex_to_cartesian(prevalences): prevalences = np.asarray(prevalences, dtype=float) prevalences = np.atleast_2d(prevalences) if prevalences.shape[1] != 3: @@ -1094,14 +1131,14 @@ return x, y -def _barycentric_from_xy(x, y): +def _barycentric_from_xy(x, y): p3 = 2 * y / np.sqrt(3) p2 = x - 0.5 * p3 p1 = 1 - p2 - p3 return np.stack([p1, p2, p3], axis=-1) -def _simplex_mesh(resolution): +def _simplex_mesh(resolution): simplex_ymax = np.sqrt(3) / 2 xs = np.linspace(0, 1, resolution) ys = np.linspace(0, simplex_ymax, resolution) @@ -1111,7 +1148,7 @@ return xs, ys, pts_bary, mask -def _evaluate_simplex_function(function, points): +def _evaluate_simplex_function(function, points): points = np.asarray(points, dtype=float) try: values = np.asarray(function(points), dtype=float) @@ -1124,14 +1161,14 @@ return np.asarray([function(point) for point in points], dtype=float) -def _region_colormap(color='blue', alpha=0.35): +def _region_colormap(color='blue', alpha=0.35): return ListedColormap([ (1.0, 1.0, 1.0, 0.0), (*mcolors.to_rgb(color), alpha), ]) -def _plot_simplex_points(ax, point_layers): +def _plot_simplex_points(ax, point_layers): for layer in point_layers: points = np.asarray(layer['points'], dtype=float) style = {'s': 25, 'alpha': 0.8} @@ -1139,7 +1176,7 @@ ax.scatter(*_simplex_to_cartesian(points), label=layer.get('label'), **style) -def _plot_simplex_regions(ax, region_layers, resolution): +def _plot_simplex_regions(ax, region_layers, resolution): xs, ys, pts_bary, simplex_mask = _simplex_mesh(resolution) valid_points = pts_bary[simplex_mask] @@ -1158,7 +1195,7 @@ ax.scatter([], [], color=layer.get('color', 'blue'), alpha=layer.get('alpha', 0.35), label=layer['label']) -def _plot_simplex_density(ax, density_function, resolution, color, alpha): +def _plot_simplex_density(ax, density_function, resolution, color, alpha): xs, ys, pts_bary, simplex_mask = _simplex_mesh(resolution) valid_points = pts_bary[simplex_mask] density = np.full(simplex_mask.shape, np.nan, dtype=float) @@ -1174,14 +1211,13 @@
    [docs] -def calibration_plot(prob_classifier, X, y, nbins=10, savepath=None): +def calibration_plot(prob_classifier, X, y, nbins=10, savepath=None): posteriors = prob_classifier.predict_proba(X) assert posteriors.ndim==2, 'calibration plot only works for binary problems' posteriors = posteriors[:,1] pred_y = posteriors>=0.5 bins = np.linspace(0, 1, nbins + 1) binned_values = np.digitize(posteriors, bins, right=False) - print(np.unique(binned_values)) correct = pred_y == y bin_centers = (bins[:-1] + bins[1:]) / 2 bins_names = np.arange(nbins) @@ -1202,8 +1238,8 @@ if __name__ == '__main__': - import quapy as qp - from sklearn.linear_model import LogisticRegression + import quapy as qp + from sklearn.linear_model import LogisticRegression data = qp.datasets.fetch_UCIBinaryDataset(qp.datasets.UCI_BINARY_DATASETS[6]) train, test = data.train_test classifier = LogisticRegression() diff --git a/docs/build/html/_modules/quapy/protocol.html b/docs/build/html/_modules/quapy/protocol.html index 50e3cb8..c14b2dc 100644 --- a/docs/build/html/_modules/quapy/protocol.html +++ b/docs/build/html/_modules/quapy/protocol.html @@ -1,104 +1,400 @@ - - - - - - quapy.protocol — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.protocol — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - +
    + + + + + + + + + + - -
    - + + + + +
    +
    + + + + + + +
    + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + + + + + +
    + +

    Source code for quapy.protocol

    +from copy import deepcopy
    +from typing import Iterable
    +
    +import quapy as qp
    +import numpy as np
    +import itertools
    +from contextlib import ExitStack
    +from abc import ABCMeta, abstractmethod
    +from quapy.data import LabelledCollection
    +import quapy.functional as F
    +from os.path import exists
    +from glob import glob
    +from collections.abc import Iterable
    +from numbers import Number
     
     
     
    [docs] -class AbstractProtocol(metaclass=ABCMeta): +class AbstractProtocol(metaclass=ABCMeta): """ Abstract parent class for sample generation protocols. """ @abstractmethod - def __call__(self): + def __call__(self): """ Implements the protocol. Yields one sample at a time along with its prevalence @@ -109,7 +405,7 @@
    [docs] - def total(self): + def total(self): """ Indicates the total number of samples that the protocol generates. @@ -122,16 +418,16 @@
    [docs] -class IterateProtocol(AbstractProtocol): +class IterateProtocol(AbstractProtocol): """ A very simple protocol which simply iterates over a list of previously generated samples :param samples: a list of :class:`quapy.data.base.LabelledCollection` """ - def __init__(self, samples: [LabelledCollection]): + def __init__(self, samples: [LabelledCollection]): self.samples = samples - def __call__(self): + def __call__(self): """ Yields one sample from the initial list at a time @@ -143,7 +439,7 @@
    [docs] - def total(self): + def total(self): """ Returns the number of samples in this protocol @@ -156,18 +452,18 @@
    [docs] -class ProtocolFromIndex(AbstractProtocol): +class ProtocolFromIndex(AbstractProtocol): """ A protocol from a list of indexes :param data: a :class:`quapy.data.base.LabelledCollection` :param indexes: a list of indexes """ - def __init__(self, data: LabelledCollection, indexes: Iterable): + def __init__(self, data: LabelledCollection, indexes: Iterable): self.data = data self.indexes = indexes - def __call__(self): + def __call__(self): """ Yields one sample at a time extracted using the indexes @@ -179,7 +475,7 @@
    [docs] - def total(self): + def total(self): """ Returns the number of samples in this protocol @@ -192,7 +488,7 @@
    [docs] -class AbstractStochasticSeededProtocol(AbstractProtocol): +class AbstractStochasticSeededProtocol(AbstractProtocol): """ An `AbstractStochasticSeededProtocol` is a protocol that generates, via any random procedure (e.g., via random sampling), sequences of :class:`quapy.data.base.LabelledCollection` samples. @@ -209,21 +505,21 @@ _random_state = -1 # means "not set" - def __init__(self, random_state=0): + def __init__(self, random_state=0): self.random_state = random_state @property - def random_state(self): + def random_state(self): return self._random_state @random_state.setter - def random_state(self, random_state): + def random_state(self, random_state): self._random_state = random_state
    [docs] @abstractmethod - def samples_parameters(self): + def samples_parameters(self): """ This function has to return all the necessary parameters to replicate the samples @@ -235,7 +531,7 @@
    [docs] @abstractmethod - def sample(self, params): + def sample(self, params): """ Extract one sample determined by the given parameters @@ -245,7 +541,7 @@ ...
    - def __call__(self): + def __call__(self): """ Yields one sample at a time. The type of object returned depends on the `collator` function. The default behaviour returns tuples of the form `(sample, prevalence)`. @@ -264,7 +560,7 @@
    [docs] - def collator(self, sample, params): + def collator(self, sample, params): """ The collator prepares the sample to accommodate the desired output format before returning the output. This collator simply returns the sample as it is. Classes inheriting from this abstract class can @@ -281,7 +577,7 @@
    [docs] -class OnLabelledCollectionProtocol(AbstractStochasticSeededProtocol): +class OnLabelledCollectionProtocol(AbstractStochasticSeededProtocol): """ Protocols that generate samples from a :class:`qp.data.LabelledCollection` object. """ @@ -290,7 +586,7 @@
    [docs] - def get_labelled_collection(self): + def get_labelled_collection(self): """ Returns the labelled collection on which this protocol acts. @@ -301,7 +597,7 @@
    [docs] - def on_preclassified_instances(self, pre_classifications, in_place=False): + def on_preclassified_instances(self, pre_classifications, in_place=False): """ Returns a copy of this protocol that acts on a modified version of the original :class:`qp.data.LabelledCollection` in which the original instances have been replaced @@ -328,7 +624,7 @@
    [docs] @classmethod - def get_collator(cls, return_type='sample_prev'): + def get_collator(cls, return_type='sample_prev'): """ Returns a collator function, i.e., a function that prepares the yielded data @@ -350,7 +646,7 @@
    [docs] - def sample(self, index): + def sample(self, index): """ Realizes the sample given the index of the instances. @@ -364,7 +660,7 @@
    [docs] -class APP(OnLabelledCollectionProtocol): +class APP(OnLabelledCollectionProtocol): """ Implementation of the artificial prevalence protocol (APP). The APP consists of exploring a grid of prevalence values containing `n_prevalences` points (e.g., @@ -388,7 +684,7 @@ to "labelled_collection" to get instead instances of LabelledCollection """ - def __init__(self, data: LabelledCollection, sample_size=None, n_prevalences=21, repeats=10, + def __init__(self, data: LabelledCollection, sample_size=None, n_prevalences=21, repeats=10, smooth_limits_epsilon=0, random_state=0, sanity_check=10000, return_type='sample_prev'): super(APP, self).__init__(random_state) self.data = data @@ -410,7 +706,7 @@
    [docs] - def prevalence_grid(self): + def prevalence_grid(self): """ Generates vectors of prevalence values from an exhaustive grid of prevalence values. The number of prevalence values explored for each dimension depends on `n_prevalences`, so that, if, for example, @@ -438,7 +734,7 @@
    [docs] - def samples_parameters(self): + def samples_parameters(self): """ Return all the necessary parameters to replicate the samples as according to the APP protocol. @@ -453,7 +749,7 @@
    [docs] - def total(self): + def total(self): """ Returns the number of samples that will be generated @@ -466,7 +762,7 @@
    [docs] -class NPP(OnLabelledCollectionProtocol): +class NPP(OnLabelledCollectionProtocol): """ A generator of samples that implements the natural prevalence protocol (NPP). The NPP consists of drawing samples uniformly at random, therefore approximately preserving the natural prevalence of the collection. @@ -481,7 +777,7 @@ to "labelled_collection" to get instead instances of LabelledCollection """ - def __init__(self, data:LabelledCollection, sample_size=None, repeats=100, random_state=0, + def __init__(self, data:LabelledCollection, sample_size=None, repeats=100, random_state=0, return_type='sample_prev'): super(NPP, self).__init__(random_state) self.data = data @@ -492,7 +788,7 @@
    [docs] - def samples_parameters(self): + def samples_parameters(self): """ Return all the necessary parameters to replicate the samples as according to the NPP protocol. @@ -507,7 +803,7 @@
    [docs] - def total(self): + def total(self): """ Returns the number of samples that will be generated (equals to "repeats") @@ -520,7 +816,7 @@
    [docs] -class UPP(OnLabelledCollectionProtocol): +class UPP(OnLabelledCollectionProtocol): """ A variant of :class:`APP` that, instead of using a grid of equidistant prevalence values, relies on the Kraemer algorithm for sampling unit (k-1)-simplex uniformly at random, with @@ -539,7 +835,7 @@ to "labelled_collection" to get instead instances of LabelledCollection """ - def __init__(self, data: LabelledCollection, sample_size=None, repeats=100, random_state=0, + def __init__(self, data: LabelledCollection, sample_size=None, repeats=100, random_state=0, return_type='sample_prev'): super(UPP, self).__init__(random_state) self.data = data @@ -550,7 +846,7 @@
    [docs] - def samples_parameters(self): + def samples_parameters(self): """ Return all the necessary parameters to replicate the samples as according to the UPP protocol. @@ -565,7 +861,7 @@
    [docs] - def total(self): + def total(self): """ Returns the number of samples that will be generated (equals to "repeats") @@ -578,7 +874,7 @@
    [docs] -class DirichletProtocol(OnLabelledCollectionProtocol): +class DirichletProtocol(OnLabelledCollectionProtocol): """ A protocol that establishes a prior Dirichlet distribution for the prevalence of the samples. Note that providing an all-ones vector of Dirichlet parameters is equivalent to invoking the @@ -596,13 +892,8 @@ to "labelled_collection" to get instead instances of LabelledCollection """ - def __init__(self, data: LabelledCollection, alpha, sample_size=None, repeats=100, random_state=0, + def __init__(self, data: LabelledCollection, alpha, sample_size=None, repeats=100, random_state=0, return_type='sample_prev'): - #assert ((isinstance(alpha, str) and alpha == 'uniform') or - # isinstance(alpha, Number) or - # (isinstance(alpha, Iterable) and all(isinstance(v, Number) for v in alpha))), \ - # f'wrong type for {alpha=}; expected "uniform", a real scalar, or an array-like of real values' - n_classes = data.n_classes if isinstance(alpha, str) and alpha == 'uniform': self.alpha = np.ones(n_classes, dtype=float) @@ -617,7 +908,6 @@ super(DirichletProtocol, self).__init__(random_state) self.data = data - #self.alpha = alpha self.sample_size = qp._get_sample_size(sample_size) self.repeats = repeats self.random_state = random_state @@ -625,7 +915,7 @@
    [docs] - def samples_parameters(self): + def samples_parameters(self): """ Return all the necessary parameters to replicate the samples. @@ -638,7 +928,7 @@
    [docs] - def total(self): + def total(self): """ Returns the number of samples that will be generated (equals to "repeats") @@ -651,7 +941,7 @@
    [docs] -class DomainMixer(AbstractStochasticSeededProtocol): +class DomainMixer(AbstractStochasticSeededProtocol): """ Generates mixtures of two domains (A and B) at controlled rates, but preserving the original class prevalence. @@ -670,7 +960,7 @@ will be the same every time the protocol is called) """ - def __init__( + def __init__( self, domainA: LabelledCollection, domainB: LabelledCollection, @@ -704,7 +994,7 @@
    [docs] - def samples_parameters(self): + def samples_parameters(self): """ Return all the necessary parameters to replicate the samples as according to the this protocol. @@ -724,7 +1014,7 @@
    [docs] - def sample(self, indexes): + def sample(self, indexes): """ Realizes the sample given a pair of indexes of the instances from A and B. @@ -739,7 +1029,7 @@
    [docs] - def total(self): + def total(self): """ Returns the number of samples that will be generated (equals to "repeats * mixture_points") @@ -757,31 +1047,75 @@ UniformPrevalenceProtocol = UPP
    -
    +
    + + + + + + + + + + + + - + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/util.html b/docs/build/html/_modules/quapy/util.html index 9a26185..486a193 100644 --- a/docs/build/html/_modules/quapy/util.html +++ b/docs/build/html/_modules/quapy/util.html @@ -29,7 +29,7 @@ - + @@ -370,26 +370,26 @@

    Source code for quapy.util

    -import contextlib
    -import itertools
    -import multiprocessing
    -import os
    -import pickle
    -import urllib
    -from pathlib import Path
    -from contextlib import ExitStack
    +import contextlib
    +import itertools
    +import multiprocessing
    +import os
    +import pickle
    +import urllib
    +from pathlib import Path
    +from contextlib import ExitStack
     
    -import pandas as pd
    +import pandas as pd
     
    -import quapy as qp
    +import quapy as qp
     
    -import numpy as np
    -from joblib import Parallel, delayed
    -from time import time
    -import signal
    +import numpy as np
    +from joblib import Parallel, delayed
    +from time import time
    +import signal
     
     
    -def _get_parallel_slices(n_tasks, n_jobs):
    +def _get_parallel_slices(n_tasks, n_jobs):
         if n_jobs == -1:
             n_jobs = multiprocessing.cpu_count()
         batch = int(n_tasks / n_jobs)
    @@ -399,7 +399,7 @@
     
     
    [docs] -def map_parallel(func, args, n_jobs): +def map_parallel(func, args, n_jobs): """ Applies func to n_jobs slices of args. E.g., if args is an array of 99 items and n_jobs=2, then func is applied in two parallel processes to args[0:50] and to args[50:99]. func is a function @@ -420,7 +420,7 @@
    [docs] -def parallel(func, args, n_jobs, seed=None, asarray=True, backend='loky'): +def parallel(func, args, n_jobs, seed=None, asarray=True, backend='loky'): """ A wrapper of multiprocessing: @@ -438,7 +438,7 @@ :param backend: indicates the backend used for handling parallel works :param open_args: if True, then the delayed function is called on *args_i, instead of on args_i """ - def func_dec(environ, seed, *args): + def func_dec(environ, seed, *args): qp.environ = environ.copy() qp.environ['N_JOBS'] = 1 #set a context with a temporal seed to ensure results are reproducibles in parallel @@ -458,7 +458,7 @@
    [docs] -def parallel_unpack(func, args, n_jobs, seed=None, asarray=True, backend='loky'): +def parallel_unpack(func, args, n_jobs, seed=None, asarray=True, backend='loky'): """ A wrapper of multiprocessing: @@ -476,7 +476,7 @@ :param backend: indicates the backend used for handling parallel works """ - def func_dec(environ, seed, *args): + def func_dec(environ, seed, *args): qp.environ = environ.copy() qp.environ['N_JOBS'] = 1 # set a context with a temporal seed to ensure results are reproducibles in parallel @@ -496,7 +496,7 @@
    [docs] @contextlib.contextmanager -def temp_seed(random_state): +def temp_seed(random_state): """ Can be used in a "with" context to set a temporal seed without modifying the outer numpy's current state. E.g.: @@ -520,14 +520,14 @@
    [docs] -def download_file(url, archive_filename): +def download_file(url, archive_filename): """ Downloads a file from a url :param url: the url :param archive_filename: destination filename """ - def progress(blocknum, bs, size): + def progress(blocknum, bs, size): total_sz_mb = '%.2f MB' % (size / 1e6) current_sz_mb = '%.2f MB' % ((blocknum * bs) / 1e6) print('\rdownloaded %s / %s' % (current_sz_mb, total_sz_mb), end='') @@ -539,7 +539,7 @@
    [docs] -def download_file_if_not_exists(url, archive_filename): +def download_file_if_not_exists(url, archive_filename): """ Downloads a file (using :meth:`download_file`) if the file does not exist. @@ -555,7 +555,7 @@
    [docs] -def create_if_not_exist(path): +def create_if_not_exist(path): """ An alias to `os.makedirs(path, exist_ok=True)` that also returns the path. This is useful in cases like, e.g.: @@ -571,7 +571,7 @@
    [docs] -def get_quapy_home(): +def get_quapy_home(): """ Gets the home directory of QuaPy, i.e., the directory where QuaPy saves permanent data, such as dowloaded datasets. This directory is `~/quapy_data` @@ -586,7 +586,7 @@
    [docs] -def create_parent_dir(path): +def create_parent_dir(path): """ Creates the parent dir (if any) of a given path, if not exists. E.g., for `./path/to/file.txt`, the path `./path/to` is created. @@ -601,7 +601,7 @@
    [docs] -def save_text_file(path, text): +def save_text_file(path, text): """ Saves a text file to disk, given its full path, and creates the parent directory if missing. @@ -616,7 +616,7 @@
    [docs] -def pickled_resource(pickle_path:str, generation_func:callable, *args): +def pickled_resource(pickle_path:str, generation_func:callable, *args): """ Allows for fast reuse of resources that are generated only once by calling generation_func(\\*args). The next times this function is invoked, it loads the pickled resource. Example: @@ -646,7 +646,7 @@ -def _check_sample_size(sample_size): +def _check_sample_size(sample_size): if sample_size is None: assert qp.environ['SAMPLE_SIZE'] is not None, \ 'error: sample_size set to None, and cannot be resolved from the environment' @@ -658,8 +658,8 @@
    [docs] -def load_report(path, as_dict=False): - def str2prev_arr(strprev): +def load_report(path, as_dict=False): + def str2prev_arr(strprev): within = strprev.strip('[]').split() float_list = [float(p) for p in within] float_list[-1] = 1. - sum(float_list[:-1]) @@ -683,7 +683,7 @@
    [docs] -class EarlyStop: +class EarlyStop: """ A class implementing the early-stopping condition typically used for training neural networks. @@ -708,7 +708,7 @@ :ivar IMPROVED: flag (boolean) indicating whether there was an improvement in the last call """ - def __init__(self, patience, lower_is_better=True): + def __init__(self, patience, lower_is_better=True): self.PATIENCE_LIMIT = patience self.better = lambda a,b: a<b if lower_is_better else a>b @@ -718,7 +718,7 @@ self.STOP = False self.IMPROVED = False - def __call__(self, watch_score, epoch): + def __call__(self, watch_score, epoch): """ Commits the new score found in epoch `epoch`. If the score improves over the best score found so far, then the patiente counter gets reset. If otherwise, the patience counter is decreased, and in case it reachs 0, @@ -742,7 +742,7 @@
    [docs] @contextlib.contextmanager -def timeout(seconds): +def timeout(seconds): """ Opens a context that will launch an exception if not closed after a given number of seconds @@ -761,7 +761,7 @@ :param seconds: number of seconds, set to <=0 to ignore the timer """ if seconds > 0: - def handler(signum, frame): + def handler(signum, frame): raise TimeoutError() signal.signal(signal.SIGALRM, handler) diff --git a/docs/source/manuals/methods.md b/docs/source/manuals/methods.md index 93aa1dd..a6a8151 100644 --- a/docs/source/manuals/methods.md +++ b/docs/source/manuals/methods.md @@ -227,6 +227,38 @@ and is suitable for problems in which the `q = Mp` matrix is nearly non-invertib Note that this quantification method requires `val_split` to be a `float` and installation of additional dependencies (`$ pip install quapy[bayes]`) needed to run Markov chain Monte Carlo sampling. Markov Chain Monte Carlo is is slower than matrix inversion methods, but is guaranteed to sample proper probability vectors, so no clipping strategies are required. An example presenting how to run the method and use posterior samples is available in `examples/bayesian_quantification.py`. +### Regularized Learning under Label Shift (RLLS) + +`RLLS` is available at `qp.method.aggregative.RLLS` and ports the regularized +importance-weight estimation procedure of +[Azizzadenesheli, K., Liu, A., Yang, F., and Anandkumar, A. (2019). Regularized +Learning for Domain Adaptation under Label Shifts. +ICLR 2019](https://arxiv.org/abs/1903.09734) to QuaPy's aggregative interface. +The method estimates the label-shift importance weights `w = q(y)/p(y)` from +the classifier's validation posteriors (or, in `mode='hard'`, its argmax +predictions) and the corresponding source labels, regularizing the estimation +by an amount controlled by `alpha` (scaled by a finite-sample confidence term +governed by `delta`). The resulting weights are then used to rescale the +training prevalence into the target prevalence estimate. + +Like ACC and PACC, RLLS requires validation predictions and therefore expects +`val_split` to be set (as an integer for k-fold cross-validation, a float for +a held-out split, or an explicit `(X, y)` tuple) whenever `fit_classifier=True`. +This method relies on the optional `cvxpy` dependency, which must be +installed separately (`$ pip install cvxpy`). + +```python +import quapy as qp +from quapy.method.aggregative import RLLS +from sklearn.linear_model import LogisticRegression + +train, test = qp.datasets.fetch_UCIBinaryDataset('haberman').train_test + +model = RLLS(LogisticRegression(max_iter=2000), val_split=5) +model.fit(*train.Xy) +estim_prevalence = model.predict(test.X) +``` + ### Expectation Maximization (EMQ) The Expectation Maximization Quantifier (EMQ), also known as @@ -274,6 +306,40 @@ or Temperature Scaling (`ts`); default is `None` (no calibration). You can use the class method `EMQ_BCTS` to effortlessly instantiate EMQ with the best performing heuristics found by [Alexandari et al. (2020)](http://proceedings.mlr.press/v119/alexandari20a.html). See the API documentation for further details. +#### BayesianMAPLS + +`BayesianMAPLS` is a Bayesian variant of EMQ/MLLS proposed by +Ye, C. et al. (2024). Label shift estimation for class-imbalance problem: A +Bayesian approach. Proceedings of the IEEE/CVF Winter Conference on +Applications of Computer Vision (WACV 2024). QuaPy's implementation is +adapted from the [authors' reference code](https://github.com/ChangkunYe/MAPLS/blob/main/label_shift/mapls.py). +Rather than returning a single point estimate for the class prevalence, it +places a Dirichlet prior over the sought prevalence vector (in an +unconstrained, Isometric-Log-Ratio-transformed space) and samples from the +resulting posterior via Markov Chain Monte Carlo (using `numpyro`/`jax`), +conditioned on a preliminary MAP estimate obtained via the underlying `mapls` +routine. Like `BayesianCC`, its `aggregate` method returns the posterior mean, +while `predict_conf` additionally returns a confidence region (`intervals`, +`ellipse`, `ellipse-clr`, or `ellipse-ilr`) built from the posterior samples. + +This method requires installation of additional dependencies +(`$ pip install quapy[bayes]`) needed to run MCMC sampling; parameters +`num_warmup` and `num_samples` control the length of the chain, and `prior` +allows choosing between a uniform Dirichlet prior (default) or one of the +data-dependent priors ("map"/"map2") proposed in the original paper. + +```python +import quapy as qp +from quapy.method._bayesian import BayesianMAPLS +from sklearn.linear_model import LogisticRegression + +train, test = qp.datasets.fetch_UCIBinaryDataset('haberman').train_test + +model = BayesianMAPLS(LogisticRegression()) +model.fit(*train.Xy) +estim_prevalence, conf_region = model.predict_conf(test.X) +``` + ### Hellinger Distance y (HDy) @@ -324,6 +390,38 @@ framework proposed by [Maletzke et al (2020)](https://ojs.aaai.org/index.php/AAA and the "SMM" method proposed by [Hassan et al (2019)](https://ieeexplore.ieee.org/document/9260028) (thanks to _Pablo González_ for the contributions!) +#### PQ + +`PQ` (Precise Quantifier), available at `qp.method.confidence.PQ`, is a +Bayesian distribution-matching variant of `HDy` proposed in +[Igiraneza, A.B., Fraser, C., and Hinch, R. (2025). Estimating prevalence +with precision and accuracy.](https://arxiv.org/abs/2507.06061) +Rather than matching a single test histogram against a mixture of two +class-conditional histograms via a divergence measure (as `HDy` does), `PQ` +places the histogram-matching problem in a Bayesian setting and samples the +full posterior distribution over the (binary) prevalence value via Markov +Chain Monte Carlo (using `stan`). Its `aggregate` method returns the +posterior mean, while `predict_conf` additionally returns a confidence +region built from the posterior samples (`intervals`, `ellipse`, or +`ellipse-clr`). + +`PQ` accepts `nbins` (the number of histogram bins, quantile-based by default, +or uniform if `fixed_bins=True`), and the usual MCMC controls `num_warmup`, +`num_samples`, and `stan_seed`. This method relies on the optional `stan` +dependency, installed via `$ pip install quapy[bayes]`. + +```python +import quapy as qp +from quapy.method.confidence import PQ +from sklearn.linear_model import LogisticRegression + +train, test = qp.datasets.fetch_UCIBinaryDataset('haberman').train_test + +model = PQ(LogisticRegression()) +model.fit(*train.Xy) +estim_prevalence, conf_region = model.predict_conf(test.X) +``` + ### Threshold Optimization methods QuaPy implements Forman's threshold optimization methods; @@ -442,6 +540,199 @@ All KDE-based methods depend on the hyperparameter `bandwidth` of the kernel. Ty that can be explored in model selection range in [0.01, 0.25]. Previous experiments reveal the methods' performance varies smoothly at small variations of this hyperparameter. +#### BayesianKDEy + +`BayesianKDEy`, available at `qp.method._bayesian.BayesianKDEy`, is a Bayesian +version of KDEy. Instead of solving for the single prevalence vector that +minimizes a divergence between the test distribution and a KDE-based mixture +model (as the KDEy variants above do), `BayesianKDEy` places a Dirichlet +prior over the prevalence vector and samples its posterior via Markov Chain +Monte Carlo (using `numpyro`/`jax`), conditioned on the same KDE mixture +components. Its `aggregate` method returns the posterior mean, while +`predict_conf` additionally returns a confidence region built from the +posterior samples. + +In addition to the `kernel` and `bandwidth` hyperparameters (with the same +`gaussian`/`aitchison`/`ilr` kernel choice, and `shrinkage` regularization +for the latter two, available in `KDEyML`), `BayesianKDEy` exposes the usual +MCMC controls: `num_warmup`, `num_samples`, `mcmc_seed`, a `temperature` for +posterior calibration, and `prior` for choosing the Dirichlet prior +(`'uniform'` by default, or a custom scalar/array). This method relies on +the optional MCMC dependencies, installed via +`$ pip install quapy[bayes]`. + +```python +import quapy as qp +from quapy.method._bayesian import BayesianKDEy +from sklearn.linear_model import LogisticRegression + +train, test = qp.datasets.fetch_UCIBinaryDataset('haberman').train_test + +model = BayesianKDEy(LogisticRegression(), bandwidth=0.1) +model.fit(*train.Xy) +estim_prevalence, conf_region = model.predict_conf(test.X) +``` + + +## Non-Aggregative Methods + +Non-aggregative methods are quantifiers that do not follow the two-step +(classify, then aggregate) pattern described above for aggregative methods. +These methods are implemented in the `qp.method.non_aggregative` module and +extend `BaseQuantifier` directly, implementing `fit` and `predict` on their own terms. + +### Maximum Likelihood Prevalence Estimation (MLPE) + +`MaximumLikelihoodPrevalenceEstimation` (MLPE) is a lazy baseline quantifier +that assumes the IID assumption holds, i.e., that there is no prior probability +shift between the training and the test distributions. Its `fit` method simply +computes and stores the training prevalence, and its `predict` method returns +that same training prevalence for any test sample, irrespective of the sample +itself. MLPE is considered a lower-bound quantifier: any quantification method +worth using should outperform it. + +```python +import quapy as qp +from quapy.method.non_aggregative import MaximumLikelihoodPrevalenceEstimation + +dataset = qp.datasets.fetch_UCIBinaryDataset('haberman') +train, test = dataset.train_test + +model = MaximumLikelihoodPrevalenceEstimation() +model.fit(*train.Xy) +estim_prevalence = model.predict(test.X) # always equals train.prevalence() +``` + +### Distribution Matching x (DMx) and Hellinger Distance x (HDx) + +`DMx` is the covariate-space counterpart of the `DMy` distribution-matching +quantifier described in {ref}`the Hellinger Distance y (HDy) section `: +instead of matching distributions built from the classifier's predictions, `DMx` matches +distributions built directly from the (discretized) feature space, and thus +requires no classifier at all. For each class, `DMx` builds one histogram per +feature from the training instances of that class; at prediction time, it +searches for the mixture of these class-conditional histograms that best +matches the (also histogram-based) representation of the test sample, in +terms of a chosen divergence. + +`DMx` accepts the following hyperparameters in its constructor: + +* `nbins`: the number of bins used to discretize each feature (default 8) +* `divergence`: a string ("HD" for Hellinger Distance, or "topsoe") or a + callable taking two histograms and returning a divergence value (default "HD") +* `cdf`: whether to match cumulative distributions (CDFs) instead of the + histograms (PDFs) themselves (default False) +* `search`: the strategy used for finding the optimal prevalence; valid + options are `optim_minimize` (default, works for binary and multiclass + problems), `linear_search`, and `ternary_search` (these last two are + binary-only) +* `n_jobs`: number of parallel workers (default None) + +`DMx` also offers the class method `DMx.HDx` (aliased as +`qp.method.non_aggregative.HDx`, and also as `HellingerDistanceX`) that +reproduces the original Hellinger Distance x (HDx) method proposed by +[González-Castro, Alaiz-Rodríguez, and Alegre (2013)](https://www.sciencedirect.com/science/article/pii/S0020025512004069), +the same paper that introduced HDy. HDx is a binary-only method that computes +the matching for `nbins` ranging over `[10, 20, ..., 110]` (via a +`MedianEstimator`, taking the median of the resulting estimates) and searches +for the best prevalence via a linear search stepping by 0.01, rather than via +the `optim_minimize` search used by `DMx` by default. + +The following code, adapted from the example comparing HDy and HDx +(`examples/11.comparing_HDy_HDx.py`), shows the two methods side-by-side: + +```python +from sklearn.linear_model import LogisticRegression +import quapy as qp +from quapy.method.aggregative import HDy +from quapy.method.non_aggregative import DMx + +train, test = qp.datasets.fetch_UCIBinaryDataset('haberman').train_test +Xtr, ytr = train.Xy + +hdy = HDy(LogisticRegression()).fit(Xtr, ytr) +estim_prevalence_hdy = hdy.predict(test.X) + +hdx = DMx.HDx(n_jobs=-1).fit(Xtr, ytr) +estim_prevalence_hdx = hdx.predict(test.X) +``` + +Note that, unlike HDy, HDx requires no classifier whatsoever, since it +operates directly on the covariates. + +### ReadMe + +`ReadMe` is a non-aggregative quantification method proposed by +[Hopkins, D. and King, G. (2007). A method of automated nonparametric content +analysis for social science. American Journal of Political Science, +54(1):229-247.](https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1540-5907.2009.00428.x) +The method estimates `Q(Y=i)` directly from `Q(X) = sum_i Q(X|Y=i) Q(Y=i)` by +solving a (constrained) least-squares regression, thus avoiding the cost of +estimating posterior probabilities `Q(Y=i|X)` altogether. + +Since `Q(X)` and `Q(X|Y=i)` can be of very high dimension for realistic +feature spaces, ReadMe renders the problem tractable by performing bagging in +the feature space: many small random subsets of features (of size +`bagging_range`) are drawn, the least-squares problem is solved on each +subset, and the resulting estimates are averaged. ReadMe additionally +combines this bagging procedure with bootstrap resampling of the training +instances in order to derive confidence regions around the point estimate; +accordingly, `ReadMe` implements the `WithConfidenceABC` interface (see the +{ref}`confidence regions section `), +and exposes a `predict_conf` method in addition to `predict`. + +`ReadMe` accepts the following hyperparameters: + +* `prob_model`: either `"full"` (default), the original Hopkins and King + formulation, in which `Q(X)` and `Q(X|Y)` are modelled empirically and thus + require the feature matrix `X` to be binary (e.g., term presence/absence); + or `"naive"`, a much faster approximation that models `Q(X)` and `Q(X|Y)` as + multinomial (bag-of-words) distributions, and that supports much larger + values of `bagging_range` +* `bootstrap_trials`: number of bootstrap resamplings of the training data + used for deriving the confidence region (default 300) +* `bagging_trials`: number of bagging trials, i.e., random feature subsets, + averaged for each point estimate (default 300) +* `bagging_range`: number of features kept in each bagging trial (default 15); + note that, when `prob_model="full"`, this value should typically be kept + small (the authors advise against values above 25) since the empirical + distribution requires enumerating `2^bagging_range` possible feature + configurations +* `confidence_level`: the confidence level for the confidence region + (default 0.95) +* `region`: the type of confidence region to construct, one of `"intervals"` + (default), `"ellipse"`, or `"ellipse-clr"` (see the + {ref}`confidence regions section ` + for details) +* `random_state`: an int for replicability, or `None` (default) +* `verbose`: whether to display progress information (default False) + +The following minimal example, adapted from +`examples/18.ReadMe_for_text_analysis.py`, shows ReadMe applied to a binary +bag-of-words text quantification problem: + +```python +from sklearn.feature_extraction.text import CountVectorizer +from sklearn.pipeline import Pipeline +import quapy as qp +from quapy.method.non_aggregative import ReadMe + +reviews = qp.datasets.fetch_reviews('imdb').reduce(n_train=1000, random_state=0) + +# ReadMe's "full" model requires a binary feature matrix +encode_0_1 = Pipeline([('0_1_terms', CountVectorizer(min_df=5, binary=True))]) +train, test = qp.data.preprocessing.instance_transformation(reviews, encode_0_1, inplace=True).train_test + +model = ReadMe(prob_model='full', bootstrap_trials=100, bagging_trials=100, bagging_range=20, random_state=0) +model.fit(*train.Xy) # lazy: only bootstrap resampling happens here + +estim_prevalence, conf_region = model.predict_conf(test.X) +``` + +Note that `ReadMe` is computationally expensive: its cost scales with the +product of `bootstrap_trials` and `bagging_trials`, each of which requires +solving a least-squares problem. + ## Composable Methods