<a id="skfolio-model-selection-walkforward"></a>

# skfolio.model_selection.WalkForward

<a id="skfolio.model_selection.WalkForward"></a>

### *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)

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_size** *int*
  : 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_size** *int | 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 pandas DateOffset or datetime timedelta it represents the date offset
    applied to the start of each period.

  **freq** *str | 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](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries-offset-aliases).
    The default (`None`) means `test_size` and `train_size` represent the number of
    observations.
    <br/>
    Below are some common examples:
    > * Rebalancing    : Monthly on the first day
    > * Test Duration  : 1 month
    > * Train Duration : 6 months
    <br/>
    > ```pycon
    > >>> cv = WalkForward(test_size=1, train_size=6, freq="MS")
    > ```
    <br/>
    > * Rebalancing    : Quarterly on the first day
    > * Test Duration  : 1 quarter
    > * Train Duration : 2 months
    <br/>
    > ```pycon
    > >>> cv = WalkForward(test_size=1, train_size=pd.DateOffset(months=2), freq="QS")
    > ```
    <br/>
    > * Rebalancing    : Monthly on the third Friday
    > * Test Duration  : 1 month
    > * Train Duration : 6 weeks
    <br/>
    > ```pycon
    > >>> cv = WalkForward(test_size=1, train_size=pd.offsets.Week(6), freq= "WOM-3FRI")
    > ```
    <br/>
    > * Rebalancing    : Semi-annually on the last day
    > * Test Duration  : 6 months
    > * Train Duration : 1 year
    <br/>
    > ```pycon
    > >>> cv = WalkForward(test_size=1, train_size=2, freq=pd.offsets.SemiMonthEnd())
    > ```
    <br/>
    > * Rebalancing    : Every 2 months on the second day
    > * Test Duration  : 2 months
    > * Train Duration : 6 months
    <br/>
    > ```pycon
    > >>> cv = WalkForward(test_size=2, train_size=6, freq="MS", freq_offset=dt.timedelta(days=2))
    > ```

  **freq_offset** *pandas DateOffset | datetime timedelta, optional*
  : Only used if `freq` is provided. Offsets the `freq` by a pandas DateOffset or a
    datetime timedelta offset.

  **previous** *bool, 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_train** *bool, default=False*
  : If set to `True`, each subsequent training set after the first one will
    use all past observations.
    The default is `False`.

  **reduce_test** *bool, default=False*
  : If set to `True`, the last train/test split will be returned even if the
    test set is partial (i.e., it contains fewer observations than `test_size`),
    otherwise, it will be ignored.
    The default is `False`.

  **purged_size** *int, default=0*
  : The number of observations to exclude from the end of each training set before
    the test set.
    The default value is `0`.
    <br/>
    #### WARNING
    **Execution timing and look-ahead control**
    <br/>
    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.
    <br/>
    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.
    <br/>
    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`](#skfolio.model_selection.WalkForward.get_metadata_routing)()       | Get metadata routing of this object.                              |
|-------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`get_n_splits`](#skfolio.model_selection.WalkForward.get_n_splits)([X, y, groups]) | Return the number of splitting iterations in the cross-validator. |
| [`split`](#skfolio.model_selection.WalkForward.split)(X[, y, groups])        | Generate indices to split data into training and test set.        |

### Examples

Tutorials using `WalkForward`:
: * [Custom Pre-selection Using Volumes](https://skfolio.org/auto_examples/pre_selection/plot_3_custom_pre_selection_volumes.html.md#sphx-glr-auto-examples-pre-selection-plot-3-custom-pre-selection-volumes-py)
  * [HRP vs HERC](https://skfolio.org/auto_examples/clustering/plot_3_hrp_vs_herc.html.md#sphx-glr-auto-examples-clustering-plot-3-hrp-vs-herc-py)
  * [L1 and L2 Regularization](https://skfolio.org/auto_examples/mean_risk/plot_8_regularization.html.md#sphx-glr-auto-examples-mean-risk-plot-8-regularization-py)
  * [NCO - Combinatorial Purged CV](https://skfolio.org/auto_examples/clustering/plot_5_nco_grid_search.html.md#sphx-glr-auto-examples-clustering-plot-5-nco-grid-search-py)
  * [Stacking Optimization](https://skfolio.org/auto_examples/ensemble/plot_1_stacking.html.md#sphx-glr-auto-examples-ensemble-plot-1-stacking-py)

```pycon
>>> 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
```

<a id="skfolio.model_selection.WalkForward.get_metadata_routing"></a>

#### get_metadata_routing()

Get metadata routing of this object.

Please check [User Guide](https://skfolio.org/user_guide/metadata_routing.html.md#metadata-routing) on how the routing
mechanism works.

* **Returns:**
  **routing** *MetadataRequest*
  : A `MetadataRequest` encapsulating
    routing information.

<a id="skfolio.model_selection.WalkForward.get_n_splits"></a>

#### get_n_splits(X=None, y=None, groups=None)

Return the number of splitting iterations in the cross-validator.

* **Parameters:**
  **X** *array-like of shape (n_observations, n_assets)*
  : Price returns of the assets.

  **y** *array-like of shape (n_observations, n_targets)*
  : Always ignored, exists for compatibility.

  **groups** *array-like of shape (n_observations,)*
  : Always ignored, exists for compatibility.
* **Returns:**
  **n_folds** *int*
  : Returns the number of splitting iterations in the cross-validator.
* **Raises:**
  ValueError
  : If `X` is `None`, if a window size has an invalid type, if a training or
    test window size is not positive, or if `purged_size` is not a
    non-negative integer.

<a id="skfolio.model_selection.WalkForward.split"></a>

#### split(X, y=None, groups=None)

Generate indices to split data into training and test set.

* **Parameters:**
  **X** *array-like of shape (n_observations, n_assets)*
  : Price returns of the assets.

  **y** *array-like of shape (n_observations, n_targets)*
  : Always ignored, exists for compatibility.

  **groups** *array-like of shape (n_observations,)*
  : Always ignored, exists for compatibility.
* **Yields:**
  **train** *ndarray*
  : The training set indices for that split.

  **test** *ndarray*
  : The testing set indices for that split.
* **Raises:**
  ValueError
  : If a window size has an invalid type, if a training or test window size
    is not positive, or if `purged_size` is not a non-negative integer.

