skfolio.model_selection.WalkForward#

class skfolio.model_selection.WalkForward(test_size, train_size, freq=None, freq_offset=None, previous=False, expand_train=False, reduce_test=False, purged_size=0)[source]#

Walk Forward Cross-Validator.

Provides train/test indices to split time series data samples using a walk-forward logic.

In each split, test indices must be higher than the previous ones. Therefore, shuffling in cross-validator is inappropriate.

Compared to sklearn.model_selection.TimeSeriesSplit, you control the train/test folds by specifying the number of training and test samples instead of the number of splits, making it more suitable for portfolio cross-validation.

If your data is a DataFrame indexed with a DatetimeIndex, you can split the data using specific datetime frequencies and offsets.

Parameters:
test_sizeint

Length of each test set. If freq is None (default), it represents the number of observations. Otherwise, it represents the number of periods defined by freq.

train_sizeint | pandas.offsets.DateOffset | datetime.timedelta

Length of each training set. If freq is None (default), it represents the number of observations. Otherwise, for integers, it represents the number of periods defined by freq. For a pandas DateOffset or datetime timedelta, that offset is subtracted from each test period’s start date to determine when training begins.

freqstr | pandas.offsets.DateOffset, optional

If provided, it must be a frequency string or a pandas DateOffset, and the returns X must be a DataFrame with an index of type DatetimeIndex. For a list of pandas frequencies and offsets, see here. The default (None) means test_size and train_size represent the number of observations. Calendar-based splits with no test observations are omitted.

Below are some common examples:

  • Rebalancing : Monthly on the first day

  • Test Duration : 1 month

  • Train Duration : 6 months

>>> cv = WalkForward(test_size=1, train_size=6, freq="MS")
  • Rebalancing : Quarterly on the first day

  • Test Duration : 1 quarter

  • Train Duration : 2 months

>>> cv = WalkForward(test_size=1, train_size=pd.DateOffset(months=2), freq="QS")
  • Rebalancing : Monthly on the third Friday

  • Test Duration : 1 month

  • Train Duration : 6 weeks

>>> cv = WalkForward(test_size=1, train_size=pd.offsets.Week(6), freq= "WOM-3FRI")
  • Rebalancing : Semi-annually on the last day

  • Test Duration : 6 months

  • Train Duration : 1 year

>>> cv = WalkForward(test_size=1, train_size=2, freq=pd.offsets.MonthEnd(6))
  • Rebalancing : Every 2 months on the second day

  • Test Duration : 2 months

  • Train Duration : 6 months

>>> cv = WalkForward(test_size=2, train_size=6, freq="MS", freq_offset=dt.timedelta(days=1))
freq_offsetpandas DateOffset | datetime timedelta, optional

Shifts the dates generated by freq by this amount. Only used when freq is provided.

previousbool, default=False

Only used if freq is provided. If set to True, and if the period start or period end is not in the DatetimeIndex, the previous observation is used. Otherwise, the next observation is used (default).

expand_trainbool, default=False

If set to True, each subsequent training set after the first one will use all past observations. The default is False.

reduce_testbool, default=False

If True, the final split is included even when there is not enough data for a full test window. Otherwise, that split is omitted. The default is False.

purged_sizeint, default=0

The number of observations to exclude from the end of each training set before the test set. The default value is 0.

Warning

Execution timing and look-ahead control

With purged_size=0:
  • Training ends at the current period and testing begins immediately.

  • Assumes you can observe, compute, and execute within the same period.

  • If observation/computation-to-execution latency is non-negligible (submission cutoffs, illiquidity, end-of-period finalization, or markets with no intraday quotation), results may be too optimistic.

With purged_size=1:
  • One observation is dropped between training and test.

  • Decisions made on the current period start affecting performance from the next period.

Rules of thumb:
  • Use purged_size=0 only when you truly can execute at the same period with minimal latency.

  • Use purged_size >= 1 when execution is delayed (daily-priced assets, illiquid markets, end-of-day data that settles after the close).

Methods

get_metadata_routing()

Get metadata routing of this object.

get_n_splits([X, y, groups])

Return the number of train/test splits without generating their indices.

split(X[, y, groups])

Generate positional indices for each training and test set.

Examples

Tutorials using WalkForward:
>>> import numpy as np
>>> from skfolio.datasets import load_sp500_dataset, load_factors_dataset
>>> from skfolio.model_selection import WalkForward
>>> from skfolio.preprocessing import prices_to_returns
>>>
>>> X = np.random.randn(6, 2) # 6 observations
>>> cv = WalkForward(test_size=1, train_size=2)
>>> for i, (train_index, test_index) in enumerate(cv.split(X)):
...     print(f"Fold {i}:")
...     print(f"  Train: index={train_index}")
...     print(f"  Test:  index={test_index}")
Fold 0:
  Train: index=[0 1]
  Test:  index=[2]
