Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

使用 DeepSeek API 进行预测

北京大学光华管理学院

本章把大语言模型从“聊天工具”放回预测流程中:我们给模型一段结构化的时间序列历史,让它按指定格式返回未来若干期的点预测,再把结果解析成数据表并画图检查。这里的重点不是证明 DeepSeek 一定优于统计模型,而是训练一个可复现的 API 工作流:数据准备、提示词约束、请求发送、JSON 解析、结果检查和误差评估。

把 CSV 上传到网页对话框,可以快速探索一个序列;但这种方式依赖人工操作,不容易复现,也不方便批量评价。API 的价值在于把同一个任务变成程序:同样的数据格式、同样的 prompt、同样的输出 schema、同样的解析和检查。只有这样,LLM 输出才有机会进入正式预测实验。

学习目标

完成本章后,你应该能够:

核心思路

DeepSeek API 的角色可以理解为一个远程模型服务(remote model service)。Notebook 不直接训练模型,而是把历史序列、任务说明和输出格式发送给 API。API 返回文本,文本里包含预测结果。课程里的 notebook 采用三个约束来降低不可复现风险:

  1. 历史数据整理成 unique_id, ds, y 三列。

  2. 提示词明确预测步长 h 和频率 freq

  3. 返回结果要求为严格 JSON,字段包含 forecast, ds, yhat

这三个约束都很重要。时间序列预测不是只问“下个月是多少”;它还需要知道历史数据的时间间隔、预测几期、是否只预测一个序列、返回值能否被程序继续处理。

一个可复现的 API 预测实验可以看成六个环节:

环节作用失败时的后果
结构化输入明确序列、日期和目标变量模型猜错列含义或时间顺序
约束 prompt明确任务、频率、步长和输出格式返回内容不可控
严格 schema(output schema)让输出能被程序解析后续画图和评估中断
解析器(parser)把文本转成数据表把说明文字误当预测
校验规则(validation rules)检查日期、行数、数值和量级格式错误进入评价
留出评价判断预测是否有增量价值只凭直觉判断好坏

因此,API 不是网页聊天框,而是把模型服务嵌入 notebook、脚本和业务系统的接口。学习重点不是手动问一次模型,而是把数据整理、请求构造、响应解析和结果评估串成可重复运行的流程。

我们强调这个差别:把 CSV 上传到对话框可以探索,但每次都依赖人工操作;API 调用则把历史数据、任务说明、换行符、输出约束和模型参数都编码成一个可记录的请求。程序把 prompt 作为字符串发送给模型,模型返回文本,Python 再把文本解析成 JSON 或 DataFrame。只有这样,LLM 预测才有机会进入批量实验和正式评估。

同样的工作流也可以扩展到本地时间序列基座模型。DeepSeek API、TimeGPT API 和离线 Chronos 模型的部署方式不同,但对使用者来说都应被包装成清楚的输入输出接口:历史数据是什么表,预测步长是多少,输出是点预测还是分位数,结果如何合并真实值并评价。TimeGPT 把预训练时间序列模型包装成 API,是理解这类工作流的一个直接参照 Garza et al., 2024。区别在于,云端 API 更方便但有成本、速率和数据外发问题;本地模型下载后可以离线运行,更适合隐私敏感或网络不稳定的课程练习,但需要管理模型文件、依赖版本和硬件资源。

API Key 与环境

不要在课程仓库或 notebook 中写真实 API key(API key)。推荐把 key 放在环境变量(environment variable)中:

export DEEPSEEK_API_KEY="你的 key"

Python 代码中再读取:

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["DEEPSEEK_API_KEY"],
    base_url="https://api.deepseek.com/v1",
)

OpenAI 兼容客户端不只用于 OpenAI 模型,也可以连接 DeepSeek 等兼容接口。关键是同时记录 base_urlmodel、调用日期、prompt 模板和主要参数。否则即使代码能跑,后来也很难复现同一次预测到底问了哪个模型、用的是什么上下文。

如果你是在 Jupyter 中运行,先确认当前 kernel 能导入依赖:

import json
import os
import re

import pandas as pd
from openai import OpenAI

示例 notebook 中为了演示会出现 YOUR_DEEPSEEK_API_KEY 这样的占位符。正式作业和公开材料中不要保留真实 key,也不要把包含 key 的输出截图放进作业。

