Skip to content

Commit 588f71c

Browse files
authored
Merge pull request #43 from simai-ml/alpha-as-list
Accept alpha as a list or np.ndarray
2 parents becd6a8 + 80c97be commit 588f71c

File tree

11 files changed

+168
-66
lines changed

11 files changed

+168
-66
lines changed

HISTORY.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ History
1111
* Remove the `n_splits`, `shuffle` and `random_state` parameters
1212
* Simplify the `method` parameter
1313
* Fix typos in documentation and add methods descriptions in sphinx
14+
* Accept alpha parameter as a list or np.ndarray
15+
* If alpha is a list, `.predict()` returns a np.ndarray of shape (n_samples, 3, len(alpha))
1416

1517
0.1.4 (2021-05-07)
1618
------------------

README.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ Here, we generate one-dimensional noisy data that we fit with a linear model.
8383
8484
Since MAPIE is compliant with the standard scikit-learn API, we follow the standard
8585
sequential ``fit`` and ``predict`` process like any scikit-learn regressor.
86+
We set two values for alpha to estimate prediction intervals at approximately one
87+
and two standard deviations from the mean.
8688

8789
.. code:: python
8890
@@ -92,8 +94,9 @@ sequential ``fit`` and ``predict`` process like any scikit-learn regressor.
9294
y_preds = mapie.predict(X)
9395
9496
95-
MAPIE returns a ``np.ndarray`` of shape (n_samples, 3) giving the predictions,
96-
as well as the lower and upper bounds of the prediction intervals for the target quantile.
97+
MAPIE returns a ``np.ndarray`` of shape (n_samples, 3, len(alpha)) giving the predictions,
98+
as well as the lower and upper bounds of the prediction intervals for the target quantile
99+
for each desired alpha value.
97100
The estimated prediction intervals can then be plotted as follows.
98101

99102
.. code:: python
@@ -111,11 +114,11 @@ The estimated prediction intervals can then be plotted as follows.
111114
)
112115
plt.show()
113116
114-
The title of the plot compares the target coverage with the effective coverage.
117+
The title of the plot compares the target coverages with the effective coverages.
115118
The target coverage, or the confidence interval, is the fraction of true labels lying in the
116119
prediction intervals that we aim to obtain for a given dataset.
117-
It is given by the alpha parameter defined in `MapieRegressor`, here equal to the default value of
118-
0.1 thus giving a target coverage of 0.9.
120+
It is given by the alpha parameter defined in ``MapieRegressor``, here equal to 0.05 and 0.32,
121+
thus giving target coverages of 0.95 and 0.68.
119122
The effective coverage is the actual fraction of true labels lying in the prediction intervals.
120123

121124

doc/images/quickstart_1.png

16.2 KB
Loading

doc/quick_start.rst

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Estimate your prediction intervals
1111
1. Download and install the module
1212
----------------------------------
1313

14-
Install via `pip`:
14+
Install via ``pip``:
1515

1616
.. code:: python
1717
@@ -28,7 +28,8 @@ To install directly from the github repository :
2828
---------------------
2929

3030
Let us start with a basic regression problem.
31-
Here, we generate one-dimensional noisy data that we fit with a linear model.
31+
Here, we generate one-dimensional noisy data with normal distribution
32+
that we fit with a linear model.
3233

3334
.. code:: python
3435
@@ -40,21 +41,25 @@ Here, we generate one-dimensional noisy data that we fit with a linear model.
4041
X, y = make_regression(n_samples=500, n_features=1, noise=20, random_state=59)
4142
4243
Since MAPIE is compliant with the standard scikit-learn API, we follow the standard
43-
sequential `fit` and `predict` process like any scikit-learn regressor.
44+
sequential ``fit`` and ``predict`` process like any scikit-learn regressor.
45+
We set two values for alpha to estimate prediction intervals at approximately one
46+
and two standard deviations from the mean.
4447