Fold 1:
  Train: index=[1 2]
  Test:  index=[3]
Fold 2:
  Train: index=[2 3]
  Test:  index=[4]
Fold 3:
  Train: index=[3 4]
  Test:  index=[5]
>>> cv = WalkForward(test_size=1, train_size=2, purged_size=1)
>>> for i, (train_index, test_index) in enumerate(cv.split(X)):
...     print(f"Fold {i}:")
...     print(f"  Train: index={train_index}")
...     print(f"  Test:  index={test_index}")
Fold 0:
  Train: index=[0 1]
  Test:  index=[3]
Fold 1:
  Train: index=[1 2]
  Test:  index=[4]
Fold 2:
  Train: index=[2 3]
  Test:  index=[5]
>>> cv = WalkForward(test_size=2, train_size=3)
>>> for i, (train_index, test_index) in enumerate(cv.split(X)):
...     print(f"Fold {i}:")
...     print(f"  Train: index={train_index}")
...     print(f"  Test:  index={test_index}")
Fold 0:
  Train: index=[0 1 2]
  Test:  index=[3 4]
>>> cv = WalkForward(test_size=2, train_size=3, reduce_test=True)
>>> for i, (train_index, test_index) in enumerate(cv.split(X)):
...     print(f"Fold {i}:")
...     print(f"  Train: index={train_index}")
...     print(f"  Test:  index={test_index}")
Fold 0:
  Train: index=[0 1 2]
  Test:  index=[3 4]
Fold 1:
  Train: index=[2 3 4]
  Test:  index=[5]
>>> cv = WalkForward(test_size=2, train_size=3, expand_train=True, reduce_test=True)
>>> for i, (train_index, test_index) in enumerate(cv.split(X)):
...     print(f"Fold {i}:")
...     print(f"  Train: index={train_index}")
...     print(f"  Test:  index={test_index}")
Fold 0:
  Train: index=[0 1 2]
  Test:  index=[3 4]
Fold 1:
  Train: index=[0 1 2 3 4]
  Test:  index=[5]
>>>
>>> # Time-based (calendar) rebalancing
>>> prices = load_sp500_dataset()
>>> X = prices_to_returns(prices)
>>> X = X["2021":"2022"]
>>> # Rebalance every 3 months on the third Friday, and train on the last 12 months.
>>> cv = WalkForward(test_size=3, train_size=12, freq="WOM-3FRI")
>>>
>>> for i, (train_index, test_index) in enumerate(cv.split(X)):
...     print(f"Fold {i}:")
...     print(f"  Train: size={len(train_index)}")
...     print(f"  Test:  size={len(test_index)}")
Fold 0:
  Train: size=256
  Test:  size=59
Fold 1:
  Train: size=253
  Test:  size=61
Fold 2:
  Train: size=251
  Test:  size=69
get_metadata_routing()#

Get metadata routing of this object.

Please check User Guide on how the routing mechanism works.

Returns:
routingMetadataRequest

A MetadataRequest encapsulating routing information.

get_n_splits(X=None, y=None, groups=None)[source]#

Return the number of train/test splits without generating their indices.

Parameters:
Xarray-like of shape (n_observations, n_assets)

Price returns of the assets.

yNone

Always ignored, exists for compatibility.

groupsNone

Always ignored, exists for compatibility.

Returns:
n_foldsint

Number of train/test splits.

Raises:
ValueError

If X is None, a window size has an invalid type, an integer window size is not positive, or purged_size is not a non-negative integer. When freq=None, also raised if training and purging leave no observations for testing. When freq is set, also raised if any split with test observations has no training observations after applying expand_train and purged_size.

split(X, y=None, groups=None)[source]#

Generate positional indices for each training and test set.

Parameters:
Xarray-like of shape (n_observations, n_assets)

Price returns of the assets.

yNone

Always ignored, exists for compatibility.

groupsNone

Always ignored, exists for compatibility.

Yields:
trainndarray

Positional indices of the training observations.

testndarray

Positional indices of the test observations.

Raises:
ValueError

If a window size has an invalid type, an integer window size is not positive, or purged_size is not a non-negative integer. When freq=None, also raised if training and purging leave no observations for testing. When freq is set, also raised if any split with test observations has no training observations after applying expand_train and purged_size. This is checked before the first split is yielded.