11from __future__ import annotations
2- from typing import Optional , Union , Tuple , List
2+ from typing import Optional , Union , Iterable , Tuple , List
33
44import numpy as np
55from 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