Skip to content

Block Cross-validation method

timecave.validation_methods.CV.BlockCV(splits, ts, fs=1, weight_function=constant_weights, params=None)

Bases: BaseSplitter

Implements the Block Cross-validation method, as well as its weighted variant.

This class implements both the Block Cross-validation method and the Weighted Block Cross-validation method. The weight_function argument allows the user to implement the latter in a convenient way.

Parameters:

Name Type Description Default
splits int

The number of folds used to partition the data.

required
ts ndarray | Series

Univariate time series.

required
fs float | int

Sampling frequency (Hz).

1
weight_function callable

Fold weighting function. Check the weights module for more details.

constant_weights
params dict

Parameters to be passed to the weighting functions.

None

Attributes:

Name Type Description
n_splits int

The number of splits.

sampling_freq int | float

The series' sampling frequency (Hz).

Methods:

Name Description
split

Split the time series into training and validation sets.

info

Provide additional information on the validation method.

statistics

Compute relevant statistics for both training and validation sets.

plot

Plot the partitioned time series.

See also

hv Block CV: A blend of Block CV and leave-one-out CV.

Adapted hv Block CV: Similar to Block CV, but the training samples that lie closest to the validation set are removed.

Notes

The Block Cross-validation method splits the data into \(N\) different folds. Then, in every iteration \(i\), the model is validated on data from the \(i^{th}\) folds and trained on data from the remaining folds. The average error on the validation sets is then taken as the estimate of the model's true error. This method does not preserve the temporal order of the observations.

block

It is reasonable to assume that when the model is validated on more recent data, the error estimate will be more accurate. To address this issue, one may use a weighted average to compute the final estimate of the error, with larger weights being assigned to the estimates obtained using models validated on more recent data. For more details on this method, the reader should refer to [1] or [2].

References

1

Christoph Bergmeir and José M Benítez. On the use of cross-validation for time series predictor evaluation. Information Sciences, 191:192–213, 2012.

2

Vitor Cerqueira, Luis Torgo, and Igor Mozetiˇc. Evaluating time series forecasting models: An empirical study on performance estimation methods. Machine Learning, 109(11):1997–2028, 2020.

Source code in timecave/validation_methods/CV.py
def __init__(
    self,
    splits: int,
    ts: np.ndarray | pd.Series,
    fs: float | int = 1,
    weight_function: callable = constant_weights,
    params: dict = None,
) -> None:

    super().__init__(splits, ts, fs)
    self._splitting_ind = self._split_ind()
    self._weights = weight_function(self.n_splits, params=params)

info()

Provide some basic information on the training and validation sets.

This method displays the number of splits, the fold size, and the weights that will be used to compute the error estimate.

Examples:

>>> import numpy as np
>>> from timecave.validation_methods.CV import BlockCV
>>> ts = np.ones(10);
>>> splitter = BlockCV(5, ts);
>>> splitter.info();
Block CV method
---------------
Time series size: 10 samples
Number of splits: 5
Fold size: 2 to 2 samples (20.0 to 20.0 %)
Weights: [1. 1. 1. 1. 1.]
Source code in timecave/validation_methods/CV.py
def info(self) -> None:
    """
    Provide some basic information on the training and validation sets.

    This method displays the number of splits, the fold size, 
    and the weights that will be used to compute the error estimate.

    Examples
    --------
    >>> import numpy as np
    >>> from timecave.validation_methods.CV import BlockCV
    >>> ts = np.ones(10);
    >>> splitter = BlockCV(5, ts);
    >>> splitter.info();
    Block CV method
    ---------------
    Time series size: 10 samples
    Number of splits: 5
    Fold size: 2 to 2 samples (20.0 to 20.0 %)
    Weights: [1. 1. 1. 1. 1.]
    """

    min_fold_size = int(np.floor(self._n_samples / self.n_splits))
    max_fold_size = min_fold_size

    remainder = self._n_samples % self.n_splits

    if remainder != 0:

        max_fold_size += 1

    min_fold_size_pct = np.round(min_fold_size / self._n_samples * 100, 2)
    max_fold_size_pct = np.round(max_fold_size / self._n_samples * 100, 2)

    print("Block CV method")
    print("---------------")
    print(f"Time series size: {self._n_samples} samples")
    print(f"Number of splits: {self.n_splits}")
    print(
        f"Fold size: {min_fold_size} to {max_fold_size} samples ({min_fold_size_pct} to {max_fold_size_pct} %)"
    )
    print(f"Weights: {np.round(self._weights, 3)}")

    return