4548
.. code:: python
4649
4750
from mapie.estimators import MapieRegressor
48-
mapie = MapieRegressor(regressor)
51+
alpha = [0.05, 0.32]
52+
mapie = MapieRegressor(regressor, alpha=alpha, method="plus")
4953
mapie.fit(X, y)
5054
y_preds = mapie.predict(X)
5155
5256
5357
3. Show the results
5458
-------------------
5559

56-
MAPIE returns a `np.ndarray` of shape (n_samples, 3) giving the predictions,
57-
as well as the lower and upper bounds of the prediction intervals for the target quantile.
60+
MAPIE returns a ``np.ndarray`` of shape (n_samples, 3, len(alpha)) giving the predictions,
61+
as well as the lower and upper bounds of the prediction intervals for the target quantile
62+
for each desired alpha value.
5863
The estimated prediction intervals can then be plotted as follows.
5964

6065
.. code:: python
@@ -64,11 +69,15 @@ The estimated prediction intervals can then be plotted as follows.
6469
plt.xlabel("x")
6570
plt.ylabel("y")
6671
plt.scatter(X, y, alpha=0.3)
67-
plt.plot(X, y_preds[:, 0], color="C1")
72+
plt.plot(X, y_preds[:, 0, 0], color="C1")
6873
order = np.argsort(X[:, 0])
69-
plt.fill_between(X[order].ravel(), y_preds[:, 1][order], y_preds[:, 2][order], alpha=0.3)
74+
plt.plot(X[order], y_preds[order][:, 1, 1], color="C1", ls="--")
75+
plt.plot(X[order], y_preds[order][:, 2, 1], color="C1", ls="--")
76+
plt.fill_between(X[order].ravel(), y_preds[:, 1, 0][order].ravel(), y_preds[:, 2, 0][order].ravel(), alpha=0.2)
77+
coverage_scores = [coverage_score(y, y_preds[:, 1, i], y_preds[:, 2, i]) for i, _ in enumerate(alpha)]
7078
plt.title(
71-
f"Target coverage = 0.9; Effective coverage = {coverage_score(y, y_preds[:, 1], y_preds[:, 2])}"
79+
f"Target and effective coverages for alpha={alpha[0]:.2f}: ({1-alpha[0]:.3f}, {coverage_scores[0]:.3f})\n" +
80+
f"Target and effective coverages for alpha={alpha[1]:.2f}: ({1-alpha[1]:.3f}, {coverage_scores[1]:.3f})"
7281
)
7382
plt.show()
7483
@@ -77,9 +86,9 @@ The estimated prediction intervals can then be plotted as follows.
7786
:width: 400
7887
:align: center
7988

80-
The title of the plot compares the target coverage with the effective coverage.
89+
The title of the plot compares the target coverages with the effective coverages.
8190
The target coverage, or the confidence interval, is the fraction of true labels lying in the
8291
prediction intervals that we aim to obtain for a given dataset.
83-
It is given by the alpha parameter defined in `MapieRegressor`, here equal to the default value of
84-
0.1 thus giving a target coverage of 0.9.
92+
It is given by the alpha parameter defined in ``MapieRegressor``, here equal to ``0.05`` and ``0.32``,
93+
thus giving target coverages of 0.95 and 0.68.
8594
The effective coverage is the actual fraction of true labels lying in the prediction intervals.

doc/tutorial.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ in order to obtain a 95% confidence for our prediction intervals.
115115
for strategy, params in STRATEGIES:
116116
mapie = MapieRegressor(polyn_model, alpha=0.05, ensemble=False, **params)
117117
mapie.fit(X_train, y_train)
118-
prediction_interval[method] = mapie.predict(X_test)
118+
prediction_interval[method] = mapie.predict(X_test)[:, :, 0]
119119
120120
Let’s now compare the confidence intervals with the predicted intervals with obtained
121121
by the Jackknife+, Jackknife-minmax, CV+, and CV-minmax strategies.
@@ -332,7 +332,7 @@ strategies.
332332
for strategy, params in STRATEGIES:
333333
mapie = MapieRegressor(polyn_model, alpha=0.05, ensemble=False, **params)
334334
mapie.fit(X_train, y_train)
335-
prediction_interval[method] = mapie.predict(X_test)
335+
prediction_interval[method] = mapie.predict(X_test)[:, :, 0]
336336
337337
338338
.. code:: python
@@ -546,7 +546,7 @@ and compare their prediction interval.
546546
for name, model in zip(model_names, models):
547547
mapie = MapieRegressor(model, alpha=0.05, method="plus", cv=5, ensemble=True)
548548
mapie.fit(X_train, y_train)
549-
prediction_interval[name] = mapie.predict(X_test)
549+
prediction_interval[name] = mapie.predict(X_test)[:, :, 0]
550550
551551
.. code:: python
552552

