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)