A utility class for splitting time series data into training, validation,
and testing sets for machine learning algorithms.
This class provides flexible splitting strategies supporting multiple input
formats (wide DataFrame, long DataFrame with MultiIndex, or dictionary of Series),
both DatetimeIndex and RangeIndex, and flexible output formats.
New in this version: Support for multiple series arguments with independent
splitting behavior. Each series can have different lengths and date ranges.
Parameters:
Name
Type
Description
Default
*series
DataFrame | dict[str, Series | DataFrame]
One or more time series data to split. Each can be:
- Wide format pandas DataFrame with DatetimeIndex or RangeIndex
- Long format pandas DataFrame with MultiIndex (series_id, datetime)
- Dictionary of pandas Series or DataFrames with identical indexes
When multiple series are provided, they are treated independently and
splits are returned as a list of tuples (one tuple per series group).
def__init__(self,*series:pd.DataFrame|dict[str,pd.Series|pd.DataFrame])->None:""" Initialize TimeSeriesSplitter with one or more series. Parameters ---------- *series : pd.DataFrame | dict[str, pd.Series | pd.DataFrame] One or more time series data in supported formats. Raises ------ ValueError If no series provided or series have invalid format. TypeError If series are not in a supported format. """iflen(series)==0:raiseValueError('At least one series must be provided.')# -- Process each series argument independentlyself.series_groups_=[]self.series_indexes_=[]self.index_types_=[]self.index_freqs_=[]self._min_indexes_=[]self._max_indexes_=[]fori,series_inputinenumerate(series):# Use inner check_preprocess_series() preprocessing for each groupseries_dict,series_indexes=check_preprocess_series(series_input)# -- Store the preprocess series data dict & index dictself.series_groups_.append(series_dict)self.series_indexes_.append(series_indexes)# -- Store index type and frequency information for this groupfirst_index=next(iter(series_indexes.values()))index_type=type(first_index)self.index_types_.append(index_type)ifisinstance(first_index,pd.DatetimeIndex):self.index_freqs_.append(first_index.freq)self._min_indexes_.append(min([idx.min()foridxinseries_indexes.values()]))self._max_indexes_.append(max([idx.max()foridxinseries_indexes.values()]))ifisinstance(first_index,pd.RangeIndex):self.index_freqs_.append(first_index.step)self._min_indexes_.append(0)self._max_indexes_.append(len(first_index)-1)# -- Store the number groups/series inputself.n_groups_=len(series)self.n_timeseries=sum(map(len,self.series_indexes_))# -- Store version informationself.skforecast_version=__version__self.python_version=sys.version.split(' ')[0]
Creates training, validation (optional), and test sets by splitting
series at specified date boundaries. Dates are inclusive.
When multiple series groups were provided to the constructor, this method
returns a list of tuples (one per group). Each group is split independently
based on its own date range.
Parameters:
Name
Type
Description
Default
end_train
str | Timestamp
Training set end date (inclusive). Required parameter.
required
start_train
str | Timestamp | None
Training set start date (inclusive). Defaults to first date in each group.
None
end_validation
str | Timestamp | None
Validation set end date (inclusive).
Defaults to end_train if not provided (no validation set created).
None
end_test
str | Timestamp | None
Test set end date (inclusive).
Defaults to last date in each group.
None
output_format
('wide', 'long', 'long_multi_index', 'dict')
Output format for the splits.
'wide'
verbose
bool
If True, print detailed split information for each group.
False
Returns:
Type
Description
list[tuple] | tuple
If single series group: tuple of splits (train, test) or (train, val, test)
If multiple series groups: list of tuples, one per group
Raises:
Type
Description
TypeError
If series don't have DatetimeIndex.
ValueError
If dates are invalid or outside available range.
Examples:
>>> # Single group>>> splitter=TimeSeriesSplitter(df1)>>> train,test=splitter.split_by_date(end_train='2023-03-11')
defsplit_by_date(self,end_train:str|pd.Timestamp,start_train:str|pd.Timestamp|None=None,end_validation:str|pd.Timestamp|None=None,end_test:str|pd.Timestamp|None=None,output_format:Literal['wide','long','long_multi_index','dict']='wide',verbose:bool=False,)->list[tuple]|tuple:""" Split time series based on date ranges. Creates training, validation (optional), and test sets by splitting series at specified date boundaries. Dates are inclusive. When multiple series groups were provided to the constructor, this method returns a list of tuples (one per group). Each group is split independently based on its own date range. Parameters ---------- end_train : str | pd.Timestamp Training set end date (inclusive). Required parameter. start_train : str | pd.Timestamp | None, default None Training set start date (inclusive). Defaults to first date in each group. end_validation : str | pd.Timestamp | None, default None Validation set end date (inclusive). Defaults to end_train if not provided (no validation set created). end_test : str | pd.Timestamp | None, default None Test set end date (inclusive). Defaults to last date in each group. output_format : {'wide', 'long', 'long_multi_index', 'dict'}, default 'wide' Output format for the splits. verbose : bool, default False If True, print detailed split information for each group. Returns ------- list[tuple] | tuple If single series group: tuple of splits (train, test) or (train, val, test) If multiple series groups: list of tuples, one per group Raises ------ TypeError If series don't have DatetimeIndex. ValueError If dates are invalid or outside available range. Examples -------- >>> # Single group >>> splitter = TimeSeriesSplitter(df1) >>> train, test = splitter.split_by_date(end_train='2023-03-11') >>> # Multiple groups >>> splitter = TimeSeriesSplitter(df1, df2, df3) >>> splits = splitter.split_by_date(end_train='2023-03-11') >>> # splits = [(df1_train, df1_test), (df2_train, df2_test), (df3_train, df3_test)] """results=[]forgroup_idxinrange(self.n_groups_):# -- Validate and get positions for current groupstart_pos,end_train_pos,end_val_pos,end_test_pos=(self._validate_date_split_args(group_idx,start_train,end_train,end_validation,end_test))# -- Define positions split dictpositions={'train':(start_pos,end_train_pos)}ifend_validationisnotNone:positions['validation']=(end_train_pos+1,end_val_pos)positions['test']=(end_val_pos+1,end_test_pos)else:positions['test']=(end_train_pos+1,end_test_pos)# -- Perform split on current groupsplit_dicts=[{k:vfork,vinsplit_dict.items()iflen(v)>0}forsplit_dictinself._split_series_dict(self.series_groups_[group_idx],positions)]# -- Convert to required outputresult=self._convert_output(split_dicts,output_format)ifverbose:self._print_split_info(group_idx,positions,output_format)results.append(result)# -- Return single tuple if only one group, otherwise list of tuplesreturnresultsifself.n_groups_>1elseresults[0]
Split time series based on size (absolute or proportional).
Creates training, validation (optional), and test sets by splitting
series at specified size boundaries. Sizes can be absolute (int) or
proportional (float between 0 and 1).
When multiple series groups were provided to the constructor, this method
returns a list of tuples (one per group). Each group is split independently
based on its own length.
Parameters:
Name
Type
Description
Default
train_size
int | float
Training set size. If int, absolute count. If float, proportion of total.
required
validation_size
int | float | None
Validation set size. Same as train_size.
If None, no validation set is created.
None
test_size
int | float | None
Test set size. Same as train_size.
If None, remainder is used as test set.
None
output_format
('wide', 'long', 'long_multi_index', 'dict')
Output format for the splits.
'wide'
verbose
bool
If True, print detailed split information for each group.
False
Returns:
Type
Description
list[tuple] | tuple
If single series group: tuple of splits (train, test) or (train, val, test)
If multiple series groups: list of tuples, one per group
Raises:
Type
Description
ValueError
If sizes are invalid or exceed series length.
Examples:
>>> # Single group with proportions>>> splitter=TimeSeriesSplitter(df1)>>> train,test=splitter.split_by_size(train_size=0.8)
>>> # Multiple groups with absolute sizes>>> splitter=TimeSeriesSplitter(df1,df2,df3)>>> splits=splitter.split_by_size(train_size=70,test_size=30)>>> # Each group split with 70 training samples and 30 test samples
Source code in skforecast/experimental/_splitter.py
defsplit_by_size(self,train_size:int|float,validation_size:int|float|None=None,test_size:int|float|None=None,output_format:Literal['wide','long','long_multi_index','dict']='wide',verbose:bool=False,)->list[tuple]|tuple:""" Split time series based on size (absolute or proportional). Creates training, validation (optional), and test sets by splitting series at specified size boundaries. Sizes can be absolute (int) or proportional (float between 0 and 1). When multiple series groups were provided to the constructor, this method returns a list of tuples (one per group). Each group is split independently based on its own length. Parameters ---------- train_size : int | float Training set size. If int, absolute count. If float, proportion of total. validation_size : int | float | None, default None Validation set size. Same as train_size. If None, no validation set is created. test_size : int | float | None, default None Test set size. Same as train_size. If None, remainder is used as test set. output_format : {'wide', 'long', 'long_multi_index', 'dict'}, default 'wide' Output format for the splits. verbose : bool, default False If True, print detailed split information for each group. Returns ------- list[tuple] | tuple If single series group: tuple of splits (train, test) or (train, val, test) If multiple series groups: list of tuples, one per group Raises ------ ValueError If sizes are invalid or exceed series length. Examples -------- >>> # Single group with proportions >>> splitter = TimeSeriesSplitter(df1) >>> train, test = splitter.split_by_size(train_size=0.8) >>> # Multiple groups with absolute sizes >>> splitter = TimeSeriesSplitter(df1, df2, df3) >>> splits = splitter.split_by_size(train_size=70, test_size=30) >>> # Each group split with 70 training samples and 30 test samples """results=[]forgroup_idxinrange(self.n_groups_):# -- Validate and get counts for current grouptrain_count,val_count,test_count=self._validate_size_split_args(group_idx,train_size,validation_size,test_size)# -- Get total length for current groupfirst_index=next(iter(self.series_indexes_[group_idx].values()))total_len=len(first_index)# -- Compute positionstrain_end=train_count-1val_end=train_end+(val_countifval_countisnotNoneelse0)test_end=total_len-1positions={'train':(0,train_end)}ifval_countisnotNone:positions['validation']=(train_end+1,val_end)positions['test']=(val_end+1,test_end)else:positions['test']=(train_end+1,test_end)# -- Perform split on current groupsplit_dicts=self._split_series_dict(self.series_groups_[group_idx],positions)# -- Convert to required outputresult=self._convert_output(split_dicts,output_format)ifverbose:self._print_split_info(group_idx,positions,output_format)results.append(result)# Return single tuple if only one group, otherwise list of tuplesreturnresults[0]ifself.n_groups_==1elseresults