tinyconformal 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tinyconformal/classifier/__init__.py +7 -0
- tinyconformal/classifier/base.py +433 -0
- tinyconformal/classifier/class_conditional.py +222 -0
- tinyconformal/classifier/marginal.py +204 -0
- tinyconformal/plot/__init__.py +6 -0
- tinyconformal/plot/plot.py +411 -0
- tinyconformal/regressor/__init__.py +8 -0
- tinyconformal/regressor/base.py +238 -0
- tinyconformal/regressor/cqr.py +127 -0
- tinyconformal/regressor/exactness_bound.py +111 -0
- tinyconformal/regressor/icp.py +99 -0
- tinyconformal-0.1.0.dist-info/METADATA +176 -0
- tinyconformal-0.1.0.dist-info/RECORD +16 -0
- tinyconformal-0.1.0.dist-info/WHEEL +5 -0
- tinyconformal-0.1.0.dist-info/licenses/LICENSE +21 -0
- tinyconformal-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Lucas Leão
|
|
2
|
+
# tinyCP - A small toolbox for conformal prediction
|
|
3
|
+
# Licensed under the MIT License
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from venn_abers import VennAbers
|
|
7
|
+
from sklearn.utils.validation import check_is_fitted
|
|
8
|
+
from sklearn.base import BaseEstimator
|
|
9
|
+
import warnings
|
|
10
|
+
import numpy as np
|
|
11
|
+
import sklearn.metrics
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
|
|
14
|
+
warnings.filterwarnings("ignore", category=RuntimeWarning, module="venn_abers")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseConformalClassifier(ABC):
|
|
18
|
+
"""
|
|
19
|
+
BaseConformalClassifier
|
|
20
|
+
|
|
21
|
+
A base class for conformal prediction using a model as the learner
|
|
22
|
+
and Venn-Abers calibration for confidence estimation.
|
|
23
|
+
This approach provides valid predictions with a specified significance level (alpha).
|
|
24
|
+
|
|
25
|
+
Conformal classifiers aim to quantify uncertainty in predictions.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
learner: BaseEstimator,
|
|
31
|
+
alpha: float = 0.05,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
Initializes the classifier with a specified learner and a Venn-Abers calibration layer.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
learner : BaseEstimator
|
|
40
|
+
The base learner to be used in the classifier.
|
|
41
|
+
alpha : float, default=0.05
|
|
42
|
+
The significance level applied in the classifier.
|
|
43
|
+
|
|
44
|
+
Attributes
|
|
45
|
+
----------
|
|
46
|
+
learner : BaseEstimator
|
|
47
|
+
The base learner employed in the classifier.
|
|
48
|
+
calibration_layer : VennAbers
|
|
49
|
+
The calibration layer utilized in the classifier.
|
|
50
|
+
decision_function_ : callable or None
|
|
51
|
+
The decision function of the learner.
|
|
52
|
+
hinge : array-like of shape (n_samples,), default=None
|
|
53
|
+
The non-conformity scores of the calibration samples.
|
|
54
|
+
alpha : float, default=0.05
|
|
55
|
+
The significance level applied in the classifier.
|
|
56
|
+
n : int or None
|
|
57
|
+
The number of calibration samples.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
self.learner = learner
|
|
61
|
+
self.alpha = alpha
|
|
62
|
+
self.calibration_layer = VennAbers()
|
|
63
|
+
self.classes = getattr(self.learner, "classes_", [0, 1])
|
|
64
|
+
self.decision_function_ = None
|
|
65
|
+
self.is_unlabeled = False
|
|
66
|
+
check_is_fitted(learner)
|
|
67
|
+
|
|
68
|
+
if learner.n_classes_ > 2:
|
|
69
|
+
raise ValueError("This classifier supports only binary classification.")
|
|
70
|
+
|
|
71
|
+
self.hinge = None
|
|
72
|
+
self.n = None
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def fit(self, y):
|
|
76
|
+
"""
|
|
77
|
+
Fits the classifier to the training data.
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def predict_set(self, X, alpha=None):
|
|
83
|
+
"""
|
|
84
|
+
Generate a prediction set for the given input.
|
|
85
|
+
This method must be implemented by subclasses.
|
|
86
|
+
"""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def _compute_qhat(self, ncscore, q_level):
|
|
91
|
+
"""
|
|
92
|
+
Compute the q-hat value based on the nonconformity scores and the quantile level.
|
|
93
|
+
"""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def _compute_set(self, ncscore, qhat):
|
|
98
|
+
"""
|
|
99
|
+
Compute a set based on the given ncscore and qhat.
|
|
100
|
+
"""
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def _compute_q_level(self, n, alpha):
|
|
105
|
+
"""
|
|
106
|
+
Compute the quantile level based on the number of samples and significance level.
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def _compute_prediction(self, prediction_set):
|
|
111
|
+
"""
|
|
112
|
+
Compute the prediction based on the given prediction set.
|
|
113
|
+
|
|
114
|
+
This method evaluates each row in the prediction set and returns 1 if all elements
|
|
115
|
+
in the row match the pattern [0, 1], otherwise returns 0.
|
|
116
|
+
"""
|
|
117
|
+
return np.where(np.all(prediction_set == [0, 1], axis=1), 1, 0)
|
|
118
|
+
|
|
119
|
+
def _bookmaker_informedness(self, y, y_pred):
|
|
120
|
+
"""
|
|
121
|
+
Calculate the bookmaker informedness score for the given true and predicted labels.
|
|
122
|
+
"""
|
|
123
|
+
return sklearn.metrics.balanced_accuracy_score(y, y_pred, adjusted=True)
|
|
124
|
+
|
|
125
|
+
def _select_scoring_function(self, scoring_func):
|
|
126
|
+
"""
|
|
127
|
+
Select the scoring function based on the provided string.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
if scoring_func == "bm":
|
|
131
|
+
func = self._bookmaker_informedness
|
|
132
|
+
elif scoring_func == "mcc":
|
|
133
|
+
func = sklearn.metrics.matthews_corrcoef
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError("Invalid metric function. Please use 'bm' or 'mcc'.")
|
|
136
|
+
return func
|
|
137
|
+
|
|
138
|
+
def _get_alpha(self, alpha):
|
|
139
|
+
"""Helper to retrieve the alpha value."""
|
|
140
|
+
return alpha or self.alpha
|
|
141
|
+
|
|
142
|
+
def generate_non_conformity_score(self, y_prob):
|
|
143
|
+
"""
|
|
144
|
+
Generates the non-conformity score based on the hinge loss.
|
|
145
|
+
|
|
146
|
+
This function calculates the non-conformity score for conformal prediction
|
|
147
|
+
using the hinge loss approach.
|
|
148
|
+
"""
|
|
149
|
+
return 1 - y_prob
|
|
150
|
+
|
|
151
|
+
def generate_conformal_quantile(self, alpha=None):
|
|
152
|
+
"""
|
|
153
|
+
Generate the conformal quantile for conformal prediction.
|
|
154
|
+
|
|
155
|
+
This method calculates the conformal quantile based on the nonconformity scores
|
|
156
|
+
of the calibration samples. The quantile serves as a threshold to determine
|
|
157
|
+
the prediction sets in conformal prediction.
|
|
158
|
+
|
|
159
|
+
Parameters:
|
|
160
|
+
-----------
|
|
161
|
+
alpha : float, optional
|
|
162
|
+
The significance level for conformal prediction. If None, the default
|
|
163
|
+
value of self.alpha is used.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
--------
|
|
167
|
+
float
|
|
168
|
+
The computed conformal quantile.
|
|
169
|
+
|
|
170
|
+
Notes:
|
|
171
|
+
------
|
|
172
|
+
- The quantile is computed as ceil((n + 1) * (1 - alpha)) / n, where n is the
|
|
173
|
+
number of calibration samples.
|
|
174
|
+
- This method relies on the self.ncscore attribute, which should contain the
|
|
175
|
+
nonconformity scores of the calibration samples.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
alpha = self._get_alpha(alpha)
|
|
179
|
+
|
|
180
|
+
q_level = self._compute_q_level(self.n, alpha)
|
|
181
|
+
|
|
182
|
+
return self._compute_qhat(self.hinge, q_level)
|
|
183
|
+
|
|
184
|
+
def predict_proba(self, X):
|
|
185
|
+
"""
|
|
186
|
+
Returns class probabilities. Uses Venn-Abers if fit() was used,
|
|
187
|
+
or raw learner probabilities if unlabeled_fit() was used.
|
|
188
|
+
|
|
189
|
+
Parameters:
|
|
190
|
+
X: array-like of shape (n_samples, n_features)
|
|
191
|
+
The input samples.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
p_prime: array-like of shape (n_samples, n_classes)
|
|
195
|
+
The calibrated class probabilities.
|
|
196
|
+
"""
|
|
197
|
+
y_score = self.learner.predict_proba(X)
|
|
198
|
+
|
|
199
|
+
if getattr(self, "is_unlabeled", True):
|
|
200
|
+
return y_score
|
|
201
|
+
|
|
202
|
+
p_prime, _ = self.calibration_layer.predict_proba(y_score)
|
|
203
|
+
return p_prime
|
|
204
|
+
|
|
205
|
+
def calibrate(self, X, y, max_alpha=0.2, func="mcc"):
|
|
206
|
+
"""
|
|
207
|
+
Calibrates the alpha value to optimize the specified metric.
|
|
208
|
+
|
|
209
|
+
This method evaluates a range of alpha values (from 0.01 to `max_alpha`)
|
|
210
|
+
to determine the optimal significance level based on the provided scoring
|
|
211
|
+
function. The alpha value that maximizes the scoring function is selected.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
X : array-like of shape (n_samples, n_features)
|
|
216
|
+
Input samples used for calibration.
|
|
217
|
+
y : array-like of shape (n_samples,)
|
|
218
|
+
True labels corresponding to the input samples.
|
|
219
|
+
max_alpha : float, optional, default=0.2
|
|
220
|
+
The maximum alpha value to consider during calibration. The range of
|
|
221
|
+
alpha values tested will be from 0.01 to `max_alpha`, inclusive.
|
|
222
|
+
func : str, optional, default="mcc"
|
|
223
|
+
The name of the scoring function to use for optimization. Supported
|
|
224
|
+
functions should be implemented in the `_select_scoring_function` method.
|
|
225
|
+
|
|
226
|
+
Raises
|
|
227
|
+
------
|
|
228
|
+
If an invalid scoring function name is provided in the `func` parameter.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
The optimal alpha value that maximizes the scoring function.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
if getattr(self, "is_unlabeled", True):
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"Calibration is not applicable for unlabeled data. Please use labeled data for calibration."
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
scoring_func = self._select_scoring_function(func)
|
|
240
|
+
|
|
241
|
+
alphas = {k: None for k in np.round(np.arange(0.01, max_alpha + 0.01, 0.01), 2)}
|
|
242
|
+
|
|
243
|
+
for alpha in alphas:
|
|
244
|
+
y_pred = self.predict(X, alpha)
|
|
245
|
+
alphas[alpha] = scoring_func(y, y_pred)
|
|
246
|
+
|
|
247
|
+
self.alpha = max(alphas, key=alphas.get)
|
|
248
|
+
|
|
249
|
+
return self.alpha
|
|
250
|
+
|
|
251
|
+
def predict(self, X, alpha=None):
|
|
252
|
+
"""
|
|
253
|
+
Predicts the classes for the input samples.
|
|
254
|
+
|
|
255
|
+
Parameters:
|
|
256
|
+
-----------
|
|
257
|
+
X: np.ndarray of shape (n_samples, n_features)
|
|
258
|
+
Input samples.
|
|
259
|
+
alpha: float, optional
|
|
260
|
+
Significance level. If None, defaults to the classifier's alpha value.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
--------
|
|
264
|
+
np.ndarray of shape (n_samples,)
|
|
265
|
+
Predicted class labels, where 1 indicates the model's certainty.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
alpha = self._get_alpha(alpha)
|
|
269
|
+
|
|
270
|
+
prediction_set = self.predict_set(X, alpha)
|
|
271
|
+
|
|
272
|
+
return self._compute_prediction(prediction_set)
|
|
273
|
+
|
|
274
|
+
def _expected_calibration_error(self, y, y_prob, M=5):
|
|
275
|
+
"""
|
|
276
|
+
Generate the expected calibration error (ECE) of the classifier.
|
|
277
|
+
|
|
278
|
+
Parameters:
|
|
279
|
+
y: array-like of shape (n_samples,)
|
|
280
|
+
The true labels.
|
|
281
|
+
y_prob: array-like of shape (n_samples, n_classes)
|
|
282
|
+
The predicted probabilities.
|
|
283
|
+
M: int, default=5
|
|
284
|
+
The number of bins for the uniform binning approach.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
ece: float
|
|
288
|
+
The expected calibration error.
|
|
289
|
+
|
|
290
|
+
The function works as follows:
|
|
291
|
+
- It first creates M bins with uniform width over the interval [0, 1].
|
|
292
|
+
- For each sample, it computes the maximum predicted probability and makes a prediction.
|
|
293
|
+
- It then checks whether each prediction is correct or not.
|
|
294
|
+
- For each bin, it calculates the empirical probability of a sample falling into the bin.
|
|
295
|
+
- If the empirical probability is greater than 0, it computes the accuracy and average confidence of the bin.
|
|
296
|
+
- It then calculates the absolute difference between the accuracy and the average confidence, multiplies it by the empirical probability, and adds it to the total ECE.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
# uniform binning approach with M number of bins
|
|
300
|
+
bin_boundaries = np.linspace(0, 1, M + 1)
|
|
301
|
+
bin_lowers = bin_boundaries[:-1]
|
|
302
|
+
bin_uppers = bin_boundaries[1:]
|
|
303
|
+
|
|
304
|
+
# get max probability per sample i
|
|
305
|
+
confidences = np.max(y_prob, axis=1)
|
|
306
|
+
# get predictions from confidences (positional in this case)
|
|
307
|
+
predicted_label = np.argmax(y_prob, axis=1)
|
|
308
|
+
|
|
309
|
+
# get a boolean list of correct/false predictions
|
|
310
|
+
predictions = predicted_label == y
|
|
311
|
+
|
|
312
|
+
ece = 0.0
|
|
313
|
+
for bin_lower, bin_upper in zip(bin_lowers, bin_uppers):
|
|
314
|
+
# determine if sample is in bin m (between bin lower & upper)
|
|
315
|
+
in_bin = np.logical_and(
|
|
316
|
+
confidences > bin_lower.item(), confidences <= bin_upper.item()
|
|
317
|
+
)
|
|
318
|
+
# can calculate the empirical probability of a sample falling into bin m: (|Bm|/n)
|
|
319
|
+
prob_in_bin = np.mean(in_bin)
|
|
320
|
+
|
|
321
|
+
if prob_in_bin > 0:
|
|
322
|
+
# get the accuracy of bin m: acc(Bm)
|
|
323
|
+
avg_pred = np.mean(predictions[in_bin])
|
|
324
|
+
# get the average confidence of bin m: conf(Bm)
|
|
325
|
+
avg_confidence_in_bin = np.mean(confidences[in_bin])
|
|
326
|
+
# calculate |acc(Bm) - conf(Bm)| * (|Bm|/n) for bin m and add to the total ECE
|
|
327
|
+
ece += np.abs(avg_pred - avg_confidence_in_bin) * prob_in_bin
|
|
328
|
+
return ece
|
|
329
|
+
|
|
330
|
+
def _false_positive_rate(self, y, y_pred):
|
|
331
|
+
"""
|
|
332
|
+
Computes the false positive rate (FPR).
|
|
333
|
+
"""
|
|
334
|
+
tn, fp, _, _ = sklearn.metrics.confusion_matrix(y, y_pred).ravel()
|
|
335
|
+
return fp / (fp + tn)
|
|
336
|
+
|
|
337
|
+
def _coverage_rate(self, X, y, alpha=None):
|
|
338
|
+
"""
|
|
339
|
+
Compute the coverage rate from conformal prediction.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
X : array-like of shape (n_samples, n_features)
|
|
344
|
+
Input features.
|
|
345
|
+
y : array-like of shape (n_samples,)
|
|
346
|
+
True labels.
|
|
347
|
+
alpha : float, optional
|
|
348
|
+
Significance level (1 - desired coverage). If None, the default value of self.alpha is used.
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
float
|
|
353
|
+
The average coverage rate, which represents the proportion of true labels covered by the prediction sets.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
alpha = self._get_alpha(alpha)
|
|
357
|
+
predict_sets = self.predict_set(X, alpha)
|
|
358
|
+
coverages = predict_sets[np.arange(len(y)), y]
|
|
359
|
+
|
|
360
|
+
return np.mean(coverages)
|
|
361
|
+
|
|
362
|
+
def evaluate(self, X, y, alpha=None):
|
|
363
|
+
"""
|
|
364
|
+
Evaluate the classifier on the given dataset.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
X : array-like of shape (n_samples, n_features)
|
|
369
|
+
Input samples.
|
|
370
|
+
y : array-like of shape (n_samples,)
|
|
371
|
+
True labels for the input samples.
|
|
372
|
+
alpha : float, optional
|
|
373
|
+
Significance level for prediction sets. If None, the classifier's default alpha is used.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
results : dict
|
|
378
|
+
A dictionary containing the following evaluation metrics:
|
|
379
|
+
- "total": Total number of samples.
|
|
380
|
+
- "alpha": Significance level used.
|
|
381
|
+
- "coverage_rate": Coverage rate of the prediction sets.
|
|
382
|
+
- "one_c": Proportion of prediction sets containing exactly one element.
|
|
383
|
+
- "avg_c": Average size of the prediction sets.
|
|
384
|
+
- "empty": Proportion of empty prediction sets.
|
|
385
|
+
- "error": Classification error rate.
|
|
386
|
+
- "log_loss": Log loss of the predictions.
|
|
387
|
+
- "ece": Expected calibration error.
|
|
388
|
+
- "bm": Bookmaker informedness score.
|
|
389
|
+
- "mcc": Matthews correlation coefficient.
|
|
390
|
+
- "f1": F1 score.
|
|
391
|
+
- "fpr": False positive rate.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
alpha = self._get_alpha(alpha)
|
|
395
|
+
|
|
396
|
+
# Helper function for rounding
|
|
397
|
+
def rounded(value):
|
|
398
|
+
return np.round(value, 3)
|
|
399
|
+
|
|
400
|
+
y_prob = self.predict_proba(X)
|
|
401
|
+
y_pred = self.predict(X, alpha)
|
|
402
|
+
predict_set = self.predict_set(X, alpha)
|
|
403
|
+
total = X.shape[0] if hasattr(X, "shape") else len(X)
|
|
404
|
+
coverage_rate = rounded(self._coverage_rate(X, y, alpha))
|
|
405
|
+
one_c = rounded(np.mean([np.sum(p) == 1 for p in predict_set]))
|
|
406
|
+
avg_c = rounded(np.mean([np.sum(p) for p in predict_set]))
|
|
407
|
+
empty = rounded(np.mean([np.sum(p) == 0 for p in predict_set]))
|
|
408
|
+
error = rounded(1 - np.mean(predict_set[np.arange(len(y)), y]))
|
|
409
|
+
log_loss = rounded(sklearn.metrics.log_loss(y, y_prob[:, 1]))
|
|
410
|
+
ece = rounded(self._expected_calibration_error(y, y_prob))
|
|
411
|
+
fpr = rounded(self._false_positive_rate(y, y_pred))
|
|
412
|
+
bookmaker_informedness = rounded(self._bookmaker_informedness(y, y_pred))
|
|
413
|
+
matthews_corr = rounded(sklearn.metrics.matthews_corrcoef(y, y_pred))
|
|
414
|
+
f1 = rounded(sklearn.metrics.f1_score(y, self.predict(X, alpha)))
|
|
415
|
+
|
|
416
|
+
# Results aggregation
|
|
417
|
+
results = {
|
|
418
|
+
"total": total,
|
|
419
|
+
"alpha": alpha,
|
|
420
|
+
"coverage_rate": coverage_rate,
|
|
421
|
+
"one_c": one_c,
|
|
422
|
+
"avg_c": avg_c,
|
|
423
|
+
"empty": empty,
|
|
424
|
+
"error": error,
|
|
425
|
+
"log_loss": log_loss,
|
|
426
|
+
"ece": ece,
|
|
427
|
+
"bm": bookmaker_informedness,
|
|
428
|
+
"mcc": matthews_corr,
|
|
429
|
+
"f1": f1,
|
|
430
|
+
"fpr": fpr,
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return results
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Lucas Leão
|
|
2
|
+
# tinyCP - A small toolbox for conformal prediction
|
|
3
|
+
# Licensed under the MIT License
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from sklearn.base import ClassifierMixin, BaseEstimator
|
|
7
|
+
import numpy as np
|
|
8
|
+
import warnings
|
|
9
|
+
from .base import BaseConformalClassifier
|
|
10
|
+
|
|
11
|
+
warnings.filterwarnings("ignore", category=RuntimeWarning, module="venn_abers")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BinaryClassConditionalConformalClassifier(
|
|
15
|
+
ClassifierMixin, BaseEstimator, BaseConformalClassifier
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
A modrian class conditional conformal classifier methodology utilizing a classifier as the underlying learner.
|
|
19
|
+
This class is inspired by the WrapperClassifier classes from the Crepes library.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
learner: BaseEstimator,
|
|
25
|
+
alpha: float = 0.05,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Constructs the classifier with a specified learner and a Venn-Abers calibration layer.
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
----------
|
|
32
|
+
learner : BaseEstimator
|
|
33
|
+
The base learner to be used in the classifier.
|
|
34
|
+
alpha : float, default=0.05
|
|
35
|
+
The significance level applied in the classifier.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
----------
|
|
39
|
+
learner : BaseEstimator
|
|
40
|
+
The base learner employed in the classifier.
|
|
41
|
+
calibration_layer : VennAbers
|
|
42
|
+
The calibration layer utilized in the classifier.
|
|
43
|
+
classes : array-like of shape (n_classes,), default=None
|
|
44
|
+
The unique class labels identified during training.
|
|
45
|
+
hinge : list of array-like, default=None
|
|
46
|
+
Nonconformity scores for each class based on the predicted probabilities.
|
|
47
|
+
n : array-like of shape (n_classes,), default=None
|
|
48
|
+
The number of calibration points for each class.
|
|
49
|
+
alpha : float, default=0.05
|
|
50
|
+
The significance level applied in the classifier.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
super().__init__(learner, alpha)
|
|
54
|
+
|
|
55
|
+
def unlabeled_fit(self, X=None):
|
|
56
|
+
"""
|
|
57
|
+
Fits the class-conditional conformal layer using unlabeled data (X) based on
|
|
58
|
+
pseudo-labels derived from the model's predictions (Flechsig & Pilz, 2025).
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
----------
|
|
62
|
+
X : array-like of shape (n_samples, n_features)
|
|
63
|
+
Unlabeled calibration features.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
-------
|
|
67
|
+
self : object
|
|
68
|
+
The fitted classifier.
|
|
69
|
+
"""
|
|
70
|
+
if X is None:
|
|
71
|
+
raise ValueError("Unlabeled calibration data (X) must be provided.")
|
|
72
|
+
|
|
73
|
+
self.is_unlabeled = True
|
|
74
|
+
y_prob = self.learner.predict_proba(X)
|
|
75
|
+
idx_max = np.argmax(y_prob, axis=1)
|
|
76
|
+
ncscore = np.min(self.generate_non_conformity_score(y_prob), axis=1)
|
|
77
|
+
self.hinge = [ncscore[idx_max == c] for c in self.classes]
|
|
78
|
+
self.n = [np.sum(idx_max == c) for c in self.classes]
|
|
79
|
+
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def fit(self, X=None, y=None, oob=False):
|
|
83
|
+
"""
|
|
84
|
+
Fits the classifier to the training data. Calculates the conformity score for each training instance.
|
|
85
|
+
|
|
86
|
+
Parameters:
|
|
87
|
+
----------
|
|
88
|
+
X : array-like of shape (n_samples, n_features), optional
|
|
89
|
+
The training data. Required if OOB predictions are not used.
|
|
90
|
+
y : array-like of shape (n_samples,)
|
|
91
|
+
The true labels. Required in all cases.
|
|
92
|
+
oob : bool, default=False
|
|
93
|
+
Whether to use Out-of-Bag (OOB) predictions if available.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
-------
|
|
97
|
+
self : object
|
|
98
|
+
The fitted classifier.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
------
|
|
102
|
+
ValueError:
|
|
103
|
+
If OOB is enabled but not supported by the learner,
|
|
104
|
+
or if `X` and `y` are not provided when `oob=False`.
|
|
105
|
+
"""
|
|
106
|
+
if y is None:
|
|
107
|
+
raise ValueError("The true labels (y) must be provided.")
|
|
108
|
+
|
|
109
|
+
if oob:
|
|
110
|
+
if (
|
|
111
|
+
not hasattr(self.learner, "oob_decision_function_")
|
|
112
|
+
or self.learner.oob_decision_function_ is None
|
|
113
|
+
):
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"OOB predictions are not available for the provided learner."
|
|
116
|
+
)
|
|
117
|
+
if X is not None:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"Training data (X) should not be provided when OOB is used. Ensure that 'y' is the same as the labels used during training."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Use OOB predictions
|
|
123
|
+
self.decision_function_ = self.learner.oob_decision_function_
|
|
124
|
+
else:
|
|
125
|
+
|
|
126
|
+
if X is None:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"Training data (X) must be provided if OOB is not used."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Use predict_proba for training data
|
|
132
|
+
self.decision_function_ = self.learner.predict_proba(X)
|
|
133
|
+
|
|
134
|
+
self.calibration_layer.fit(self.decision_function_, y)
|
|
135
|
+
|
|
136
|
+
y_prob, _ = self.calibration_layer.predict_proba(self.decision_function_)
|
|
137
|
+
|
|
138
|
+
y_prob = y_prob[np.arange(len(y)), y]
|
|
139
|
+
hinge = self.generate_non_conformity_score(y_prob)
|
|
140
|
+
self.hinge = [hinge[y == c] for c in self.classes]
|
|
141
|
+
self.n = [np.sum(y == c) for c in self.classes]
|
|
142
|
+
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def _compute_q_level(self, n, alpha):
|
|
146
|
+
"""
|
|
147
|
+
Compute the quantile level for each class based on the number of samples and significance level.
|
|
148
|
+
"""
|
|
149
|
+
alpha = self._get_alpha(alpha)
|
|
150
|
+
q_level = np.zeros(len(self.classes))
|
|
151
|
+
for c in self.classes:
|
|
152
|
+
q_level[c] = np.ceil((n[c] + 1) * (1 - alpha)) / n[c]
|
|
153
|
+
return q_level
|
|
154
|
+
|
|
155
|
+
def _compute_qhat(self, ncscore, q_level):
|
|
156
|
+
"""
|
|
157
|
+
Compute the q-hat value based on the nonconformity scores and the quantile level.
|
|
158
|
+
"""
|
|
159
|
+
qhat = np.zeros(len(self.classes))
|
|
160
|
+
for c in self.classes:
|
|
161
|
+
qhat[c] = np.quantile(ncscore[c], q_level[c], method="higher")
|
|
162
|
+
return qhat
|
|
163
|
+
|
|
164
|
+
def _compute_set(self, ncscore, qhat):
|
|
165
|
+
"""
|
|
166
|
+
Compute a predict set based on the given ncscore and qhat.
|
|
167
|
+
"""
|
|
168
|
+
prediction_set = np.zeros((len(ncscore), len(self.classes)))
|
|
169
|
+
for c in self.classes:
|
|
170
|
+
prediction_set[:, c] = (ncscore <= qhat[c])[:, c]
|
|
171
|
+
return prediction_set
|
|
172
|
+
|
|
173
|
+
def predict_set(self, X, alpha=None):
|
|
174
|
+
"""
|
|
175
|
+
Predicts the possible set of classes for the instances in X based on the predefined significance level.
|
|
176
|
+
|
|
177
|
+
Parameters:
|
|
178
|
+
X: array-like of shape (n_samples, n_features)
|
|
179
|
+
The input samples.
|
|
180
|
+
alpha: float, default=None
|
|
181
|
+
The significance level. If None, the value of self.alpha is used.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
prediction_set: array-like of shape (n_samples, n_classes)
|
|
185
|
+
The predicted set of classes. A class is included in the set if its non-conformity score is less
|
|
186
|
+
than or equal to the quantile of the hinge loss distribution at the (n+1)*(1-alpha)/n level.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
alpha = self._get_alpha(alpha)
|
|
190
|
+
|
|
191
|
+
y_prob = self.predict_proba(X)
|
|
192
|
+
ncscore = self.generate_non_conformity_score(y_prob)
|
|
193
|
+
qhat = self.generate_conformal_quantile(alpha)
|
|
194
|
+
|
|
195
|
+
return self._compute_set(ncscore, qhat)
|
|
196
|
+
|
|
197
|
+
def predict_p(self, X):
|
|
198
|
+
"""
|
|
199
|
+
Calculate the p-values for each instance in the input data X using a non-conformity score.
|
|
200
|
+
|
|
201
|
+
Parameters:
|
|
202
|
+
-----------
|
|
203
|
+
X : array-like of shape (n_samples, n_features)
|
|
204
|
+
The input data for which the p-values need to be predicted.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
--------
|
|
208
|
+
p_values : array-like of shape (n_samples, n_classes)
|
|
209
|
+
The p-values for each instance in X for each class.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
y_prob = self.predict_proba(X)
|
|
213
|
+
ncscore = self.generate_non_conformity_score(y_prob)
|
|
214
|
+
p_values = np.zeros_like(ncscore)
|
|
215
|
+
|
|
216
|
+
for i in range(ncscore.shape[0]):
|
|
217
|
+
for j in range(ncscore.shape[1]):
|
|
218
|
+
numerator = np.sum(self.hinge[j] >= ncscore[i][j]) + 1
|
|
219
|
+
denumerator = self.n[j] + 1
|
|
220
|
+
p_values[i, j] = numerator / denumerator
|
|
221
|
+
|
|
222
|
+
return p_values
|