plot(height, width)

Plot the partitioned time series.

This method allows the user to plot the partitioned time series. The training and validation sets are plotted using different colours.

Parameters:

Name Type Description Default
height int

The figure's height.

required
width int

The figure's width.

required

Examples:

>>> import numpy as np
>>> from timecave.validation_methods.CV import BlockCV
>>> ts = np.ones(100);
>>> splitter = BlockCV(5, ts);
>>> splitter.plot(10, 10);

block_plot

Source code in timecave/validation_methods/CV.py
def plot(self, height: int, width: int) -> None:
    """
    Plot the partitioned time series.

    This method allows the user to plot the partitioned time series. The training and validation sets are plotted using different colours.

    Parameters
    ----------
    height : int
        The figure's height.

    width : int
        The figure's width.

    Examples
    --------
    >>> import numpy as np
    >>> from timecave.validation_methods.CV import BlockCV
    >>> ts = np.ones(100);
    >>> splitter = BlockCV(5, ts);
    >>> splitter.plot(10, 10);

    ![block_plot](../../../images/BlockCV_plot.png)
    """

    fig, axs = plt.subplots(self.n_splits, 1, sharex=True)
    fig.set_figheight(height)
    fig.set_figwidth(width)
    fig.supxlabel("Samples")
    fig.supylabel("Time Series")
    fig.suptitle("Block CV method")

    for it, (training, validation, w) in enumerate(self.split()):

        axs[it].scatter(training, self._series[training], label="Training set")
        axs[it].scatter(
            validation, self._series[validation], label="Validation set"
        )
        axs[it].set_title("Fold: {} Weight: {}".format(it + 1, np.round(w, 3)))
        axs[it].set_ylim([self._series.min() - 1, self._series.max() + 1])
        axs[it].set_xlim([- 1, self._n_samples + 1])
        axs[it].legend()

    plt.show()

    return

split()

Split the time series into training and validation sets.

This method splits the series' indices into disjoint sets containing the training and validation indices. At every iteration, an array of training indices and another one containing the validation indices are generated. Note that this method is a generator. To access the indices, use the next() method or a for loop.

Yields:

Type Description
ndarray

Array of training indices.

ndarray

Array of validation indices.

float

Weight assigned to the error estimate.

Examples:

>>> import numpy as np
>>> from timecave.validation_methods.CV import BlockCV
>>> ts = np.ones(10);
>>> splitter = BlockCV(5, ts); # Split the data into 5 different folds
>>> for ind, (train, val, _) in enumerate(splitter.split()):
... 
...     print(f"Iteration {ind+1}");
...     print(f"Training set indices: {train}");
...     print(f"Validation set indices: {val}");
Iteration 1
Training set indices: [2 3 4 5 6 7 8 9]
Validation set indices: [0 1]
Iteration 2
Training set indices: [0 1 4 5 6 7 8 9]
Validation set indices: [2 3]
Iteration 3
Training set indices: [0 1 2 3 6 7 8 9]
Validation set indices: [4 5]
Iteration 4
Training set indices: [0 1 2 3 4 5 8 9]
Validation set indices: [6 7]
Iteration 5
Training set indices: [0 1 2 3 4 5 6 7]
Validation set indices: [8 9]

If the number of samples is not divisible by the number of folds, the first folds will contain more samples:

>>> ts2 = np.ones(17);
>>> splitter = BlockCV(5, ts2);
>>> for ind, (train, val, _) in enumerate(splitter.split()):
... 
...     print(f"Iteration {ind+1}");
...     print(f"Training set indices: {train}");
...     print(f"Validation set indices: {val}");
Iteration 1
Training set indices: [ 4  5  6  7  8  9 10 11 12 13 14 15 16]
Validation set indices: [0 1 2 3]
Iteration 2
Training set indices: [ 0  1  2  3  8  9 10 11 12 13 14 15 16]
Validation set indices: [4 5 6 7]
Iteration 3
Training set indices: [ 0  1  2  3  4  5  6  7 11 12 13 14 15 16]
Validation set indices: [ 8  9 10]
Iteration 4
Training set indices: [ 0  1  2  3  4  5  6  7  8  9 10 14 15 16]
Validation set indices: [11 12 13]
Iteration 5
Training set indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13]
Validation set indices: [14 15 16]

Weights can be assigned to the error estimates (Weighted Rolling Window method). The parameters for the weighting functions must be passed to the class constructor:

