Skip to content

experimental¶

skforecast.experimental._splitter.TimeSeriesSplitter ¶

TimeSeriesSplitter(*series)

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).

()

Attributes:

Name Type Description
series_groups_ list[dict[str, Series]]

List of series dictionaries, one per input argument.

series_indexes_ list[dict[str, Index]]

List of index dictionaries, one per series group.

n_groups_ int

Number of series groups (number of *series arguments).

index_types_ list[type]

Type of index for each group (pd.DatetimeIndex or pd.RangeIndex).

index_freqs_ list[str | int | None]

Frequency (for DatetimeIndex) or step (for RangeIndex) for each group.

skforecast_version str

Version of skforecast library used to create the splitter.

python_version str

Version of Python used to create the splitter.

Raises:

Type Description
ValueError

If no series provided or series have invalid format.

TypeError

If inputs are not in supported format.

Examples:

>>> import pandas as pd
>>> from skforecast.utils.splitter import TimeSeriesSplitter
>>> # Single series (backward compatible)
>>> df1 = pd.DataFrame(
...     {'series_a': range(100), 'series_b': range(100, 200)},
...     index=pd.date_range('2023-01-01', periods=100, freq='d')
... )
>>> splitter = TimeSeriesSplitter(df1)
>>> train_set, test_set = splitter.split_by_date(
...     end_train='2023-03-11',
...     output_format='wide'
... )

Initialize TimeSeriesSplitter with one or more series.

Parameters:

Name Type Description Default
*series DataFrame | dict[str, Series | DataFrame]

One or more time series data in supported formats.

()

Raises:

Type Description
ValueError

If no series provided or series have invalid format.

TypeError

If series are not in a supported format.

Methods:

Name Description
split_by_date

Split time series based on date ranges.

split_by_size

Split time series based on size (absolute or proportional).

Source code in skforecast/experimental/_splitter.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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.
    """
    if len(series) == 0:
        raise ValueError('At least one series must be provided.')

    # -- Process each series argument independently
    self.series_groups_ = []
    self.series_indexes_ = []
    self.index_types_ = []
    self.index_freqs_ = []
    self._min_indexes_ = []
    self._max_indexes_ = []

    for i, series_input in enumerate(series):
        # Use inner check_preprocess_series() preprocessing for each group
        series_dict, series_indexes = check_preprocess_series(series_input)

        # -- Store the preprocess series data dict & index dict
        self.series_groups_.append(series_dict)
        self.series_indexes_.append(series_indexes)

        # -- Store index type and frequency information for this group
        first_index = next(iter(series_indexes.values()))
        index_type = type(first_index)
        self.index_types_.append(index_type)

        if isinstance(first_index, pd.DatetimeIndex):
            self.index_freqs_.append(first_index.freq)
            self._min_indexes_.append(
                min([idx.min() for idx in series_indexes.values()])
            )
            self._max_indexes_.append(
                max([idx.max() for idx in series_indexes.values()])
            )
        if isinstance(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 input
    self.n_groups_ = len(series)
    self.n_timeseries = sum(map(len, self.series_indexes_))

    # -- Store version information
    self.skforecast_version = __version__
    self.python_version = sys.version.split(' ')[0]

Attributes¶

series_groups_ instance-attribute ¶

series_groups_ = []

series_indexes_ instance-attribute ¶

series_indexes_ = []

index_types_ instance-attribute ¶

index_types_ = []

index_freqs_ instance-attribute ¶

index_freqs_ = []

n_groups_ instance-attribute ¶

n_groups_ = len(series)

n_timeseries instance-attribute ¶

n_timeseries = sum(map(len, self.series_indexes_))

skforecast_version instance-attribute ¶

skforecast_version = __version__

python_version instance-attribute ¶

python_version = sys.version.split(' ')[0]

Methods:¶

split_by_date ¶

split_by_date(
    end_train,
    start_train=None,
    end_validation=None,
    end_test=None,
    output_format="wide",
    verbose=False,
)

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:

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')
>>> # 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)]
Source code in skforecast/experimental/_splitter.py
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
def split_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 = []

    for group_idx in range(self.n_groups_):
        # -- Validate and get positions for current group
        start_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 dict
        positions = {'train': (start_pos, end_train_pos)}

        if end_validation is not None:
            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 group
        split_dicts = [
            {k: v for k, v in split_dict.items() if len(v) > 0}
            for split_dict in self._split_series_dict(
                self.series_groups_[group_idx], positions
            )
        ]

        # -- Convert to required output
        result = self._convert_output(split_dicts, output_format)

        if verbose:
            self._print_split_info(group_idx, positions, output_format)

        results.append(result)

    # -- Return single tuple if only one group, otherwise list of tuples
    return results if self.n_groups_ > 1 else results[0]

split_by_size ¶

split_by_size(
    train_size,
    validation_size=None,
    test_size=None,
    output_format="wide",
    verbose=False,
)

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
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
def split_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 = []

    for group_idx in range(self.n_groups_):
        # -- Validate and get counts for current group
        train_count, val_count, test_count = self._validate_size_split_args(
            group_idx, train_size, validation_size, test_size
        )

        # -- Get total length for current group
        first_index = next(iter(self.series_indexes_[group_idx].values()))
        total_len = len(first_index)

        # -- Compute positions
        train_end = train_count - 1
        val_end = train_end + (val_count if val_count is not None else 0)
        test_end = total_len - 1

        positions = {'train': (0, train_end)}

        if val_count is not None:
            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 group
        split_dicts = self._split_series_dict(
            self.series_groups_[group_idx], positions
        )

        # -- Convert to required output
        result = self._convert_output(split_dicts, output_format)

        if verbose:
            self._print_split_info(group_idx, positions, output_format)

        results.append(result)

    # Return single tuple if only one group, otherwise list of tuples
    return results[0] if self.n_groups_ == 1 else results