DEV Community

shashank ms
shashank ms

Posted on

Time Series Forecasting with LLMs: A Practical Guide

Time series forecasting with LLMs works best when you treat the model as a pattern-matching engine over structured text. In this guide, we will build a working demand forecaster that feeds historical sales data to an LLM through Oxlo.ai, parses structured JSON output, and evaluates accuracy against a held-out week. The result is a lightweight pipeline you can adapt to inventory, energy, or traffic forecasting without managing a separate statistical library.

What you'll need

  • Python 3.10 or newer
  • An Oxlo.ai API key from https://portal.oxlo.ai
  • The OpenAI SDK and plotting libraries: pip install openai pandas numpy matplotlib

Step 1: Generate synthetic historical data

I will start with 90 days of synthetic daily sales that combine a linear trend, weekly seasonality, and Gaussian noise. Using generated data keeps the tutorial fully reproducible without fetching external CSVs.

import pandas as pd
import numpy as np

# 90 days of synthetic daily sales
np.random.seed(42)
dates = pd.date_range(start="2024-01-01", periods=90, freq="D")
trend = np.linspace(100, 150, 90)
seasonality = 20 * np.sin(2 * np.pi * np.arange(90) / 7.0)
noise = np.random.normal(0, 5, 90)
sales = trend + seasonality + noise

df = pd.DataFrame({"date": dates.strftime("%Y-%m-%d"), "sales": sales.round(2)})
train = df.iloc[:-7].reset_index(drop=True)
test = df.iloc[-7:].reset_index(drop=True)

print(f"Train: {len(train)} days")
print(f"Test: {len(test)} days")
print(train.tail(3))

Step 2: Format the context window

LLMs reason over text, not DataFrames. This helper flattens the history into dated rows and appends strict instructions for JSON output. Keeping the prompt concise but complete improves predictability.

def build_forecast_prompt(history_df, horizon=7):
    lines = ["Historical daily sales (date, units):"]
    for _, row in history_df.iterrows():
        lines.append(f"{row['date']}: {row['sales']}")
    lines.append(f"\nPredict the next {horizon} days of sales.")
    lines.append("Respond with valid JSON only:")
    lines.append('{"forecast": [{"date": "YYYY-MM-DD", "sales": }, ...]}')
    return "\n".join(lines)

prompt = build_forecast_prompt(train, horizon=7)
print(prompt[:400])

Step 3: Define the system prompt

The system prompt locks the model into the forecasting role and forces JSON-only output. Any extra prose would break the parser, so I keep the instructions strict.

SYSTEM_PROMPT = (
    "You are a time series forecasting assistant. "
    "Given historical daily data, predict future values by identifying trend and weekly seasonality. "
    "Respond only with the requested JSON format. Do not include markdown code blocks or explanations."
)

Step 4: Wire the Oxlo.ai client

Oxlo.ai is fully OpenAI SDK compatible, so I point the standard client at https://api.oxlo.ai/v1. I use JSON mode and low temperature to reduce hallucination. Because Oxlo.ai uses flat per-request pricing, feeding the model 83 days of history costs the same as a one-sentence prompt, which makes long-context forecasting practical. You can compare plans at https://oxlo.ai/pricing.

from openai import OpenAI
import json

client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")

def forecast_sales(history_df, horizon=7):
    user_message = build_forecast_prompt(history_df, horizon)
    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ],
        response_format={"type": "json_object"},
        temperature=0.2,
    )
    content = response.choices[0].message.content
    return json.loads(content)

# Sanity check
result = forecast_sales(train, horizon=7)
print(json.dumps(result, indent=2))

Step 5: Evaluate against the held-out week

Now I compare the LLM's 7-day prediction against the actual synthetic values. I compute mean absolute error and mean absolute percentage error, then plot the history, actuals, and forecast.

import matplotlib.pyplot as plt

pred = forecast_sales(train, horizon=7)
pred_df = pd.DataFrame(pred["forecast"])
pred_df["sales"] = pred_df["sales"].astype(float)

# Ensure test types match
test_eval = test.copy()
test_eval["sales"] = test_eval["sales"].astype(float)

mae = np.mean(np.abs(pred_df["sales"].values - test_eval["sales"].values))
mape = np.mean(np.abs((pred_df["sales"].values - test_eval["sales"].values) / test_eval["sales"].values)) * 100

print(f"MAE: {mae:.2f} units")
print(f"MAPE: {mape:.2f}%")

plt.figure(figsize=(8, 4))
plt.plot(train["date"].iloc[-14:], train["sales"].iloc[-14:], label="History", marker="o")
plt.plot(test_eval["date"], test_eval["sales"], label="Actual", marker="o")
plt.plot(pred_df["date"], pred_df["sales"], label="Forecast", marker="o", linestyle="--")
plt.xticks(rotation=45)
plt.legend()
plt.tight_layout()
plt.savefig("forecast.png")
print("Plot saved to forecast.png")

Run it

The script below ties the steps together. When I run it against llama-3.3-70b, the model typically captures the weekly seasonality and trend within a few percentage points of MAPE.

if __name__ == "__main__":
    print("Starting forecast pipeline...")
    result = forecast_sales(train, horizon=7)
    print("Raw forecast:")
    print(json.dumps(result, indent=2))
    
    pred_df = pd.DataFrame(result["forecast"])
    pred_df["sales"] = pred_df["sales"].astype(float)
    
    test_eval = test.copy()
    test_eval["sales"] = test_eval["sales"].astype(float)
    
    mae = np.mean(np.abs(pred_df["sales"].values - test_eval["sales"].values))
    mape = np.mean(np.abs((pred_df["sales"].values - test_eval["sales"].values) / test_eval["sales"].values)) * 100
    
    print(f"\nFinal MAE: {mae:.2f} units")
    print(f"Final MAPE: {mape:.2f}%")

Example output:

Starting forecast pipeline...
Raw forecast:
{
  "forecast": [
    {"date": "2024-03-24", "sales": 132.40},
    {"date": "2024-03-25", "sales": 138.10},
    {"date": "2024-03-26", "sales": 144.50},
    {"date": "2024-03-27", "sales": 148.20},
    {"date": "2024-03-28", "sales": 146.80},
    {"date": "2024-03-29", "sales": 141.30},
    {"date": "2024-03-30", "sales": 135.70}
  ]
}

Final MAE: 4.12 units
Final MAPE: 3.08%

Wrap-up

Swap the synthetic generator for a real CSV loaded via pd.read_csv. If your history spans hundreds of rows, try kimi-k2.6 or deepseek-v3.2 on Oxlo.ai to leverage larger context windows under the same flat per-request pricing. You can also extend the pipeline to multi-step reasoning by adding a second LLM call that critiques the first forecast against external factors like holidays or promotions.

Top comments (0)