Skip to content

Commit 1c733f3

Browse files
committed
Merge branch 'devel' of github.com:HLT-ISTI/QuaPy into devel
2 parents b59d8cb + 2034392 commit 1c733f3

File tree

13 files changed

+595
-21
lines changed

13 files changed

+595
-21
lines changed

CHANGE_LOG.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
Change Log 0.1.10
2+
-----------------
3+
4+
- Added (aggregative) bootstrap for deriving confidence regions (confidence intervals, ellipses in the simplex, or
5+
ellipses in the CLR space). This method is efficient as it leverages the two-phases of the aggregative quantifiers.
6+
This method applies resampling only to the aggregation phase, thus avoiding to train many quantifiers, or
7+
classify multiple times the instances of a sample. See the new example no. 15.
8+
9+
110
Change Log 0.1.9
211
----------------
312

TODO.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
- [TODO] adapt BayesianCC to WithConfidence interface
2+
- [TODO] Test the return_type="index" in protocols and finish the "distributin_samples.py" example
3+
- [TODO] Add EDy (an implementation is available at quantificationlib)
14
- [TODO] add ensemble methods SC-MQ, MC-SQ, MC-MQ
25
- [TODO] add HistNetQ
36
- [TODO] add CDE-iteration and Bayes-CDE methods

examples/0.basics.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,8 @@
3333
print(f'training prevalence = {F.strprev(train.prevalence())}')
3434

3535
# let us train one quantifier, for example, PACC using a sklearn's Logistic Regressor as the underlying classifier
36-
# classifier = LogisticRegression()
37-
38-
# pacc = qp.method.aggregative.PACC(classifier)
39-
pacc = qp.method.aggregative.PACC()
36+
classifier = LogisticRegression()
37+
pacc = qp.method.aggregative.PACC(classifier)
4038

4139
print(f'training {pacc}')
4240
pacc.fit(train)

examples/15.confidence_regions.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from quapy.method.confidence import AggregativeBootstrap
2+
from quapy.method.aggregative import PACC
3+
import quapy.functional as F
4+
import quapy as qp
5+
6+
"""
7+
Just like any other type of estimator, quantifier predictions are affected by error. It is therefore useful to provide,
8+
along with the point estimate (the class prevalence values) a measure of uncertainty. These, typically come in the
9+
form of credible regions around the point estimate.
10+
11+
QuaPy implements a method for deriving confidence regions around point estimates of class prevalence based on bootstrap.
12+
13+
Bootstrap method comes down to resampling the population several times, thus generating a series of point estimates.
14+
QuaPy provides a variant of bootstrap for aggregative quantifiers, that only applies resampling to the pre-classified
15+
instances.
16+
17+
Let see one example:
18+
"""
19+
20+
# load some data
21+
data = qp.datasets.fetch_UCIMulticlassDataset('molecular')
22+
train, test = data.train_test
23+
24+
# by simply wrapping an aggregative quantifier within the AggregativeBootstrap class, we can obtain confidence
25+
# intervals around the point estimate, in this case, at 95% of confidence
26+
pacc = AggregativeBootstrap(PACC(), confidence_level=0.95)
27+
28+
with qp.util.temp_seed(0):
29+
# we train the quantifier the usual way
30+
pacc.fit(train)
31+
32+
# let us simulate some shift in the test data
33+
random_prevalence = F.uniform_prevalence_sampling(n_classes=test.n_classes)
34+
shifted_test = test.sampling(200, *random_prevalence)
35+
true_prev = shifted_test.prevalence()
36+
37+
# by calling "quantify_conf", we obtain the point estimate and the confidence intervals around it
38+
pred_prev, conf_intervals = pacc.quantify_conf(shifted_test.X)
39+
40+
# conf_intervals is an instance of ConfidenceRegionABC, which provides some useful utilities like:
41+
# - coverage: a function which computes the fraction of true values that belong to the confidence region
42+
# - simplex_proportion: estimates the proportion of the simplex covered by the confidence region (amplitude)
43+
# ideally, we are interested in obtaining confidence regions with high level of coverage and small amplitude
44+
45+
# the point estimate is computed as the mean of all bootstrap predictions; let us see the prediction error
46+
error = qp.error.ae(true_prev, pred_prev)
47+
48+
# some useful outputs
49+
print(f'train prevalence: {F.strprev(train.prevalence())}')
50+
print(f'test prevalence: {F.strprev(true_prev)}')
51+
print(f'point-estimate: {F.strprev(pred_prev)}')
52+
print(f'absolute error: {error:.3f}')
53+
print(f'Is the true value in the confidence region?: {conf_intervals.coverage(true_prev)==1}')
54+
print(f'Proportion of simplex covered at {pacc.confidence_level*100:.1f}%: {conf_intervals.simplex_portion()*100:.2f}%')
55+
56+
"""
57+
Final remarks:
58+
There are various ways for performing bootstrap:
59+
- the population-based approach (default): performs resampling of the test instances
60+
e.g., use AggregativeBootstrap(PACC(), n_train_samples=1, n_test_samples=100, confidence_level=0.95)
61+
- the model-based approach: performs resampling of the training instances, thus training several quantifiers
62+
e.g., use AggregativeBootstrap(PACC(), n_train_samples=100, n_test_samples=1, confidence_level=0.95)
63+
this implementation avoids retraining the classifier, and performs resampling only to train different aggregation functions
64+
- the combined approach: a combination of the above
65+
e.g., use AggregativeBootstrap(PACC(), n_train_samples=100, n_test_samples=100, confidence_level=0.95)
66+
this example will generate 100 x 100 predictions
67+
68+
There are different ways for constructing confidence regions implemented in QuaPy:
69+
- confidence intervals: the simplest way, and one that typically works well in practice
70+
use: AggregativeBootstrap(PACC(), confidence_level=0.95, method='intervals')
71+
- confidence ellipse in the simplex: creates an ellipse, which lies on the probability simplex, around the point estimate
72+
use: AggregativeBootstrap(PACC(), confidence_level=0.95, method='ellipse')
73+
- confidence ellipse in the Centered-Log Ratio (CLR) space: creates an ellipse in the CLR space (this should be
74+
convenient for taking into account the inner structure of the probability simplex)
75+
use: AggregativeBootstrap(PACC(), confidence_level=0.95, method='ellipse-clr')
76+
"""
77+
78+

