Skip to content

Commit 36968b3

Browse files
refactor: move cov shift stuff
1 parent 7f67f93 commit 36968b3

File tree

4 files changed

+931
-1
lines changed

4 files changed

+931
-1
lines changed

environment.dev.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ dependencies:
1717
- sphinx_rtd_theme=1.0.0
1818
- twine=3.7.1
1919
- wheel=0.37.0
20+
- notebook=6.4.11
21+
- seaborn=0.11.2

mapie/covariate_shift_regression.py

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
from __future__ import annotations
2+
3+
from typing import Iterable, Optional, Tuple, Union, cast
4+
5+
import numpy as np
6+
from sklearn.base import RegressorMixin
7+
from sklearn.model_selection import BaseCrossValidator
8+
from sklearn.utils.validation import (
9+
check_is_fitted,
10+
)
11+
12+
from ._typing import ArrayLike
13+
from .dre import DensityRatioEstimator, ProbClassificationDRE
14+
from .utils import (
15+
check_alpha,
16+
check_alpha_and_n_samples,
17+
empirical_quantile
18+
)
19+
from .regression import MapieRegressor
20+
21+
22+
class MapieCovShiftRegressor(MapieRegressor): # type: ignore
23+
"""
24+
Prediction interval with out-of-fold residuals.
25+
26+
This class implements the jackknife+ strategy and its variations
27+
for estimating prediction intervals on single-output data. The
28+
idea is to evaluate out-of-fold residuals on hold-out validation
29+
sets and to deduce valid confidence intervals with strong theoretical
30+
guarantees.
31+
32+
Parameters
33+
----------
34+
estimator : Optional[RegressorMixin]
35+
Any regressor with scikit-learn API
36+
(i.e. with fit and predict methods), by default ``None``.
37+
If ``None``, estimator defaults to a ``LinearRegression`` instance.
38+
39+
dr_estimator : Optional[DensityRatioEstimator]
40+
Any density ratio estimator with scikit-learn API
41+
(i.e. with fit and predict methods), by default ``None``.
42+
If ``None``, dr_estimator defaults to a ``ProbClassificationDRE``
43+
instance with ``LogisticRegression`` model.
44+
45+
method: str, optional
46+
Method to choose for prediction interval estimates.
47+
Choose among:
48+
49+
- "naive", based on training set residuals,
50+
- "base", based on validation sets residuals,
51+
- "plus", based on validation residuals and testing predictions,
52+
- "minmax", based on validation residuals and testing predictions
53+
(min/max among cross-validation clones).
54+
55+
By default "plus".
56+
57+
cv: Optional[Union[int, str, BaseCrossValidator]]
58+
The cross-validation strategy for computing residuals.
59+
It directly drives the distinction between jackknife and cv variants.
60+
Choose among:
61+
62+
- ``None``, to use the default 5-fold cross-validation
63+
- integer, to specify the number of folds.
64+
If equal to -1, equivalent to
65+
``sklearn.model_selection.LeaveOneOut()``.
66+
- CV splitter: any ``sklearn.model_selection.BaseCrossValidator``
67+
Main variants are:
68+
- ``sklearn.model_selection.LeaveOneOut`` (jackknife),
69+
- ``sklearn.model_selection.KFold`` (cross-validation),
70+
- ``subsample.Subsample`` object (bootstrap).
71+
- ``"prefit"``, assumes that ``estimator`` has been fitted already,
72+
and the ``method`` parameter is ignored.
73+
All data provided in the ``fit`` method is then used
74+
for computing residuals only.
75+
At prediction time, quantiles of these residuals are used to provide
76+
a prediction interval with fixed width.
77+
The user has to take care manually that data for model fitting and
78+
residual estimate are disjoint.
79+
80+
By default ``None``.
81+
82+
n_jobs: Optional[int]
83+
Number of jobs for parallel processing using joblib
84+
via the "locky" backend.
85+
If ``-1`` all CPUs are used.
86+
If ``1`` is given, no parallel computing code is used at all,
87+
which is useful for debugging.
88+
For n_jobs below ``-1``, ``(n_cpus + 1 - n_jobs)`` are used.
89+
None is a marker for `unset` that will be interpreted as ``n_jobs=1``
90+
(sequential execution).
91+
92+
By default ``None``.
93+
94+
agg_function : str
95+
Determines how to aggregate predictions from perturbed models, both at
96+
training and prediction time.
97+
98+
If ``None``, it is ignored except if cv class is ``Subsample``,
99+
in which case an error is raised.
100+
If "mean" or "median", returns the mean or median of the predictions
101+
computed from the out-of-folds models.
102+
Note: if you plan to set the ``ensemble`` argument to ``True`` in the
103+
``predict`` method, you have to specify an aggregation function.
104+
Otherwise an error would be raised.
105+
106+
The Jackknife+ interval can be interpreted as an interval around the
107+
median prediction, and is guaranteed to lie inside the interval,
108+
unlike the single estimator predictions.
109+
110+
When the cross-validation strategy is Subsample (i.e. for the
111+
Jackknife+-after-Bootstrap method), this function is also used to
112+
aggregate the training set in-sample predictions.
113+
114+
If cv is ``"prefit"``, ``agg_function`` is ignored.
115+
116+
By default "mean".
117+
118+
verbose : int, optional
119+
The verbosity level, used with joblib for multiprocessing.
120+
The frequency of the messages increases with the verbosity level.
121+
If it more than ``10``, all iterations are reported.
122+
Above ``50``, the output is sent to stdout.
123+
124+
By default ``0``.
125+
126+
Attributes
127+
----------
128+
valid_methods: List[str]
129+
List of all valid methods.
130+
131+
single_estimator_ : sklearn.RegressorMixin
132+
Estimator fitted on the whole training set.
133+
134+
estimators_ : list
135+
List of out-of-folds estimators.
136+
137+
residuals_ : ArrayLike of shape (n_samples_train,)
138+
Residuals between ``y_train`` and ``y_pred``.
139+
140+
k_ : ArrayLike
141+
- Array of nans, of shape (len(y), 1) if cv is ``"prefit"``
142+
(defined but not used)
143+
- Dummy array of folds containing each training sample, otherwise.
144+
Of shape (n_samples_train, cv.get_n_splits(X_train, y_train)).
145+
146+
n_features_in_: int
147+
Number of features passed to the fit method.
148+
149+
n_samples_: List[int]
150+
Number of samples passed to the fit method.
151+
152+
References
153+
----------
154+
155+
Examples
156+
--------
157+
158+
"""
159+
valid_methods_ = ["naive", "base"]
160+
valid_agg_functions_ = [None, "median", "mean"]
161+
fit_attributes = [
162+
"single_estimator_",
163+
"estimators_",
164+
"k_",
165+
"residuals_",
166+
"residuals_dre_",
167+
"n_features_in_",
168+
"n_samples_",
169+
]
170+
171+
def __init__(
172+
self,
173+
estimator: Optional[RegressorMixin] = None,
174+
dr_estimator: Optional[DensityRatioEstimator] = None,
175+
method: str = "base",
176+
cv: Optional[Union[int, str, BaseCrossValidator]] = None,
177+
n_jobs: Optional[int] = None,
178+
agg_function: Optional[str] = "mean",
179+
verbose: int = 0,
180+
) -> None:
181+
self.dr_estimator = dr_estimator
182+
if cv != "prefit":
183+
raise NotImplementedError
184+
super().__init__(
185+
estimator=estimator,
186+
method=method,
187+
cv=cv,
188+
n_jobs=n_jobs,
189+
agg_function=agg_function,
190+
verbose=verbose,
191+
)
192+
193+
def _check_dr_estimator(
194+
self,
195+
dr_estimator: Optional[DensityRatioEstimator] = None
196+
) -> DensityRatioEstimator:
197+
"""
198+
Check if estimator is ``None``, and returns a ``ProbClassificationDRE``
199+
instance with ``LogisticRegression`` model if necessary.
200+
If the ``cv`` attribute is ``"prefit"``, check if estimator is indeed
201+
already fitted.
202+
203+
Parameters
204+
----------
205+
dr_estimator : Optional[DensityRatioEstimator], optional
206+
Estimator to check, by default ``None``.
207+
208+
Returns
209+
-------
210+
DensityRatioEstimator
211+
The estimator itself or a default ``ProbClassificationDRE``
212+
instance with ``LogisticRegression`` model.
213+
214+
Raises
215+
------
216+
ValueError
217+
If the estimator is not ``None``
218+
and has no fit nor predict methods.
219+
220+
NotFittedError
221+
If the estimator is not fitted and ``cv`` attribute is "prefit".
222+
"""
223+
if dr_estimator is None:
224+
return ProbClassificationDRE(clip_min=0.01, clip_max=0.99)
225+
if not (hasattr(dr_estimator, "fit") and
226+
hasattr(dr_estimator, "predict")):
227+
raise ValueError(
228+
"Invalid estimator. "
229+
"Please provide a density ratio estimator with fit"
230+
"and predict methods."
231+
)
232+
if self.cv == "prefit":
233+
dr_estimator.check_is_fitted()
234+
235+
return dr_estimator
236+
237+
def fit(
238+
self,
239+
X: ArrayLike,
240+
y: ArrayLike,
241+
sample_weight: Optional[ArrayLike] = None,
242+
) -> MapieRegressor:
243+
"""
244+
Fit estimator and compute residuals used for prediction intervals.
245+
Fit the base estimator under the ``single_estimator_`` attribute.
246+
Fit all cross-validated estimator clones
247+
and rearrange them into a list, the ``estimators_`` attribute.
248+
Out-of-fold residuals are stored under the ``residuals_`` attribute.
249+
250+
Parameters
251+
----------
252+
X : ArrayLike of shape (n_samples, n_features)
253+
Training data.
254+
255+
y : ArrayLike of shape (n_samples,)
256+
Training labels.
257+
258+
sample_weight : Optional[ArrayLike] of shape (n_samples,)
259+
Sample weights for fitting the out-of-fold models.
260+
If None, then samples are equally weighted.
261+
If some weights are null,
262+
their corresponding observations are removed
263+
before the fitting process and hence have no residuals.
264+
If weights are non-uniform, residuals are still uniformly weighted.
265+
266+
By default ``None``.
267+
268+
Returns
269+
-------
270+
MapieRegressor
271+
The model itself.
272+
"""
273+
super().fit(X=X, y=y, sample_weight=sample_weight)
274+
self.residuals_dre_ = self.dr_estimator.predict(X)
275+
276+
def predict(
277+
self,
278+
X: ArrayLike,
279+
ensemble: bool = False,
280+
alpha: Optional[Union[float, Iterable[float]]] = None,
281+
) -> Union[ArrayLike, Tuple[ArrayLike, ArrayLike]]:
282+
"""
283+
Predict target on new samples with confidence intervals.
284+
Residuals from the training set and predictions from the model clones
285+
are central to the computation.
286+
Prediction Intervals for a given ``alpha`` are deduced from either
287+
288+
- quantiles of residuals (naive and base methods),
289+
- quantiles of (predictions +/- residuals) (plus method),
290+
- quantiles of (max/min(predictions) +/- residuals) (minmax method).
291+
292+
Parameters
293+
----------
294+
X : ArrayLike of shape (n_samples, n_features)
295+
Test data.
296+
297+
ensemble: bool
298+
Boolean determining whether the predictions are ensembled or not.
299+
If False, predictions are those of the model trained on the whole
300+
training set.
301+
If True, predictions from perturbed models are aggregated by
302+
the aggregation function specified in the ``agg_function``
303+
attribute.
304+
305+
If cv is ``"prefit"``, ``ensemble`` is ignored.
306+
307+
By default ``False``.
308+
309+
alpha: Optional[Union[float, Iterable[float]]]
310+
Can be a float, a list of floats, or a ``ArrayLike`` of floats.
311+
Between 0 and 1, represents the uncertainty of the confidence
312+
interval.
313+
Lower ``alpha`` produce larger (more conservative) prediction
314+
intervals.
315+
``alpha`` is the complement of the target coverage level.
316+
317+
By default ``None``.
318+
319+
Returns
320+
-------
321+
Union[ArrayLike, Tuple[ArrayLike, ArrayLike]]
322+
323+
- ArrayLike of shape (n_samples,) if alpha is None.
324+
325+
- Tuple[ArrayLike, ArrayLike] of shapes
326+
(n_samples,) and (n_samples, 2, n_alpha) if alpha is not None.
327+
328+
- [:, 0, :]: Lower bound of the prediction interval.
329+
- [:, 1, :]: Upper bound of the prediction interval.
330+
"""
331+
# Checks
332+
check_is_fitted(self, self.fit_attributes)
333+
self._check_ensemble(ensemble)
334+
alpha_ = check_alpha(alpha)
335+
336+
y_pred = self.single_estimator_.predict(X)
337+
dre_pred = self.dr_estimator.predict(X)
338+
dre_calib = self.residuals_dre_
339+
340+
if alpha is None:
341+
return np.array(y_pred)
342+
else:
343+
alpha_ = cast(ArrayLike, alpha_)
344+
check_alpha_and_n_samples(alpha_, self.residuals_.shape[0])
345+
if self.method in ["naive", "base"] or self.cv == "prefit":
346+
347+
# Denominator in weight calculation (array; differs based
348+
# on each test point)
349+
denom = dre_calib.sum() + dre_pred
350+
351+
y_pred_low = np.empty(
352+
(y_pred.shape[0], len(alpha_)), dtype=y_pred.dtype)
353+
y_pred_up = np.empty_like(y_pred_low, dtype=y_pred.dtype)
354+
for i in range(dre_pred.shape[0]):
355+
356+
# Numerator in weight calculation
357+
# Calibration (array)
358+
cal_weights = dre_calib / denom[i]
359+
# Test (float)
360+
test_weight = dre_pred[i] / denom[i]
361+
362+
# Calculate the quantile for constructing interval
363+
quantile = empirical_quantile(
364+
np.hstack([self.residuals_, np.array([np.inf])]),
365+
alphas=1-alpha_,
366+
weights=np.hstack(
367+
[cal_weights, np.array([test_weight])]),
368+
)
369+
370+
y_pred_low[i, :] = y_pred[i] - quantile
371+
y_pred_up[i, :] = y_pred[i] + quantile
372+
373+
else:
374+
raise NotImplementedError
375+
376+
return y_pred, np.stack([y_pred_low, y_pred_up], axis=1)

0 commit comments

Comments
 (0)