Time-series forecasting is the task of predicting future values based on past observations. A time series is a sequence of data points collected or recorded at successive points in time, often at uniform intervals.
Typical examples include sales, electricity demand, prices, web traffic, and sensor readings. Unlike a regular regression problem, where the order of the data points does not matter, time-series forecasting requires that the order of the observations is preserved. This is important to capture trends, like day/night cycles, day of the week effects, and seasonal patterns. For example, electricity demand might be higher during the day than at night, and sales might be higher on weekends than on weekdays.
The reason to do time-series forecasting is to anticipate future demand and make decisions accordingly. Forecasts help organizations plan inventory, staffing, capacity, and energy purchases before demand occurs.
Common time-series forecasting methods include statistical models, machine learning models, and deep learning models. Statistical models are often simpler and easier to interpret, while machine learning and deep learning models can capture complex patterns in the data. However, all of these methods typically require training on historical data before they can make predictions.
In this blog post we take a look at a new approach: zero-shot forecasting with a pretrained time-series foundation model. This approach allows us to forecast a new input series without first fitting a model to that series.
TimesFM is a pretrained time-series foundation model from Google Research. It can do zero-shot forecasting without any additional training. The model is trained on a large and diverse set of time-series data, which allows it to generalize to new time series that it has not seen before.
TimesFM ¶
TimesFM uses a patched, decoder-only architecture. Instead of treating words as tokens, it groups consecutive time points into patches and predicts future patches. The TimesFM paper describes the architecture and its zero-shot evaluation in more detail.
Current version of the model is TimesFM 2.5, which is available as a PyTorch checkpoint on Hugging Face. The checkpoint is 925 MB and contains the model weights for the 200-million-parameter version of TimesFM 2.5.
To access the model, Google provides a Python library called timesfm on GitHub. The library provides a simple interface for loading the model and making forecasts in Python.
The model and code are licensed under the Apache 2.0 license. The library and model checkpoints are not officially supported Google products. Check out TimesFM on BigQuery for an officially supported product that uses TimesFM for time-series forecasting.
Setup ¶
For this example I use uv to manage the Python environment. The project requires at least Python 3.11 and includes the latest timesfm package for pytorch.
requires-python = ">=3.11"
dependencies = [
"jax>=0.4.38",
"matplotlib>=3.10.0",
"numpy>=2.2.0",
"pandas>=2.2.0",
"scikit-learn>=1.6.0",
"statsmodels>=0.14.4",
"timesfm[torch]==2.0.2",
"yfinance>=0.2.60",
]
Install the dependencies with:
uv sync
The first time you start the application the timesfm library downloads the model checkpoint from Hugging Face. The library caches the checkpoint in ~/.cache/huggingface/hub for future runs. The PyTorch implementation of the timesfm library uses CUDA when it is available and otherwise runs on the CPU.
Loading the model ¶
To load the model call the from_pretrained() method and then compile the model with a ForecastConfig object.
def load_model(
max_context: int,
max_horizon: int,
*,
return_backcast: bool = False,
) -> timesfm.TimesFM_2p5_200M_torch:
model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
"google/timesfm-2.5-200m-pytorch"
)
model.compile(
timesfm.ForecastConfig(
max_context=max_context,
max_horizon=max_horizon,
normalize_inputs=True,
use_continuous_quantile_head=True,
force_flip_invariance=True,
fix_quantile_crossing=True,
return_backcast=return_backcast,
)
)
return model
The config object specifies:
max_context: the maximum number of past observations to use for forecasting. The model can handle up to 16,384 observations.max_horizon: the maximum number of future observations to forecast. The maximum value is 512.normalize_inputs: whether to normalize the input values before forecasting. Normalizing means scaling the input values to have zero mean and unit variance. This can help the model to make more accurate forecasts, especially when the input values have different scales or units.use_continuous_quantile_head: whether to use a continuous quantile head for forecasting. A quantile is a value that divides the distribution of the forecast into intervals. For example, the 0.5 quantile is the median, which is the value that separates the lower half of the distribution from the upper half. The continuous quantile head allows the model to predict multiple quantiles at once, which can provide more information about the uncertainty of the forecast.force_flip_invariance: whether to enforce flip invariance in the model. Flip invariance means that the model's predictions should be the same regardless of whether the input values are flipped (i.e., reversed in order).fix_quantile_crossing: whether to fix quantile crossing issues. Quantile crossing occurs when a lower quantile prediction is higher than a higher quantile prediction, which is logically inconsistent. This option ensures that the predicted quantiles are properly ordered.return_backcast: whether to return the backcast values along with the forecast. The backcast is the model's reconstruction of the input values based on the learned patterns. This can be useful for understanding how well the model captures the historical data.
Forecasting ¶
The TimesFM model is a zero-shot model, which means it can forecast new time series without any additional training. The forecast() method takes a horizon and a list of input series. horizon is the number of future observations to forecast, and inputs is a list of one or more time series. Each time series is an ordered array of values. The array can be a NumPy array, a Python list, or a PyTorch tensor. The model does not require timestamps or any other information about the time series.
history = build_series()
model = load_model(max_context=512, max_horizon=48)
point_forecast, quantile_forecast = model.forecast(horizon=48, inputs=[history])
The method forecast() returns two arrays: the point forecast and the quantile forecast.
point forecast is a single array of predicted values for the specified horizon. The quantile forecast is a two-dimensional array of predicted quantiles for the specified horizon. The first array contains the p50, or median, forecast. The second array contains the mean followed by p10, p20, through p90.
TimesFM 2.5 supports covariates through its separate XReg API. Covariates are additional time series that can help the model to make more accurate forecasts. For example, a covariate could be a weather forecast, a holiday calendar, or a promotional campaign.
Here a simple example of how to use the XReg API:
covariates = np.array([[1, 0], [0, 1], [1, 1], [0, 0]])
point_forecast, quantile_forecast = model.forecast(
horizon=horizon,
inputs=[history],
xreg=covariates,
)
A covariate array is a two-dimensional array where each row corresponds to a time point in the input series and each column corresponds to a covariate.
Demos ¶
Next we take a look at four different time series and how TimesFM can forecast them.
Concert event spike ¶
The first example simulates a three-day concert event with a spike in hourly data. We assume that we have some kind of a demand that increases during the event. The first few days are normal. Then the event starts and the demand increases. The values are all randomly generated.
history = build_series()
model = load_model(max_context=512, max_horizon=48)
point_forecast, quantile_forecast = model.forecast(horizon=48, inputs=[history])
The demo uses matplotlib to plot the input series and the forecast. The chart shows the observed values in blue, the p50 forecast in orange, and the p10-p90 interval as blue shading.
The forecast stays at the elevated level and continues the daily oscillation. It does not return to the pre-event baseline. This result makes sense given the input: the elevated period runs all the way to the forecast boundary, and the model receives no information that the event is about to end.
The p10-p90 interval also becomes wider as the forecast horizon increases. That shows increasing predictive dispersion from the model, but it does not tell us why the interval widens or whether the model identified the event as a one-off occurrence.
We can give the model more information about the event by adding a covariate that indicates whether the event is happening or not. The covariate in this example is a binary array with 1 for the event days and 0 for the normal days.
def build_series_and_covariate() -> tuple[np.ndarray, np.ndarray]:
hours = np.arange(14 * 24)
daily_cycle = 40 + 8 * np.sin(2 * np.pi * hours / 24)
weekly_cycle = 4 * np.sin(2 * np.pi * hours / (24 * 7))
history = daily_cycle + weekly_cycle
event_active = np.zeros(len(history) + HORIZON, dtype=np.float32)
event_start = len(history) - 72
event_active[event_start : len(history)] = 1
history[event_start : event_start + 72] += 28
history[event_start + 24 : event_start + 48] += 12
return history.astype(np.float32), event_active
def main() -> None:
history, event_active = build_series_and_covariate()
model = load_model(
max_context=512,
max_horizon=HORIZON,
return_backcast=True,
)
point_forecasts, quantile_forecasts = model.forecast_with_covariates(
inputs=[history],
dynamic_numerical_covariates={"event_active": [event_active]},
xreg_mode="xreg + timesfm",
ridge=0.01,
force_on_cpu=True,
)
With the covariate, the model can see that the event is ending and can forecast a return to the baseline. The chart shows that the p50 forecast drops back to the normal level after the event ends, and the p10-p90 interval narrows as well.
Walmart weekly sales ¶
The second example uses the Walmart Recruiting - Store Sales Forecasting dataset. The local train.csv contains 421,570 store-department rows for 143 Fridays between February 2010 and October 2012.
The demo application aggregates all store-department rows in the dataset into one weekly series:
frame = pd.read_csv(csv_path, usecols=["Date", "Weekly_Sales"])
frame["Date"] = pd.to_datetime(frame["Date"])
weekly_sales = frame.groupby("Date")["Weekly_Sales"].sum().sort_index()
Then it loads the model and forecasts the next 12 weeks:
weekly_sales = load_weekly_sales(args.csv)
output_dir = ensure_output_dir("walmart-sales")
decomposition_path = save_decomposition_plot(weekly_sales, output_dir)
history = weekly_sales.to_numpy(dtype="float32")
model = load_model(max_context=len(history), max_horizon=args.horizon)
point_forecast, quantile_forecast = model.forecast(
horizon=args.horizon, inputs=[history]
)
The observed series contains two large year-end peaks. The seasonal component captures these recurring peaks, while the trend changes much more gradually.
NVIDIA adjusted closes ¶
In the third example we forecast the adjusted closing prices of NVIDIA stock. The data is downloaded from Yahoo Finance using the yfinance library. The script selects a rolling two-year window of daily data and uses the adjusted closing prices for forecasting.
prices = yf.download(
"NVDA", period="2y", interval="1d", auto_adjust=True, progress=False
)
close = prices["Close"]
if getattr(close, "ndim", 1) != 1:
close = close.squeeze("columns")
close_series = close.dropna().to_numpy(dtype="float32")
if len(close_series) < 200:
raise RuntimeError("Expected at least 200 closing prices from Yahoo Finance")
history = close_series[-512:]
The model forecasts 30 trading observations, not 30 calendar days. That usually spans about six calendar weeks.
history = close_series[-512:]
model = load_model(max_context=512, max_horizon=30)
point_forecast, quantile_forecast = model.forecast(horizon=30, inputs=[history])
The p50 forecast stays nearly flat and close to the last observed value, while the p10-p90 interval expands substantially over the 30 steps. The model sees only the price history; it does not see trading volume, earnings, news, the market calendar, or broader market data.
German electricity load ¶
The final example uses the Open Power System Data time-series package, version 2020-10-06. The selected DE_load_actual_entsoe_transparency column contains Germany's total load in MW as published by the ENTSO-E Transparency Platform.
The script downloads the hourly CSV, forward-fills missing values, gives TimesFM the final 28 days as context, and forecasts 168 hours. The chart displays only the last seven days of the input next to the seven-day forecast.
def load_energy_series() -> np.ndarray:
try:
frame = pd.read_csv(URL, index_col=0, parse_dates=True)
return frame[COLUMN].ffill().dropna().to_numpy(dtype="float32")
load = load_energy_series()
history = load[-24 * 28 :]
model = load_model(max_context=len(history), max_horizon=24 * 7)
point_forecast, quantile_forecast = model.forecast(horizon=24 * 7, inputs=[history])
The p50 forecast continues the strong within-day cycle and the differences between days visible in the history. The predictive interval grows over the week but remains structured around the recurring load pattern.
Conclusion ¶
TimesFM makes it easy to produce a zero-shot baseline for very different univariate time series. The same forecast() call works for hourly demand, weekly sales, daily market data, and hourly electricity load.
With the XReg API, the model can also use covariates to improve forecasts. The covariates can be any additional time series that might influence the target series, such as weather, holidays, or promotions.
As with all forecasting models, the quality of the forecast depends on the quality and quantity of the input data. TimesFM can provide a good starting point for forecasting, but it may not always capture all the nuances of a particular time series. It is important to evaluate the forecasts and consider additional modeling techniques or domain knowledge when necessary.