This class computes rolling features. To avoid data leakage, the last point
in the window is excluded from calculations, ('closed': 'left' and
'center': False).
Currently, the following statistics are supported: 'mean', 'std', 'min', 'max',
'sum', 'median', 'ratio_min_max', 'coef_variation', 'ewm'. For 'ewm', the
alpha parameter can be set in the kwargs_stats dictionary, default is
{'ewm': {'alpha': 0.3}}.
Parameters:
Name
Type
Description
Default
stats
(str, list)
Statistics to compute over the rolling window. Can be a string or a list,
and can have repeats. Available statistics are: 'mean', 'std', 'min', 'max',
'sum', 'median', 'ratio_min_max', 'coef_variation', 'ewm'. For 'ewm', the
alpha parameter can be set in the kwargs_stats dictionary, default is
{'ewm': {'alpha': 0.3}}.
required
window_sizes
(int, list)
Size of the rolling window for each statistic. If an int, all stats share
the same window size. If a list, it should have the same length as stats.
required
min_periods
(int, list)
Minimum number of observations in window required to have a value.
Same as the min_periods argument of pandas rolling. If None,
defaults to window_sizes.
None
features_names
list
Names of the output features. If None, default names will be used in the
format 'roll_stat_window_size', for example 'roll_mean_7'.
None
fillna
(str, float)
Fill missing values in transform_batch method. Available
methods are: 'mean', 'median', 'ffill', 'bfill', or a float value.
None
kwargs_stats
dict
Dictionary with additional arguments for the statistics. The keys are the
statistic names and the values are dictionaries with the arguments for the
corresponding statistic. For example, {'ewm': {'alpha': 0.3}}.
Validate the parameters of the RollingFeatures class.
Parameters:
Name
Type
Description
Default
stats
(str, list)
Statistics to compute over the rolling window. Can be a string or a list,
and can have repeats. Available statistics are: 'mean', 'std', 'min', 'max',
'sum', 'median', 'ratio_min_max', 'coef_variation', 'ewm'.
required
window_sizes
(int, list)
Size of the rolling window for each statistic. If an int, all stats share
the same window size. If a list, it should have the same length as stats.
required
min_periods
(int, list)
Minimum number of observations in window required to have a value.
Same as the min_periods argument of pandas rolling. If None,
defaults to window_sizes.
None
features_names
list
Names of the output features. If None, default names will be used in the
format 'roll_stat_window_size', for example 'roll_mean_7'.
None
fillna
(str, float)
Fill missing values in transform_batch method. Available
methods are: 'mean', 'median', 'ffill', 'bfill', or a float value.
None
kwargs_stats
dict
Dictionary with additional arguments for the statistics. The keys are the
statistic names and the values are dictionaries with the arguments for the
corresponding statistic. For example, {'ewm': {'alpha': 0.3}}.
None
Returns:
Type
Description
None
Source code in skforecast/preprocessing/_preprocessing.py
def_validate_params(self,stats:str|list[str],window_sizes:int|list[int],min_periods:int|list[int]|None=None,features_names:list[str]|None=None,fillna:str|float|None=None,kwargs_stats:dict[str,dict[str,object]]|None=None)->None:""" Validate the parameters of the RollingFeatures class. Parameters ---------- stats : str, list Statistics to compute over the rolling window. Can be a `string` or a `list`, and can have repeats. Available statistics are: 'mean', 'std', 'min', 'max', 'sum', 'median', 'ratio_min_max', 'coef_variation', 'ewm'. window_sizes : int, list Size of the rolling window for each statistic. If an `int`, all stats share the same window size. If a `list`, it should have the same length as stats. min_periods : int, list, default None Minimum number of observations in window required to have a value. Same as the `min_periods` argument of pandas rolling. If `None`, defaults to `window_sizes`. features_names : list, default None Names of the output features. If `None`, default names will be used in the format 'roll_stat_window_size', for example 'roll_mean_7'. fillna : str, float, default None Fill missing values in `transform_batch` method. Available methods are: 'mean', 'median', 'ffill', 'bfill', or a float value. kwargs_stats : dict, default None Dictionary with additional arguments for the statistics. The keys are the statistic names and the values are dictionaries with the arguments for the corresponding statistic. For example, {'ewm': {'alpha': 0.3}}. Returns ------- None """# statsallowed_stats=['mean','std','min','max','sum','median','ratio_min_max','coef_variation','ewm']ifnotisinstance(stats,(str,list)):raiseTypeError(f"`stats` must be a string or a list of strings. Got {type(stats)}.")ifisinstance(stats,str):stats=[stats]forstatinset(stats):ifstatnotinallowed_stats:raiseValueError(f"Statistic '{stat}' is not allowed. Allowed stats are: {allowed_stats}.")n_stats=len(stats)# window_sizesifnotisinstance(window_sizes,(int,list)):raiseTypeError(f"`window_sizes` must be an int or a list of ints. Got {type(window_sizes)}.")ifisinstance(window_sizes,list):n_window_sizes=len(window_sizes)ifn_window_sizes!=n_stats:raiseValueError(f"Length of `window_sizes` list ({n_window_sizes}) "f"must match length of `stats` list ({n_stats}).")# Check duplicates (stats, window_sizes)ifisinstance(window_sizes,int):window_sizes=[window_sizes]*n_statsiflen(set(zip(stats,window_sizes)))!=n_stats:raiseValueError(f"Duplicate (stat, window_size) pairs are not allowed.\n"f" `stats` : {stats}\n"f" `window_sizes` : {window_sizes}")# min_periodsifnotisinstance(min_periods,(int,list,type(None))):raiseTypeError(f"`min_periods` must be an int, list of ints, or None. Got {type(min_periods)}.")ifmin_periodsisnotNone:ifisinstance(min_periods,int):min_periods=[min_periods]*n_statselifisinstance(min_periods,list):n_min_periods=len(min_periods)ifn_min_periods!=n_stats:raiseValueError(f"Length of `min_periods` list ({n_min_periods}) "f"must match length of `stats` list ({n_stats}).")fori,min_periodinenumerate(min_periods):ifmin_period>window_sizes[i]:raiseValueError("Each `min_period` must be less than or equal to its ""corresponding `window_size`.")# features_namesifnotisinstance(features_names,(list,type(None))):raiseTypeError(f"`features_names` must be a list of strings or None. Got {type(features_names)}.")ifisinstance(features_names,list):n_features_names=len(features_names)ifn_features_names!=n_stats:raiseValueError(f"Length of `features_names` list ({n_features_names}) "f"must match length of `stats` list ({n_stats}).")# fillnaiffillnaisnotNone:ifnotisinstance(fillna,(int,float,str)):raiseTypeError(f"`fillna` must be a float, string, or None. Got {type(fillna)}.")ifisinstance(fillna,str):allowed_fill_strategy=['mean','median','ffill','bfill']iffillnanotinallowed_fill_strategy:raiseValueError(f"'{fillna}' is not allowed. Allowed `fillna` "f"values are: {allowed_fill_strategy} or a float value.")# kwargs_statsallowed_kwargs_stats=['ewm']ifkwargs_statsisnotNone:ifnotisinstance(kwargs_stats,dict):raiseTypeError(f"`kwargs_stats` must be a dictionary or None. Got {type(kwargs_stats)}.")forstatinkwargs_stats.keys():ifstatnotinallowed_kwargs_stats:raiseValueError(f"Invalid statistic '{stat}' found in `kwargs_stats`. "f"Allowed statistics with additional arguments are: "f"{allowed_kwargs_stats}. Please ensure all keys in "f"`kwargs_stats` are among the allowed statistics.")
def_apply_stat_pandas(self,rolling_obj:pd.core.window.rolling.Rolling,stat:str)->pd.Series:""" Apply the specified statistic to a pandas rolling object. Parameters ---------- rolling_obj : pandas Rolling Rolling object to apply the statistic. stat : str Statistic to compute. Returns ------- stat_series : pandas Series Series with the computed statistic. """ifstat=='mean':returnrolling_obj.mean()elifstat=='std':returnrolling_obj.std()elifstat=='min':returnrolling_obj.min()elifstat=='max':returnrolling_obj.max()elifstat=='sum':returnrolling_obj.sum()elifstat=='median':returnrolling_obj.median()elifstat=='ratio_min_max':returnrolling_obj.min()/rolling_obj.max()elifstat=='coef_variation':returnrolling_obj.std()/rolling_obj.mean()elifstat=='ewm':kwargs=self.kwargs_stats.get(stat,{})returnrolling_obj.apply(lambdax:_ewm_jit(x,**kwargs),raw=True)else:raiseValueError(f"Statistic '{stat}' is not implemented.")
deftransform_batch(self,X:pd.Series)->pd.DataFrame:""" Transform an entire pandas Series using rolling windows and compute the specified statistics. Parameters ---------- X : pandas Series The input data series to transform. Returns ------- rolling_features : pandas DataFrame A DataFrame containing the rolling features. """forkinself.unique_rolling_windows.keys():rolling_obj=X.rolling(**self.unique_rolling_windows[k]['params'])self.unique_rolling_windows[k]['rolling_obj']=rolling_objrolling_features=[]fori,statinenumerate(self.stats):window_size=self.window_sizes[i]min_periods=self.min_periods[i]key=f"{window_size}_{min_periods}"rolling_obj=self.unique_rolling_windows[key]['rolling_obj']stat_series=self._apply_stat_pandas(rolling_obj=rolling_obj,stat=stat)rolling_features.append(stat_series)rolling_features=pd.concat(rolling_features,axis=1)rolling_features.columns=self.features_namesrolling_features=rolling_features.iloc[self.max_window_size:]ifself.fillnaisnotNone:ifself.fillna=='mean':rolling_features=rolling_features.fillna(rolling_features.mean())elifself.fillna=='median':rolling_features=rolling_features.fillna(rolling_features.median())elifself.fillna=='ffill':rolling_features=rolling_features.ffill()elifself.fillna=='bfill':rolling_features=rolling_features.bfill()else:rolling_features=rolling_features.fillna(self.fillna)returnrolling_features
def_apply_stat_numpy_jit(self,X_window:np.ndarray,stat:str)->float:""" Apply the specified statistic to a numpy array using Numba JIT. Parameters ---------- X_window : numpy array Array with the rolling window. stat : str Statistic to compute. Returns ------- stat_value : float Value of the computed statistic. """ifstat=='mean':return_np_mean_jit(X_window)elifstat=='std':return_np_std_jit(X_window)elifstat=='min':return_np_min_jit(X_window)elifstat=='max':return_np_max_jit(X_window)elifstat=='sum':return_np_sum_jit(X_window)elifstat=='median':return_np_median_jit(X_window)elifstat=='ratio_min_max':return_np_min_max_ratio_jit(X_window)elifstat=='coef_variation':return_np_cv_jit(X_window)elifstat=='ewm':kwargs=self.kwargs_stats.get(stat,{})return_ewm_jit(X_window,**kwargs)else:raiseValueError(f"Statistic '{stat}' is not implemented.")
Transform a numpy array using rolling windows and compute the
specified statistics. The returned array will have the shape
(X.shape[1] if exists, n_stats). For example, if X is a flat
array, the output will have shape (n_stats,). If X is a 2D array,
the output will have shape (X.shape[1], n_stats).
Parameters:
Name
Type
Description
Default
X
numpy ndarray
The input data array to transform.
required
Returns:
Name
Type
Description
rolling_features
numpy ndarray
An array containing the computed statistics.
Source code in skforecast/preprocessing/_preprocessing.py
deftransform(self,X:np.ndarray)->np.ndarray:""" Transform a numpy array using rolling windows and compute the specified statistics. The returned array will have the shape (X.shape[1] if exists, n_stats). For example, if X is a flat array, the output will have shape (n_stats,). If X is a 2D array, the output will have shape (X.shape[1], n_stats). Parameters ---------- X : numpy ndarray The input data array to transform. Returns ------- rolling_features : numpy ndarray An array containing the computed statistics. """array_ndim=X.ndimifarray_ndim==1:X=X[:,np.newaxis]vectorizable_stats={'mean','std','min','max','sum','median'}has_vectorizable=bool(set(self.stats)&vectorizable_stats)rolling_features=np.full(shape=(X.shape[1],self.n_stats),fill_value=np.nan,dtype=float)# Compute vectorized stats if any are requestedifhas_vectorizable:self._transform_vectorized(X,rolling_features)# Compute non-vectorizable statsforiinrange(X.shape[1]):forj,statinenumerate(self.stats):ifstatinvectorizable_stats:continueX_window=X[-self.window_sizes[j]:,i]X_window=X_window[~np.isnan(X_window)]iflen(X_window)>0:rolling_features[i,j]=self._apply_stat_numpy_jit(X_window,stat)else:rolling_features[i,j]=np.nanifarray_ndim==1:rolling_features=rolling_features.ravel()returnrolling_features
Vectorized transform using NumPy axis operations for vectorizable stats.
Modifies rolling_features in place for the vectorizable statistics.
This method is specifically designed to speed up the computation of
statistics in predict_bootstrap method of forecasters.
Parameters:
Name
Type
Description
Default
X
numpy ndarray
Input array of shape (window_length, n_samples).
required
rolling_features
numpy ndarray
Output array of shape (n_samples, n_stats) to fill in.
required
Returns:
Type
Description
None
Modifies rolling_features in place.
Some statistics do not follow the numpy behavior exactly:
- For 'std', if the window has only one non-NaN value, the result is 0.0
instead of NaN (to match _np_std_jit behavior).
- For 'sum', if the window has all NaN values, the result is NaN
instead of 0.0 (to match _np_sum_jit behavior).
Source code in skforecast/preprocessing/_preprocessing.py
def_transform_vectorized(self,X:np.ndarray,rolling_features:np.ndarray)->np.ndarray:""" Vectorized transform using NumPy axis operations for vectorizable stats. Modifies rolling_features in place for the vectorizable statistics. This method is specifically designed to speed up the computation of statistics in `predict_bootstrap` method of forecasters. Parameters ---------- X : numpy ndarray Input array of shape (window_length, n_samples). rolling_features : numpy ndarray Output array of shape (n_samples, n_stats) to fill in. Returns ------- None Modifies rolling_features in place. Some statistics do not follow the numpy behavior exactly: - For 'std', if the window has only one non-NaN value, the result is 0.0 instead of NaN (to match _np_std_jit behavior). - For 'sum', if the window has all NaN values, the result is NaN instead of 0.0 (to match _np_sum_jit behavior). """vectorizable_stats={'mean','std','min','max','sum','median'}forj,statinenumerate(self.stats):ifstatnotinvectorizable_stats:continuewindow=X[-self.window_sizes[j]:,:]withwarnings.catch_warnings():warnings.filterwarnings('ignore',message='Mean of empty slice')warnings.filterwarnings('ignore',message='Degrees of freedom <= 0 for slice')warnings.filterwarnings('ignore',message='All-NaN slice encountered')ifstat=='mean':rolling_features[:,j]=np.nanmean(window,axis=0)elifstat=='std':result=np.nanstd(window,axis=0,ddof=1)# Note: np.nanstd returns nan for single non-NaN values (ddof=1),# but it is replaced by 0.0 to match the behavior of the non-vectorized# _np_std_jit functionn_valid=np.sum(~np.isnan(window),axis=0)result[n_valid==1]=0.0rolling_features[:,j]=resultelifstat=='min':rolling_features[:,j]=np.nanmin(window,axis=0)elifstat=='max':rolling_features[:,j]=np.nanmax(window,axis=0)elifstat=='sum':result=np.nansum(window,axis=0,dtype=float)# Note: np.nansum returns 0 for all-NaN slices, but it is replaced by NaN# to match the behavior of the non-vectorized _np_sum_jit functionall_nan_mask=np.all(np.isnan(window),axis=0)result[all_nan_mask]=np.nanrolling_features[:,j]=resultelifstat=='median':rolling_features[:,j]=np.nanmedian(window,axis=0)
This class computes rolling features for classification problems. To avoid data
leakage, the last point in the window is excluded from calculations,
('closed': 'left' and 'center': False).
Currently, the following statistics are supported: 'proportion', 'mode',
'entropy', 'n_changes', 'n_unique'.
Parameters:
Name
Type
Description
Default
stats
(str, list)
Statistics to compute over the rolling window. Can be a string or a list,
and can have repeats. Available statistics are: 'proportion', 'mode',
'entropy', 'n_changes', 'n_unique'.
required
window_sizes
(int, list)
Size of the rolling window for each statistic. If an int, all stats share
the same window size. If a list, it should have the same length as stats.
required
min_periods
(int, list)
Minimum number of observations in window required to have a value.
Same as the min_periods argument of pandas rolling. If None,
defaults to window_sizes.
None
features_names
list
Names of the output features. If None, default names will be used in the
format 'roll_stat_window_size', for example 'roll_mode_7'. For 'proportion',
class-specific names are appended, e.g., 'roll_proportion_7_class_0'.
None
fillna
(str, float)
Fill missing values in transform_batch method. Available
methods are: 'mean', 'median', 'ffill', 'bfill', or a float value.
Validate the parameters of the RollingFeaturesClassification class.
Parameters:
Name
Type
Description
Default
stats
(str, list)
Statistics to compute over the rolling window. Can be a string or a list,
and can have repeats. Available statistics are: 'proportion', 'mode',
'entropy', 'n_changes', 'n_unique'.
required
window_sizes
(int, list)
Size of the rolling window for each statistic. If an int, all stats share
the same window size. If a list, it should have the same length as stats.
required
min_periods
(int, list)
Minimum number of observations in window required to have a value.
Same as the min_periods argument of pandas rolling. If None,
defaults to window_sizes.
None
features_names
list
Names of the output features. If None, default names will be used in the
format 'roll_stat_window_size', for example 'roll_mode_7'. For 'proportion',
class-specific names are appended, e.g., 'roll_proportion_7_class_0'.
None
fillna
(str, float)
Fill missing values in transform_batch method. Available
methods are: 'mean', 'median', 'ffill', 'bfill', or a float value.
None
Returns:
Type
Description
None
Source code in skforecast/preprocessing/_preprocessing.py
def_validate_params(self,stats:str|list[str],window_sizes:int|list[int],min_periods:int|list[int]|None=None,features_names:list[str]|None=None,fillna:str|float|None=None)->None:""" Validate the parameters of the RollingFeaturesClassification class. Parameters ---------- stats : str, list Statistics to compute over the rolling window. Can be a `string` or a `list`, and can have repeats. Available statistics are: 'proportion', 'mode', 'entropy', 'n_changes', 'n_unique'. window_sizes : int, list Size of the rolling window for each statistic. If an `int`, all stats share the same window size. If a `list`, it should have the same length as `stats`. min_periods : int, list, default None Minimum number of observations in window required to have a value. Same as the `min_periods` argument of pandas rolling. If `None`, defaults to `window_sizes`. features_names : list, default None Names of the output features. If `None`, default names will be used in the format 'roll_stat_window_size', for example 'roll_mode_7'. For 'proportion', class-specific names are appended, e.g., 'roll_proportion_7_class_0'. fillna : str, float, default None Fill missing values in `transform_batch` method. Available methods are: 'mean', 'median', 'ffill', 'bfill', or a float value. Returns ------- None """# statsallowed_stats=['proportion','mode','entropy','n_changes','n_unique']ifnotisinstance(stats,(str,list)):raiseTypeError(f"`stats` must be a string or a list of strings. Got {type(stats)}.")ifisinstance(stats,str):stats=[stats]forstatinset(stats):ifstatnotinallowed_stats:raiseValueError(f"Statistic '{stat}' is not allowed. Allowed stats are: {allowed_stats}.")n_stats=len(stats)# window_sizesifnotisinstance(window_sizes,(int,list)):raiseTypeError(f"`window_sizes` must be an int or a list of ints. Got {type(window_sizes)}.")ifisinstance(window_sizes,list):n_window_sizes=len(window_sizes)ifn_window_sizes!=n_stats:raiseValueError(f"Length of `window_sizes` list ({n_window_sizes}) "f"must match length of `stats` list ({n_stats}).")# Check duplicates (stats, window_sizes)ifisinstance(window_sizes,int):window_sizes=[window_sizes]*n_statsiflen(set(zip(stats,window_sizes)))!=n_stats:raiseValueError(f"Duplicate (stat, window_size) pairs are not allowed.\n"f" `stats` : {stats}\n"f" `window_sizes` : {window_sizes}")# min_periodsifnotisinstance(min_periods,(int,list,type(None))):raiseTypeError(f"`min_periods` must be an int, list of ints, or None. Got {type(min_periods)}.")ifmin_periodsisnotNone:ifisinstance(min_periods,int):min_periods=[min_periods]*n_statselifisinstance(min_periods,list):n_min_periods=len(min_periods)ifn_min_periods!=n_stats:raiseValueError(f"Length of `min_periods` list ({n_min_periods}) "f"must match length of `stats` list ({n_stats}).")fori,min_periodinenumerate(min_periods):ifmin_period>window_sizes[i]:raiseValueError("Each `min_period` must be less than or equal to its ""corresponding `window_size`.")# features_namesifnotisinstance(features_names,(list,type(None))):raiseTypeError(f"`features_names` must be a list of strings or None. Got {type(features_names)}.")ifisinstance(features_names,list):n_features_names=len(features_names)ifn_features_names!=n_stats:raiseValueError(f"Length of `features_names` list ({n_features_names}) "f"must match length of `stats` list ({n_stats}).")# TODO: Not used as ForecasterRecursiveClassifier doesn't allow NaNs. Check# when creating ForecasterRecursiveMultiSeriesClassifier# fillnaiffillnaisnotNone:ifnotisinstance(fillna,(int,float,str)):raiseTypeError(f"`fillna` must be a float, string, or None. Got {type(fillna)}.")ifisinstance(fillna,str):allowed_fill_strategy=['mean','median','ffill','bfill']iffillnanotinallowed_fill_strategy:raiseValueError(f"'{fillna}' is not allowed. Allowed `fillna` "f"values are: {allowed_fill_strategy} or a float value.")
def_apply_stat_pandas(self,X:pd.Series,rolling_obj:pd.core.window.rolling.Rolling,stat:str)->pd.Series:""" Apply the specified statistic to a pandas rolling object. Parameters ---------- rolling_obj : pandas Rolling Rolling object to apply the statistic. stat : str Statistic to compute. Returns ------- stat_series : pandas Series Series with the computed statistic. """ifstat=='proportion':rolling_params={'window':rolling_obj.window,'min_periods':rolling_obj.min_periods,'center':rolling_obj.center,'closed':rolling_obj.closed}dummies=pd.get_dummies(X,prefix='class')proportions=dummies.rolling(**rolling_params).sum()/rolling_obj.windowreturnproportionselifstat=='mode':returnrolling_obj.apply(lambdax:scipy_mode(x)[0],raw=True)elifstat=='entropy':returnrolling_obj.apply(_entropy,raw=True)elifstat=='n_changes':returnrolling_obj.apply(_n_changes_jit,raw=True)elifstat=='n_unique':returnrolling_obj.apply(_n_unique_jit,raw=True)else:raiseValueError(f"Statistic '{stat}' is not implemented.")
deftransform_batch(self,X:pd.Series)->pd.DataFrame:""" Transform an entire pandas Series using rolling windows and compute the specified statistics. Parameters ---------- X : pandas Series The input data series to transform. Returns ------- rolling_features : pandas DataFrame A DataFrame containing the rolling features. """ifself.classesisNone:self.classes=list(np.sort(X.unique()))features_names=[]forstat,feature_nameinzip(self.stats,self.features_names):ifstat!='proportion':features_names.append(feature_name)else:forclsinself.classes:feature_name_class=f"{feature_name}_class_{cls}"features_names.append(feature_name_class)self.features_names=features_namesforkinself.unique_rolling_windows.keys():rolling_obj=X.rolling(**self.unique_rolling_windows[k]['params'])self.unique_rolling_windows[k]['rolling_obj']=rolling_objrolling_features=[]fori,statinenumerate(self.stats):window_size=self.window_sizes[i]min_periods=self.min_periods[i]key=f"{window_size}_{min_periods}"rolling_obj=self.unique_rolling_windows[key]['rolling_obj']stat_series=self._apply_stat_pandas(X=X,rolling_obj=rolling_obj,stat=stat)rolling_features.append(stat_series)rolling_features=pd.concat(rolling_features,axis=1)rolling_features.columns=self.features_namesrolling_features=rolling_features.iloc[self.max_window_size:]ifself.fillnaisnotNone:ifself.fillna=='mean':rolling_features=rolling_features.fillna(rolling_features.mean())elifself.fillna=='median':rolling_features=rolling_features.fillna(rolling_features.median())elifself.fillna=='ffill':rolling_features=rolling_features.ffill()elifself.fillna=='bfill':rolling_features=rolling_features.bfill()else:rolling_features=rolling_features.fillna(self.fillna)returnrolling_features
def_apply_stat_numpy_jit(self,X_window:np.ndarray,stat:str)->float:""" Apply the specified statistic to a numpy array using Numba JIT. Parameters ---------- X_window : numpy array Array with the rolling window. stat : str Statistic to compute. Returns ------- stat_value : float Value of the computed statistic. """ifstat=='proportion':# Calculate proportions for each classproportions=np.zeros(len(self.classes))len_window=len(X_window)fori,clsinenumerate(self.classes):proportions[i]=np.sum(X_window==cls)/len_windowreturnproportionselifstat=='mode':returnscipy_mode(X_window)[0]elifstat=='entropy':return_entropy(X_window)elifstat=='n_changes':return_n_changes_jit(X_window)elifstat=='n_unique':return_n_unique_jit(X_window)else:raiseValueError(f"Statistic '{stat}' is not implemented.")
Transform a numpy array using rolling windows and compute the
specified statistics. The returned array will have the shape
(X.shape[1] if exists, n_stats). For example, if X is a flat
array, the output will have shape (n_stats,). If X is a 2D array,
the output will have shape (X.shape[1], n_stats).
Parameters:
Name
Type
Description
Default
X
numpy ndarray
The input data array to transform.
required
Returns:
Name
Type
Description
rolling_features
numpy ndarray
An array containing the computed statistics.
Source code in skforecast/preprocessing/_preprocessing.py
deftransform(self,X:np.ndarray)->np.ndarray:""" Transform a numpy array using rolling windows and compute the specified statistics. The returned array will have the shape (X.shape[1] if exists, n_stats). For example, if X is a flat array, the output will have shape (n_stats,). If X is a 2D array, the output will have shape (X.shape[1], n_stats). Parameters ---------- X : numpy ndarray The input data array to transform. Returns ------- rolling_features : numpy ndarray An array containing the computed statistics. """ifself.classesisNone:raiseValueError("Classes must be specified before calling transform. ""Call `transform_batch` first to infer classes from data.")array_ndim=X.ndimifarray_ndim==1:X=X[:,np.newaxis]# TODO: If more than one columns 2d Array, maybe the classes doesn't come# from the same column. Col 1 has classes [0, 1], col 2 has classes [3, 4].n_classes=len(self.classes)n_output_features=0forstatinself.stats:ifstat=='proportion':n_output_features+=n_classeselse:n_output_features+=1rolling_features=np.full(shape=(X.shape[1],n_output_features),fill_value=np.nan,dtype=float)foriinrange(X.shape[1]):feature_idx=0forj,statinenumerate(self.stats):X_window=X[-self.window_sizes[j]:,i]X_window=X_window[~np.isnan(X_window)]iflen(X_window)>=0:result=self._apply_stat_numpy_jit(X_window,stat)ifstat=='proportion':# Result is an array with one value per classrolling_features[i,feature_idx:feature_idx+n_classes]=resultfeature_idx+=n_classeselse:# Result is a single valuerolling_features[i,feature_idx]=resultfeature_idx+=1else:ifstat=='proportion':rolling_features[i,feature_idx:feature_idx+n_classes]=np.nanfeature_idx+=n_classeselse:rolling_features[i,feature_idx]=np.nanfeature_idx+=1ifarray_ndim==1:rolling_features=rolling_features.ravel()returnrolling_features
A transformer for extracting datetime features from the DateTime index of a
pandas DataFrame or Series, or from a pandas DatetimeIndex directly. It can
also apply encoding to the extracted features.
The allowed features to extract are: 'year', 'month', 'week', 'day_of_week',
'day_of_month', 'day_of_year', 'weekend', 'hour', 'minute', 'second',
and 'quarter'. If not specified, all of these features are extracted.
Parameters:
Name
Type
Description
Default
features
list
List of calendar features (strings) to extract from the index. When
None, the following features are extracted: 'year', 'month',
'week', 'day_of_week', 'day_of_month', 'day_of_year',
'weekend', 'hour', 'minute', 'second', 'quarter'.
None
features_to_encode
list
List of calendar features (strings) to encode. When None, all
extracted features are encoded. If a feature is not in features, a
ValueError is raised at fit/transform time. If the explicit list
contains features that cannot be encoded with the chosen encoding
(e.g. 'year' or 'weekend', which are never encodable), an
IgnoredArgumentWarning is issued and those features are kept as raw
integers.
None
encoding
str
Encoding method for the extracted features. Options are None,
'cyclical', 'onehot' or 'spline'. Features that cannot be
encoded under the chosen mode are kept as raw integers. By default,
'year' and 'weekend' are never encoded. 'onehot' excludes them
via the known-category set, while 'cyclical' and 'spline' exclude
them via max_values.
'cyclical'
max_values
dict
Dictionary of maximum values for the cyclical and spline encoding.
User-provided values are merged with the defaults: keys passed by
the user override the corresponding default, and missing keys fall
back to the defaults {'month': 12, 'week': 53, 'day_of_week': 7,
'day_of_month': 31, 'day_of_year': 366, 'hour': 24, 'minute': 60,
'second': 60, 'quarter': 4}. For example, passing
max_values={'month': 6} overrides only month; the other features
keep their defaults. Features that are not in the defaults (e.g.
'year', 'weekend') are left as raw integers.
None
spline_kwargs
dict
Additional keyword arguments for the spline encoding. Only used when
encoding='spline'. When None, defaults to {'degree': 3,
'include_bias': True, 'extrapolation': 'periodic'}; n_knots
defaults to max_values[feature] + 1 per feature. Knots are placed
uniformly over [min_val, min_val + max_val] (e.g. 1-13 for month,
0-24 for hour), giving a periodic period of exactly max_val for
both 0-indexed and 1-indexed features and ensuring consistent
encoding across training and prediction. Any keyword argument
accepted by
sklearn.preprocessing.SplineTransformer is allowed (e.g. n_knots,
degree, include_bias, extrapolation, order) except
knots (computed internally from max_values) and sparse_output
(incompatible with the DataFrame output). Passing either of these or
an unknown key raises ValueError at fit/transform time.
None
keep_original_columns
bool
If True, the original columns of X are kept in the output
DataFrame. If False, only the extracted datetime features are
returned. When True and X is an unnamed pandas Series
(X.name is None), a ValueError is raised at fit/transform time;
either set X.name to a string, or pass keep_original_columns=False.
When X is a pandas DatetimeIndex this argument has no effect, as
there are no original columns to keep.
True
tol
(float, None)
Absolute tolerance for clamping near-zero values produced by cyclical
encoding to exactly 0. Floating point arithmetic can produce values
such as -2.4e-16 instead of 0 (e.g. sin(2*pi*12/12)). When not None,
any sin or cos value whose absolute value is smaller than tol is
replaced with 0.0. Set to None to disable clamping. Only used when
encoding='cyclical'.
Keyword arguments for the spline encoding. None means the default
values are used (degree=3, include_bias=True,
extrapolation='periodic', n_knots=max_val+1 per feature).
Absolute tolerance for clamping near-zero cyclical encoding values.
None means clamping is disabled.
feature_names_out_
list
Names of the output features. Set after calling fit or transform.
Notes
The default max_values use 53 for 'week' and 366 for 'day_of_year'
to accommodate the maximum possible values across all calendar years:
ISO week 53 occurs in some years (e.g. 2015, 2020, 2026) and
day-of-year 366 occurs in leap years. Because the encoding must be
stateless (the same for any year, without prior knowledge of whether it
is a leap year or contains ISO week 53), the period is fixed at the
maximum-possible value. This implies:
Onehot: the week_53 and day_of_year_366 columns are always
present in the output and equal 0 for rows whose year never reaches
those values. This guarantees a consistent column schema across
training and prediction.
Cyclical / spline: in years where the maximum value is reached,
the cyclical wrap-around is exact (e.g. sin(2π·366/366) = 0 matches
sin(2π·0/366) = 0). In years where it is not, there is a one-step
"phantom gap" between the highest observed value and 1. The
cyclical distance is two steps instead of one. This residual
asymmetry is numerically small (≈ 1.7% for day_of_year, ≈ 12% for
week) and is strictly preferable to the alternative (period
52 / 365), which would silently collapse week 53 onto week 1 and day
366 onto day 1 in years where those values occur.
For high-cardinality features such as day_of_year (366 columns) or
day_of_month (31 columns), encoding='cyclical' (2 columns per
feature) or 'spline' (≈ max_val columns per feature, but dense
rather than sparse) are typically more memory-efficient than
'onehot', especially on multi-year hourly or sub-daily data.
Output column order: encoded features appear in the order given by
features. Non-encoded extracted features (e.g. 'year' or
'weekend', which are never encoded) appear first in the output, in
features-list order; encoded features follow, also in
features-list order. For example, with
features=['year', 'month', 'weekend', 'hour'] and
encoding='cyclical' the output columns are
[year, weekend, month_sin, month_cos, hour_sin, hour_cos].
def__init__(self,features:list[str]|None=None,features_to_encode:list[str]|None=None,encoding:str="cyclical",max_values:dict[str,int]|None=None,spline_kwargs:dict|None=None,keep_original_columns:bool=True,tol:float|None=1e-12,)->None:allowed_features=["year","month","week","day_of_week","day_of_month","day_of_year","weekend","hour","minute","second","quarter",]iffeaturesisnotNone:not_supported_features=set(features)-set(allowed_features)ifnot_supported_features:raiseValueError(f"Calendar features {not_supported_features} are not supported. "f"Supported features are {allowed_features}.")else:features=allowed_featuresifencodingnotin["cyclical","onehot","spline",None]:raiseValueError("Encoding must be one of 'cyclical', 'onehot', 'spline' or None")self.features=featuresself.features_to_encode=features_to_encodeself.encoding=encodingself.max_values=max_valuesself.spline_kwargs=spline_kwargsself.keep_original_columns=keep_original_columnsself.tol=tol
deffit(self,X,y=None):""" Fit the transformer by computing the output feature names. Parameters ---------- X : pandas Series, pandas DataFrame, pandas DatetimeIndex Input DataFrame or Series with a datetime index, or a pandas DatetimeIndex directly. y : ignored Not used, present for API compatibility. Returns ------- self : CalendarFeatures Fitted transformer. """# Slice to the first 2 rows: the encoding is stateless (column# names depend on parameters and the index frequency, not on data# values), so any non-empty slice yields the same output schema.# Non-pandas inputs are passed through unchanged so that# `create_calendar_features` raises the appropriate `TypeError`.ifisinstance(X,pd.DatetimeIndex):X_sample=X[:2]elifisinstance(X,(pd.DataFrame,pd.Series)):X_sample=X.iloc[:2]else:X_sample=Xresult=create_calendar_features(X=X_sample,features=self.features,features_to_encode=self.features_to_encode,encoding=self.encoding,max_values=self.max_values,spline_kwargs=self.spline_kwargs,keep_original_columns=self.keep_original_columns,tol=self.tol,)self.feature_names_out_=list(result.columns)returnself
deftransform(self,X:pd.Series|pd.DataFrame|pd.DatetimeIndex)->pd.DataFrame:""" Create datetime features from the DateTime index of a pandas DataFrame or Series. Parameters ---------- X : pandas Series, pandas DataFrame, pandas DatetimeIndex Input DataFrame or Series with a datetime index, or a pandas DatetimeIndex directly. Returns ------- X_new : pandas DataFrame DataFrame with the extracted (and optionally encoded) datetime features. """X_new=create_calendar_features(X=X,features=self.features,features_to_encode=self.features_to_encode,encoding=self.encoding,max_values=self.max_values,spline_kwargs=self.spline_kwargs,keep_original_columns=self.keep_original_columns,tol=self.tol,)returnX_new
defget_feature_names_out(self,input_features:list[str]|None=None)->list[str]:""" Get the names of the output features. Parameters ---------- input_features : list, default None Ignored. Present for API compatibility with sklearn. Returns ------- feature_names_out_ : list Names of the output features. """check_is_fitted(self,"feature_names_out_")returnself.feature_names_out_
Convert a pandas DataFrame where each column represents a different time series
into a long format DataFrame with a MultiIndex. The index of the input DataFrame
must be a pandas DatetimeIndex with a defined frequency. The function reshapes the
DataFrame from wide format to long format, where each row corresponds to a
specific time point and series ID. The resulting DataFrame will have a MultiIndex
with the series IDs as the first level and a pandas DatetimeIndex as the second
level. If return_multi_index is set to False, the returned DataFrame have three
columns: 'series_id', 'datetime' and 'value', with a regular index.
Parameters:
Name
Type
Description
Default
data
DataFrame
Wide format series. The index must be a pandas DatetimeIndex with a
defined frequency and each column must represent a different time series.
required
return_multi_index
bool
If True, the returned DataFrame will have a MultiIndex with the series IDs
as the first level and a pandas DatetimeIndex as the second level. If False,
the returned DataFrame will have a regular index.
True
Returns:
Name
Type
Description
data
pandas DataFrame
Long format series with a MultiIndex. The first level contains the series IDs,
and the second level contains a pandas DatetimeIndex with the same frequency
for each series.
Source code in skforecast/preprocessing/_preprocessing.py
defreshape_series_wide_to_long(data:pd.DataFrame,return_multi_index:bool=True)->pd.DataFrame:""" Convert a pandas DataFrame where each column represents a different time series into a long format DataFrame with a MultiIndex. The index of the input DataFrame must be a pandas DatetimeIndex with a defined frequency. The function reshapes the DataFrame from wide format to long format, where each row corresponds to a specific time point and series ID. The resulting DataFrame will have a MultiIndex with the series IDs as the first level and a pandas DatetimeIndex as the second level. If `return_multi_index` is set to False, the returned DataFrame have three columns: 'series_id', 'datetime' and 'value', with a regular index. Parameters ---------- data: pandas DataFrame Wide format series. The index must be a pandas DatetimeIndex with a defined frequency and each column must represent a different time series. return_multi_index: bool, default True If True, the returned DataFrame will have a MultiIndex with the series IDs as the first level and a pandas DatetimeIndex as the second level. If False, the returned DataFrame will have a regular index. Returns ------- data: pandas DataFrame Long format series with a MultiIndex. The first level contains the series IDs, and the second level contains a pandas DatetimeIndex with the same frequency for each series. """ifnotisinstance(data,pd.DataFrame):raiseTypeError("`data` must be a pandas DataFrame.")ifnotisinstance(data.index,pd.DatetimeIndex):raiseTypeError("`data` index must be a pandas DatetimeIndex.")freq=data.index.freqdata.index.name="datetime"data=data.reset_index()data=pd.melt(data,id_vars="datetime",var_name="series_id",value_name="value")data=data.groupby("series_id",sort=False).apply(lambdax:x.set_index("datetime").asfreq(freq),include_groups=False)ifnotreturn_multi_index:data=data.reset_index()returndata
Convert a long-format DataFrame into a dictionary of pandas Series with the
specified frequency. Supports two input formats:
A pandas DataFrame with explicit columns for the series identifier, time
index, and values.
A pandas DataFrame with a MultiIndex, where the first level contains the
series IDs, and the second level contains a pandas DatetimeIndex.
Parameters:
Name
Type
Description
Default
data
DataFrame
Long-format series.
required
freq
str
Frequency of the series.
required
series_id
str | None
Column name with the series identifier. Not needed if the input data
is a pandas DataFrame with MultiIndex.
None
index
str | None
Column name with the time index. Not needed if the input data is a pandas
DataFrame with MultiIndex.
None
values
str | None
Column name with the values. Not needed if the input data is a pandas
DataFrame with MultiIndex.
None
fill_value
float | None
Value to use for filling gaps created when setting the frequency with
asfreq (note this does not fill NaNs that already were present). If
None, gaps will contain NaN values.
None
suppress_warnings
bool
If True, suppress warnings when a series is incomplete after setting the
frequency.
False
Returns:
Name
Type
Description
series_dict
dict
Dictionary with the series.
Source code in skforecast/preprocessing/_preprocessing.py
defreshape_series_long_to_dict(data:pd.DataFrame,freq:str,series_id:str|None=None,index:str|None=None,values:str|None=None,fill_value:float|None=None,suppress_warnings:bool=False)->dict[str,pd.Series]:""" Convert a long-format DataFrame into a dictionary of pandas Series with the specified frequency. Supports two input formats: - A pandas DataFrame with explicit columns for the series identifier, time index, and values. - A pandas DataFrame with a MultiIndex, where the first level contains the series IDs, and the second level contains a pandas DatetimeIndex. Parameters ---------- data: pandas DataFrame Long-format series. freq: str Frequency of the series. series_id: str, default None Column name with the series identifier. Not needed if the input data is a pandas DataFrame with MultiIndex. index: str, default None Column name with the time index. Not needed if the input data is a pandas DataFrame with MultiIndex. values: str, default None Column name with the values. Not needed if the input data is a pandas DataFrame with MultiIndex. fill_value: float, default None Value to use for filling gaps created when setting the frequency with `asfreq` (note this does not fill NaNs that already were present). If None, gaps will contain NaN values. suppress_warnings: bool, default False If True, suppress warnings when a series is incomplete after setting the frequency. Returns ------- series_dict: dict Dictionary with the series. """ifnotisinstance(data,pd.DataFrame):raiseTypeError("`data` must be a pandas DataFrame.")ifisinstance(data.index,pd.MultiIndex):data=data.copy()first_col=data.columns[0]data.index=data.index.set_names([data.index.names[0],None])series_dict={}fork,groupindata.groupby(level=0,sort=True,observed=True):group=group.droplevel(0)original_size=len(group)series_dict[k]=group[first_col].rename(k).asfreq(freq,fill_value=fill_value)ifnotsuppress_warningsandlen(series_dict[k])!=original_size:fill_msg=("NaNs have been introduced"iffill_valueisNoneelsef"Missing values have been filled with {fill_value}")warnings.warn(f"Series '{k}' is incomplete. {fill_msg} after "f"setting the frequency.",MissingValuesWarning)else:forcolin[series_id,index,values]:ifcolisNone:raiseValueError("Arguments `series_id`, `index`, and `values` must be ""specified when the input DataFrame does not have a MultiIndex. ""Please provide a value for each of these arguments.")ifcolnotindata.columns:raiseValueError(f"Column '{col}' not found in `data`.")data_grouped=data.groupby(series_id,observed=True)original_sizes=data_grouped.size()series_dict={}fork,vindata_grouped:series_dict[k]=v.set_index(index)[values].asfreq(freq,fill_value=fill_value).rename(k)series_dict[k].index.name=Noneifnotsuppress_warningsandlen(series_dict[k])!=original_sizes[k]:fill_msg=("NaNs have been introduced"iffill_valueisNoneelsef"Missing values have been filled with {fill_value}")warnings.warn(f"Series '{k}' is incomplete. {fill_msg} after "f"setting the frequency.",MissingValuesWarning)returnseries_dict
Convert a long-format DataFrame of exogenous variables into a dictionary
of pandas DataFrames with the specified frequency. Supports two input formats:
A pandas DataFrame with explicit columns for the series identifier, time
index, and exogenous variables.
A pandas DataFrame with a MultiIndex, where the first level contains the
series IDs, and the second level contains a pandas DatetimeIndex.
Parameters:
Name
Type
Description
Default
data
DataFrame
Long format exogenous variables.
required
freq
str
Frequency of the series.
required
series_id
str | None
Column name with the series identifier. Not needed if the input data
is a pandas DataFrame with MultiIndex.
None
index
str | None
Column name with the time index. Not needed if the input data is a pandas
DataFrame with MultiIndex.
None
fill_value
float | None
Value to use for filling gaps created when setting the frequency with
asfreq (note this does not fill NaNs that already were present). If
None, gaps will contain NaN values. Only applied to numeric columns;
non-numeric columns (e.g. string, categorical) will still contain NaN
in the gaps.
None
drop_all_nan_cols
bool
If True, drop columns with all values as NaN. This is useful when
there are series without some exogenous variables.
False
consolidate_dtypes
bool
Consolidate the data types of the exogenous variables across all series.
If, after setting the frequency, NaNs are introduced in any series and
cause a column's dtype to change to float, that column is also cast to
float in every other series so that all series share the same dtypes.
True
suppress_warnings
bool
If True, suppress warnings when exog is incomplete after setting the
frequency.
False
Returns:
Name
Type
Description
exog_dict
dict
Dictionary with the exogenous variables.
Source code in skforecast/preprocessing/_preprocessing.py
defreshape_exog_long_to_dict(data:pd.DataFrame,freq:str,series_id:str|None=None,index:str|None=None,fill_value:float|None=None,drop_all_nan_cols:bool=False,consolidate_dtypes:bool=True,suppress_warnings:bool=False)->dict[str,pd.DataFrame]:""" Convert a long-format DataFrame of exogenous variables into a dictionary of pandas DataFrames with the specified frequency. Supports two input formats: - A pandas DataFrame with explicit columns for the series identifier, time index, and exogenous variables. - A pandas DataFrame with a MultiIndex, where the first level contains the series IDs, and the second level contains a pandas DatetimeIndex. Parameters ---------- data: pandas DataFrame Long format exogenous variables. freq: str Frequency of the series. series_id: str, default None Column name with the series identifier. Not needed if the input data is a pandas DataFrame with MultiIndex. index: str, default None Column name with the time index. Not needed if the input data is a pandas DataFrame with MultiIndex. fill_value: float, default None Value to use for filling gaps created when setting the frequency with `asfreq` (note this does not fill NaNs that already were present). If None, gaps will contain NaN values. Only applied to numeric columns; non-numeric columns (e.g. string, categorical) will still contain NaN in the gaps. drop_all_nan_cols: bool, default False If True, drop columns with all values as NaN. This is useful when there are series without some exogenous variables. consolidate_dtypes: bool, default True Consolidate the data types of the exogenous variables across all series. If, after setting the frequency, NaNs are introduced in any series and cause a column's dtype to change to float, that column is also cast to float in every other series so that all series share the same dtypes. suppress_warnings: bool, default False If True, suppress warnings when exog is incomplete after setting the frequency. Returns ------- exog_dict: dict Dictionary with the exogenous variables. """ifnotisinstance(data,pd.DataFrame):raiseTypeError("`data` must be a pandas DataFrame.")ifisinstance(data.index,pd.MultiIndex):data=data.copy()data.index=data.index.set_names([data.index.names[0],None])exog_dict={}cols_float_dtype=set()nans_introduced=Falsefork,groupindata.groupby(level=0,sort=True,observed=True):group=group.droplevel(0)original_index=group.indexoriginal_size=len(group)exog_dict[k]=group.asfreq(freq)iflen(exog_dict[k])!=original_size:nans_introduced=Truenon_numeric_cols=[]iffill_valueisnotNone:numeric_cols=exog_dict[k].select_dtypes(include='number').columnsnon_numeric_cols=exog_dict[k].columns.difference(numeric_cols)new_rows_mask=~exog_dict[k].index.isin(original_index)iflen(numeric_cols)>0:exog_dict[k].loc[new_rows_mask,numeric_cols]=(exog_dict[k].loc[new_rows_mask,numeric_cols].fillna(fill_value))ifnotsuppress_warnings:iffill_valueisNone:fill_msg="NaNs have been introduced"else:fill_msg=(f"Missing values have been filled with {fill_value}")iflen(non_numeric_cols)>0:fill_msg+=(f" in numeric columns only. Non-numeric columns "f"{list(non_numeric_cols)} still contain NaN")warnings.warn(f"Exogenous variables for series '{k}' are incomplete. "f"{fill_msg} after setting the frequency.",MissingValuesWarning)ifconsolidate_dtypes:cols_float_dtype.update({colforcolinexog_dict[k].columnsifpd.api.types.is_float_dtype(exog_dict[k][col])})else:forcolin[series_id,index]:ifcolisNone:raiseValueError("Arguments `series_id`, and `index` must be ""specified when the input DataFrame does not have a MultiIndex. ""Please provide a value for each of these arguments.")ifcolnotindata.columns:raiseValueError(f"Column '{col}' not found in `data`.")cols_float_dtype={colforcolindata.columnsifcolnotin(series_id,index)andpd.api.types.is_float_dtype(data[col])}data_grouped=data.groupby(series_id,observed=True)original_sizes=data_grouped.size()exog_dict=dict(tuple(data_grouped))original_indices={k:set(v[index])fork,vinexog_dict.items()}exog_dict={k:v.set_index(index).drop(columns=series_id).asfreq(freq)fork,vinexog_dict.items()}forkinexog_dict.keys():exog_dict[k].index.name=Nonenans_introduced=Falsefork,vinexog_dict.items():iflen(v)!=original_sizes[k]:nans_introduced=Truenon_numeric_cols=[]iffill_valueisnotNone:numeric_cols=v.select_dtypes(include='number').columnsnon_numeric_cols=v.columns.difference(numeric_cols)new_rows_mask=~v.index.isin(original_indices[k])iflen(numeric_cols)>0:exog_dict[k].loc[new_rows_mask,numeric_cols]=(v.loc[new_rows_mask,numeric_cols].fillna(fill_value))ifnotsuppress_warnings:iffill_valueisNone:fill_msg="NaNs have been introduced"else:fill_msg=(f"Missing values have been filled with {fill_value}")iflen(non_numeric_cols)>0:fill_msg+=(f" in numeric columns only. Non-numeric columns "f"{list(non_numeric_cols)} still contain NaN")warnings.warn(f"Exogenous variables for series '{k}' are incomplete. "f"{fill_msg} after setting the frequency.",MissingValuesWarning)ifconsolidate_dtypes:cols_float_dtype.update({colforcolinv.columnsifpd.api.types.is_float_dtype(v[col])})ifconsolidate_dtypesandnans_introduced:new_dtypes={col:floatforcolincols_float_dtype}exog_dict={k:v.astype(new_dtypes,copy=False)fork,vinexog_dict.items()}ifdrop_all_nan_cols:exog_dict={k:v.dropna(how="all",axis=1)fork,vinexog_dict.items()}returnexog_dict
Convert dictionaries of series and exogenous variables to a long-format
pandas DataFrame with MultiIndex. The first level of the MultiIndex contains the
series identifiers, and the second level contains the temporal index. If both
series and exog are provided, they are merged into a single DataFrame.
Parameters:
Name
Type
Description
Default
series
dict[str, Series] | None
Dictionary with multiple time series (expected: dict[str, pd.Series]).
required
exog
dict[str, Series | DataFrame] | None
Dictionary with exogenous variables (expected: dict[str, pd.Series or pd.DataFrame]).
required
series_col_name
str
Column name for the series values in the resulting DataFrame.
'series_value'
index_names
list[str]
Names for the levels of the MultiIndex in the resulting DataFrame. The first
name corresponds to the series identifier, and the second name corresponds
to the temporal index.
['series_id', 'datetime']
merge_how
str
Type of merge to perform when combining series and exog. Options are:
'left': Keep only indices from series (default)
'right': Keep only indices from exog
'outer': Keep all indices from both series and exog
'inner': Keep only indices present in both
'left'
Returns:
Name
Type
Description
long_df
DataFrame
Long-format DataFrame with a MultiIndex of two levels:
- First level: series identifier (named by index_names[0], default 'series_id')
- Second level: temporal index (named by index_names[1], default 'datetime')
Columns include:
- Series values (named by series_col_name, default 'series_value') if series is provided.
- Exogenous variable columns (from exog) if exog is provided.
If both series and exog are provided, columns from both are present.
If only one is provided, only its columns are present.
Source code in skforecast/preprocessing/_preprocessing.py
defreshape_series_exog_dict_to_long(series:dict[str,pd.Series]|None,exog:dict[str,pd.Series|pd.DataFrame]|None,series_col_name:str='series_value',index_names:list[str]=['series_id','datetime'],merge_how:str='left')->pd.DataFrame:""" Convert dictionaries of series and exogenous variables to a long-format pandas DataFrame with MultiIndex. The first level of the MultiIndex contains the series identifiers, and the second level contains the temporal index. If both series and exog are provided, they are merged into a single DataFrame. Parameters ---------- series: dict, None Dictionary with multiple time series (expected: dict[str, pd.Series]). exog: dict, None Dictionary with exogenous variables (expected: dict[str, pd.Series or pd.DataFrame]). series_col_name: str, default 'series_value' Column name for the series values in the resulting DataFrame. index_names: list[str], default ['series_id', 'datetime'] Names for the levels of the MultiIndex in the resulting DataFrame. The first name corresponds to the series identifier, and the second name corresponds to the temporal index. merge_how: str, default 'left' Type of merge to perform when combining `series` and `exog`. Options are: - 'left': Keep only indices from `series` (default) - 'right': Keep only indices from `exog` - 'outer': Keep all indices from both `series` and `exog` - 'inner': Keep only indices present in both Returns ------- long_df : pandas.DataFrame Long-format DataFrame with a MultiIndex of two levels: - First level: series identifier (named by `index_names[0]`, default 'series_id') - Second level: temporal index (named by `index_names[1]`, default 'datetime') Columns include: - Series values (named by `series_col_name`, default 'series_value') if `series` is provided. - Exogenous variable columns (from `exog`) if `exog` is provided. If both `series` and `exog` are provided, columns from both are present. If only one is provided, only its columns are present. """ifseriesisNoneandexogisNone:raiseValueError("Both `series` and `exog` cannot be None.")ifseriesisnotNone:ifnotisinstance(series,dict):raiseTypeError(f"`series` must be a dictionary. Got {type(series)}.")fork,vinseries.items():ifnotisinstance(v,pd.Series):raiseTypeError(f"`series['{k}']` must be a pandas Series.")series=pd.concat(series,names=index_names).to_frame(series_col_name)ifexogisnotNone:ifnotisinstance(exog,dict):raiseTypeError(f"`exog` must be a dictionary. Got {type(exog)}.")fork,vinexog.items():ifnotisinstance(v,(pd.Series,pd.DataFrame)):raiseTypeError(f"`exog['{k}']` must be a pandas Series or a pandas DataFrame.")exog=pd.concat(exog,names=index_names)ifisinstance(exog,pd.Series):exog=exog.to_frame(name='exog_value')ifseriesisnotNoneandexogisnotNone:series_idx_type=type(series.index.get_level_values(1))exog_idx_type=type(exog.index.get_level_values(1))ifseries_idx_type!=exog_idx_type:raiseTypeError(f"Index type mismatch: series has index of type "f"{series_idx_type}, but `exog` has {exog_idx_type}. "f"Ensure all indices are compatible.")ifseries_col_nameinexog.columns:raiseValueError(f"Column name conflict: '{series_col_name}' already exists in `exog`. "f"Please choose a different `series_col_name` value.")ifseriesisNone:long_df=exogelifexogisNone:long_df=serieselse:long_df=pd.merge(series,exog,left_index=True,right_index=True,how=merge_how)returnlong_df
Transformer that calibrates the prediction interval to achieve the desired
coverage based on conformity scores. It uses the conformal split method.
Parameters:
Name
Type
Description
Default
nominal_coverage
float
Desired coverage. This is the desired probability that the true value
falls within the calibrated interval.
0.8
symmetric_calibration
bool
If True, the calibration factor is the same for the lower and upper bounds.
If False, the calibration factor is different for the lower and upper bounds.
If True, the calibration factor is the same for the lower and upper bounds.
If False, the calibration factor is different for the lower and upper bounds.
def__init__(self,nominal_coverage:float=0.8,symmetric_calibration:bool=True)->None:ifnominal_coverage<0ornominal_coverage>1:raiseValueError(f"`nominal_coverage` must be a float between 0 and 1. Got {nominal_coverage}")self.nominal_coverage=nominal_coverageself.symmetric_calibration=symmetric_calibrationself.correction_factor_={}self.correction_factor_lower_={}self.correction_factor_upper_={}self.fit_coverage_={}self.fit_input_type_=Noneself.fit_series_names_=Noneself.is_fitted=False
Learn the correction factor needed to achieve the desired coverage.
Parameters:
Name
Type
Description
Default
y_true
pandas Series, pandas DataFrame, dict
True values of the time series.
If pandas Series, it is assumed that only one series is available.
If pandas DataFrame, it is assumed that each column is a different
series which will be calibrated separately. The column names are
used as series names.
If dict, it is assumed that each key is a series name and the
corresponding value is a pandas Series with the true values.
required
y_pred_interval
pandas DataFrame
Prediction interval estimated for the time series.
If y_true contains only one series, y_pred_interval must have
two columns, 'lower_bound' and 'upper_bound'.
If y_true contains multiple series, y_pred_interval must be
a long-format DataFrame with three columns: 'level', 'lower_bound',
and 'upper_bound'. The 'level' column identifies the series to which
each interval belongs.
required
Returns:
Type
Description
None
Source code in skforecast/preprocessing/_preprocessing.py
deffit(self,y_true:pd.Series|pd.DataFrame|dict[str,pd.Series],y_pred_interval:pd.DataFrame,)->None:""" Learn the correction factor needed to achieve the desired coverage. Parameters ---------- y_true : pandas Series, pandas DataFrame, dict True values of the time series. - If pandas Series, it is assumed that only one series is available. - If pandas DataFrame, it is assumed that each column is a different series which will be calibrated separately. The column names are used as series names. - If dict, it is assumed that each key is a series name and the corresponding value is a pandas Series with the true values. y_pred_interval : pandas DataFrame Prediction interval estimated for the time series. - If `y_true` contains only one series, `y_pred_interval` must have two columns, 'lower_bound' and 'upper_bound'. - If `y_true` contains multiple series, `y_pred_interval` must be a long-format DataFrame with three columns: 'level', 'lower_bound', and 'upper_bound'. The 'level' column identifies the series to which each interval belongs. Returns ------- None """self.correction_factor_={}self.correction_factor_lower_={}self.correction_factor_upper_={}self.fit_coverage_={}self.fit_input_type_=Noneself.fit_series_names_=Noneself.is_fitted=Falseifnotisinstance(y_true,(pd.Series,pd.DataFrame,dict)):raiseTypeError("`y_true` must be a pandas Series, pandas DataFrame, or a dictionary.")ifnotisinstance(y_pred_interval,(pd.DataFrame)):raiseTypeError("`y_pred_interval` must be a pandas DataFrame.")ifnotset(["lower_bound","upper_bound"]).issubset(y_pred_interval.columns):raiseValueError("`y_pred_interval` must have columns 'lower_bound' and 'upper_bound'.")ifisinstance(y_true,(pd.DataFrame,dict))and'level'notiny_pred_interval.columns:raiseValueError("If `y_true` is a pandas DataFrame or a dictionary, `y_pred_interval` ""must have an additional column 'level' to identify each series.")ifisinstance(y_true,pd.Series):name=y_true.nameify_true.nameisnotNoneelse'y'self.fit_input_type_="single_series"y_true={name:y_true}if"level"notiny_pred_interval.columns:y_pred_interval=y_pred_interval.copy()y_pred_interval["level"]=nameelse:ify_pred_interval["level"].nunique()>1:raiseValueError("If `y_true` is a pandas Series, `y_pred_interval` must have ""only one series. Found multiple values in column 'level'.")ify_pred_interval["level"].iat[0]!=name:raiseValueError(f"Series name in `y_true`, '{name}', does not match the level "f"name in `y_pred_interval`, '{y_pred_interval['level'].iat[0]}'.")elifisinstance(y_true,pd.DataFrame):self.fit_input_type_="multiple_series"y_true=y_true.to_dict(orient='series')else:self.fit_input_type_="multiple_series"fork,viny_true.items():ifnotisinstance(v,pd.Series):raiseValueError(f"When `y_true` is a dict, all its values must be pandas "f"Series. Got {type(v)} for series '{k}'.")y_pred_interval={k:v[['lower_bound','upper_bound']]fork,viny_pred_interval.groupby('level')}ifnoty_pred_interval.keys()==y_true.keys():raiseValueError(f"Series names in `y_true` and `y_pred_interval` do not match.\n"f" `y_true` series names : {list(y_true.keys())}\n"f" `y_pred_interval` series names : {list(y_pred_interval.keys())}")forkiny_true.keys():ifnoty_true[k].index.equals(y_pred_interval[k].index):raiseIndexError(f"Index of `y_true` and `y_pred_interval` must match. Different "f"indices found for series '{k}'.")y_true_=np.asarray(y_true[k])y_pred_interval_=np.asarray(y_pred_interval[k])lower_bound=y_pred_interval_[:,0]upper_bound=y_pred_interval_[:,1]conformity_scores_lower=lower_bound-y_true_conformity_scores_upper=y_true_-upper_boundconformity_scores=np.max([conformity_scores_lower,conformity_scores_upper,],axis=0,)self.correction_factor_[k]=float(np.quantile(conformity_scores,self.nominal_coverage))self.correction_factor_lower_[k]=float(-1*np.quantile(-1*conformity_scores_lower,(1-self.nominal_coverage)/2))self.correction_factor_upper_[k]=float(np.quantile(conformity_scores_upper,1-(1-self.nominal_coverage)/2))coverage_fit_=calculate_coverage(y_true=y_true_,lower_bound=lower_bound,upper_bound=upper_bound,)self.fit_coverage_[k]=float(coverage_fit_)self.is_fitted=Trueself.fit_series_names_=list(y_true.keys())
Apply the correction factor to the prediction interval to achieve the desired
coverage.
Parameters:
Name
Type
Description
Default
y_pred_interval
pandas DataFrame
Prediction interval to be calibrated using conformal method.
If only intervals for one series are available, y_pred_interval
must have two columns, 'lower_bound' and 'upper_bound'.
If multiple series are available, y_pred_interval must be
a long-format DataFrame with three columns: 'level', 'lower_bound',
and 'upper_bound'. The 'level' column identifies the series to which
each interval belongs.
required
Returns:
Name
Type
Description
y_pred_interval_conformal
pandas DataFrame
Prediction interval with the correction factor applied.
Source code in skforecast/preprocessing/_preprocessing.py
deftransform(self,y_pred_interval:pd.DataFrame)->pd.DataFrame:""" Apply the correction factor to the prediction interval to achieve the desired coverage. Parameters ---------- y_pred_interval : pandas DataFrame Prediction interval to be calibrated using conformal method. - If only intervals for one series are available, `y_pred_interval` must have two columns, 'lower_bound' and 'upper_bound'. - If multiple series are available, `y_pred_interval` must be a long-format DataFrame with three columns: 'level', 'lower_bound', and 'upper_bound'. The 'level' column identifies the series to which each interval belongs. Returns ------- y_pred_interval_conformal : pandas DataFrame Prediction interval with the correction factor applied. """ifnotself.is_fitted:raiseNotFittedError("ConformalIntervalCalibrator not fitted yet. Call 'fit' with ""training data first.")ifnotisinstance(y_pred_interval,pd.DataFrame):raiseTypeError("`y_pred_interval` must be a pandas DataFrame.")ifnotset(["lower_bound","upper_bound"]).issubset(y_pred_interval.columns):raiseValueError("`y_pred_interval` must have columns 'lower_bound' and 'upper_bound'.")ifself.fit_input_type_=="single_series"and'level'notiny_pred_interval.columns:y_pred_interval=y_pred_interval.copy()y_pred_interval["level"]=self.fit_series_names_[0]ifself.fit_input_type_=="multiple_series"and'level'notiny_pred_interval.columns:raiseValueError("The transformer was fitted with multiple series. `y_pred_interval` ""must be a long-format DataFrame with three columns: 'level', ""'lower_bound', and 'upper_bound'. The 'level' column identifies ""the series to which each interval belongs.")conformalized_intervals=[]fork,y_pred_interval_iny_pred_interval.groupby('level')[['lower_bound','upper_bound']]:ifknotinself.fit_series_names_:raiseValueError(f"Series '{k}' was not seen during fit. Available series are: "f"{self.fit_series_names_}.")correction_factor=self.correction_factor_[k]correction_factor_lower=self.correction_factor_lower_[k]correction_factor_upper=self.correction_factor_upper_[k]index=y_pred_interval_.indexy_pred_interval_=y_pred_interval_.to_numpy()y_pred_interval_conformal=y_pred_interval_.copy()ifself.symmetric_calibration:y_pred_interval_conformal[:,0]=(y_pred_interval_conformal[:,0]-correction_factor)y_pred_interval_conformal[:,1]=(y_pred_interval_conformal[:,1]+correction_factor)else:y_pred_interval_conformal[:,0]=(y_pred_interval_conformal[:,0]-correction_factor_lower)y_pred_interval_conformal[:,1]=(y_pred_interval_conformal[:,1]+correction_factor_upper)# If upper bound is less than lower bound, swap themmask=(y_pred_interval_conformal[:,1]<y_pred_interval_conformal[:,0])(y_pred_interval_conformal[mask,0],y_pred_interval_conformal[mask,1],)=(y_pred_interval_conformal[mask,1],y_pred_interval_conformal[mask,0],)y_pred_interval_conformal=pd.DataFrame(data=y_pred_interval_conformal,columns=['lower_bound','upper_bound'],index=index)y_pred_interval_conformal.insert(0,'level',k)conformalized_intervals.append(y_pred_interval_conformal)conformalized_intervals=pd.concat(conformalized_intervals)returnconformalized_intervals
Transforms a time series into a differentiated time series of a specified order
and provides functionality to revert the differentiation.
When using a direct module Forecaster, the model in step 1 must be
used if you want to reverse the differentiation of the training time
series with the inverse_transform_training method.
Parameters:
Name
Type
Description
Default
order
int
The order of differentiation to be applied.
1
window_size
int
The window size used by the forecaster. This is required to revert the
differentiation for the target variable y or its predicted values.
List with the first value of the time series before each differentiation.
If order = 2, first value correspond with the first value of the original
time series and the second value correspond with the first value of the
differentiated time series of order 1. These values are necessary to
revert the differentiation and reconstruct the original time series.
List with the first training value of the time series before each differentiation.
For order = 1, the value correspond with the last value of the window used to
create the predictors. For order > 1, the value correspond with the first
value of the differentiated time series prior to the next differentiation.
These values are necessary to revert the differentiation and reconstruct the
training time series.
List with the last value of the time series before each differentiation,
used to revert differentiation on subsequent data windows. If order = 2,
first value correspond with the last value of the original time series
and the second value correspond with the last value of the differentiated
time series of order 1. This is essential for correctly transforming a
time series that follows immediately after the series used to fit the
transformer.
def__init__(self,order:int=1,window_size:int|None=None)->None:ifnotisinstance(order,(int,np.integer)):raiseTypeError(f"Parameter `order` must be an integer greater than 0. Found {type(order)}.")iforder<1:raiseValueError(f"Parameter `order` must be an integer greater than 0. Found {order}.")ifwindow_sizeisnotNone:ifnotisinstance(window_size,(int,np.integer)):raiseTypeError(f"Parameter `window_size` must be an integer greater than 0. "f"Found {type(window_size)}.")ifwindow_size<1:raiseValueError(f"Parameter `window_size` must be an integer greater than 0. "f"Found {window_size}.")self.order=orderself.window_size=window_sizeself.initial_values=[]self.pre_train_values=[]self.last_values=[]
Fits the transformer. Stores the values needed to revert the
differentiation of different window of the time series, original
time series, training time series, and a time series that follows
immediately after the series used to fit the transformer.
Parameters:
Name
Type
Description
Default
X
numpy ndarray
Time series to be differentiated.
required
y
Ignored
Not used, present here for API consistency by convention.
@_check_X_numpy_ndarray_1d()deffit(self,X:np.ndarray,y:Any=None)->TimeSeriesDifferentiator:""" Fits the transformer. Stores the values needed to revert the differentiation of different window of the time series, original time series, training time series, and a time series that follows immediately after the series used to fit the transformer. Parameters ---------- X : numpy ndarray Time series to be differentiated. y : Ignored Not used, present here for API consistency by convention. Returns ------- self : TimeSeriesDifferentiator """self.initial_values=[]self.pre_train_values=[]self.last_values=[]foriinrange(self.order):ifi==0:self.initial_values.append(X[0])ifself.window_sizeisnotNone:self.pre_train_values.append(X[self.window_size-self.order])self.last_values.append(X[-1])X_diff=np.diff(X,n=1)else:self.initial_values.append(X_diff[0])ifself.window_sizeisnotNone:self.pre_train_values.append(X_diff[self.window_size-self.order])self.last_values.append(X_diff[-1])X_diff=np.diff(X_diff,n=1)returnself
@_check_X_numpy_ndarray_1d()deftransform(self,X:np.ndarray,y:Any=None)->np.ndarray:""" Transforms a time series into a differentiated time series of order n. Parameters ---------- X : numpy ndarray Time series to be differentiated. y : Ignored Not used, present here for API consistency by convention. Returns ------- X_diff : numpy ndarray Differentiated time series. The length of the array is the same as the original time series but the first n `order` values are nan. """X_diff=np.diff(X,n=self.order)X_diff=np.append((np.full(shape=self.order,fill_value=np.nan)),X_diff)returnX_diff
@_check_X_numpy_ndarray_1d()definverse_transform(self,X:np.ndarray,y:Any=None)->np.ndarray:""" Reverts the differentiation. To do so, the input array is assumed to be the same time series used to fit the transformer but differentiated. Parameters ---------- X : numpy ndarray Differentiated time series. y : Ignored Not used, present here for API consistency by convention. Returns ------- X_diff : numpy ndarray Reverted differentiated time series. """# Remove initial nan values if presentX=X[np.argmax(~np.isnan(X)):]foriinrange(self.order):ifi==0:X_undiff=np.insert(X,0,self.initial_values[-1])X_undiff=np.cumsum(X_undiff,dtype=float)else:X_undiff=np.insert(X_undiff,0,self.initial_values[-(i+1)])X_undiff=np.cumsum(X_undiff,dtype=float)returnX_undiff
Reverts the differentiation. To do so, the input array is assumed to be
the differentiated training time series generated with the original
time series used to fit the transformer.
When using a direct module Forecaster, the model in step 1 must be
used if you want to reverse the differentiation of the training time
series with the inverse_transform_training method.
Parameters:
Name
Type
Description
Default
X
numpy ndarray
Differentiated time series.
required
y
Ignored
Not used, present here for API consistency by convention.
None
Returns:
Name
Type
Description
X_diff
numpy ndarray
Reverted differentiated time series.
Source code in skforecast/preprocessing/_preprocessing.py
@_check_X_numpy_ndarray_1d()definverse_transform_training(self,X:np.ndarray,y:Any=None)->np.ndarray:""" Reverts the differentiation. To do so, the input array is assumed to be the differentiated training time series generated with the original time series used to fit the transformer. When using a `direct` module Forecaster, the model in step 1 must be used if you want to reverse the differentiation of the training time series with the `inverse_transform_training` method. Parameters ---------- X : numpy ndarray Differentiated time series. y : Ignored Not used, present here for API consistency by convention. Returns ------- X_diff : numpy ndarray Reverted differentiated time series. """ifnotself.pre_train_values:raiseValueError("The `window_size` parameter must be set before fitting the ""transformer to revert the differentiation of the training ""time series.")# Remove initial nan values if presentX=X[np.argmax(~np.isnan(X)):]foriinrange(self.order):ifi==0:X_undiff=np.insert(X,0,self.pre_train_values[-1])X_undiff=np.cumsum(X_undiff,dtype=float)else:X_undiff=np.insert(X_undiff,0,self.pre_train_values[-(i+1)])X_undiff=np.cumsum(X_undiff,dtype=float)# Remove initial values as they are not part of the training time seriesX_undiff=X_undiff[self.order:]returnX_undiff
Reverts the differentiation. The input array X is assumed to be a
differentiated time series of order n that starts right after the
the time series used to fit the transformer.
Parameters:
Name
Type
Description
Default
X
numpy ndarray
Differentiated time series. It is assumed o start right after
the time series used to fit the transformer.
required
y
Ignored
Not used, present here for API consistency by convention.
None
Returns:
Name
Type
Description
X_undiff
numpy ndarray
Reverted differentiated time series.
Source code in skforecast/preprocessing/_preprocessing.py
@_check_X_numpy_ndarray_1d(ensure_1d=False)definverse_transform_next_window(self,X:np.ndarray,y:Any=None)->np.ndarray:""" Reverts the differentiation. The input array `X` is assumed to be a differentiated time series of order n that starts right after the the time series used to fit the transformer. Parameters ---------- X : numpy ndarray Differentiated time series. It is assumed o start right after the time series used to fit the transformer. y : Ignored Not used, present here for API consistency by convention. Returns ------- X_undiff : numpy ndarray Reverted differentiated time series. """array_ndim=X.ndimifarray_ndim==1:X=X[:,np.newaxis]# Remove initial rows with nan values if presentX=X[~np.isnan(X).any(axis=1)]foriinrange(self.order):ifi==0:X_undiff=np.cumsum(X,axis=0,dtype=float)+self.last_values[-1]else:X_undiff=np.cumsum(X_undiff,axis=0,dtype=float)+self.last_values[-(i+1)]ifarray_ndim==1:X_undiff=X_undiff.ravel()returnX_undiff
defset_params(self,**params):""" Set the parameters of the TimeSeriesDifferentiator. Parameters ---------- params : dict A dictionary of the parameters to set. Returns ------- None """forparam,valueinparams.items():setattr(self,param,value)
QuantileBinner class to bin data into quantile-based bins using numpy.percentile.
This class is similar to KBinsDiscretizer but optimized for performance using
numpy.searchsorted for fast bin assignment. Bin intervals are defined following
the convention: bins[i-1] <= x < bins[i]. Values outside the range are clipped
to the first or last bin. See more information in numpy.percentile and
numpy.searchsorted.
Parameters:
Name
Type
Description
Default
n_bins
int
The number of quantile-based bins to create.
required
method
str
The method used to compute the quantiles. This parameter is passed to
numpy.percentile. Default is 'linear'. Valid values are "inverse_cdf",
"averaged_inverse_cdf", "closest_observation", "interpolated_inverse_cdf",
"hazen", "weibull", "linear", "median_unbiased", "normal_unbiased".
'linear'
subsample
int
The number of samples to use for computing quantiles. If the dataset
has more samples than subsample, a random subset will be used.
200000
dtype
data type
The data type to use for the bin indices. Default is numpy.float64.
numpy.float64
random_state
int
The random seed to use for generating a random subset of the data.
The method used to compute the quantiles. This parameter is passed to
numpy.percentile. Default is 'linear'. Valid values are 'linear',
'lower', 'higher', 'midpoint', 'nearest'.
def_validate_params(self,n_bins:int,method:str,subsample:int,dtype:type,random_state:int):""" Validate the parameters passed to the class initializer. """ifnotisinstance(n_bins,int)orn_bins<2:raiseValueError(f"`n_bins` must be an int greater than 1. Got {n_bins}.")valid_methods=["inverse_cdf","averaged_inverse_cdf","closest_observation","interpolated_inverse_cdf","hazen","weibull","linear","median_unbiased","normal_unbiased",]ifmethodnotinvalid_methods:raiseValueError(f"`method` must be one of {valid_methods}. Got {method}.")ifnotisinstance(subsample,int)orsubsample<1:raiseValueError(f"`subsample` must be an integer greater than or equal to 1. "f"Got {subsample}.")ifnotisinstance(random_state,int)orrandom_state<0:raiseValueError(f"`random_state` must be an integer greater than or equal to 0. "f"Got {random_state}.")ifnotisinstance(dtype,type):raiseValueError(f"`dtype` must be a valid numpy dtype. Got {dtype}.")
deffit(self,X:np.ndarray):""" Learn the bin edges based on quantiles from the training data. Parameters ---------- X : numpy ndarray The training data used to compute the quantiles. Returns ------- None """ifX.size==0:raiseValueError("Input data `X` cannot be empty.")iflen(X)>self.subsample:rng=np.random.default_rng(self.random_state)X=X[rng.integers(0,len(X),self.subsample)]bin_edges=np.percentile(a=X,q=np.linspace(0,100,self.n_bins+1),method=self.method)# Remove duplicate edges (can happen when data has many repeated values)# to ensure bins are always numbered 0 to n_bins_-1self.bin_edges_=np.unique(bin_edges)# Ensure at least 1 bin when all values are identicaliflen(self.bin_edges_)==1:# Create artificial edges around the single valueself.bin_edges_=np.array([self.bin_edges_.item(),self.bin_edges_.item()])self.n_bins_=len(self.bin_edges_)-1ifself.n_bins_!=self.n_bins:warnings.warn(f"The number of bins has been reduced from {self.n_bins} to "f"{self.n_bins_} due to duplicated edges caused by repeated predicted "f"values.",IgnoredArgumentWarning)# Internal edges for optimized transform with searchsortedself.internal_edges_=self.bin_edges_[1:-1]self.intervals_={int(i):(float(self.bin_edges_[i]),float(self.bin_edges_[i+1]))foriinrange(self.n_bins_)}
The indices of the bins each value belongs to.
Values less than the smallest bin edge are assigned to the first bin,
and values greater than the largest bin edge are assigned to the last bin.
Source code in skforecast/preprocessing/_preprocessing.py
deftransform(self,X:np.ndarray):""" Assign new data to the learned bins. Parameters ---------- X : numpy ndarray The data to assign to the bins. Returns ------- bin_indices : numpy ndarray The indices of the bins each value belongs to. Values less than the smallest bin edge are assigned to the first bin, and values greater than the largest bin edge are assigned to the last bin. """ifself.bin_edges_isNone:raiseNotFittedError("The model has not been fitted yet. Call 'fit' with training data first.")bin_indices=np.searchsorted(self.internal_edges_,X,side='right').astype(self.dtype,copy=False)returnbin_indices
Fit the model to the data and return the bin indices for the same data.
Parameters:
Name
Type
Description
Default
X
ndarray
The data to fit and transform.
required
Returns:
Name
Type
Description
bin_indices
ndarray
The indices of the bins each value belongs to.
Values less than the smallest bin edge are assigned to the first bin,
and values greater than the largest bin edge are assigned to the last bin.
Source code in skforecast/preprocessing/_preprocessing.py
deffit_transform(self,X):""" Fit the model to the data and return the bin indices for the same data. Parameters ---------- X : numpy.ndarray The data to fit and transform. Returns ------- bin_indices : numpy.ndarray The indices of the bins each value belongs to. Values less than the smallest bin edge are assigned to the first bin, and values greater than the largest bin edge are assigned to the last bin. """self.fit(X)returnself.transform(X)
defget_params(self):""" Get the parameters of the quantile binner. Parameters ---------- self Returns ------- params : dict A dictionary of the parameters of the quantile binner. """return{"n_bins":self.n_bins,"method":self.method,"subsample":self.subsample,"dtype":self.dtype,"random_state":self.random_state,}
defset_params(self,**params):""" Set the parameters of the QuantileBinner. Parameters ---------- params : dict A dictionary of the parameters to set. Returns ------- None """forparam,valueinparams.items():setattr(self,param,value)
Input DataFrame or Series with a datetime index, or a pandas
DatetimeIndex directly. When a DatetimeIndex is passed, it is used as
the datetime source and there are no original columns to keep, so
keep_original_columns has no effect.
required
features
list
List of calendar features (strings) to extract from the index. When
None, the following features are extracted: 'year', 'month',
'week', 'day_of_week', 'day_of_month', 'day_of_year',
'weekend', 'hour', 'minute', 'second', 'quarter'.
None
features_to_encode
list
List of calendar features (strings) to encode. When None, all
extracted features are encoded. If a feature is not in features, a
ValueError is raised. If the explicit list contains features that
cannot be encoded with the chosen encoding (e.g. 'year' or
'weekend', which are never encodable), an IgnoredArgumentWarning
is issued and those features are kept as raw integers.
None
encoding
str
Encoding method for the extracted features. Options are None,
'cyclical', 'onehot' or 'spline'. Features that cannot be
encoded under the chosen mode are kept as raw integers. By default,
'year' and 'weekend' are never encoded. 'onehot' excludes them
via the known-category set, while 'cyclical' and 'spline' exclude
them via max_values.
'cyclical'
max_values
dict
Dictionary of maximum values for the cyclical and spline encoding.
User-provided values are merged with the defaults: keys passed by
the user override the corresponding default, and missing keys fall
back to the defaults {'month': 12, 'week': 53, 'day_of_week': 7,
'day_of_month': 31, 'day_of_year': 366, 'hour': 24, 'minute': 60,
'second': 60, 'quarter': 4}. For example, passing
max_values={'month': 6} overrides only month; the other features
keep their defaults. Features that are not in the defaults (e.g.
'year', 'weekend') are left as raw integers.
None
spline_kwargs
dict
Additional keyword arguments for the spline encoding. Only used when
encoding='spline'. When None, defaults to {'degree': 3,
'include_bias': True, 'extrapolation': 'periodic'}; n_knots
defaults to max_values[feature] + 1 per feature, which produces one
spline column per distinct period value (analogous to a smooth
one-hot encoding). Knots are placed uniformly over [min_val,
min_val + max_val] (e.g. 1-13 for month, 0-24 for hour), giving a
periodic period of exactly max_val for both 0-indexed and
1-indexed features and keeping the encoding stateless and consistent
between training and prediction. Any keyword argument accepted by
sklearn.preprocessing.SplineTransformer is allowed (e.g. n_knots,
degree, include_bias, extrapolation, order) except
knots (computed internally from max_values) and sparse_output
(incompatible with the DataFrame output). Passing either of these or
an unknown key raises ValueError.
None
keep_original_columns
bool
If True, the original columns of X are kept in the output
DataFrame. If False, only the extracted datetime features are
returned. When True and X is an unnamed pandas Series
(X.name is None), a ValueError is raised; either set X.name to
a string, or pass keep_original_columns=False. When X is a pandas
DatetimeIndex this argument has no effect, as there are no original
columns to keep.
True
tol
(float, None)
Absolute tolerance for clamping near-zero values produced by cyclical
encoding to exactly 0. Floating point arithmetic can produce values
such as -2.4e-16 instead of 0 (e.g. sin(2*pi*12/12)). When not None,
any sin or cos value whose absolute value is smaller than tol is
replaced with 0.0. Set to None to disable clamping. Only used when
encoding='cyclical'.
1e-12
Returns:
Name
Type
Description
X_new
pandas DataFrame
DataFrame with the extracted (and optionally encoded) datetime features.
Notes
The default max_values use 53 for 'week' and 366 for 'day_of_year'
to accommodate the maximum possible values across all calendar years:
ISO week 53 occurs in some years (e.g. 2015, 2020, 2026) and
day-of-year 366 occurs in leap years. Because the encoding must be
stateless (the same for any year, without prior knowledge of whether it
is a leap year or contains ISO week 53), the period is fixed at the
maximum-possible value. This implies:
Onehot: the week_53 and day_of_year_366 columns are always
present in the output and equal 0 for rows whose year never reaches
those values. This guarantees a consistent column schema across
training and prediction.
Cyclical / spline: in years where the maximum value is reached,
the cyclical wrap-around is exact (e.g. sin(2π·366/366) = 0 matches
sin(2π·0/366) = 0). In years where it is not, there is a one-step
"phantom gap" between the highest observed value and 1. The
cyclical distance is two steps instead of one. This residual
asymmetry is numerically small (≈ 1.7% for day_of_year, ≈ 12% for
week) and is strictly preferable to the alternative (period
52 / 365), which would silently collapse week 53 onto week 1 and day
366 onto day 1 in years where those values occur.
For high-cardinality features such as day_of_year (366 columns) or
day_of_month (31 columns), encoding='cyclical' (2 columns per
feature) or 'spline' (≈ max_val columns per feature, but dense
rather than sparse) are typically more memory-efficient than
'onehot', especially on multi-year hourly or sub-daily data.
Output column order: encoded features appear in the order given by
features. Non-encoded extracted features (e.g. 'year' or
'weekend', which are never encoded) appear first in the output, in
features-list order; encoded features follow, also in
features-list order. For example, with
features=['year', 'month', 'weekend', 'hour'] and
encoding='cyclical' the output columns are
[year, weekend, month_sin, month_cos, hour_sin, hour_cos].
Source code in skforecast/preprocessing/_calendar.py
defcreate_calendar_features(X:pd.Series|pd.DataFrame|pd.DatetimeIndex,features:list[str]|None=None,features_to_encode:list[str]|None=None,encoding:str="cyclical",max_values:dict[str,int]|None=None,spline_kwargs:dict|None=None,keep_original_columns:bool=True,tol:float|None=1e-12,)->pd.DataFrame:""" Extract datetime features from the DateTime index of a pandas DataFrame or Series. Parameters ---------- X : pandas Series, pandas DataFrame, pandas DatetimeIndex Input DataFrame or Series with a datetime index, or a pandas DatetimeIndex directly. When a DatetimeIndex is passed, it is used as the datetime source and there are no original columns to keep, so `keep_original_columns` has no effect. features : list, default None List of calendar features (strings) to extract from the index. When `None`, the following features are extracted: `'year'`, `'month'`, `'week'`, `'day_of_week'`, `'day_of_month'`, `'day_of_year'`, `'weekend'`, `'hour'`, `'minute'`, `'second'`, `'quarter'`. features_to_encode : list, default None List of calendar features (strings) to encode. When `None`, all extracted features are encoded. If a feature is not in `features`, a `ValueError` is raised. If the explicit list contains features that cannot be encoded with the chosen `encoding` (e.g. `'year'` or `'weekend'`, which are never encodable), an `IgnoredArgumentWarning` is issued and those features are kept as raw integers. encoding : str, default 'cyclical' Encoding method for the extracted features. Options are `None`, `'cyclical'`, `'onehot'` or `'spline'`. Features that cannot be encoded under the chosen mode are kept as raw integers. By default, `'year'` and `'weekend'` are never encoded. `'onehot'` excludes them via the known-category set, while `'cyclical'` and `'spline'` exclude them via `max_values`. max_values : dict, default None Dictionary of maximum values for the cyclical and spline encoding. User-provided values are merged with the defaults: keys passed by the user override the corresponding default, and missing keys fall back to the defaults `{'month': 12, 'week': 53, 'day_of_week': 7, 'day_of_month': 31, 'day_of_year': 366, 'hour': 24, 'minute': 60, 'second': 60, 'quarter': 4}`. For example, passing `max_values={'month': 6}` overrides only `month`; the other features keep their defaults. Features that are not in the defaults (e.g. `'year'`, `'weekend'`) are left as raw integers. spline_kwargs : dict, default None Additional keyword arguments for the spline encoding. Only used when `encoding='spline'`. When `None`, defaults to `{'degree': 3, 'include_bias': True, 'extrapolation': 'periodic'}`; `n_knots` defaults to `max_values[feature] + 1` per feature, which produces one spline column per distinct period value (analogous to a smooth one-hot encoding). Knots are placed uniformly over `[min_val, min_val + max_val]` (e.g. 1-13 for month, 0-24 for hour), giving a periodic period of exactly `max_val` for both 0-indexed and 1-indexed features and keeping the encoding stateless and consistent between training and prediction. Any keyword argument accepted by `sklearn.preprocessing.SplineTransformer` is allowed (e.g. `n_knots`, `degree`, `include_bias`, `extrapolation`, `order`) except `knots` (computed internally from `max_values`) and `sparse_output` (incompatible with the DataFrame output). Passing either of these or an unknown key raises `ValueError`. keep_original_columns : bool, default True If True, the original columns of `X` are kept in the output DataFrame. If False, only the extracted datetime features are returned. When `True` and `X` is an unnamed pandas Series (`X.name is None`), a `ValueError` is raised; either set `X.name` to a string, or pass `keep_original_columns=False`. When `X` is a pandas DatetimeIndex this argument has no effect, as there are no original columns to keep. tol : float, None, default 1e-12 Absolute tolerance for clamping near-zero values produced by cyclical encoding to exactly 0. Floating point arithmetic can produce values such as -2.4e-16 instead of 0 (e.g. sin(2*pi*12/12)). When not None, any sin or cos value whose absolute value is smaller than `tol` is replaced with 0.0. Set to None to disable clamping. Only used when `encoding='cyclical'`. Returns ------- X_new : pandas DataFrame DataFrame with the extracted (and optionally encoded) datetime features. Notes ----- The default `max_values` use 53 for `'week'` and 366 for `'day_of_year'` to accommodate the maximum possible values across all calendar years: ISO week 53 occurs in some years (e.g. 2015, 2020, 2026) and day-of-year 366 occurs in leap years. Because the encoding must be stateless (the same for any year, without prior knowledge of whether it is a leap year or contains ISO week 53), the period is fixed at the maximum-possible value. This implies: - Onehot: the `week_53` and `day_of_year_366` columns are always present in the output and equal 0 for rows whose year never reaches those values. This guarantees a consistent column schema across training and prediction. - Cyclical / spline: in years where the maximum value is reached, the cyclical wrap-around is exact (e.g. `sin(2π·366/366) = 0` matches `sin(2π·0/366) = 0`). In years where it is not, there is a one-step "phantom gap" between the highest observed value and 1. The cyclical distance is two steps instead of one. This residual asymmetry is numerically small (≈ 1.7% for `day_of_year`, ≈ 12% for `week`) and is strictly preferable to the alternative (period 52 / 365), which would silently collapse week 53 onto week 1 and day 366 onto day 1 in years where those values occur. For high-cardinality features such as `day_of_year` (366 columns) or `day_of_month` (31 columns), `encoding='cyclical'` (2 columns per feature) or `'spline'` (≈ `max_val` columns per feature, but dense rather than sparse) are typically more memory-efficient than `'onehot'`, especially on multi-year hourly or sub-daily data. Output column order: encoded features appear in the order given by `features`. Non-encoded extracted features (e.g. `'year'` or `'weekend'`, which are never encoded) appear first in the output, in `features`-list order; encoded features follow, also in `features`-list order. For example, with `features=['year', 'month', 'weekend', 'hour']` and `encoding='cyclical'` the output columns are `[year, weekend, month_sin, month_cos, hour_sin, hour_cos]`. """ifnotisinstance(X,(pd.DataFrame,pd.Series,pd.DatetimeIndex)):raiseTypeError("Input `X` must be a pandas Series, DataFrame or DatetimeIndex")ifisinstance(X,pd.DatetimeIndex):datetime_index=Xelse:ifnotisinstance(X.index,pd.DatetimeIndex):raiseTypeError("Input `X` must have a pandas DatetimeIndex")datetime_index=X.indexiflen(X)==0:raiseValueError("Cannot fit on empty input.")ifencodingnotin["cyclical","onehot","spline",None]:raiseValueError("Encoding must be one of 'cyclical', 'onehot', 'spline' or None")ifisinstance(X,pd.Series)andX.nameisNoneandkeep_original_columns:raiseValueError("When `keep_original_columns=True`, the input Series must have a ""name (`X.name`). Either set `X.name` to a string, or pass ""`keep_original_columns=False`.")datetime_attrs={"year":"year","month":"month","week":lambdaidx:idx.isocalendar().week.astype(int),"day_of_week":"dayofweek","day_of_month":"day","day_of_year":"dayofyear","weekend":lambdaidx:(idx.dayofweek>=5).astype(int),"hour":"hour","minute":"minute","second":"second","quarter":"quarter",}iffeaturesisNone:features=list(datetime_attrs.keys())resolved_max_values=_DEFAULT_MAX_VALUES.copy()ifmax_valuesisnotNone:unknown=set(max_values)-set(_DEFAULT_MAX_VALUES)ifunknown:warnings.warn(f"Unknown keys in `max_values`: {sorted(unknown)}. "f"Valid keys: {sorted(_DEFAULT_MAX_VALUES)}. Unknown keys "f"are ignored.",IgnoredArgumentWarning,)max_values={k:vfork,vinmax_values.items()ifknotinunknown}ifmax_valuesandencoding=="onehot":warnings.warn("`max_values` is ignored when `encoding='onehot'`; onehot ""uses the fixed known-category set. Pass ""`encoding='cyclical'` or `encoding='spline'` for ""`max_values` to take effect.",IgnoredArgumentWarning,)resolved_max_values.update(max_values)max_values=resolved_max_valuesX_new=pd.DataFrame(index=datetime_index)not_supported_features=set(features)-set(datetime_attrs.keys())ifnot_supported_features:raiseValueError(f"Features {not_supported_features} are not supported. "f"Supported features are {list(datetime_attrs.keys())}.")forfeatureinfeatures:attr=datetime_attrs[feature]X_new[feature]=(attr(datetime_index)ifcallable(attr)elsegetattr(datetime_index,attr).astype(int))iffeatures_to_encodeisnotNone:not_supported_features_to_encode=set(features_to_encode)-set(features)ifnot_supported_features_to_encode:raiseValueError(f"Features {not_supported_features_to_encode} are not present in `features`.")ifencodingisnotNone:ifencoding=="onehot":encodable=set(_FEATURE_KNOWN_CATEGORIES.keys())else:# encoding in ("cyclical", "spline")encodable=set(max_values.keys())not_encodable=[fforfinfeatures_to_encodeiffnotinencodable]ifnot_encodable:warnings.warn(f"Features {not_encodable} cannot be encoded with "f"encoding={encoding!r}. Encodable features for this encoding "f"are: {sorted(encodable)}. These features will be kept as "f"raw integers.",IgnoredArgumentWarning,)else:features_to_encode=featuresifencoding=="cyclical":cols_to_drop=[]forfeatureinfeatures:iffeatureinfeatures_to_encodeandfeatureinmax_values:max_val=max_values[feature]sin_vals=np.sin(X_new[feature]*(2.0*np.pi/max_val))cos_vals=np.cos(X_new[feature]*(2.0*np.pi/max_val))iftolisnotNone:sin_vals=np.where(np.abs(sin_vals)<tol,0.0,sin_vals)cos_vals=np.where(np.abs(cos_vals)<tol,0.0,cos_vals)X_new[f"{feature}_sin"]=sin_valsX_new[f"{feature}_cos"]=cos_valscols_to_drop.append(feature)X_new=X_new.drop(columns=cols_to_drop)elifencoding=="onehot":effective_encode=[fforfinfeaturesiffinfeatures_to_encodeandfin_FEATURE_KNOWN_CATEGORIES]forfeatureineffective_encode:X_new[feature]=pd.Categorical(X_new[feature],categories=_FEATURE_KNOWN_CATEGORIES[feature],)ifeffective_encode:X_new=pd.get_dummies(X_new,columns=effective_encode,drop_first=False,sparse=False,dtype=int)# Match the cyclical / spline ordering: non-encoded features# first (in `features` order), then encoded dummies grouped per# feature (also in `features` order). `pd.get_dummies` keeps# non-encoded columns at their original position, so the dummies# are otherwise interleaved.non_encoded=[fforfinfeaturesiffnotineffective_encode]encoded_cols=[f"{feature}_{cat}"forfeatureineffective_encodeforcatin_FEATURE_KNOWN_CATEGORIES[feature]]X_new=X_new[non_encoded+encoded_cols]elifencoding=="spline":ifspline_kwargsisnotNone:invalid=set(spline_kwargs)-_SPLINE_ALLOWED_KWARGSifinvalid:blocked_passed=invalid&_SPLINE_BLOCKED_KWARGSunknown=invalid-_SPLINE_BLOCKED_KWARGSmsgs=[]ifblocked_passed:msgs.append(f"Keys {sorted(blocked_passed)} are not allowed in "f"`spline_kwargs`: `knots` is computed internally from "f"`max_values`, and `sparse_output` is incompatible "f"with the DataFrame output.")ifunknown:msgs.append(f"Unknown keys in `spline_kwargs`: {sorted(unknown)}. "f"Allowed keys: {sorted(_SPLINE_ALLOWED_KWARGS)}.")raiseValueError(" ".join(msgs))resolved_spline_kwargs={"degree":3,"include_bias":True,"extrapolation":"periodic",}ifspline_kwargsisnotNone:resolved_spline_kwargs.update(spline_kwargs)n_knots_global=resolved_spline_kwargs.pop("n_knots",None)cols_to_drop=[]spline_cols={}forfeatureinfeatures:iffeatureinfeatures_to_encodeandfeatureinmax_values:max_val=max_values[feature]n_knots=n_knots_globalifn_knots_globalisnotNoneelsemax_val+1min_val=_DEFAULT_MIN_VALUES.get(feature,0)# Knots span [min_val, min_val + max_val] so that the periodic# period is exactly `max_val` for both 0-indexed (e.g. hour:# [0..24]) and 1-indexed features (e.g. month: [1..13]). With# the alternative `linspace(min_val, max_val, ...)`, 1-indexed# features would have period `max_val - min_val`, collapsing# the last value onto the first (e.g. month 12 = month 1).knots=np.linspace(min_val,min_val+max_val,n_knots).reshape(-1,1)spt=SplineTransformer(knots=knots,**resolved_spline_kwargs,)values=X_new[feature].to_numpy().reshape(-1,1)spline_out=spt.fit_transform(values)col_names=spt.get_feature_names_out([feature])forcol_name,col_valuesinzip(col_names,spline_out.T):spline_cols[col_name]=col_valuescols_to_drop.append(feature)X_new=X_new.drop(columns=cols_to_drop)X_new=pd.concat([X_new,pd.DataFrame(spline_cols,index=X_new.index)],axis=1)ifkeep_original_columnsandnotisinstance(X,pd.DatetimeIndex):X_df=X.to_frame()ifisinstance(X,pd.Series)elseXoverlapping_cols=set(X_df.columns).intersection(set(X_new.columns))ifoverlapping_cols:container="Series"ifisinstance(X,pd.Series)else"DataFrame"rename_target="Series"ifisinstance(X,pd.Series)else"columns"raiseValueError(f"The following extracted feature names already exist in the input "f"{container}: {list(overlapping_cols)}. To avoid duplicate columns, "f"rename the original {rename_target} or avoid extracting these features.")X_new=pd.concat([X_df,X_new],axis=1)returnX_new
Calculate the number of periods to the next and since the last holiday.
The time unit used for the calculation (days, hours, minutes, …) is inferred
from the frequency of the index when date_column=None, and is always days
when a date column is used instead. Output columns are always named
time_to_holiday and time_since_holiday regardless of the unit.
Parameters:
Name
Type
Description
Default
X
pandas Series, pandas DataFrame
Input data containing the holiday indicator. When a Series is passed,
its values are used directly as the holiday indicator (boolean or 0/1)
and holiday_column is ignored. When a DataFrame is passed,
holiday_column must be specified.
required
holiday_column
(str, None)
Name of the boolean column indicating holidays (True or 1 on holiday
dates, False or 0 otherwise). Required when X is a pandas
DataFrame. Ignored when X is a pandas Series.
None
date_column
(str, None)
Name of the column containing dates to use as reference. When None,
the index is used and must be a pandas DatetimeIndex.
None
fill_na
(int, float)
Value used to fill rows where no previous or next holiday exists (i.e.
before the first holiday or after the last). Must be an int, a
numpy.integer, or numpy.nan. Booleans and other floats are
rejected because the output columns have Int64 dtype. Pass
numpy.nan to keep those entries as pd.NA.
0
Returns:
Name
Type
Description
result
pandas DataFrame
DataFrame with two new columns and the same index as X:
time_to_holiday: periods until the next holiday.
time_since_holiday: periods since the last holiday.
Notes
When date_column is specified, the unit is always days regardless of the
data frequency, because no index frequency information is available.
When date_column=None, the time unit is inferred from the index frequency:
Daily or coarser (weekly, monthly, …): days
Hourly: hours
Minute: minutes
Second: seconds
Millisecond: milliseconds
Microsecond: microseconds
Nanosecond: nanoseconds
When the index has no frequency set, pd.infer_freq is attempted. If the
frequency still cannot be determined (e.g. irregular spacing or fewer than
three observations), the unit defaults to hours and a UserWarning is
issued.
In date_column mode, fractional days (from arbitrary intra-day
timestamps) are truncated, not rounded e.g. a distance of 0.99
days is reported as 0. To preserve sub-day precision, use the
index-based mode with an appropriate frequency ('h', 'min', etc.).
When a row corresponds to a holiday, both time_to_holiday and
time_since_holiday are 0, the date is at distance 0 from itself.
Source code in skforecast/preprocessing/_calendar.py
defcalculate_distance_from_holiday(X:pd.DataFrame|pd.Series,holiday_column:str|None=None,date_column:str|None=None,fill_na:int|float=0,)->pd.DataFrame:""" Calculate the number of periods to the next and since the last holiday. The time unit used for the calculation (days, hours, minutes, …) is inferred from the frequency of the index when `date_column=None`, and is always days when a date column is used instead. Output columns are always named `time_to_holiday` and `time_since_holiday` regardless of the unit. Parameters ---------- X : pandas Series, pandas DataFrame Input data containing the holiday indicator. When a Series is passed, its values are used directly as the holiday indicator (boolean or 0/1) and `holiday_column` is ignored. When a DataFrame is passed, `holiday_column` must be specified. holiday_column : str, None, default None Name of the boolean column indicating holidays (`True` or `1` on holiday dates, `False` or `0` otherwise). Required when `X` is a pandas DataFrame. Ignored when `X` is a pandas Series. date_column : str, None, default None Name of the column containing dates to use as reference. When `None`, the index is used and must be a pandas DatetimeIndex. fill_na : int, float, default 0 Value used to fill rows where no previous or next holiday exists (i.e. before the first holiday or after the last). Must be an `int`, a `numpy.integer`, or `numpy.nan`. Booleans and other floats are rejected because the output columns have `Int64` dtype. Pass `numpy.nan` to keep those entries as `pd.NA`. Returns ------- result : pandas DataFrame DataFrame with two new columns and the same index as `X`: - `time_to_holiday`: periods until the next holiday. - `time_since_holiday`: periods since the last holiday. Notes ----- When `date_column` is specified, the unit is always days regardless of the data frequency, because no index frequency information is available. When `date_column=None`, the time unit is inferred from the index frequency: - Daily or coarser (weekly, monthly, …): days - Hourly: hours - Minute: minutes - Second: seconds - Millisecond: milliseconds - Microsecond: microseconds - Nanosecond: nanoseconds When the index has no frequency set, `pd.infer_freq` is attempted. If the frequency still cannot be determined (e.g. irregular spacing or fewer than three observations), the unit defaults to hours and a `UserWarning` is issued. In `date_column` mode, fractional days (from arbitrary intra-day timestamps) are truncated, not rounded e.g. a distance of 0.99 days is reported as 0. To preserve sub-day precision, use the index-based mode with an appropriate frequency (`'h'`, `'min'`, etc.). When a row corresponds to a holiday, both `time_to_holiday` and `time_since_holiday` are 0, the date is at distance 0 from itself. """ifnotisinstance(X,(pd.DataFrame,pd.Series)):raiseTypeError("Input `X` must be a pandas Series or pandas DataFrame.")ifisinstance(fill_na,bool)ornot(isinstance(fill_na,(int,np.integer))or(isinstance(fill_na,(float,np.floating))andnp.isnan(fill_na))):raiseTypeError("`fill_na` must be an int, np.integer, or numpy.nan, "f"got {type(fill_na).__name__}={fill_na!r}. The output columns ""have `Int64` dtype, so floats other than NaN cannot be used.")ifisinstance(X,pd.Series):ifholiday_columnisnotNone:warnings.warn("`holiday_column` is ignored when `X` is a pandas Series. ""The Series values are used directly as the holiday indicator.",IgnoredArgumentWarning,stacklevel=2,)col_name=X.nameifX.nameisnotNoneelse"is_holiday"X=X.rename(col_name).to_frame()holiday_column=col_nameelse:ifholiday_columnisNone:raiseValueError("`holiday_column` must be specified when `X` is a pandas DataFrame.")ifholiday_columnnotinX.columns:raiseValueError(f"`holiday_column='{holiday_column}'` is not a column of `X`. "f"Available columns: {list(X.columns)}.")ifdate_columnisnotNone:ifdate_columnnotinX.columns:raiseValueError(f"`date_column='{date_column}'` is not a column of `X`. "f"Available columns: {list(X.columns)}.")ifpd.to_datetime(X[date_column],errors="coerce").isna().any():raiseValueError(f"`date_column='{date_column}'` contains NaN or unparseable "f"values. All entries must be valid datetimes.")ifX[holiday_column].isna().any():warnings.warn(f"`{holiday_column}` contains NaN values. "f"They are filled with `False` (treated as non-holidays) "f"before computing distances.",UserWarning,stacklevel=2,)X=X.copy()withpd.option_context("future.no_silent_downcasting",True):X[holiday_column]=X[holiday_column].fillna(False).astype(bool)ifdate_columnisNone:ifnotisinstance(X.index,pd.DatetimeIndex):raiseTypeError("When `date_column=None`, the index must be a pandas DatetimeIndex.")dates=X.index.to_numpy()holiday_dates=X.index[X[holiday_column].astype(bool)].to_numpy()freq_str=X.index.freqstrifX.index.freqisnotNoneelseNoneiffreq_strisNone:freq_str=pd.infer_freq(X.index)iffreq_strisNone:warnings.warn("Could not determine the frequency of the index. ""The output column unit defaults to 'hours'. To avoid this ""warning, either set `X.index.freq = pd.infer_freq(X.index)` ""or pass an index with a known frequency.",UserWarning,stacklevel=2,)freq_str='h'unit=_freq_to_timedelta_unit(freq_str)else:dates=pd.to_datetime(X[date_column]).to_numpy()holiday_dates=pd.to_datetime(X.loc[X[holiday_column].astype(bool),date_column]).to_numpy()unit='D'holiday_dates_sorted=np.sort(holiday_dates)# Periods until the next holidaynext_idx=np.searchsorted(holiday_dates_sorted,dates,side='left')has_next=next_idx<len(holiday_dates_sorted)to_holiday=np.full(len(dates),np.nan)to_holiday[has_next]=(holiday_dates_sorted[next_idx[has_next]]-dates[has_next]).astype(f'timedelta64[{unit}]').astype(int)# Periods since the last holidayprev_idx=np.searchsorted(holiday_dates_sorted,dates,side='right')-1has_prev=prev_idx>=0since_holiday=np.full(len(dates),np.nan)since_holiday[has_prev]=(dates[has_prev]-holiday_dates_sorted[prev_idx[has_prev]]).astype(f'timedelta64[{unit}]').astype(int)to_col=pd.Series(to_holiday,index=X.index,dtype="Int64").fillna(fill_na)since_col=pd.Series(since_holiday,index=X.index,dtype="Int64").fillna(fill_na)returnpd.DataFrame({"time_to_holiday":to_col,"time_since_holiday":since_col})