>>> from timecave.validation_methods.weights import exponential_weights
>>> splitter = BlockCV(5, ts, weight_function=exponential_weights, params={"base": 2});
>>> for ind, (train, val, weight) in enumerate(splitter.split()):
... 
...     print(f"Iteration {ind+1}");
...     print(f"Training set indices: {train}");
...     print(f"Validation set indices: {val}");
...     print(f"Weight: {np.round(weight, 3)}");
Iteration 1
Training set indices: [2 3 4 5 6 7 8 9]
Validation set indices: [0 1]
Weight: 0.032
Iteration 2
Training set indices: [0 1 4 5 6 7 8 9]
Validation set indices: [2 3]
Weight: 0.065
Iteration 3
Training set indices: [0 1 2 3 6 7 8 9]
Validation set indices: [4 5]
Weight: 0.129
Iteration 4
Training set indices: [0 1 2 3 4 5 8 9]
Validation set indices: [6 7]
Weight: 0.258
Iteration 5
Training set indices: [0 1 2 3 4 5 6 7]
Validation set indices: [8 9]
Weight: 0.516
Source code in timecave/validation_methods/CV.py
def split(self) -> Generator[tuple[np.ndarray, np.ndarray, float], None, None]:
    """
    Split the time series into training and validation sets.

    This method splits the series' indices into disjoint sets containing the training and validation indices.
    At every iteration, an array of training indices and another one containing the validation indices are generated.
    Note that this method is a generator. To access the indices, use the `next()` method or a `for` loop.

    Yields
    ------
    np.ndarray
        Array of training indices.

    np.ndarray
        Array of validation indices.

    float
        Weight assigned to the error estimate.

    Examples
    --------
    >>> import numpy as np
    >>> from timecave.validation_methods.CV import BlockCV
    >>> ts = np.ones(10);
    >>> splitter = BlockCV(5, ts); # Split the data into 5 different folds
    >>> for ind, (train, val, _) in enumerate(splitter.split()):
    ... 
    ...     print(f"Iteration {ind+1}");
    ...     print(f"Training set indices: {train}");
    ...     print(f"Validation set indices: {val}");
    Iteration 1
    Training set indices: [2 3 4 5 6 7 8 9]
    Validation set indices: [0 1]
    Iteration 2
    Training set indices: [0 1 4 5 6 7 8 9]
    Validation set indices: [2 3]
    Iteration 3
    Training set indices: [0 1 2 3 6 7 8 9]
    Validation set indices: [4 5]
    Iteration 4
    Training set indices: [0 1 2 3 4 5 8 9]
    Validation set indices: [6 7]
    Iteration 5
    Training set indices: [0 1 2 3 4 5 6 7]
    Validation set indices: [8 9]

    If the number of samples is not divisible by the number of folds, the first folds will contain more samples:

    >>> ts2 = np.ones(17);
    >>> splitter = BlockCV(5, ts2);
    >>> for ind, (train, val, _) in enumerate(splitter.split()):
    ... 
    ...     print(f"Iteration {ind+1}");
    ...     print(f"Training set indices: {train}");
    ...     print(f"Validation set indices: {val}");
    Iteration 1
    Training set indices: [ 4  5  6  7  8  9 10 11 12 13 14 15 16]
    Validation set indices: [0 1 2 3]
    Iteration 2
    Training set indices: [ 0  1  2  3  8  9 10 11 12 13 14 15 16]
    Validation set indices: [4 5 6 7]
    Iteration 3
    Training set indices: [ 0  1  2  3  4  5  6  7 11 12 13 14 15 16]
    Validation set indices: [ 8  9 10]
    Iteration 4
    Training set indices: [ 0  1  2  3  4  5  6  7  8  9 10 14 15 16]
    Validation set indices: [11 12 13]
    Iteration 5
    Training set indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13]
    Validation set indices: [14 15 16]

    Weights can be assigned to the error estimates (Weighted Rolling Window method). 
    The parameters for the weighting functions must be passed to the class constructor:

    >>> from timecave.validation_methods.weights import exponential_weights
    >>> splitter = BlockCV(5, ts, weight_function=exponential_weights, params={"base": 2});
    >>> for ind, (train, val, weight) in enumerate(splitter.split()):
    ... 
    ...     print(f"Iteration {ind+1}");
    ...     print(f"Training set indices: {train}");
    ...     print(f"Validation set indices: {val}");
    ...     print(f"Weight: {np.round(weight, 3)}");
    Iteration 1
    Training set indices: [2 3 4 5 6 7 8 9]
    Validation set indices: [0 1]
    Weight: 0.032
    Iteration 2
    Training set indices: [0 1 4 5 6 7 8 9]
    Validation set indices: [2 3]
    Weight: 0.065
    Iteration 3
    Training set indices: [0 1 2 3 6 7 8 9]
    Validation set indices: [4 5]
    Weight: 0.129
    Iteration 4
    Training set indices: [0 1 2 3 4 5 8 9]
    Validation set indices: [6 7]
    Weight: 0.258
    Iteration 5
    Training set indices: [0 1 2 3 4 5 6 7]
    Validation set indices: [8 9]
    Weight: 0.516
    """

    for i, (ind, weight) in enumerate(zip(self._splitting_ind[:-1], self._weights)):

        next_ind = self._splitting_ind[i + 1]

        validation = self._indices[ind:next_ind]
        train = np.array([el for el in self._indices if el not in validation])

        yield (train, validation, weight)