examples/distributing_samples.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
Imagine we want to generate many samples out of a collection, that we want to distribute for others to run their
3+
own experiments in the very same test samples. One naive solution would come down to applying a given protocol to
4+
our collection (say the artificial prevalence protocol on the 'academic-success' UCI dataset), store all those samples
5+
on disk and make them available online. Distributing many such samples is undesirable.
6+
In this example, we generate the indexes that allow anyone to regenerate the samples out of the original collection.
7+
"""
8+
9+
import quapy as qp
10+
from quapy.method.aggregative import PACC
11+
from quapy.protocol import UPP
12+
13+
data = qp.datasets.fetch_UCIMulticlassDataset('academic-success')
14+
train, test = data.train_test
15+
16+
# let us train a quantifier to check whether we can actually replicate the results
17+
quantifier = PACC()
18+
quantifier.fit(train)
19+
20+
# let us simulate our experimental results
21+
protocol = UPP(test, sample_size=100, repeats=100, random_state=0)
22+
our_mae = qp.evaluation.evaluate(quantifier, protocol=protocol, error_metric='mae')
23+
24+
print(f'We have obtained a MAE={our_mae:.3f}')
25+
26+
# let us distribute the indexes; we specify that we want the indexes, not the samples
27+
protocol = UPP(test, sample_size=100, repeats=100, random_state=0, return_type='index')
28+
indexes = protocol.samples_parameters()
29+
30+
# Imagine we distribute the indexes; now we show how to replicate our experiments.
31+
from quapy.protocol import ProtocolFromIndex
32+
data = qp.datasets.fetch_UCIMulticlassDataset('academic-success')
33+
train, test = data.train_test
34+
protocol = ProtocolFromIndex(data=test, indexes=indexes)
35+
their_mae = qp.evaluation.evaluate(quantifier, protocol=protocol, error_metric='mae')
36+
37+
print(f'Another lab obtains a MAE={our_mae:.3f}')
38+

examples/ensembles.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from sklearn.linear_model import LogisticRegression
2+
from statsmodels.sandbox.distributions.genpareto import quant
3+
4+
import quapy as qp
5+
from quapy.protocol import UPP
6+
from quapy.method.aggregative import PACC, DMy, EMQ, KDEyML
7+
from quapy.method.meta import SCMQ
8+
9+
qp.environ["SAMPLE_SIZE"]=100
10+
11+
def train_and_test_model(quantifier, train, test):
12+
quantifier.fit(train)
13+
report = qp.evaluation.evaluation_report(quantifier, UPP(test), error_metrics=['mae', 'mrae'])
14+
print(quantifier.__class__.__name__)
15+
print(report.mean(numeric_only=True))
16+
17+
18+
quantifiers = [
19+
PACC(),
20+
DMy(),
21+
EMQ(),
22+
KDEyML()
23+
]
24+
25+
classifier = LogisticRegression()
26+
27+
dataset_name = qp.datasets.UCI_MULTICLASS_DATASETS[0]
28+
data = qp.datasets.fetch_UCIMulticlassDataset(dataset_name)
29+
train, test = data.train_test
30+
31+
scmq = SCMQ(classifier, quantifiers)
32+
33+
train_and_test_model(scmq, train, test)
34+
35+
for quantifier in quantifiers:
36+
train_and_test_model(quantifier, train, test)

quapy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from . import classification
1515
import os
1616

17-
__version__ = '0.1.9'
17+
__version__ = '0.1.10'
1818

1919
environ = {
2020
'SAMPLE_SIZE': None,

quapy/error.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,31 @@ def nmd(prevs, prevs_hat):
298298
return (1./(n-1))*np.mean(match_distance(prevs, prevs_hat))
299299

300300

301+
def bias_binary(prevs, prevs_hat):
302+
"""
303+
Computes the (positive) bias in a binary problem. The bias is simply the difference between the
304+
predicted positive value and the true positive value, so that a positive such value indicates the
305+
prediction has positive bias (i.e., it tends to overestimate) the true value, and negative otherwise.
306+
:math:`bias(p,\\hat{p})=\\hat{p}_1-p_1`,
307+
:param prevs: array-like of shape `(n_samples, n_classes,)` with the true prevalence values
308+
:param prevs_hat: array-like of shape `(n_samples, n_classes,)` with the predicted
309+
prevalence values
310+
:return: binary bias
311+
"""
312+
assert prevs.shape[-1] == 2 and prevs.shape[-1] == 2, f'bias_binary can only be applied to binary problems'
313+
return prevs_hat[...,1]-prevs[...,1]
314+
315+
316+
def mean_bias_binary(prevs, prevs_hat):
317+
"""
318+
Computes the mean of the (positive) bias in a binary problem.
319+
:param prevs: array-like of shape `(n_classes,)` with the true prevalence values
320+
:param prevs_hat: array-like of shape `(n_classes,)` with the predicted prevalence values
321+
:return: mean binary bias
322+
"""
323+
return np.mean(bias_binary(prevs, prevs_hat))
324+
325+
301326
def md(prevs, prevs_hat, ERROR_TOL=1E-3):
302327
"""
303328
Computes the Match Distance, under the assumption that the cost in mistaking class i with class i+1 is 1 in
@@ -338,8 +363,8 @@ def __check_eps(eps=None):
338363

