Skip to content

Commit bd63980

Browse files
authored
Merge pull request #146 from kapytaine/feat/allow_custom_score_function
Feat/allow custom score function
2 parents c89eb1c + 3f7d916 commit bd63980

File tree

13 files changed

+773
-95
lines changed

13 files changed

+773
-95
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ Contributors
2222
* Julien Roussel <[email protected]>
2323
* Vincent Blot <[email protected]>
2424
* Louis Lacombe <[email protected]>
25+
* Arnaud Capitaine <[email protected]>
2526
To be continued ...

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ The full documentation can be found `on this link <https://mapie.readthedocs.io/
167167

168168
**How does MAPIE work on regression ?** It is basically based on cross-validation and relies on:
169169

170-
- Residuals on the whole training set obtained by cross-validation,
170+
- Conformity scores on the whole training set obtained by cross-validation,
171171
- Perturbed models generated during the cross-validation.
172172

173173
**MAPIE** then combines all these elements in a way that provides prediction intervals on new data with strong theoretical guarantees [1-2].

doc/theoretical_description_regression.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ The so-called naive method computes the residuals of the training data to estima
3333
typical error obtained on a new test data point.
3434
The prediction interval is therefore given by the prediction obtained by the
3535
model trained on the entire training set :math:`\pm` the quantiles of the
36-
residuals of the same training set:
36+
conformity scores of the same training set:
3737

3838
.. math:: \hat{\mu}(X_{n+1}) \pm ((1-\alpha) \textrm{quantile of} |Y_1-\hat{\mu}(X_1)|, ..., |Y_n-\hat{\mu}(X_n)|)
3939

@@ -43,7 +43,7 @@ or
4343

4444
where :math:`\hat{q}_{n, \alpha}^+` is the :math:`(1-\alpha)` quantile of the distribution.
4545

46-
Since this method estimates the residuals only on the training set, it tends to be too
46+
Since this method estimates the conformity scores only on the training set, it tends to be too
4747
optimistic and under-estimates the width of prediction intervals because of a potential overfit.
4848
As a result, the probability that a new point lies in the interval given by the
4949
naive method would be lower than the target level :math:`(1-\alpha)`.
@@ -65,11 +65,11 @@ Estimating the prediction intervals is carried out in three main steps:
6565
:math:`\hat{\mu}_{-i}` on the entire training set with the :math:`i^{th}` point removed,
6666
resulting in *n* leave-one-out models.
6767

68-
- The corresponding leave-one-out residual is computed for each :math:`i^{th}` point
68+
- The corresponding leave-one-out conformity score is computed for each :math:`i^{th}` point
6969
:math:`|Y_i - \hat{\mu}_{-i}(X_i)|`.
7070

7171
- We fit the regression function :math:`\hat{\mu}` on the entire training set and we compute
72-
the prediction interval using the computed leave-one-out residuals:
72+
the prediction interval using the computed leave-one-out conformity scores:
7373

7474
.. math:: \hat{\mu}(X_{n+1}) \pm ((1-\alpha) \textrm{ quantile of } |Y_1-\hat{\mu}_{-1}(X_1)|, ..., |Y_n-\hat{\mu}_{-n}(X_n)|)
7575

@@ -81,7 +81,7 @@ where
8181

8282
.. math:: R_i^{\rm LOO} = |Y_i - \hat{\mu}_{-i}(X_i)|
8383

84-
is the *leave-one-out* residual.
84+
is the *leave-one-out* conformity score.
8585

8686
This method avoids the overfitting problem but can lose its predictive
8787
cover when :math:`\hat{\mu}` becomes unstable, for example when the
@@ -146,7 +146,7 @@ is performed in four main steps:
146146
- *K* regression functions :math:`\hat{\mu}_{-S_k}` are fitted on the training set with the
147147
corresponding :math:`k^{th}` fold removed.
148148

149-
- The corresponding *out-of-fold* residual is computed for each :math:`i^{th}` point
149+
- The corresponding *out-of-fold* conformity score is computed for each :math:`i^{th}` point
150150
:math:`|Y_i - \hat{\mu}_{-S_{k(i)}}(X_i)|` where *k(i)* is the fold containing *i*.
151151

152152
- Similar to the jackknife+, the regression functions :math:`\hat{\mu}_{-S_{k(i)}}(X_i)`
@@ -198,7 +198,7 @@ jackknife+-after-bootstrap is performed in four main steps:
198198

199199

200200
- These predictions are aggregated according to a given aggregation function
201-
:math:`{\rm agg}`, typically :math:`{\rm mean}` or :math:`{\rm median}`, and the residuals
201+
:math:`{\rm agg}`, typically :math:`{\rm mean}` or :math:`{\rm median}`, and the conformity scores
202202
:math:`|Y_j - {\rm agg}(\hat{\mu}(B_{K(j)}(X_j)))|` are computed for each :math:`X_j`
203203
(with :math:`K(j)` the boostraps not containing :math:`X_j`).
204204

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""
2+
===========================================================
3+
Estimating prediction intervals of Gamma distributed target
4+
===========================================================
5+
This example uses :class:`mapie.regression.MapieRegressor` to estimate
6+
prediction intervals associated with Gamma distributed target.
7+
The limit of the absolute residual conformity score is illustrated.
8+
9+
We use here the OpenML house_prices dataset:
10+
https://www.openml.org/search?type=data&sort=runs&id=42165&status=active.
11+
12+
The data is modelled by a Random Forest model
13+
:class:`sklearn.ensemble.RandomForestRegressor` with a fixed parameter set.
14+
The prediction intervals are determined by means of the MAPIE regressor
15+
:class:`mapie.regression.MapieRegressor` considering two conformity scores:
16+
:class:`mapie.conformity_scores.AbsoluteConformityScore` which
17+
considers the absolute residuals as the conformity scores and
18+
:class:`mapie.conformity_scores.GammaConformityScore` which
19+
considers the residuals divided by the predicted means as conformity scores.
20+
We consider the standard CV+ resampling method.
21+
22+
We would like to emphasize one main limitation with this example.
23+
With the default conformity score, the prediction intervals
24+
are approximately equal over the range of house prices which may
25+
be inapporpriate when the price range is wide. The Gamma conformity score
26+
overcomes this issue by considering prediction intervals with width
27+
proportional to the predicted mean. For low prices, the Gamma prediction
28+
intervals are narrower than the default ones, conversely to high prices
29+
for which the conficence intervals are higher but visually more relevant.
30+
The empirical coverage is similar between the two conformity scores.
31+
"""
32+
import matplotlib.pyplot as plt
33+
import numpy as np
34+
35+
from sklearn.datasets import fetch_openml
36+
from sklearn.ensemble import RandomForestRegressor
37+
from sklearn.model_selection import train_test_split
38+
39+
from mapie.conformity_scores import GammaConformityScore
40+
from mapie.metrics import regression_coverage_score
41+
from mapie.regression import MapieRegressor
42+
43+
np.random.seed(0)
44+
45+
# Parameters
46+
features = [
47+
"MSSubClass",
48+
"LotArea",
49+
"OverallQual",
50+
"OverallCond",
51+
"GarageArea",
52+
]
53+
alpha = 0.05
54+
rf_kwargs = {"n_estimators": 10, "random_state": 0}
55+
model = RandomForestRegressor(**rf_kwargs)
56+
57+
##############################################################################
58+
# 1. Load dataset with a target following approximativeley a Gamma distribution
59+
# -----------------------------------------------------------------------------
60+
#
61+
# We start by loading a dataset with a target following approximately
62+
# a Gamma distribution. The GammaConformityScore is relevant in such cases.
63+
# Two sub datasets are extracted: the training and test ones.
64+
65+
X, y = fetch_openml(name="house_prices", return_X_y=True)
66+
67+
X_train, X_test, y_train, y_test = train_test_split(
68+
X[features], y, test_size=0.2
69+
)
70+
71+
##############################################################################
72+
# 2. Train model with two conformity scores
73+
# -----------------------------------------
74+
#
75+
# Two models are trained with two different conformity score:
76+
#
77+
# - :class:mapie.conformity_scores.AbsoluteConformityScore (default conformity
78+
# score) relevant for target positive as well as negative.
79+
# The prediction interval widths are, in this case, approximately the same
80+
# over the range of prediction.
81+
#
82+
# - :class:mapie.conformity_scores.GammaConformityScore relevant for target
83+
# following roughly a Gamma distribution. The prediction interval widths
84+
# scale with the predicted value.
85+
86+
##############################################################################
87+
# First, train model with
88+
# :class:mapie.conformity_scores.AbsoluteConformityScore.
89+
mapie = MapieRegressor(model)
90+
mapie.fit(X_train, y_train)
91+
y_pred_absconfscore, y_pis_absconfscore = mapie.predict(X_test, alpha=alpha)
92+
93+
coverage_absconfscore = regression_coverage_score(
94+
y_test, y_pis_absconfscore[:, 0, 0], y_pis_absconfscore[:, 1, 0]
95+
)
96+
97+
##############################################################################
98+
# Prepare the results for matplotlib. Get the prediction intervals and their
99+
# corresponding widths.
100+
101+
102+
def get_yerr(y_pred, y_pis):
103+
return np.concatenate(
104+
[
105+
np.expand_dims(y_pred, 0) - y_pis[:, 0, 0].T,
106+
y_pis[:, 1, 0].T - np.expand_dims(y_pred, 0),
107+
],
108+
axis=0,
109+
)
110+
111+
112+
yerr_absconfscore = get_yerr(y_pred_absconfscore, y_pis_absconfscore)
113+
pred_int_width_absconfscore = (
114+
y_pis_absconfscore[:, 1, 0] - y_pis_absconfscore[:, 0, 0]
115+
)
116+
117+
##############################################################################
118+
# Then, train the model with
119+
# :class:mapie.conformity_scores.GammaConformityScore.
120+
mapie = MapieRegressor(model, conformity_score=GammaConformityScore())
121+
mapie.fit(X_train, y_train)
122+
y_pred_gammaconfscore, y_pis_gammaconfscore = mapie.predict(
123+
X_test, alpha=[alpha]
124+
)
125+
126+
coverage_gammaconfscore = regression_coverage_score(
127+
y_test, y_pis_gammaconfscore[:, 0, 0], y_pis_gammaconfscore[:, 1, 0]
128+
)
129+
130+
yerr_gammaconfscore = get_yerr(y_pred_gammaconfscore, y_pis_gammaconfscore)
131+
pred_int_width_gammaconfscore = (
132+
y_pis_gammaconfscore[:, 1, 0] - y_pis_gammaconfscore[:, 0, 0]
133+
)
134+
135+
136+
##############################################################################
137+
# 3. Compare the prediction intervals
138+
# -----------------------------------
139+
#
140+
# Once the models have been trained, we now compare the prediction intervals
141+
# obtained from the two conformity scores. We can see that the
142+
# :class:AbsoluteConformityScore generates prediction interval with almost the
143+
# same width for all the predicted values. Converly, the GammaConformityScore
144+
# yields prediction interval with width scaling with the predicted values.
145+
#
146+
# The choice of the conformity score depends on the problem we face.
147+
148+
fig, axs = plt.subplots(2, 2, figsize=(10, 10))
149+
150+
for img_id, y_pred, y_err, cov, class_name, int_width in zip(
151+
[0, 1],
152+
[y_pred_absconfscore, y_pred_gammaconfscore],
153+
[yerr_absconfscore, yerr_gammaconfscore],
154+
[coverage_absconfscore, coverage_gammaconfscore],
155+
["AbsoluteResidualScore", "GammaResidualScore"],
156+
[pred_int_width_absconfscore, pred_int_width_gammaconfscore],
157+
):
158+
axs[0, img_id].errorbar(
159+
y_test,
160+
y_pred,
161+
yerr=y_err,
162+
alpha=0.5,
163+
linestyle="None",
164+
)
165+
axs[0, img_id].scatter(y_test, y_pred, s=1, color="black")
166+
axs[0, img_id].plot(
167+
[0, max(max(y_test), max(y_pred))],
168+
[0, max(max(y_test), max(y_pred))],
169+
"-r",
170+
)
171+
axs[0, img_id].set_xlabel("Actual price [$]")
172+
axs[0, img_id].set_ylabel("Predicted price [$]")
173+
axs[0, img_id].grid()
174+
axs[0, img_id].set_title(f"{class_name} - coverage={cov:.0%}")
175+
176+
xmin, xmax = axs[0, img_id].get_xlim()
177+
ymin, ymax = axs[0, img_id].get_ylim()
178+
axs[1, img_id].scatter(y_test, int_width, marker="+")
179+
axs[1, img_id].set_xlabel("Actual price [$]")
180+
axs[1, img_id].set_ylabel("Prediction interval width [$]")
181+
axs[1, img_id].grid()
182+
axs[1, img_id].set_xlim([xmin, xmax])
183+
axs[1, img_id].set_ylim([ymin, ymax])
184+
185+
fig.suptitle(
186+
f"Predicted values with the prediction intervals of level {alpha}"
187+
)
188+
plt.subplots_adjust(wspace=0.3, hspace=0.3)
189+
plt.show()

examples/regression/1-quickstart/plot_prefit_nn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
def f(x: NDArray) -> NDArray:
2626
"""Polynomial function used to generate one-dimensional data."""
27-
return np.array(5 * x + 5 * x ** 4 - 9 * x ** 2)
27+
return np.array(5 * x + 5 * x**4 - 9 * x**2)
2828

2929

3030
# Generate data

examples/regression/1-quickstart/plot_timeseries_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
=======================================================
55
This example uses :class:`mapie.regression.MapieRegressor` to estimate
66
prediction intervals associated with time series forecast. We use the
7-
standard cross-validation approach to estimate residuals and associated
7+
standard cross-validation approach to estimate conformity scores and associated
88
prediction intervals.
99
1010
We use here the Victoria electricity demand dataset used in the book
@@ -37,7 +37,7 @@
3737

3838
from mapie.metrics import (
3939
regression_coverage_score,
40-
regression_mean_width_score
40+
regression_mean_width_score,
4141
)
4242
from mapie.regression import MapieRegressor
4343

examples/regression/2-advanced-analysis/plot_nested-cv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
A limitation of this method is that residuals used by MAPIE are computed on
1414
the validation dataset, which can be subject to overfitting as far as
1515
hyperparameter tuning is concerned.
16-
This fools MAPIE into being slightly too optimistic with confidence intervals.
1716
17+
This fools MAPIE into being slightly too optimistic with confidence intervals.
1818
To solve this problem, an alternative option is to perform a nested
1919
cross-validation parameter search directly within the MAPIE estimator on each
2020
*out-of-fold* dataset.
@@ -39,7 +39,7 @@
3939
effective coverages.
4040
4141
In the general case, the recommended approach is to use nested
42-
cross-validation, since it does not underestimate residuals and hence
42+
cross-validation, since it does not underestimate conformity scores and hence
4343
prediction intervals. However, in this particular example, effective
4444
coverages of both nested and non-nested methods are the same.
4545
"""

examples/regression/3-scientific-articles/plot_kim2020_simulations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def compute_PIs(
161161
method : str
162162
Method for estimating prediction intervals.
163163
cv : Any
164-
Strategy for computing residuals.
164+
Strategy for computing conformity scores.
165165
alpha : float
166166
1 - (target coverage level).
167167
agg_function: str

0 commit comments

Comments
 (0)