API key 相当于可计费的访问凭证。小型课程实验可能只花很少费用,但重复调用、长历史窗口、多预测步长和多序列实验会很快放大成本。正式报告应说明模型、参数、调用次数和大致成本,以便复现实验。

项目推荐做法
Key 保存位置环境变量或本地密钥管理,不写进 notebook
Git 提交不提交 key,不提交包含 key 的日志
截图和报告不展示 key,不展示完整敏感请求
模型记录记录 modelbase_url、调用日期和关键参数
成本记录记录调用次数、历史窗口长度和预测步长
数据敏感性上传前判断是否包含个人、客户、收入或内部经营数据

数据格式

本章示例使用航空乘客数据,输入表至少包含三列:

这套格式的好处是清楚、可扩展,并且和许多预测工具兼容。即使现在只有一条航空乘客序列,保留 unique_id 也能让同一套代码扩展到多条产品、门店、地区或资产序列。

读取后先统一列名、解析时间、排序:

data_path = "../data/air_passengers_with_id.csv"
df = pd.read_csv(data_path)
df.columns = [c.lower() for c in df.columns]
df["ds"] = pd.to_datetime(df["ds"], errors="raise")
df = df.sort_values(["unique_id", "ds"]).reset_index(drop=True)
uid = df["unique_id"].iloc[0]

在真正调用 API 之前,先画历史序列。这个步骤不是装饰,而是为了发现明显的数据错误:时间顺序是否反了,是否有缺失月份,是否存在异常尖峰,训练样本长度是否足够。

hist = df[df["unique_id"] == uid].sort_values("ds").set_index("ds")
hist["y"].plot(title=f"{uid} - history", figsize=(10, 4), legend=True)

还要判断数据能不能上传到云端。航空乘客公开数据可以用于教学演示;如果数据是家庭收入、企业销售、客户订单、财务流水或内部产能,就不能随意发送给外部 API。API 工作流越方便,越要先建立数据边界。

构造提示词

提示词的目标是把预测任务说清楚,并让返回内容稳定到可以被程序解析。一个实用模板如下:

def build_prompt(df, h=12, freq="MS", tail=120):
    sub = df[df["unique_id"] == uid].sort_values("ds").tail(tail)
    hist = [
        {"ds": row.ds.strftime("%Y-%m-%d"), "y": float(row.y)}
        for row in sub.itertuples(index=False)
    ]
    return (
        f"你是一名时间序列预测助手。下面给出乘客序列的历史数据(频率 {freq})。\n"
        f"请预测未来 {h} 期,并只输出严格 JSON。\n\n"
        f"历史数据:\n{json.dumps(hist, ensure_ascii=False, indent=2)}\n\n"
        "{\n"
        f'  "h": {h},\n'
        f'  "freq": "{freq}",\n'
        '  "forecast": [ { "ds": "YYYY-MM-DD", "yhat": <float> } ]\n'
        "}"
    )

一个预测 prompt 至少包含这些部件:

部件作用
角色告诉模型它是一名时间序列预测助手
目标变量说明要预测 y,而不是其他列
时间频率说明月度、日度或其他频率
预测步长明确未来几期
历史数据给出机器可读的日期和值
输出 schema固定字段名、日期格式和数值类型
限制条件要求只输出 JSON,不编造不存在的数据

这里的 tail 控制给模型看的历史长度。历史太短,模型看不到季节性;历史太长,提示词会变长、成本会上升,也可能超过上下文限制。示例中使用 tail=120,实际任务中应根据业务周期和数据频率调整。

从模型角度看,build_prompt 不是简单地把表格贴进字符串,而是在定义时间序列如何被模型读取:日期、数值、频率、预测步长、输出字段和限制条件都会转成 token,并进一步进入模型的表示空间。这个过程类似为模型搭建一个受控输入接口。数据越结构化,模型越不容易把列含义、时间顺序或未来日期猜错。

如果下一步要解析结果,就不要要求模型写漂亮说明。说明文字可以另开一个字段或另做一次请求;核心预测请求应优先保证结构稳定。

调用模型并解析结果

调用时建议把 temperature(温度参数)设为 0,降低输出波动。即便如此,模型返回的内容仍然是文本,所以需要解析和校验。

