-
Notifications
You must be signed in to change notification settings - Fork 8
rolling_origin
ro(data, h=10, origins=10, call, value=NULL, step=1,
ci=FALSE, co=TRUE, silent=TRUE, parallel=FALSE, ...)def rolling_origin(
data: np.ndarray,
h: int = 10,
origins: int = 10,
step: int = 1,
ci: bool = False,
co: bool = True,
call: Callable | None = None,
silent: bool = True,
) -> RollingOriginResult: ...Rolling origin evaluation (also known as time series cross-validation) produces forecasts from multiple time origins, each time expanding (or sliding) the training window and forecasting h steps ahead. This is the standard approach for evaluating forecast accuracy on time series data where random train/test splits are not appropriate.
# R — function is in the greybox namespace
library(greybox)# Python
from greybox import rolling_origin, RollingOriginResultGiven a series of length n, the initial in-sample window size is:
obs_in_sample = n - (origins * step + (h - step) * co)
where co is 1 (constant holdout) or 0 (decreasing holdout). This matches R's ro() formula exactly.
Per origin i (0-based):
-
Expanding window (
ci=False, default): training indices[0, obs_in_sample + step*i) -
Sliding window (
ci=True): training indices[step*i, obs_in_sample + step*i)(constant size) -
Holdout: indices
[train_end, train_end + h_actual)-
co=True:h_actual = h(all origins forecast exactly h steps) -
co=False:h_actual = min(h, step * (origins - i))(later origins may forecast fewer steps)
-
| Parameter | R | Python | Type | Default | Description |
|---|---|---|---|---|---|
| data | data |
data |
vector / ts / np.ndarray
|
— | Time series data |
| h | h |
h |
integer / int
|
10 |
Forecast horizon |
| origins | origins |
origins |
integer / int
|
10 |
Number of rolling origins |
| call | call |
call |
character / Callable
|
— | R: string model call; Python: callable(data, h) → forecasts
|
| step | step |
step |
integer / int
|
1 |
Step between origins |
| ci | ci |
ci |
logical / bool
|
FALSE / False
|
Constant in-sample (sliding window) |
| co | co |
co |
logical / bool
|
TRUE / True
|
Constant out-of-sample (holdout) |
| silent | — | silent |
— / bool
|
True |
Suppress progress output |
The call parameter is a single callable with signature:
call(data: np.ndarray, h: int, **optional) -> forecastsdata is the training slice, h is the forecast horizon. The function may optionally accept keyword arguments counti, counto, and/or countf (index arrays into the original series) — they are injected automatically if present in the signature:
-
counti— indices of training observations in the original series -
counto— indices of holdout observations -
countf— concatenation ofcountiandcounto
Return value (any of these):
| Return type | Stored as |
|---|---|
np.ndarray or list |
result.mean |
dict with string keys |
one attribute per key |
object with .mean / .lower / .upper
|
same as dict |
3-tuple (mean, lower, upper)
|
result.mean, result.lower, result.upper
|
RollingOriginResult with shape (h, origins) — rows = forecast step, columns = origin.
| Attribute | Type | Description |
|---|---|---|
.actuals |
np.ndarray |
Original data |
.holdout |
(h, origins) float64, NaN-filled |
Actual holdout values |
.mean |
(h, origins) |
Point forecasts (always present) |
.lower |
(h, origins) or absent |
Lower interval bound |
.upper |
(h, origins) or absent |
Upper interval bound |
.origins |
int |
Number of origins evaluated |
.h |
int |
Forecast horizon |
._fields |
list[str] |
Names of all forecast fields |
NaN is used in .holdout and forecast matrices for steps beyond h_actual (co=False near end of series).
Rolling origin with constant holdout was done.
Forecast horizon: 10
Number of origins: 10
# R
library(greybox)
y <- rnorm(100)
ourCall <- "predict(alm(y~1, data=data), h=5)"
result <- ro(y, h=5, origins=10, call=ourCall)
print(result)# Python
import numpy as np
from greybox import rolling_origin
np.random.seed(42)
y = np.cumsum(np.random.randn(100))
result = rolling_origin(y, h=5, origins=10,
call=lambda data, h: np.full(h, data.mean()))
print(result)
# Rolling origin with constant holdout was done.
# Forecast horizon: 5
# Number of origins: 10
print("holdout shape:", result.holdout.shape) # (5, 10)
print("mean shape:", result.mean.shape) # (5, 10)import numpy as np
from greybox import rolling_origin, ALM
np.random.seed(42)
y = np.cumsum(np.random.randn(100))
def call_alm(data, h):
X = np.column_stack([np.ones(len(data)),
np.arange(len(data))])
model = ALM(distribution="dnorm").fit(X, data)
n = len(data)
X_new = np.column_stack([np.ones(h), np.arange(n, n + h)])
return model.predict(X_new, interval="none").mean
result = rolling_origin(y, h=5, origins=10, call=call_alm)
print(result.mean) # (5, 10) point forecastsimport numpy as np
from greybox import rolling_origin
y = np.cumsum(np.random.randn(100))
def call_with_intervals(data, h):
m = np.full(h, data.mean())
s = data.std()
return {"mean": m, "lower": m - 1.96 * s, "upper": m + 1.96 * s}
result = rolling_origin(y, h=5, origins=10, call=call_with_intervals)
print("lower:", result.lower.shape) # (5, 10)
print("upper:", result.upper.shape) # (5, 10)import numpy as np
from statsmodels.tsa.arima.model import ARIMA
from greybox import rolling_origin
y = np.cumsum(np.random.randn(200))
def call_arima(data, h):
model = ARIMA(data, order=(1, 1, 0)).fit()
fc = model.forecast(h)
return fc
result = rolling_origin(y, h=10, origins=20, call=call_arima)import numpy as np
from greybox import rolling_origin
y = np.cumsum(np.random.randn(200))
x1 = np.random.randn(200)
# counti/counto are injected automatically when in the signature
result = rolling_origin(y, h=10, origins=20,
call=lambda data, h, counti, counto: (
np.full(h, np.linalg.lstsq(
np.column_stack([np.ones_like(data), x1[counti]]),
data, rcond=None)[0] @ np.column_stack([np.ones(h), x1[counto]]).T
)
))# R — expanding window (default)
result_exp <- ro(y, h=5, origins=10, co=FALSE, call=ourCall)
# R — sliding window (constant training size)
result_slide <- ro(y, h=5, origins=10, ci=TRUE, co=FALSE, call=ourCall)# Python — expanding window (default)
result_exp = rolling_origin(y, h=5, origins=10, ci=False, call=call_alm)
# Python — sliding window (constant training size)
result_slide = rolling_origin(y, h=5, origins=10, ci=True, call=call_alm)import numpy as np
from greybox import rolling_origin
y = np.cumsum(np.random.randn(100))
result = rolling_origin(y, h=5, origins=10,
call=lambda data, h: np.full(h, data.mean()))
from greybox.point_measures import rmse
# RMSE per forecast step (averaged over origins)
for step in range(result.holdout.shape[0]):
err = rmse(result.holdout[step], result.mean[step])
print(f"h={step + 1}: RMSE={err:.4f}")- Tashman, L.J. (2000). Out-of-sample tests of forecasting accuracy: an analysis and review. International Journal of Forecasting, 16(4), pp.437-450.
- Svetunkov, I. (2023). Forecasting and Analytics with the Augmented Dynamic Adaptive Model. Chapter 2. https://openforecast.org/adam/