> ## Documentation Index
> Fetch the complete documentation index at: https://docs.priorlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Interpreting Time-Series Forecasts with TabPFN-TS

> Explain what drives a TabPFN-TS forecast with partial dependence, Window SHAP, and time-series decomposition

<div className="cookbook-meta">
  <div className="cookbook-authors">
    <div className="cookbook-author-bar">
      <span className="cookbook-author-by">By</span>
      <span className="cookbook-author-list"><span className="cookbook-author-entry"><span className="cookbook-author-name">Prior Labs</span><span className="cookbook-author-links"><a href="https://www.linkedin.com/company/prior-labs" className="cookbook-author-icon-link" aria-label="LinkedIn" target="_blank" rel="noopener noreferrer"><svg className="cookbook-author-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 1 1 0-4.124 2.062 2.062 0 0 1 0 4.124zM7.119 20.452H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" /></svg></a><a href="https://twitter.com/prior_labs" className="cookbook-author-icon-link" aria-label="X" target="_blank" rel="noopener noreferrer"><svg className="cookbook-author-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg></a></span></span></span>
    </div>
  </div>

  <div className="cookbook-colab">
    <a href="https://colab.research.google.com/github/PriorLabs/tabpfn-cookbook/blob/main/notebooks/time_series_interpretability.ipynb" className="cookbook-colab-button" target="_blank" rel="noopener noreferrer">
      <svg className="cookbook-colab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path fill="#F9AB00" d="M16.9414 4.9757a7.033 7.033 0 0 0-4.9308 2.0646 7.033 7.033 0 0 0-.1232 9.8068l2.395-2.395a3.6455 3.6455 0 0 1 5.1497-5.1478l2.397-2.3989a7.033 7.033 0 0 0-4.8877-1.9297zM7.07 4.9855a7.033 7.033 0 0 0-4.8878 1.9316l2.3911 2.3911a3.6434 3.6434 0 0 1 5.0227.1271l1.7341-2.9737-.0997-.0802A7.033 7.033 0 0 0 7.07 4.9855zm15.0093 2.1721l-2.3892 2.3911a3.6455 3.6455 0 0 1-5.1497 5.1497l-2.4067 2.4068a7.0362 7.0362 0 0 0 9.9456-9.9476zM1.932 7.1674a7.033 7.033 0 0 0-.002 9.6816l2.397-2.397a3.6434 3.6434 0 0 1-.004-4.8916zm7.664 7.4235c-1.38 1.3816-3.5863 1.411-5.0168.1134l-2.397 2.395c2.4693 2.3328 6.263 2.5753 9.0072.5455l.1368-.1115z" />
      </svg>

      <span className="cookbook-colab-label">Open in Colab</span>
    </a>
  </div>
</div>

*With partial dependence, Window SHAP, and additive decomposition.*

Forecasts that we cannot explain rarely help us make better decisions. In this notebook, we forecast German day-ahead electricity prices and then explain the results with three different methods:

* Partial Dependence Plots: to understand how the forecast reacts to different feature values
* Window SHAP: SHAP values of different features and feature groups over time
* Series Decomposition: plotting the different components (trend, seasonality and residuals) of the series

With this electricity use-case, it is easy to understand how different factors affect the price. Electricity demand is seasonal, we see hour-of-day and weekday patterns. Demand increases prices, supply (solar+wind) decreases prices. Let's start interpreting some forecasts!

## Setup

*Installing TabPFN-TS with its optional explainability tools.*

The explainability extra includes `shapiq` for grouped Shapley values and Matplotlib for the plotting helpers. `pyarrow` is used to read the compact Parquet dataset.

```python theme={null}
!pip install "tabpfn-time-series[explainability]" pyarrow
```

```python theme={null}
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from tabpfn_time_series import TabPFNMode, TabPFNTSPipeline
from tabpfn_time_series.explainability import (
    TabPFNTSExplainer,
    plot_decomposition,
    plot_pdp_grid,
    plot_window_shap_spectrogram,
)

plt.rcParams.update({
    "axes.spines.top": False,
    "axes.spines.right": False,
    "axes.titleweight": "bold",
    "figure.dpi": 120,
})
```

## Loading the Data

*Two weeks of hourly German electricity prices, demand, and renewable generation.*

The target is the German day-ahead electricity price. We use both time-based features computed automatically by the `TabPFNTSPipeline` and two known covariates: Amprion's load to model demand and combined photovoltaic and wind forecast to model supply.