def parse_json(text):
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    if "```" in text:
        for part in text.split("```"):
            candidate = part.strip()
            if candidate.lower().startswith("json"):
                candidate = candidate[4:].strip()
            try:
                return json.loads(candidate)
            except json.JSONDecodeError:
                pass

    match = re.search(r"\{[\s\S]*\}", text)
    if match:
        return json.loads(match.group(0))
    raise ValueError(f"无法解析 JSON: {text[:200]}")

一个 6 期预测请求如下:

prompt = build_prompt(df, h=6, freq="MS")
resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": prompt}],
    temperature=0,
)

raw = resp.choices[0].message.content
obj = parse_json(raw)
fc6 = pd.DataFrame(obj["forecast"]).assign(unique_id=uid)
fc6["ds"] = pd.to_datetime(fc6["ds"])
fc6["yhat"] = pd.to_numeric(fc6["yhat"], errors="coerce")
fc6 = fc6.dropna().sort_values("ds").reset_index(drop=True)

模型响应只是文本;解析后的表格才是候选预测。解析后至少检查:

assert len(fc6) == 6
assert fc6["yhat"].notna().all()
assert fc6["ds"].is_unique
assert fc6["ds"].min() > df["ds"].max()
assert fc6["yhat"].between(0, df["y"].max() * 3).all()

这些检查不能保证预测正确,但能避免把格式错误当成预测结果。严格 JSON 输出是这个流程的关键。LLM 生成的是文本;如果文本不能稳定解析,后面的画图、合并、误差计算都会中断。

可视化预测

把预测和历史放在同一张图中,是最便宜的质量控制方法:

hist = df[df["unique_id"] == uid].sort_values("ds").set_index("ds")
fc_plot = fc6.set_index("ds")

ax = hist["y"].plot(label="history", figsize=(10, 4))
fc_plot["yhat"].plot(ax=ax, label="forecast")
ax.set_title(f"{uid} - history and 6-step forecast")
ax.legend()

观察图形时不要只看曲线是否平滑。平滑曲线可能只是模型生成了一个“看起来合理”的故事。对航空乘客这种月度数据,至少要问三个问题:预测是否延续长期上升趋势?是否保留月度季节性?未来值的量级是否和历史末端衔接?

如果同时做 6 期、12 期和 36 期预测,建议分开画图,再比较它们的形状。不要只展示最长预测步长,因为长步长图形更容易显得平滑,却未必更可靠。

预测步长的影响

示例 notebook 分别演示了 6 期、12 期和 36 期预测。预测步长越长,模型越容易回到“看起来合理”的平滑曲线,但这不一定代表更可靠。长步长预测通常需要额外解释:

预测步长还会影响 API 成本和输出稳定性。一次 36 期预测通常比 6 期预测需要更长输出;如果你还要重复调用、比较提示词或处理多条序列,费用和等待时间都会增加。在正式实验中,应先确定评价目标,再决定需要哪些预测步长。

评估与决策

API 返回的 yhat 只是一个候选预测。要判断它能不能用于决策,需要留出一段真实数据做验证。例如把最后 12 个月作为测试集,用更早的数据构造提示词,再比较预测和真实值。

test_h = 12
train = df.iloc[:-test_h].copy()
test = df.iloc[-test_h:].copy()

prompt = build_prompt(train, h=test_h, freq="MS")
resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": prompt}],
    temperature=0,
)
obj = parse_json(resp.choices[0].message.content)
fc = pd.DataFrame(obj["forecast"]).assign(unique_id=uid)
fc["ds"] = pd.to_datetime(fc["ds"])
fc["yhat"] = pd.to_numeric(fc["yhat"], errors="coerce")

eval_df = test.merge(fc[["ds", "yhat"]], on="ds", how="left")
mae = (eval_df["y"] - eval_df["yhat"]).abs().mean()
rmse = ((eval_df["y"] - eval_df["yhat"]) ** 2).mean() ** 0.5

还要和简单基准比较。一个很朴素的基准是 naive:未来每一期都等于训练集最后一个观测值。月度季节性数据还可以用 seasonal naive(季节性朴素法):未来某个月等于上一年同月。

eval_df["yhat_naive"] = train["y"].iloc[-1]

seasonal = train.tail(12)[["y"]].reset_index(drop=True)
if len(seasonal) == test_h:
    eval_df["yhat_snaive"] = seasonal["y"].to_numpy()

mae_naive = (eval_df["y"] - eval_df["yhat_naive"]).abs().mean()
mae_snaive = (eval_df["y"] - eval_df["yhat_snaive"]).abs().mean()