statistics()

Compute relevant statistics for both training and validation sets.

This method computes relevant time series features, such as mean, strength-of-trend, etc. for both the whole time series, the training set and the validation set. It can and should be used to ensure that the characteristics of both the training and validation sets are, statistically speaking, similar to those of the time series one wishes to forecast. If this is not the case, using the validation method will most likely lead to a poor assessment of the model's performance.

Returns:

Type Description
DataFrame

Relevant features for the entire time series.

DataFrame

Relevant features for the training set.

DataFrame

Relevant features for the validation set.

Raises:

Type Description
ValueError

If the time series is composed of less than three samples.

ValueError

If the folds comprise less than two samples.

Examples:

>>> import numpy as np
>>> from timecave.validation_methods.CV import BlockCV
>>> ts = np.hstack((np.ones(5), np.zeros(5)));
>>> splitter = BlockCV(5, ts);
>>> ts_stats, training_stats, validation_stats = splitter.statistics();
Frequency features are only meaningful if the correct sampling frequency is passed to the class.
>>> ts_stats
   Mean  Median  Min  Max  Variance  P2P_amplitude  Trend_slope  Spectral_centroid  Spectral_rolloff  Spectral_entropy  Strength_of_trend  Mean_crossing_rate  Median_crossing_rate
0   0.5     0.5  0.0  1.0      0.25            1.0    -0.151515           0.114058               0.5           0.38717            1.59099            0.111111              0.111111
>>> training_stats
    Mean  Median  Min  Max  Variance  P2P_amplitude  Trend_slope  Spectral_centroid  Spectral_rolloff  Spectral_entropy  Strength_of_trend  Mean_crossing_rate  Median_crossing_rate
0  0.375     0.0  0.0  1.0  0.234375            1.0    -0.178571           0.154195             0.500          0.600876           1.383496            0.142857              0.142857
0  0.375     0.0  0.0  1.0  0.234375            1.0    -0.178571           0.154195             0.500          0.600876           1.383496            0.142857              0.142857
0  0.500     0.5  0.0  1.0  0.250000            1.0    -0.190476           0.095190             0.375          0.600876           1.428869            0.142857              0.142857
0  0.625     1.0  0.0  1.0  0.234375            1.0    -0.178571           0.122818             0.500          0.600876           1.383496            0.142857              0.142857
0  0.625     1.0  0.0  1.0  0.234375            1.0    -0.178571           0.122818             0.500          0.600876           1.383496            0.142857              0.142857
>>> validation_stats
   Mean  Median  Min  Max  Variance  P2P_amplitude   Trend_slope  Spectral_centroid  Spectral_rolloff  Spectral_entropy  Strength_of_trend  Mean_crossing_rate  Median_crossing_rate
