Skip to content

Commit d00198e

Browse files
authored
Merge pull request #752 from bashtage/test-forecast-exog
TST: Add tests for fcasts with exog
2 parents 2c8e9ef + abd020e commit d00198e

File tree

3 files changed

+103
-5
lines changed

3 files changed

+103
-5
lines changed

arch/tests/univariate/test_forecast.py

+101
Original file line numberDiff line numberDiff line change
@@ -1104,3 +1104,104 @@ def test_multistep(analytical_model_spec):
11041104
rv_ri = fcasts_ri.residual_variance
11051105
assert_frame_equal(rv, rv_ri.iloc[-1:])
11061106
assert rv_ri.shape == (SP500.shape[0], 10)
1107+
1108+
1109+
def test_forecast_exog_single_exog():
1110+
rg = np.random.default_rng(0)
1111+
y = rg.standard_normal(100)
1112+
x = pd.DataFrame(rg.standard_normal((100, 1)), columns=["x"])
1113+
x_oos = rg.standard_normal((1, 5))
1114+
mod = ARX(y, x=x, lags=1)
1115+
res = mod.fit()
1116+
# Direct forecast
1117+
c, p, b, _ = res.params
1118+
oos = np.zeros((1, 5))
1119+
oos[0, 0] = c + p * y[-1] + b * x_oos[0, 0]
1120+
oos[0, 1] = c + p * oos[0, 0] + b * x_oos[0, 1]
1121+
oos[0, 2] = c + p * oos[0, 1] + b * x_oos[0, 2]
1122+
oos[0, 3] = c + p * oos[0, 2] + b * x_oos[0, 3]
1123+
oos[0, 4] = c + p * oos[0, 3] + b * x_oos[0, 4]
1124+
fcast = res.forecast(horizon=5, x=x_oos)
1125+
assert_allclose(oos, fcast.mean)
1126+
1127+
x_oos2 = np.tile(x_oos, (100, 1))
1128+
fcast2 = res.forecast(horizon=5, x=x_oos2)
1129+
assert_allclose(fcast.mean, fcast2.mean)
1130+
1131+
x_oos3 = np.tile(x_oos, (1, 100, 1))
1132+
fcast3 = res.forecast(horizon=5, x=x_oos3)
1133+
assert_allclose(fcast.mean, fcast3.mean)
1134+
1135+
x_oos4 = {"x": x_oos}
1136+
fcast4 = res.forecast(horizon=5, x=x_oos4)
1137+
assert_allclose(fcast.mean, fcast4.mean)
1138+
1139+
1140+
def test_forecast_exog_multi_exog():
1141+
rg = np.random.default_rng(0)
1142+
y = rg.standard_normal(100)
1143+
x = pd.DataFrame(rg.standard_normal((100, 2)), columns=["x1", "x2"])
1144+
x_oos = rg.standard_normal((2, 1, 5))
1145+
mod = ARX(y, x=x, lags=1)
1146+
res = mod.fit()
1147+
1148+
c, p, b1, b2, _ = res.params
1149+
oos = np.zeros((1, 5))
1150+
oos[0, 0] = c + p * y[-1] + b1 * x_oos[0, 0, 0] + b2 * x_oos[1, 0, 0]
1151+
oos[0, 1] = c + p * oos[0, 0] + b1 * x_oos[0, 0, 1] + b2 * x_oos[1, 0, 1]
1152+
oos[0, 2] = c + p * oos[0, 1] + b1 * x_oos[0, 0, 2] + b2 * x_oos[1, 0, 2]
1153+
oos[0, 3] = c + p * oos[0, 2] + b1 * x_oos[0, 0, 3] + b2 * x_oos[1, 0, 3]
1154+
oos[0, 4] = c + p * oos[0, 3] + b1 * x_oos[0, 0, 4] + b2 * x_oos[1, 0, 4]
1155+
1156+
fcast = res.forecast(horizon=5, x=x_oos)
1157+
assert_allclose(oos, fcast.mean)
1158+
1159+
x_oos2 = np.tile(x_oos, (100, 1))
1160+
fcast2 = res.forecast(horizon=5, x=x_oos2)
1161+
assert_allclose(fcast.mean, fcast2.mean)
1162+
1163+
x_oos3 = {"x1": x_oos[0], "x2": x_oos[1]}
1164+
fcast3 = res.forecast(horizon=5, x=x_oos3)
1165+
assert_allclose(fcast.mean, fcast3.mean)
1166+
1167+
1168+
def test_forecast_exog_single_exog_limited_sample():
1169+
rg = np.random.default_rng(0)
1170+
y = rg.standard_normal(100)
1171+
x = pd.DataFrame(rg.standard_normal((100, 1)), columns=["x"])
1172+
x_oos = rg.standard_normal((3, 5))
1173+
mod = ARX(y, x=x, lags=1)
1174+
res = mod.fit(first_obs=0, last_obs=98)
1175+
oos = np.zeros((3, 5))
1176+
# Direct forecast
1177+
c, p, b, _ = res.params
1178+
for idx in range(3):
1179+
oos[idx, 0] = c + p * y[(-3 + idx)] + b * x_oos[idx, 0]
1180+
oos[idx, 1] = c + p * oos[idx, 0] + b * x_oos[idx, 1]
1181+
oos[idx, 2] = c + p * oos[idx, 1] + b * x_oos[idx, 2]
1182+
oos[idx, 3] = c + p * oos[idx, 2] + b * x_oos[idx, 3]
1183+
oos[idx, 4] = c + p * oos[idx, 3] + b * x_oos[idx, 4]
1184+
fcast = res.forecast(horizon=5, x=x_oos)
1185+
assert_allclose(oos, fcast.mean)
1186+
1187+
x_oos2 = np.concatenate([np.zeros((97, 5)), x_oos], axis=0)
1188+
fcast2 = res.forecast(horizon=5, x=x_oos2)
1189+
assert_allclose(fcast.mean, fcast2.mean)
1190+
1191+
x_oos3 = x_oos2[None, :, :]
1192+
fcast3 = res.forecast(horizon=5, x=x_oos3)
1193+
assert_allclose(fcast.mean, fcast3.mean)
1194+
1195+
x_oos4 = {"x": x_oos}
1196+
fcast4 = res.forecast(horizon=5, x=x_oos4)
1197+
assert_allclose(fcast.mean, fcast4.mean)
1198+
1199+
1200+
def test_forecast_simulation_horizon_1():
1201+
rg = np.random.default_rng(0)
1202+
y = rg.standard_normal(100)
1203+
x = pd.DataFrame(rg.standard_normal((100, 1)), columns=["x"])
1204+
mod = ARX(y, x=x, lags=1)
1205+
res = mod.fit(first_obs=0, last_obs=98)
1206+
res.forecast(start=1, x=x, method="simulation", simulations=2)
1207+
res.forecast(start=1, x=x, method="simulation", simulations=1)