examples/plot_barber2020_simulations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def PIs_vs_dimensions(
103103
**params
104104
)
105105
mapie.fit(X_train, y_train)
106-
y_preds = mapie.predict(X_test)
106+
y_preds = mapie.predict(X_test)[:, :, 0]
107107
results[strategy][dimension]["coverage"][trial] = coverage_score(
108108
y_test, y_preds[:, 1], y_preds[:, 2]
109109
)

examples/plot_homoscedastic_1d_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def plot_1d_data(
149149
**params
150150
)
151151
mapie.fit(X_train.reshape(-1, 1), y_train)
152-
y_preds = mapie.predict(X_test.reshape(-1, 1))
152+
y_preds = mapie.predict(X_test.reshape(-1, 1))[:, :, 0]
153153
plot_1d_data(
154154
X_train,
155155
y_train,

examples/plot_nested-cv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
n_jobs=-1
9696
)
9797
mapie_non_nested.fit(X_train, y_train)
98-
y_preds_non_nested = mapie_non_nested.predict(X_test)
98+
y_preds_non_nested = mapie_non_nested.predict(X_test)[:, :, 0]
9999
widths_non_nested = y_preds_non_nested[:, 2] - y_preds_non_nested[:, 1]
100100
coverage_non_nested = coverage_score(y_test, y_preds_non_nested[:, 1], y_preds_non_nested[:, 2])
101101
score_non_nested = mean_squared_error(y_test, y_preds_non_nested[:, 0], squared=False)
@@ -120,7 +120,7 @@
120120
ensemble=True
121121
)
122122
mapie_nested.fit(X_train, y_train)
123-
y_preds_nested = mapie_nested.predict(X_test)
123+
y_preds_nested = mapie_nested.predict(X_test)[:, :, 0]
124124
widths_nested = y_preds_nested[:, 2] - y_preds_nested[:, 1]
125125
coverage_nested = coverage_score(y_test, y_preds_nested[:, 1], y_preds_nested[:, 2])
126126
score_nested = mean_squared_error(y_test, y_preds_nested[:, 0], squared=False)

examples/plot_toy_model.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,22 @@
1616
regressor = LinearRegression()
1717
X, y = make_regression(n_samples=500, n_features=1, noise=20, random_state=59)
1818

19-
mapie = MapieRegressor(regressor, n_jobs=-1)
19+
alpha = [0.05, 0.32]
20+
mapie = MapieRegressor(regressor, alpha=alpha, method="plus")
2021
mapie.fit(X, y)
2122
y_preds = mapie.predict(X)
2223

2324
plt.xlabel("x")
2425
plt.ylabel("y")
2526
plt.scatter(X, y, alpha=0.3)
26-
plt.plot(X, y_preds[:, 0], color="C1")
27+
plt.plot(X, y_preds[:, 0, 0], color="C1")
2728
order = np.argsort(X[:, 0])
28-
plt.fill_between(X[order].ravel(), y_preds[:, 1][order], y_preds[:, 2][order], alpha=0.3)
29+
plt.plot(X[order], y_preds[order][:, 1, 1], color="C1", ls="--")
30+
plt.plot(X[order], y_preds[order][:, 2, 1], color="C1", ls="--")
31+
plt.fill_between(X[order].ravel(), y_preds[:, 1, 0][order].ravel(), y_preds[:, 2, 0][order].ravel(), alpha=0.2)
32+
coverage_scores = [coverage_score(y, y_preds[:, 1, i], y_preds[:, 2, i]) for i, _ in enumerate(alpha)]
2933
plt.title(
30-
f"Target coverage = 0.9; Effective coverage = {coverage_score(y, y_preds[:, 1], y_preds[:, 2])}"
34+
f"Target and effective coverages for alpha={alpha[0]:.2f}: ({1-alpha[0]:.3f}, {coverage_scores[0]:.3f})\n" +
35+
f"Target and effective coverages for alpha={alpha[1]:.2f}: ({1-alpha[1]:.3f}, {coverage_scores[1]:.3f})"
3136
)
3237
plt.show()