339364

340365
CLASSIFICATION_ERROR = {f1e, acce}
341-
QUANTIFICATION_ERROR = {mae, mnae, mrae, mnrae, mse, mkld, mnkld}
342-
QUANTIFICATION_ERROR_SINGLE = {ae, nae, rae, nrae, se, kld, nkld}
366+
QUANTIFICATION_ERROR = {mae, mnae, mrae, mnrae, mse, mkld, mnkld, mean_bias_binary}
367+
QUANTIFICATION_ERROR_SINGLE = {ae, nae, rae, nrae, se, kld, nkld, bias_binary}
343368
QUANTIFICATION_ERROR_SMOOTH = {kld, nkld, rae, nrae, mkld, mnkld, mrae}
344369
CLASSIFICATION_ERROR_NAMES = {func.__name__ for func in CLASSIFICATION_ERROR}
345370
QUANTIFICATION_ERROR_NAMES = {func.__name__ for func in QUANTIFICATION_ERROR}

quapy/method/aggregative.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,6 @@ def _check_init_parameters(self):
591591
if self.norm not in ACC.NORMALIZATIONS:
592592
raise ValueError(f"unknown normalization; valid ones are {ACC.NORMALIZATIONS}")
593593

594-
595594
def aggregation_fit(self, classif_predictions: LabelledCollection, data: LabelledCollection):
596595
"""
597596
Estimates the misclassification rates
@@ -870,13 +869,13 @@ def aggregation_fit(self, classif_predictions: LabelledCollection, data: Labelle
870869
:param data: a :class:`quapy.data.base.LabelledCollection` consisting of the training data
871870
"""
872871
pred_labels, true_labels = classif_predictions.Xy
873-
self._n_and_c_labeled = confusion_matrix(y_true=true_labels, y_pred=pred_labels, labels=self.classifier.classes_)
872+
self._n_and_c_labeled = confusion_matrix(y_true=true_labels, y_pred=pred_labels, labels=self.classifier.classes_).astype(float)
874873

875874
def sample_from_posterior(self, classif_predictions):
876875
if self._n_and_c_labeled is None:
877876
raise ValueError("aggregation_fit must be called before sample_from_posterior")
878877

879-
n_c_unlabeled = F.counts_from_labels(classif_predictions, self.classifier.classes_)
878+
n_c_unlabeled = F.counts_from_labels(classif_predictions, self.classifier.classes_).astype(float)
880879

881880
self._samples = _bayesian.sample_posterior(
882881
n_c_unlabeled=n_c_unlabeled,

0 commit comments

Comments
 (0)