Skip to content

Commit 67d4dbd

Browse files
authored
Fix/optimized hfc prob regr (unit8co#2534)
1 parent 41e1177 commit 67d4dbd

File tree

3 files changed

+118
-25
lines changed

3 files changed

+118
-25
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ but cannot always guarantee backwards compatibility. Changes that may **break co
2424

2525
**Fixed**
2626

27+
- Fixed a bug when performing probabilistic optimized historical forecasts (`num_samples>1, retrain=False, enable_optimization=True`) with regression models, where reshaping the array resulted in a wrong order of samples across components and forecasts. [#2534](https://github.com/unit8co/darts/pull/2534) by [Dennis Bader](https://github.com/dennisbader).
2728
- Fixed bug when plotting a probabilistic multivariate series, where all confidence intervals (starting from 2nd component) had the same color as the median line. [#2532](https://github.com/unit8co/darts/pull/2532) by [Dennis Bader](https://github.com/dennisbader).
2829
- Fixed a bug when passing an empty array to `TimeSeries.prepend/append_values()` raised an error. [#2522](https://github.com/unit8co/darts/pull/2522) by [Alessio Pellegrini](https://github.com/AlessiopSymplectic)
2930
- Fixed a bug with `TimeSeries.prepend/append_values()`, where the name of the (time) index was lost. [#2522](https://github.com/unit8co/darts/pull/2522) by [Alessio Pellegrini](https://github.com/AlessiopSymplectic)

darts/tests/models/forecasting/test_historical_forecasts.py

+87
Original file line numberDiff line numberDiff line change
@@ -2314,6 +2314,93 @@ def test_predict_likelihood_parameters(self, model_type):
23142314
)
23152315
assert len(hist_fc) == n + 1
23162316

2317+
@pytest.mark.parametrize(
2318+
"config",
2319+
product(
2320+
[False, True], # last_points_only
2321+
[True, False], # multi_models
2322+
[1, 2, 3], # horizon
2323+
),
2324+
)
2325+
def test_probabilistic_optimized_hist_fc_regression(self, config):
2326+
"""Tests optimized probilistic historical forecasts for regression models."""
2327+
np.random.seed(42)
2328+
lpo, multi_models, n = config
2329+
ocl = 2
2330+
q = [0.05, 0.50, 0.95]
2331+
2332+
y = tg.linear_timeseries(length=20)
2333+
y = y.stack(y + 1.0)
2334+
y = [y, y]
2335+
2336+
icl = 3
2337+
model = LinearRegressionModel(
2338+
lags=icl,
2339+
output_chunk_length=ocl,
2340+
likelihood="quantile",
2341+
quantiles=q,
2342+
multi_models=multi_models,
2343+
)
2344+
model.fit(y)
2345+
# probabilistic forecasts non-optimized
2346+
hfcs_no_opt = model.historical_forecasts(
2347+
series=y,
2348+
forecast_horizon=n,
2349+
last_points_only=lpo,
2350+
retrain=False,
2351+
enable_optimization=False,
2352+
num_samples=1000,
2353+
stride=n,
2354+
)
2355+
# probabilistic forecasts optimized
2356+
hfcs_opt = model.historical_forecasts(
2357+
series=y,
2358+
forecast_horizon=n,
2359+
last_points_only=lpo,
2360+
retrain=False,
2361+
enable_optimization=True,
2362+
num_samples=1000,
2363+
stride=n,
2364+
)
2365+
if n <= ocl:
2366+
# quantile forecasts optimized
2367+
hfcs_opt_q = model.historical_forecasts(
2368+
series=y,
2369+
forecast_horizon=n,
2370+
last_points_only=lpo,
2371+
retrain=False,
2372+
enable_optimization=True,
2373+
predict_likelihood_parameters=True,
2374+
stride=n,
2375+
)
2376+
if lpo:
2377+
q_med = hfcs_opt_q[0].components[1::3].tolist()
2378+
else:
2379+
q_med = hfcs_opt_q[0][0].components[1::3].tolist()
2380+
hfcs_opt_q = (
2381+
[concatenate(hfc) for hfc in hfcs_opt_q]
2382+
if hfcs_opt_q is not None
2383+
else hfcs_opt_q
2384+
)
2385+
hfcs_opt_q = (
2386+
[hfc[q_med] for hfc in hfcs_opt_q]
2387+
if hfcs_opt_q is not None
2388+
else hfcs_opt_q
2389+
)
2390+
else:
2391+
hfcs_opt_q = [None] * len(hfcs_opt)
2392+
2393+
if not lpo:
2394+
hfcs_opt = [concatenate(hfc) for hfc in hfcs_opt]
2395+
hfcs_no_opt = [concatenate(hfc) for hfc in hfcs_no_opt]
2396+
2397+
for hfc_opt, mean_opt_q, hfc_no_opt in zip(hfcs_opt, hfcs_opt_q, hfcs_no_opt):
2398+
mean_opt = hfc_opt.all_values().mean(axis=2)
2399+
mean_no_opt = hfc_no_opt.all_values().mean(axis=2)
2400+
assert np.abs(mean_opt - mean_no_opt).max() < 0.1
2401+
if mean_opt_q is not None:
2402+
assert np.abs(mean_opt - mean_opt_q.values()).max() < 0.1
2403+
23172404
@pytest.mark.parametrize(
23182405
"model_type,enable_optimization",
23192406
product(["regression", "torch"], [True, False]),

darts/utils/historical_forecasts/optimized_historical_forecasts_regression.py

+30-25
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,23 @@ def _optimized_historical_forecasts_last_points_only(
141141
)
142142
# forecast has shape ((forecastable_index_length-1)*num_samples, k, n_component)
143143
# where k = output_chunk length if multi_models, 1 otherwise
144-
145-
# reshape into (forecasted indexes, n_components, n_samples), components are interleaved
146-
forecast = forecast.reshape(X.shape[0], -1, num_samples)
144+
# reshape into (forecasted indexes, output_chunk_length, n_components, n_samples)
145+
forecast = np.moveaxis(
146+
forecast.reshape(
147+
X.shape[0],
148+
num_samples,
149+
model.output_chunk_length if model.multi_models else 1,
150+
-1,
151+
),
152+
1,
153+
-1,
154+
)
147155

148156
# extract the last sub-model forecast for each component
149157
if model.multi_models:
150-
forecast = forecast[
151-
:,
152-
(forecast_horizon - 1) * len(forecast_components) : (forecast_horizon)
153-
* len(forecast_components),
154-
:,
155-
]
158+
forecast = forecast[:, forecast_horizon - 1]
159+
else:
160+
forecast = forecast[:, 0]
156161

157162
forecasts_list.append(
158163
TimeSeries.from_times_and_values(
@@ -237,7 +242,6 @@ def _optimized_historical_forecasts_all_points(
237242

238243
# Additional shift, to account for the model output_chunk_length
239244
shift_start = 0
240-
# shift_end = 0
241245
if model.output_chunk_length > 1:
242246
# used to convert the shift into the appropriate unit
243247
unit = freq if series_.has_datetime_index else 1
@@ -296,26 +300,27 @@ def _optimized_historical_forecasts_all_points(
296300
predict_likelihood_parameters=predict_likelihood_parameters,
297301
**kwargs,
298302
)
299-
300-
# reshape and stride the forecast into (forecastable_index, forecast_horizon, n_components, num_samples)
301-
if model.multi_models:
302-
# forecast has shape ((forecastable_index_length-1)*num_samples, output_chunk_length, n_component)
303-
# and the components are interleaved
304-
forecast = forecast.reshape(
303+
# forecast has shape ((forecastable_index_length-1)*num_samples, k, n_component)
304+
# where k = output_chunk length if multi_models, 1 otherwise
305+
# reshape into (forecasted indexes, output_chunk_length, n_components, n_samples)
306+
forecast = np.moveaxis(
307+
forecast.reshape(
305308
X.shape[0],
306-
model.output_chunk_length,
307-
len(forecast_components),
308309
num_samples,
309-
)
310+
model.output_chunk_length if model.multi_models else 1,
311+
-1,
312+
),
313+
1,
314+
-1,
315+
)
316+
317+
if model.multi_models:
310318
forecast = forecast[::stride, :forecast_horizon]
311319
else:
312-
# forecast has shape ((forecastable_index_length-1)*num_samples, 1, n_component)
313-
# and the components are interleaved
314-
forecast = forecast.reshape(X.shape[0], -1, num_samples)
315-
316-
# forecasts depend on lagged data only, output_chunk_length is reconstitued by applying a sliding window
320+
# entire forecast horizon is given by multiple (previous) forecasts -> apply sliding window
317321
forecast = sliding_window_view(
318-
forecast, (forecast_horizon, len(forecast_components), num_samples)
322+
forecast[:, 0],
323+
(forecast_horizon, len(forecast_components), num_samples),
319324
)
320325

321326
# apply stride, remove the last windows, slice output_chunk_length to keep forecast_horizon values

0 commit comments

Comments
 (0)