arch/univariate/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1457,7 +1457,7 @@ def forecast(
14571457
(1000, 10), and only the final row is used to produce forecasts.
14581458
* A dictionary of 2-d array-like: This format is identical to the
14591459
previous except that the dictionary keys must match the names of
1460-
the exog variables. Requires that the exog variables were pass
1460+
the exog variables. Requires that the exog variables were passed
14611461
as a pandas DataFrame.
14621462
* A 3-d NumPy array (or equivalent). In this format, each panel
14631463
(0th axis) is a 2-d array that must have shape (nforecast, horizon)

arch/univariate/mean.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -1018,10 +1018,7 @@ def forecast(
10181018
for i in range(horizon):
10191019
_impulses = impulse[i::-1][:, None]
10201020
lrvp = variance_paths[:, :, : (i + 1)].dot(_impulses**2)
1021-
lrvp = np.squeeze(lrvp)
1022-
if lrvp.ndim < 2:
1023-
lrvp = np.atleast_1d(lrvp)
1024-
lrvp = lrvp[:, None]
1021+
lrvp = lrvp[:, :, 0]
10251022
long_run_variance_paths[:, :, i] = lrvp
10261023
t, m = self._y.shape[0], self._max_lags
10271024
mean_paths = np.empty(shocks.shape[:2] + (m + horizon,))

0 commit comments

Comments
 (0)