```python theme={null}
DATA_URL = (
    "https://autogluon.s3.amazonaws.com/datasets/timeseries/"
    "electricity_price/train.parquet"
)
COVARIATES = ["Amprion Load Forecast", "PV+Wind Forecast"]

raw = pd.read_parquet(DATA_URL)
electricity = (
    raw.loc[raw["id"] == "DE"]
    .assign(timestamp=lambda frame: pd.to_datetime(frame["timestamp"]))
    .sort_values("timestamp")
    .head(360)
    .drop(columns="id")
    .rename(columns={"Ampirion Load Forecast": "Amprion Load Forecast"})
    .reset_index(drop=True)
)

print(
    f"Loaded {len(electricity)} hourly observations from "
    f"{electricity.timestamp.min():%d %b} to {electricity.timestamp.max():%d %b %Y}."
)
```

```console theme={null}
Loaded 360 hourly observations from 09 Jan to 23 Jan 2012.
```

### Visualising the series

We start by plotting the data. We can see seasonal patterns with some shocks, including a negative price event.

```python theme={null}
COLORS = {
    "target": "#6D28D9",
    "Amprion Load Forecast": "#F97316",
    "PV+Wind Forecast": "#10B981",
}
LABELS = {
    "target": "Day-ahead price",
    "Amprion Load Forecast": "Load forecast",
    "PV+Wind Forecast": "PV + wind forecast",
}

fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)
for ax, column in zip(axes, ["target", *COVARIATES]):
    ax.plot(
        electricity["timestamp"], electricity[column],
        color=COLORS[column], linewidth=1.7,
    )
    ax.set_ylabel(LABELS[column])
    ax.grid(axis="y", alpha=0.2)

for day in pd.date_range(
    electricity.timestamp.min().normalize(),
    electricity.timestamp.max().normalize(),
    freq="D",
):
    if day.dayofweek >= 5:
        for ax in axes:
            ax.axvspan(day, day + pd.Timedelta(days=1), color="#E5E7EB", alpha=0.5)

price_low = electricity["target"].idxmin()
axes[0].annotate(
    "negative-price event",
    xy=(electricity.loc[price_low, "timestamp"], electricity.loc[price_low, "target"]),
    xytext=(25, -30), textcoords="offset points",
    arrowprops={"arrowstyle": "->", "color": "#374151"},
)
axes[0].set_title("Two weeks in the German electricity market")
axes[-1].set_xlabel("Timestamp (shaded bands are weekends)")
fig.tight_layout()
plt.show()
```