如果 DeepSeek 预测不能超过简单基准,就不能说这个 API 工作流带来了预测价值。它仍然可能有教学价值、解释价值或原型价值,但不应直接进入业务决策。预测实践中的共同原则是,任何新工具都要和简单、透明、可复现的基准放在同一评价框架下比较 Petropoulos et al., 2022

这里还要区分“API 生成了预测”和“模型真正理解了时间序列”。有些系统可能把表格识别出来后,在背后调用 ARIMA、ETS 或其他工具;有些系统可能主要依赖自身的上下文表示直接生成数值。二者都可以有工程价值,但都必须接受同样的留出测试、滚动验证和基准比较。对使用者来说,关键不是先判断它到底属于哪一类,而是记录输入、输出、模型和评价证据,避免把一条曲线误认为可靠结论。

评价指标要和业务问题对应。库存和产能问题通常关心低估或高估的非对称成本;预算或收入预测可能更关心总量误差;运营监控可能更关心短期 T+1T+1T+3T+3 的表现。更深入的评价指标会在后续章节展开,本章只要求把 API 输出放进可检验实验。

成本、隐私与可复现

使用 API 做预测时,技术流程之外还要管理三件事。

第一是成本。API 调用按 token、请求量或模型规格计费。历史窗口越长、预测步长越长、重复实验越多,成本越高。课程实验可以从一条序列、小步长和短窗口开始,再逐步扩展。

第二是隐私。模型服务如果在云端 API(cloud API)中运行,输入数据就会离开本地环境。公开数据和脱敏数据适合教学演示;客户、收入、交易、医疗、财务和企业内部经营数据需要严格审批或本地部署。

第三是可复现。一次预测结果如果没有记录 prompt、模型名、参数、数据切分和调用时间,就很难复查。正式报告至少应记录:

这个记录不是形式主义。LLM 预测的输出可能随模型版本、参数、上下文和提示词变化。没有实验记录,就无法判断结果来自模型能力、提示词调整,还是偶然生成;这也是 LLM 进入预测流程后必须强调复现、验证和边界控制的原因 Makridakis et al., 2023

常见错误

练习

  1. 将一个自己的月度业务序列整理成 unique_id, ds, y 三列,运行 6 期预测,并画出历史与预测曲线。

  2. 固定数据不变,分别使用 h=6, h=12, h=36,比较预测形状、输出长度和可解释性。

  3. 固定 h=12,分别使用不同的 tail,观察预测结果和 prompt 长度如何变化。

  4. 修改提示词,要求模型说明使用了哪些时间序列特征,但仍然必须返回 JSON。检查解释是否与图形一致。

  5. 留出最后 12 期作为测试集,计算 MAE 和 RMSE,并写一段话说明这个结果是否足以支持业务决策。

  6. 与 naive 和 seasonal naive 比较。如果 DeepSeek 不能超过基准,解释可能原因。

  7. 对同一 prompt 重复调用三次,比较预测是否稳定,并说明是否适合进入自动化流程。

  8. 将航空乘客预测扩展成一个真实业务问题,说明还需要哪些外部变量,例如航线、机场、票价、节假日、天气、前序航班状态或机场拥堵。

  9. 写一段不超过三页的实验报告,包含数据、切分、prompt、模型、参数、指标、基准和决策结论。

参考文献

References
  1. Garza, A., Challu, C., & Mergenthaler-Canseco, M. (2024). TimeGPT-1. 10.48550/arXiv.2310.03589
  2. Petropoulos, F., Apiletti, D., Assimakopoulos, V., Babai, M. Z., Barrow, D. K., Ben Taieb, S., Bergmeir, C., Bessa, R. J., Bijak, J., Boylan, J. E., Browell, J., Carnevale, C., Castle, J. L., Cirillo, P., Clements, M. P., Cordeiro, C., Cyrino Oliveira, F. L., De Baets, S., Dokumentov, A., … Ziel, F. (2022). Forecasting: Theory and Practice. International Journal of Forecasting, 38(3), 705–871. 10.1016/j.ijforecast.2021.11.001
  3. Makridakis, S., Petropoulos, F., & Kang, Y. (2023). Large Language Models: Their Success and Impact. Forecasting, 5(3), 536–549. 10.3390/forecast5030030