0   1.0     1.0  1.0  1.0      0.00            0.0 -7.850462e-17               0.00               0.0               0.0                inf                 0.0                   0.0
0   1.0     1.0  1.0  1.0      0.00            0.0 -7.850462e-17               0.00               0.0               0.0                inf                 0.0                   0.0
0   0.5     0.5  0.0  1.0      0.25            1.0 -1.000000e+00               0.25               0.5               0.0                inf                 1.0                   1.0
0   0.0     0.0  0.0  0.0      0.00            0.0  0.000000e+00               0.00               0.0               0.0                inf                 0.0                   0.0
0   0.0     0.0  0.0  0.0      0.00            0.0  0.000000e+00               0.00               0.0               0.0                inf                 0.0                   0.0
Source code in timecave/validation_methods/CV.py
def statistics(self) -> tuple[pd.DataFrame]:
    """
    Compute relevant statistics for both training and validation sets.

    This method computes relevant time series features, such as mean, strength-of-trend, etc. for both the whole time series, the training set and the validation set.
    It can and should be used to ensure that the characteristics of both the training and validation sets are, statistically speaking, similar to those of the time series one wishes to forecast.
    If this is not the case, using the validation method will most likely lead to a poor assessment of the model's performance.

    Returns
    -------
    pd.DataFrame
        Relevant features for the entire time series.

    pd.DataFrame
        Relevant features for the training set.

    pd.DataFrame
        Relevant features for the validation set.

    Raises
    ------
    ValueError
        If the time series is composed of less than three samples.

    ValueError
        If the folds comprise less than two samples.

    Examples
    --------
    >>> import numpy as np
    >>> from timecave.validation_methods.CV import BlockCV
    >>> ts = np.hstack((np.ones(5), np.zeros(5)));
    >>> splitter = BlockCV(5, ts);
    >>> ts_stats, training_stats, validation_stats = splitter.statistics();
    Frequency features are only meaningful if the correct sampling frequency is passed to the class.
    >>> ts_stats
       Mean  Median  Min  Max  Variance  P2P_amplitude  Trend_slope  Spectral_centroid  Spectral_rolloff  Spectral_entropy  Strength_of_trend  Mean_crossing_rate  Median_crossing_rate
    0   0.5     0.5  0.0  1.0      0.25            1.0    -0.151515           0.114058               0.5           0.38717            1.59099            0.111111              0.111111
    >>> training_stats
        Mean  Median  Min  Max  Variance  P2P_amplitude  Trend_slope  Spectral_centroid  Spectral_rolloff  Spectral_entropy  Strength_of_trend  Mean_crossing_rate  Median_crossing_rate
    0  0.375     0.0  0.0  1.0  0.234375            1.0    -0.178571           0.154195             0.500          0.600876           1.383496            0.142857              0.142857
    0  0.375     0.0  0.0  1.0  0.234375            1.0    -0.178571           0.154195             0.500          0.600876           1.383496            0.142857              0.142857
    0  0.500     0.5  0.0  1.0  0.250000            1.0    -0.190476           0.095190             0.375          0.600876           1.428869            0.142857              0.142857
    0  0.625     1.0  0.0  1.0  0.234375            1.0    -0.178571           0.122818             0.500          0.600876           1.383496            0.142857              0.142857
    0  0.625     1.0  0.0  1.0  0.234375            1.0    -0.178571           0.122818             0.500          0.600876           1.383496            0.142857              0.142857
    >>> validation_stats
       Mean  Median  Min  Max  Variance  P2P_amplitude   Trend_slope  Spectral_centroid  Spectral_rolloff  Spectral_entropy  Strength_of_trend  Mean_crossing_rate  Median_crossing_rate
    0   1.0     1.0  1.0  1.0      0.00            0.0 -7.850462e-17               0.00               0.0               0.0                inf                 0.0                   0.0
    0   1.0     1.0  1.0  1.0      0.00            0.0 -7.850462e-17               0.00               0.0               0.0                inf                 0.0                   0.0
    0   0.5     0.5  0.0  1.0      0.25            1.0 -1.000000e+00               0.25               0.5               0.0                inf                 1.0                   1.0
    0   0.0     0.0  0.0  0.0      0.00            0.0  0.000000e+00               0.00               0.0               0.0                inf                 0.0                   0.0
    0   0.0     0.0  0.0  0.0      0.00            0.0  0.000000e+00               0.00               0.0               0.0                inf                 0.0                   0.0
    """

    if self._n_samples <= 2:

        raise ValueError(
            "Basic statistics can only be computed if the time series comprises more than two samples."
        )

    if int(np.round(self._n_samples / self.n_splits)) < 2:

        raise ValueError(
            "The folds are too small to compute most meaningful features."
        )

    print("Frequency features are only meaningful if the correct sampling frequency is passed to the class.")

    full_features = get_features(self._series, self.sampling_freq)
    training_stats = []
    validation_stats = []

    for (training, validation, _) in self.split():

        training_feat = get_features(self._series[training], self.sampling_freq)
        training_stats.append(training_feat)

        validation_feat = get_features(self._series[validation], self.sampling_freq)
        validation_stats.append(validation_feat)

    training_features = pd.concat(training_stats)
    validation_features = pd.concat(validation_stats)

    return (full_features, training_features, validation_features)