diff --git a/docs/source/tutorials/dlinear_v2_example.ipynb b/docs/source/tutorials/dlinear_v2_example.ipynb new file mode 100644 index 000000000..46f54dc55 --- /dev/null +++ b/docs/source/tutorials/dlinear_v2_example.ipynb @@ -0,0 +1,499 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9f56c2ab", + "metadata": {}, + "source": [ + "# DLinear v2 tutorial\n" + ] + }, + { + "cell_type": "markdown", + "id": "11ff8fe1", + "metadata": {}, + "source": [ + "
\n", + ":warning: TimeSeries, TslibDataModule, and DLinear are part of the experimental v2 API. The API is unstable and may change without notice. This notebook is a demonstration, not a production recipe. Feedback is welcome in issue 1736.\n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "id": "d48d5124", + "metadata": {}, + "source": [ + "This notebook trains **DLinear** on toy data with the v2 package API.\n", + "\n", + "DLinear splits each series into a trend and a seasonal part with a moving average, applies a linear layer to each part, and adds the two forecasts. The current v2 implementation uses continuous features only.\n", + "\n", + "The notebook builds three objects:\n", + "\n", + "1. `TimeSeries`, the raw dataset.\n", + "2. `TslibDataModule`, which windows the series and exposes `metadata`.\n", + "3. `DLinear_pkg_v2`, which calls `fit` and `predict`.\n" + ] + }, + { + "cell_type": "markdown", + "id": "e570a441", + "metadata": {}, + "source": [ + "## Toy data\n", + "\n", + "Ten short series, each a trend plus a 12-step seasonal wave. `x` is a lagged copy of the target and is unknown in the future. `future_known` is the same wave shifted into a cosine, so its future values are known at forecast time. There are no categorical columns.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fbb16d2b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-03T13:53:13.656460Z", + "iopub.status.busy": "2026-10-03T13:53:13.656161Z", + "iopub.status.idle": "2026-10-03T13:53:14.178794Z", + "shell.execute_reply": "2026-10-03T13:53:14.178158Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
series_idtime_idxyxfuture_known
0000.0496710.0496711.000000e+00
1010.5161740.0496718.660254e-01
2020.9907940.5161745.000000e-01
3031.2423030.9907946.123234e-17
4040.9626101.242303-5.000000e-01
\n", + "
" + ], + "text/plain": [ + " series_id time_idx y x future_known\n", + "0 0 0 0.049671 0.049671 1.000000e+00\n", + "1 0 1 0.516174 0.049671 8.660254e-01\n", + "2 0 2 0.990794 0.516174 5.000000e-01\n", + "3 0 3 1.242303 0.990794 6.123234e-17\n", + "4 0 4 0.962610 1.242303 -5.000000e-01" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "np.random.seed(42)\n", + "\n", + "num_series = 10\n", + "seq_length = 48\n", + "rows = []\n", + "for series_id in range(num_series):\n", + " time_idx = np.arange(seq_length)\n", + " seasonal = np.sin(2 * np.pi * time_idx / 12)\n", + " trend = 0.03 * time_idx\n", + " y = trend + seasonal + np.random.normal(0, 0.1, seq_length)\n", + " x = np.roll(y, 1)\n", + " x[0] = y[0]\n", + " future_known = np.cos(2 * np.pi * time_idx / 12)\n", + " for t in range(seq_length):\n", + " rows.append(\n", + " {\n", + " \"series_id\": series_id,\n", + " \"time_idx\": int(time_idx[t]),\n", + " \"y\": float(y[t]),\n", + " \"x\": float(x[t]),\n", + " \"future_known\": float(future_known[t]),\n", + " }\n", + " )\n", + "\n", + "data_df = pd.DataFrame(rows)\n", + "data_df.head()\n" + ] + }, + { + "cell_type": "markdown", + "id": "fbd5a14a", + "metadata": {}, + "source": [ + "## `TimeSeries`\n", + "\n", + "`TimeSeries` names the columns the rest of the pipeline uses:\n", + "\n", + "- `time`: integer time index within each series.\n", + "- `target`: column to forecast.\n", + "- `group`: columns that identify a series.\n", + "- `num`: continuous inputs.\n", + "- `known`: inputs whose future values are available.\n", + "- `unknown`: inputs available only in the history.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "61eec68f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-03T13:53:14.180112Z", + "iopub.status.busy": "2026-10-03T13:53:14.179990Z", + "iopub.status.idle": "2026-10-03T13:53:17.423703Z", + "shell.execute_reply": "2026-10-03T13:53:17.423027Z" + } + }, + "outputs": [], + "source": [ + "from pytorch_forecasting.data.timeseries import TimeSeries\n", + "\n", + "dataset = TimeSeries(\n", + " data=data_df,\n", + " time=\"time_idx\",\n", + " target=\"y\",\n", + " group=[\"series_id\"],\n", + " num=[\"x\", \"future_known\"],\n", + " known=[\"future_known\"],\n", + " unknown=[\"x\"],\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "4de64fbb", + "metadata": {}, + "source": [ + "## `TslibDataModule`\n", + "\n", + "`TslibDataModule` is the data module linked to `DLinear_pkg_v2`. `context_length` is the history window and `prediction_length` is the forecast horizon. The split is by series, so each of train, validation, and test needs at least one series. `metadata` is what the model reads for input and output sizes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "70773776", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-03T13:53:17.425215Z", + "iopub.status.busy": "2026-10-03T13:53:17.425048Z", + "iopub.status.idle": "2026-10-03T13:53:17.433445Z", + "shell.execute_reply": "2026-10-03T13:53:17.432867Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'feature_names': {'categorical': [],\n", + " 'continuous': ['x', 'future_known'],\n", + " 'static': [],\n", + " 'known': ['future_known'],\n", + " 'unknown': ['x'],\n", + " 'target': ['y'],\n", + " 'all': ['x', 'future_known'],\n", + " 'static_categorical': [],\n", + " 'static_continuous': []},\n", + " 'feature_indices': {'categorical': [],\n", + " 'continuous': [0, 1],\n", + " 'static': [],\n", + " 'known': [1],\n", + " 'unknown': [0],\n", + " 'target': [0]},\n", + " 'n_features': {'categorical': 0,\n", + " 'continuous': 2,\n", + " 'static': 0,\n", + " 'known': 1,\n", + " 'unknown': 1,\n", + " 'target': 1,\n", + " 'all': 2,\n", + " 'static_categorical': 0,\n", + " 'static_continuous': 0},\n", + " 'context_length': 16,\n", + " 'prediction_length': 4,\n", + " 'freq': 'h',\n", + " 'features': 'MS'}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sklearn.preprocessing import StandardScaler\n", + "\n", + "from pytorch_forecasting.data.data_module import TslibDataModule\n", + "from pytorch_forecasting.data.encoders import TorchNormalizer\n", + "\n", + "datamodule_cfg = dict(\n", + " context_length=16,\n", + " prediction_length=4,\n", + " batch_size=16,\n", + " train_val_test_split=(0.6, 0.2, 0.2),\n", + " target_normalizer=TorchNormalizer(),\n", + " scalers={\n", + " \"x\": StandardScaler(),\n", + " \"future_known\": StandardScaler(),\n", + " },\n", + ")\n", + "\n", + "data_module = TslibDataModule(time_series_dataset=dataset, **datamodule_cfg)\n", + "data_module.metadata\n" + ] + }, + { + "cell_type": "markdown", + "id": "c599e5d5", + "metadata": {}, + "source": [ + "## `DLinear_pkg_v2`\n", + "\n", + "`moving_avg` is the kernel width of the trend filter. `individual=False` shares one linear layer across features. Pass the same `datamodule_cfg` used above so a later `fit` on a `TimeSeries` rebuilds this module.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ff6a5fe1", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-03T13:53:17.434644Z", + "iopub.status.busy": "2026-10-03T13:53:17.434537Z", + "iopub.status.idle": "2026-10-03T13:53:17.438970Z", + "shell.execute_reply": "2026-10-03T13:53:17.438436Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'loss': MAE(), 'moving_avg': 5, 'individual': False, 'logging_metrics': [MAE(), SMAPE()], 'optimizer': 'adam', 'optimizer_params': {'lr': 0.001}}\n" + ] + } + ], + "source": [ + "from pytorch_forecasting.metrics import MAE, SMAPE\n", + "from pytorch_forecasting.models.dlinear import DLinear_pkg_v2\n", + "\n", + "model_cfg = dict(\n", + " loss=MAE(),\n", + " moving_avg=5,\n", + " individual=False,\n", + " logging_metrics=[MAE(), SMAPE()],\n", + " optimizer=\"adam\",\n", + " optimizer_params={\"lr\": 1e-3},\n", + ")\n", + "\n", + "trainer_cfg = dict(\n", + " max_epochs=5,\n", + " accelerator=\"cpu\",\n", + " devices=1,\n", + " enable_progress_bar=False,\n", + " enable_model_summary=True,\n", + " enable_checkpointing=False,\n", + " logger=False,\n", + " log_every_n_steps=1,\n", + ")\n", + "\n", + "model_pkg = DLinear_pkg_v2(\n", + " model_cfg=model_cfg,\n", + " trainer_cfg=trainer_cfg,\n", + " datamodule_cfg=datamodule_cfg,\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "ed0b12ef", + "metadata": {}, + "source": [ + "## Fit\n", + "\n", + "`fit` accepts the `TslibDataModule` built above. Passing the `TimeSeries` instead makes the package construct the module from `datamodule_cfg`. `save_ckpt=False` skips the package checkpoint. `enable_checkpointing=False` also turns off the trainer's own checkpoint. The warning filter hides Lightning's \"no logger configured\" message, which appears because the model still records metrics.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c8a45b14", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-03T13:53:17.440094Z", + "iopub.status.busy": "2026-10-03T13:53:17.440014Z", + "iopub.status.idle": "2026-10-03T13:53:18.411199Z", + "shell.execute_reply": "2026-10-03T13:53:18.410612Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + " | Name | Type | Params | Mode | FLOPs\n", + "------------------------------------------------------------------------\n", + "0 | loss | MAE | 0 | train | 0 \n", + "1 | logging_metrics | ModuleList | 0 | train | 0 \n", + "2 | decomposition | SeriesDecomposition | 0 | train | 0 \n", + "3 | linear_seasonal | Linear | 68 | train | 0 \n", + "4 | linear_trend | Linear | 68 | train | 0 \n", + "------------------------------------------------------------------------\n", + "136 Trainable params\n", + "0 Non-trainable params\n", + "136 Total params\n", + "0.001 Total estimated model params size (MB)\n", + "9 Modules in train mode\n", + "0 Modules in eval mode\n", + "0 Total Flops\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "`Trainer.fit` stopped: `max_epochs=5` reached.\n" + ] + } + ], + "source": [ + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\", message=\".*no logger configured.*\")\n", + "\n", + "model_pkg.fit(data_module, save_ckpt=False)\n" + ] + }, + { + "cell_type": "markdown", + "id": "6aa6cd87", + "metadata": {}, + "source": [ + "## Predict\n", + "\n", + "`predict` returns a dict. With a point loss such as `MAE`, `prediction` has shape `(n_windows, prediction_length)`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f6773336", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-03T13:53:18.412583Z", + "iopub.status.busy": "2026-10-03T13:53:18.412488Z", + "iopub.status.idle": "2026-10-03T13:53:18.599661Z", + "shell.execute_reply": "2026-10-03T13:53:18.599146Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prediction shape: (290, 4)\n", + "first forecast: [0.82, 0.623, 0.302, 0.105]\n" + ] + } + ], + "source": [ + "preds = model_pkg.predict(\n", + " data_module,\n", + " trainer_kwargs=dict(\n", + " accelerator=\"cpu\",\n", + " logger=False,\n", + " enable_progress_bar=False,\n", + " enable_model_summary=False,\n", + " enable_checkpointing=False,\n", + " ),\n", + ")\n", + "\n", + "print(\"prediction shape:\", tuple(preds[\"prediction\"].shape))\n", + "print(\"first forecast:\", [round(v, 3) for v in preds[\"prediction\"][0].tolist()])\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/tutorials_v2.rst b/docs/source/tutorials_v2.rst index bebab6f39..d05c7b63d 100644 --- a/docs/source/tutorials_v2.rst +++ b/docs/source/tutorials_v2.rst @@ -15,3 +15,4 @@ The following tutorials for version 2 can be also found as `notebooks on GitHub tutorials/ptf_V2_example tutorials/tslib_v2_example + tutorials/dlinear_v2_example