When Traditional Forecasting Hits a Wall
ARIMA models are fine for univariate time series with stable patterns. Prophet handles seasonality and holidays without much tuning. But once you're dealing with multiple related time series, exogenous variables that influence the target at different time horizons, and complex temporal patterns that change over time, that's where Temporal Fusion Transformers start to shine.
I've deployed TFT models for demand forecasting in retail, energy load prediction, and financial metric projection. The architecture handles all three well, which is unusual. Let me walk through why TFT works and the practical details that papers tend to skip.
Architecture Walkthrough
TFT combines several ideas that individually aren't new but together create something more capable than the sum of its parts. The key components are: variable selection networks, static enrichment, temporal self-attention, and quantile outputs.
The variable selection network is what makes TFT practical for real-world data. Instead of manually selecting which features to include, the network learns feature importance weights. This is particularly useful when you have dozens of potential covariates and don't know which ones matter for which prediction horizons.
import pytorch_forecasting
from pytorch_forecasting import TemporalFusionTransformer, TimeSeriesDataSet
training = TimeSeriesDataSet(
data[lambda x: x.date < "2024-06-01"],
time_idx="time_idx",
target="sales",
group_ids=["store_id", "product_id"],
max_encoder_length=90,
max_prediction_length=30,
static_categoricals=["store_type", "region"],
static_reals=["store_size", "avg_income"],
time_varying_known_categoricals=["day_of_week", "month", "holiday"],
time_varying_known_reals=["temperature", "planned_promotions"],
time_varying_unknown_reals=["sales", "foot_traffic"],
target_normalizer=pytorch_forecasting.data.GroupNormalizer(
groups=["store_id", "product_id"],
transformation="softplus"
),
)
train_dataloader = training.to_dataloader(train=True, batch_size=64, num_workers=4)
Static vs Temporal Features
One of TFT clever design choices is how it separates static features from temporal features. Static features get processed through a separate network and then used to condition the temporal processing via a gated residual network. Think of it as: the static features set the context, and the temporal features operate within that context.
In my retail forecasting work, this separation improved accuracy by about 8 percent compared to a model that just concatenated all features. The reason makes intuitive sense. A store type doesn't change day-to-day, but it fundamentally affects how the store responds to promotions, seasonality, and weather.
Training and Hyperparameter Tuning
TFT has more hyperparameters than simpler models, but in my experience, most of them have reasonable defaults. The ones that actually matter for performance are:
We covered a related topic in GPU Resource Management for Shared ML Training Clusters.
Hidden size: 64-256 depending on dataset complexity. Bigger isn't always better here. I've seen 128 outperform 256 on datasets with strong seasonal patterns because the larger model overfits to noise in the training period.
Attention head count: 4 is the sweet spot for most time series. Going higher adds parameters without meaningfully improving the temporal attention patterns.
Learning rate: We use a one-cycle scheduler starting at 1e-3, which pytorch-forecasting sets up automatically. The trick is getting the batch size right. Too small and the gradient estimates are noisy; too large and you lose generalization. 64-128 works for most datasets.
tft = TemporalFusionTransformer.from_dataset(
training,
learning_rate=1e-3,
hidden_size=160,
attention_head_size=4,
dropout=0.1,
hidden_continuous_size=32,
output_size=7,
loss=pytorch_forecasting.metrics.QuantileLoss(),
optimizer="ranger",
)
trainer = pl.Trainer(
max_epochs=50,
accelerator="gpu",
gradient_clip_val=0.1,
callbacks=[
pl.callbacks.EarlyStopping(
monitor="val_loss", patience=5, mode="min"
),
],
)
trainer.fit(tft, train_dataloaders=train_dataloader,
val_dataloaders=val_dataloader)
Interpretability: The Underrated Feature
The attention weights in TFT aren't just a model debugging tool. They're a product feature. When stakeholders ask why does the model predict a sales spike next Tuesday, you can point to the attention weights showing that the model is paying attention to the promotional calendar entry and a similar pattern from the same week last year.
The variable importance outputs are equally useful. They tell you which features the model is actually using, and more importantly, which features it's ignoring. I once discovered that a feature we thought was critical (competitor pricing data) had near-zero importance because it was highly correlated with another feature (our own pricing) that the model found more reliable.
Production Deployment Patterns
TFT models are heavier than traditional forecasting models, and you need to plan for that. A single forecast for one product at one store takes about 15ms on a T4 GPU. That's fine for batch forecasting jobs but too slow for real-time serving if you need to forecast across thousands of combinations on demand.
See also: Attention Mechanism Variants Beyond Standard Self-Attention.
Our production setup runs batch inference nightly, generating forecasts for all product-store combinations for the next 30 days. The results go into a feature store (Feast) that the serving layer reads from. When a forecast is needed, it's a simple key-value lookup, not a model inference call.
For intra-day updates, like incorporating a flash sale that just started, we keep a lightweight linear model that adjusts the TFT forecast based on real-time signals. This two-model approach gives us the accuracy of TFT with the latency of a simple model.
Handling Missing Data and Irregular Time Steps
Real-world time series data is rarely complete. Sensors go offline, stores close on holidays, and data collection systems have outages. TFT handles missing values in the encoder (historical) window reasonably well because the attention mechanism can learn to ignore gaps. But missing values in the known future covariates (like planned promotions) cause more problems because the model was trained to expect them.
Our approach: for historical missing values, we forward-fill with the last known value and add a binary indicator feature (is_imputed) to each time step. The model learns to weight imputed values less heavily. For future covariates, we fill with the most recent non-missing value or a domain-specific default (zero for promotions, seasonal average for temperature).
Irregular time steps are a related challenge. If your data has a mix of daily and weekly observations, or if some time series start later than others, you need to handle this in the data preparation. The TimeSeriesDataSet in pytorch-forecasting handles variable-length histories, but you need to set the encoder length to accommodate the longest reasonable history, and shorter series will be padded.
Multi-Horizon Forecasting Trade-offs
TFT can predict multiple horizons simultaneously, but in my experience, single-horizon accuracy degrades as you increase the prediction length. A model predicting 7 days ahead will be more accurate than a model predicting 30 days ahead for the same 7-day window. This is expected, but the magnitude of the degradation varies by domain.
Related reading: Model Serving Latency Optimization: Batching, Caching, and H.
For retail demand forecasting, we see roughly 2 percent MAPE increase per additional week of prediction horizon. The first week is usually within 8 percent MAPE; the fourth week is closer to 15 percent. If your business only needs 7-day forecasts, don't train for 30 days. The extra prediction horizon adds model complexity without benefiting the time horizon you actually care about.
For energy load forecasting, the degradation is steeper because energy demand is heavily influenced by weather, and weather forecasts lose accuracy quickly beyond 5-7 days. We address this by using ensemble weather forecasts (10+ model runs) as covariates rather than a single deterministic forecast. The ensemble spread captures the increasing uncertainty, and TFT attention mechanism learns to weight certain ensemble members more heavily for different horizons.
Common Failure Modes
Three failure modes that I've encountered repeatedly with TFT in production:
First, the model memorizes seasonal patterns from the encoder but fails to generalize when seasonality shifts. This happens when you have limited training data (less than 2 full seasonal cycles) or when external factors (like a pandemic) permanently change the seasonal pattern. The fix is to include explicit seasonal features (month, week-of-year) as known future covariates rather than relying on the encoder to infer seasonality from raw history.
Second, the model produces overconfident predictions when the quantile loss is dominated by the median. If your loss weights are uniform across quantiles, the median (q=0.5) gets the most gradient signal because it has the smallest expected loss. The tails (q=0.05, q=0.95) are noisier and converge more slowly. We use a weighted quantile loss that upweights the tails during training.
Third, the model struggles with sudden level shifts (like a store renovation that doubles foot traffic). The attention mechanism can identify similar patterns from history, but if the level shift is genuinely unprecedented, the model will underpredict. For known future events that will cause level shifts, adding them as binary covariates helps. For unknown shifts, you need an online learning component that quickly adapts the forecast based on the first few observations after the shift.