![Visualising the series](https://raw.githubusercontent.com/PriorLabs/tabpfn-cookbook/main/visuals/time_series_interpretability/plot-01.png)

## Fit and Forecast

*Holding out the final day so the forecast can be seen against reality.*

TabPFN-TS turns the series into a tabular regression problem using calendar, trend, and automatically detected seasonal features. We give it seven days of context and the future load and renewables forecasts, then predict the final 24 hours.

```python theme={null}
HORIZON = 24
CONTEXT_LENGTH = 168

pipeline = TabPFNTSPipeline(
    tabpfn_mode=TabPFNMode.LOCAL,
    max_context_length=CONTEXT_LENGTH,
    tabpfn_model_config={"device": "auto"},
)
explainer = TabPFNTSExplainer(pipeline)

context = electricity.iloc[-(CONTEXT_LENGTH + HORIZON):-HORIZON].copy()
future = electricity.iloc[-HORIZON:].drop(columns="target").copy()
actual = electricity.iloc[-HORIZON:].copy()
forecast = pipeline.predict_df(context, future_df=future)
```

```python theme={null}
fig, ax = plt.subplots(figsize=(12, 4.5))
visible_context = context.tail(96)
ax.plot(
    visible_context["timestamp"], visible_context["target"],
    color="#9CA3AF", linewidth=1.5, label="Observed context",
)
ax.plot(
    actual["timestamp"], actual["target"],
    color="#111827", linewidth=2.2, label="Held-out actual",
)
ax.plot(
    future["timestamp"], forecast["target"],
    color="#6D28D9", linewidth=2.2, linestyle="--", label="TabPFN-TS forecast",
)
ax.axvspan(future.timestamp.min(), future.timestamp.max(), color="#EDE9FE", alpha=0.45)
ax.axvline(future.timestamp.min(), color="#6B7280", linestyle=":")
ax.set(
    title="Forecasting the final 24 hours",
    xlabel="Timestamp",
    ylabel="Day-ahead electricity price",
)
ax.legend(ncols=3, frameon=False, loc="upper left")
ax.grid(axis="y", alpha=0.2)
fig.tight_layout()
plt.show()
```

![Fit and Forecast](https://raw.githubusercontent.com/PriorLabs/tabpfn-cookbook/main/visuals/time_series_interpretability/plot-02.png)

## Partial Dependence

*How the average forecast changes as one input moves.*

To better understand how the model responds to changes in its features, we built a specific time-series Partial Dependence Plot. We plot the average forecast value over several forecast windows as we change one of the feature values.

We make sure to keep calendar features in the time domain (`hour_of_day`) from 0 to 23, to better understand their impact on the target variable.

Looking at the exogenous variables (Amprion's load and PV+Wind forecasts), we see the relationships that we expect. Higher demand leads to higher prices. Higher supply is associated with lower prices.

```python theme={null}
pdps = {}
for feature in ["hour_of_day", "day_of_week", *COVARIATES]:
    pdps[feature] = explainer.partial_dependence(
        electricity,
        feature,
        prediction_length=HORIZON,
        context_length=CONTEXT_LENGTH,
        n_contexts=3,
    )

fig = plot_pdp_grid(pdps, ncols=2)
fig.set_size_inches(12, 7)
fig.suptitle(
    "What moves the forecast?\n"
    "Lines show the mean response; bands show variation across forecast windows",
    y=1.03, fontsize=13,
)
plt.show()
```

![Partial Dependence](https://raw.githubusercontent.com/PriorLabs/tabpfn-cookbook/main/visuals/time_series_interpretability/plot-03.png)

## Window SHAP

*A feature-by-time map of what pushed each forecast up or down.*

The above plots are nice, but they do not tell us what drove the forecast on a given day. To do so, we implemented a Window SHAP that shows SHAP values of different features and feature groups for several time windows. This may remind you of a spectrogram.

The blue cells push the value down, and the red cells push the value up. For example, on the 21st of January, the Amprion Load forecast pushed the forecast value down significantly.

Here again, we made sure to keep calendar columns in the time domain instead of using raw Fourier-based encodings.

```python theme={null}
shap_by_window = explainer.window_shap(
    electricity,
    prediction_length=HORIZON,
    context_length=CONTEXT_LENGTH,
    n_windows=8,
    budget=64,
)

fig, ax = plt.subplots(figsize=(12, 5))
plot_window_shap_spectrogram(shap_by_window, ax=ax)
ax.set_title(
    "Why the forecast changes over time\n"
    "red = pushes price up, blue = pushes price down"
)
fig.tight_layout()
plt.show()
```

![Window SHAP](https://raw.githubusercontent.com/PriorLabs/tabpfn-cookbook/main/visuals/time_series_interpretability/plot-04.png)

### Which groups matter most overall?

*Ranking the same attributions by average absolute contribution.*

Beyond direction, we can also calculate the magnitude of the SHAP values for each feature over the different time windows. This answers the question: which feature groups had the largest influence across all inspected windows?

```python theme={null}
importance = shap_by_window.abs().mean(axis=1).sort_values()

fig, ax = plt.subplots(figsize=(9, 4.5))
bars = ax.barh(importance.index, importance.values, color="#6D28D9", alpha=0.85)
ax.bar_label(bars, fmt="%.2f", padding=4, fontsize=9)
ax.set(
    title="Overall influence across forecast windows",
    xlabel="Mean absolute Shapley value",
    ylabel="",
)
ax.grid(axis="x", alpha=0.2)
fig.tight_layout()
plt.show()
```

![Which groups matter most overall?](https://raw.githubusercontent.com/PriorLabs/tabpfn-cookbook/main/visuals/time_series_interpretability/plot-05.png)

## Decomposing the Observed Series

*Separating trend, daily rhythm, weekly rhythm, and surprises.*

The previous tools interpret model predictions. Decomposition is a model-free description of the target. It is built using an additive formulation:

`observed = trend + hour_of_day + day_of_week + residual`

This is particularly useful for time-series to better understand the underlying structure of a signal.

```python theme={null}
decomposition = explainer.decompose(
    electricity,
    features=["hour_of_day", "day_of_week"],
    period=24,
)

fig = plot_decomposition(decomposition)
fig.set_size_inches(12, 9)
fig.suptitle(
    "What structure lives in the observed price series?\n"
    "Recurring patterns are separated from events left in the residual",
    y=1.02, fontsize=13,
)
plt.show()
```

![Decomposing the Observed Series](https://raw.githubusercontent.com/PriorLabs/tabpfn-cookbook/main/visuals/time_series_interpretability/plot-06.png)

## Reading the Explanations Together

*Each view answers a different question.*

* **Partial dependence** shows the average response shape for one input at a time.
* **Window SHAP** shows which feature groups push forecasts up or down, and when their influence changes.
* **Decomposition** shows the recurring structure and exceptional events in the observed target.

These tools enable us to explain the forecasts generated by TabPFN and make better decisions. Try it out for yourself!