mapie/estimators.py

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import Optional, Union, Tuple, List
2+
from typing import Optional, Union, Iterable, Tuple, List
33

44
import numpy as np
55
from joblib import Parallel, delayed
@@ -29,7 +29,8 @@ class MapieRegressor(BaseEstimator, RegressorMixin): # type: ignore
2929
Any regressor with scikit-learn API (i.e. with fit and predict methods), by default None.
3030
If ``None``, estimator defaults to a ``LinearRegression`` instance.
3131
32-
alpha: float, optional
32+
alpha: Union[float, Iterable[float]], optional
33+
Can be a float, a list of floats, or a np.ndarray of floats.
3334
Between 0 and 1, represent the uncertainty of the confidence interval.
3435
Lower alpha produce larger (more conservative) prediction intervals.
3536
alpha is the complement of the target coverage level.
@@ -118,7 +119,7 @@ class MapieRegressor(BaseEstimator, RegressorMixin): # type: ignore
118119
>>> X_toy = np.array([0, 1, 2, 3, 4, 5]).reshape(-1, 1)
119120
>>> y_toy = np.array([5, 7.5, 9.5, 10.5, 12.5, 15])
120121
>>> pireg = MapieRegressor(LinearRegression())
121-
>>> print(pireg.fit(X_toy, y_toy).predict(X_toy))
122+
>>> print(pireg.fit(X_toy, y_toy).predict(X_toy)[:, :, 0])
122123
[[ 5.28571429 4.61627907 6. ]
123124
[ 7.17142857 6.51744186 7.8 ]
124125
[ 9.05714286 8.4 9.68023256]
@@ -137,7 +138,7 @@ class MapieRegressor(BaseEstimator, RegressorMixin): # type: ignore
137138
def __init__(
138139
self,
139140
estimator: Optional[RegressorMixin] = None,
140-
alpha: float = 0.1,
141+
alpha: Union[float, Iterable[float]] = 0.1,
141142
method: str = "plus",
142143
cv: Optional[Union[int, BaseCrossValidator]] = None,
143144
n_jobs: Optional[int] = None,
@@ -161,9 +162,6 @@ def _check_parameters(self) -> None:
161162
ValueError
162163
Is parameters are not valid.
163164
"""
164-
if not isinstance(self.alpha, float) or not 0 < self.alpha < 1:
165-
raise ValueError("Invalid alpha. Allowed values are between 0 and 1.")
166-
167165
if self.method not in self.valid_methods_:
168166
raise ValueError("Invalid method. Allowed values are 'naive', 'base', 'plus' and 'minmax'.")
169167

@@ -241,6 +239,43 @@ def _check_cv(self, cv: Optional[Union[int, BaseCrossValidator]] = None) -> Base
241239
return cv
242240
raise ValueError("Invalid cv argument. Allowed values are None, -1, int >= 2, KFold or LeaveOneOut.")
243241

242+
def _check_alpha(self, alpha: Union[float, Iterable[float]]) -> np.ndarray:
243+
"""
244+
Check alpha and prepare it as a np.ndarray
245+
246+
Parameters
247+
----------
248+
alpha : Union[float, Iterable[float]]
249+
Can be a float, a list of floats, or a np.ndarray of floats.
250+
Between 0 and 1, represent the uncertainty of the confidence interval.
251+
Lower alpha produce larger (more conservative) prediction intervals.
252+
alpha is the complement of the target coverage level.
253+
Only used at prediction time. By default 0.1.
254+
255+
Returns
256+
-------
257+
np.ndarray
258+
Prepared alpha.
259+
260+
Raises
261+
------
262+
ValueError
263+
If alpha is not a float or an Iterable of floats between 0 and 1.
264+
"""
265+
if isinstance(alpha, float):
266+
alpha_np = np.array([alpha])
267+
elif isinstance(alpha, Iterable):
268+
alpha_np = np.array(alpha)
269+
else:
270+
raise ValueError("Invalid alpha. Allowed values are float or Iterable.")
271+
if len(alpha_np.shape) != 1:
272+
raise ValueError("Invalid alpha. Please provide a one-dimensional list of values.")
273+
if alpha_np.dtype.type not in [np.float64, np.float32]:
274+
raise ValueError("Invalid alpha. Allowed values are Iterable of floats.")
275+
if np.any((alpha_np <= 0) | (alpha_np >= 1)):
276+
raise ValueError("Invalid alpha. Allowed values are between 0 and 1.")
277+
return alpha_np
278+
244279
def _fit_and_predict_oof_model(
245280
self,
246281
estimator: RegressorMixin,
@@ -350,19 +385,21 @@ def predict(self, X: ArrayLike) -> np.ndarray:
350385
351386
Returns
352387
-------
353-
np.ndarray of shape (n_samples, 3)
388+
np.ndarray of shape (n_samples, 3, len(alpha))
354389
355-
- [0]: Center of the prediction interval
356-
- [1]: Lower bound of the prediction interval
357-
- [2]: Upper bound of the prediction interval
390+
- [:, 0, :]: Center of the prediction interval
391+
- [:, 1, :]: Lower bound of the prediction interval
392+
- [:, 2, :]: Upper bound of the prediction interval
358393
"""
359394
check_is_fitted(self, ["single_estimator_", "estimators_", "k_", "residuals_"])
360395
X = check_array(X, force_all_finite=False, dtype=["float64", "object"])
361396
y_pred = self.single_estimator_.predict(X)
397+
alpha = self._check_alpha(self.alpha)
362398
if self.method in ["naive", "base"]:
363-
quantile = np.quantile(self.residuals_, 1 - self.alpha, interpolation="higher")
364-
y_pred_low = y_pred - quantile
365-
y_pred_up = y_pred + quantile
399+
quantile = np.quantile(self.residuals_, 1 - alpha, interpolation="higher")
400+
# broadcast y_pred to get y_pred_low/up of shape (n_samples_test, len(alpha))
401+
y_pred_low = y_pred[:, np.newaxis] - quantile
402+
y_pred_up = y_pred[:, np.newaxis] + quantile
366403
else:
367404
y_pred_multi = np.stack([e.predict(X) for e in self.estimators_], axis=1)
368405
if self.method == "plus":
@@ -373,8 +410,14 @@ def predict(self, X: ArrayLike) -> np.ndarray:
373410
if self.method == "minmax":
374411
lower_bounds = np.min(y_pred_multi, axis=1, keepdims=True) - self.residuals_
375412
upper_bounds = np.max(y_pred_multi, axis=1, keepdims=True) + self.residuals_
376-
y_pred_low = np.quantile(lower_bounds, self.alpha, axis=1, interpolation="lower")
377-
y_pred_up = np.quantile(upper_bounds, 1 - self.alpha, axis=1, interpolation="higher")
413+
y_pred_low = np.stack([
414+
np.quantile(lower_bounds, _alpha, axis=1, interpolation="lower") for _alpha in alpha
415+
], axis=1)
416+
y_pred_up = np.stack([
417+
np.quantile(upper_bounds, 1 - _alpha, axis=1, interpolation="higher") for _alpha in alpha
418+
], axis=1)
378419
if self.ensemble:
379420
y_pred = np.median(y_pred_multi, axis=1)
421+
# tile y_pred to get same shape as y_pred_low/up
422+
y_pred = np.tile(y_pred, (alpha.shape[0], 1)).T
380423
return np.stack([y_pred, y_pred_low, y_pred_up], axis=1)

0 commit comments

Comments
 (0)