bashtage/arch

Local block bootstrap

Opened this issue · 0 comments

One feature I think would be useful is bootstrap methods for nonstationary time-series. I am currently looking at the Local block bootstrap (LBB), which was relatively straightforward to implement.

The LBB works by assuming that the time-series is almost stationary, but with slowly changing properties (e.g. the seasons in a year). Then, it creates bootstrap samples by sampling blocks near each other and stitching them together.

I have created a prototype, which seems to work, but I don't have any unit tests yet.

import numpy as np
from arch.bootstrap.base import IIDBootstrap, _get_random_integers, ArrayLike, RandomState, Generator, Int64Array


class LocalBlockBootstrap(IIDBootstrap):
    _name = "Local Block Bootstrap"

    def __init__(
        self,
        block_size: int,
        max_step: int,
        *args: ArrayLike,
        random_state: RandomState | None = None,
        seed: None | int | Generator | RandomState = None,
        **kwargs: ArrayLike,
    ) -> None:
        super().__init__(*args, random_state=random_state, seed=seed, **kwargs)
        self.block_size: int = block_size
        self.max_step: int = max_step
        self._parameters = [block_size, max_step]

    def clone(
        self,
        *args: ArrayLike,
        seed: None | int | Generator | RandomState = None,
        **kwargs: ArrayLike,
    ) -> 'LocalBlockBootstrap':

        block_size = self._parameters[0]
        max_step = self._parameters[1]
        return self.__class__(block_size, max_step, *args, random_state=None, seed=seed, **kwargs)

    def update_indices(self) -> Int64Array:
        num_blocks = self._num_items // self.block_size
        if num_blocks * self.block_size < self._num_items:
            num_blocks += 1

        m = np.arange(num_blocks)
        lower = np.maximum(0, self.block_size * m - self.max_step - 1)
        upper = np.minimum(self._num_items - self.block_size, self.block_size * m + self.max_step - 1)
        step = upper - lower
        indices = lower + _get_random_integers(self.generator, step, size=len(step))

        indices = indices[:, np.newaxis] + np.arange(self.block_size)[np.newaxis, :]
        indices = indices.flatten()

        if indices.shape[0] > self._num_items:
            return indices[: self._num_items]
        else:
            return indices