复现 Hamilton(1989) 的时变马尔可夫区制转换,是 statsmodels 的 MarkovAutoregression 最经典的练兵场,而 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end )能让你把 Codex 顺畅地接进这个复现流程。很多人的第一次尝试都卡在同一个地方:k_regimes=2、order=4、switch_ar=False 三个参数明明都写了,滤波概率和平滑概率也能画出来,expected durations 却总跟论文对不上。这篇文章我会带着 Codex 走一遍完整流程——先去官网创建 API Key,再把 Codex 的 Base URL 指到 https://taotoken.net/api,剩下的数据读取、模型拟合、概率绘制、持续时间核对,都交给 Codex 按原文步骤重写脚本,你在本地执行验证。原文里的 gndata、MarkovRegression、summary、smoothed_marginal_probabilities 这些关键词,我们逐一落到可跑的代码上,最后在终端看到请求成功、无 401,再去官网看一眼这次调用记在用量里,整个链路才算闭环。
1. Hamilton 1989 复现为什么总卡在参数和概率上
1.1 三个参数决定了模型形态
原文用MarkovAutoregression复现 Hamilton(1989) 的开创性论文,模型的数学形式并不复杂:一个4阶自回归过程,均值在两个区制之间切换,转移概率矩阵固定不变。落到 statsmodels 里,三个参数必须一起出现:
mod = sm.tsa.MarkovAutoregression( hamilton, k_regimes=2, order=4, switch_ar=False, )k_regimes=2是区制数量,order=4是自回归阶数,switch_ar=False表示自回归系数不随区制切换。最后一个参数最容易漏,因为默认模型会切换自回归系数,一旦漏掉,估计出的参数含义就和原论文对不上。新手经常在这里遇到第一个坑:switch_ar漏写之后模型也能跑,但summary()里多出一大堆区制相关的 AR 系数,和 Hamilton(1989) 的设定完全不是一回事。这时候去对论文的表格,折腾半天也找不出差异在哪。
1.2 滤波概率和平滑概率是两回事
Hamilton 的滤波器给出的是filtered概率:基于截至 t 时刻的数据,对 t 时刻处于某个区制的概率估计。平滑器(Kim 1994)给出的是smoothed概率:用全样本信息反过来修正 t 时刻的概率。statsmodels 在fit()之后直接暴露了这两个属性:
res = mod.fit() res.filtered_marginal_probabilities res.smoothed_marginal_probabilities很多复现稿画出来的图只有一条线,其实应该画两条:滤波概率在转折点附近抖动更明显,平滑概率更接近 NBER 标注的衰退区间。如果只画filtered,会觉得模型切换得太频繁;只画smoothed,又会觉得模型反应太慢。两者对照才能理解 Hamilton 的设计意图。Codex 在这里能帮上忙的地方是:你不需要记住这些概念的名字,只要把「滤波和平滑都要画」写进提示词,它生成的代码会自动把两个子图拼在一起。
1.3 expected durations 算不对的根源
原文最后计算了衰退与扩张的预期持续时间。计算公式本身很简单:如果转移概率矩阵是
P = [[p_00, p_01], [p_10, p_11]]那么区制 0 的预期持续时间为1 / (1 - p_00),区制 1 的预期持续时间为1 / (1 - p_11)。问题在于新手经常读错summary()里的转移矩阵方向:statsmodels 输出的矩阵行是当前区制、列是下一期区制,还是反过来?如果读反,预期持续时间可能变成 1.5 个季度和 2 个季度,跟原文的「衰退约一年、扩张约两年半」差了十万八千里。所以先放一放代码细节,把 Codex 的访问通道配好,后面的改写和输出核对才有地方落地。
2. 把 Codex 接到 TaoToken:Base URL 和 Key 的一次性配置
2.1 去官网创建 API Key
准备材料就一样东西:API Key。打开 TaoToken,注册并创建一个 Key。注意这里拿到的 Key 是一串真实值,创建之后只显示一次,记得复制下来。官网落地页只做三件事:注册账号、创建 Key、看模型广场。创建 Key 之后不要急着关页面,去模型广场看一眼当前可用的模型 ID。后面 Codex 的config.toml里model字段填什么,是以模型广场显示的为准,不是随便猜一个。
2.2 编辑 ~/.codex/config.toml
Codex 是一个命令行工具,它的配置集中在~/.codex/config.toml。要让 Codex 走 TaoToken 的统一接入通道,需要新增一个model_provider,并把默认 provider 指过去。完整的配置如下:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"base_url填的是接口地址https://taotoken.net/api,末尾不要加/v1。很多人在这一步习惯性补一个/v1,结果 Codex 请求发到https://taotoken.net/api/v1,返回 404。另外,模型 ID 那一项填YOUR_MODEL_ID只是占位符,实际值要去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的模型广场复制。然后在终端导出环境变量:
export TAOTOKEN_API_KEY="YOUR_API_KEY"env_key告诉 Codex 去哪个环境变量里读取 Key,所以这里的环境变量名必须和config.toml里一致。
2.3 先跑一个最小请求确认连通
配置好之后,不要直接开肝 Hamilton 复现,先用一句话让 Codex 回答,确认请求真的通了:
codex exec "用一句话说明你已经准备好复现 Hamilton 1989"如果 Codex 正常返回,说明 TaoToken 通道没问题。如果返回 401,先检查TAOTOKEN_API_KEY是否导出成功;如果返回 404,检查base_url是不是多了/v1。这个最小验证只需要十秒钟,但能省掉后面排查环境变量的时间。
3. 让 Codex 按原文步骤重写 Hamilton 1989 脚本
3.1 给 Codex 的提示词
现在把原文的步骤翻译成一段清晰的提示词。原文的路径是:读取gndata→ 用MarkovRegression拟合 → 输出summary()→ 画滤波和平滑概率 → 计算预期持续时间。我把这些要求原样交给 Codex:
请用 statsmodels 复现 Hamilton(1989) 的马尔可夫区制转换自回归模型。 要求: 1. 读取本地数据文件 gndata(CSV 或 Excel 均可,请先读取并打印前几行确认列名) 2. 用 MarkovAutoregression 建模,k_regimes=2,order=4,switch_ar=False 3. 拟合后输出 res.summary() 4. 分别绘制 filtered_marginal_probabilities 和 smoothed_marginal_probabilities 5. 从转移矩阵计算两个区制的预期持续时间,并输出结果 6. 代码要能直接运行,数据文件路径请用变量标注注意,原文里gndata是从本地读取的,Codex 不会替你下载数据,也不会替你在生产机器上执行脚本。它只负责生成代码、解释结果、对照输出。数据文件需要你自己放到当前目录,脚本也要你在本地终端运行,再把输出贴回对话。这一步是 AI 编程工具的基本边界:生成、解释、对照是它的事,执行和验证始终在你手里。
3.2 Codex 生成的代码长什么样
按照上面的提示词,Codex 会生成类似下面的脚本。注意,这不代表原文代码,而是符合原文步骤的可运行版本:
import numpy as np import pandas as pd import statsmodels.api as sm import matplotlib.pyplot as plt # 读取本地数据,请确认 gndata 的格式 df = pd.read_csv("gndata.csv", index_col=0, parse_dates=True) data = df.iloc[1:] # 原文从第二行开始 # Hamilton(1989):k_regimes=2, order=4, switch_ar=False mod = sm.tsa.MarkovAutoregression( data, k_regimes=2, order=4, switch_ar=False, ) res = mod.fit() print(res.summary()) # 滤波概率与平滑概率 filtered = res.filtered_marginal_probabilities smoothed = res.smoothed_marginal_probabilities fig, axes = plt.subplots(2, figsize=(10, 7)) axes[0].plot(filtered[0]) axes[0].set_title("Filtered probability of regime 0") axes[1].plot(smoothed[0]) axes[1].set_title("Smoothed probability of regime 0") plt.tight_layout() plt.show() # 预期持续时间 trans = res.regime_transition p00 = trans[0, 0] p11 = trans[1, 1] expected_duration_0 = 1 / (1 - p00) expected_duration_1 = 1 / (1 - p11) print("Expected duration of regime 0:", expected_duration_0) print("Expected duration of regime 1:", expected_duration_1)res.regime_transition是拟合后得到的转移概率矩阵。如果 Codex 的版本里属性名不同,让它先打印dir(res)再调整。这段代码引用filtered[0]是因为filtered_marginal_probabilities返回的是 DataFrame,列名是区制编号,取0表示第一个区制。
3.3 本地执行,把输出贴回去
脚本生成后,在本地终端运行:
python hamilton1989.py然后把summary()的输出和两张概率图的结果描述贴回 Codex 对话。Codex 会根据输出判断参数是否合理、概率曲线是否符合 NBER 标注的衰退区间。这个「生成 → 运行 → 回贴 → 修正」的循环,就是 Codex 在这条工作流里的真正价值:它不是替你跑模型,而是陪你调模型。
4. 滤波概率、平滑概率与 expected durations 的验证口径
4.1 先核对转移矩阵的方向
summary()里有几个关键表格:系数估计、转移概率矩阵、区制持续期。其中转移概率矩阵是个 2×2 的矩阵,行是当前区制,列是下一期区制。用这个口径去读res.regime_transition:
print(res.regime_transition)假设输出是
[[0.904, 0.096], [0.228, 0.772]]那么区制 0 的预期持续时间是1 / (1 - 0.904),也就是 10.4 个季度左右;区制 1 的预期持续时间是1 / (1 - 0.772),也就是 4.4 个季度左右。Hamilton(1989) 的经典结论是衰退持续约一年、扩张约两年半,对应过来就是区制 1 约 4 个季度,区制 0 约 10 个季度。两组数一对上,说明switch_ar=False和转移矩阵的读取方向都没问题。
4.2 用代码验证 expected durations
原文在模型拟合后直接调用了预期持续时间的计算。Codex 重写的代码里,这一步是手算的,好处是你能看到每个数字从哪来。如果你想直接读结果对象的属性,也可以接着上面的res运行:
print(res.expected_durations)res.expected_durations会返回一个数组,两个区制各给一个持续时间。如果这个数组的值和手算的1/(1-p_ii)不一致,说明转移矩阵的读取方向出了问题,需要回头检查行列顺序。大多数情况下,expected_durations的输出会直接给出[9.8, 4.3]这样的数字,对应扩张约两年半、衰退约一年。
4.3 平滑概率的图形核对
滤波概率在衰退刚开始时会快速跳变,平滑概率则会把这种跳变磨平一些。两者之间的差异如果过大,通常意味着转移概率的估计对样本区间很敏感。原文把 NBER 的衰退阴影画在图上作对比,这一步在 Codex 生成的代码里可以补上:
import pandas_datareader.data as web from datetime import datetime # NBER 衰退区间,这里只是获取方式,需要联网 # 如果没有网络,可以跳过这一段,直接看概率曲线不过要注意,pandas_datareader拉 NBER 数据依赖网络,有时候会失败。如果只做复现验证,不联网也没关系:平滑概率在高位持续四个季度左右的区间,基本就是对衰退持续时间的直观印证。把smoothed[0]的图像和expected_durations的数字放在一起看,整个 Hamilton 模型的结论就完整了。
5. Filardo 时变转移概率:让 Codex 改 exog_tvtp
5.1 从固定概率到时变概率
前面 Hamilton(1989) 假设转移概率矩阵不随时间变化。Filardo(1994) 放松了这个假设,允许转移概率依赖上一期的外生变量。这时每个时期的转移概率变成p_ij_t = logistic(x_{t-1} * beta_ij)。statsmodels 的实现里,这个外生变量通过exog_tvtp参数传入。原文用的数据里有一个先行指标,标准化之后作为exog_tvtp。对应的建模代码是:
mod_tvtp = sm.tsa.MarkovAutoregression( data["dlip"], k_regimes=2, order=4, exog_tvtp=data["leading"], ) res_tvtp = mod_tvtp.fit(search=20)注意这里仍然用MarkovAutoregression,因为 Filardo 模型保留了 Hamilton 的自回归结构。真正改用MarkovRegression的是原文中间演示的 KNS 三状态方差转换模型,那个模型没有自回归成分,需要指定k_regimes=3、switching_variance=True、trend='nc'。两条路径不要混。data["leading"]是范文里的先行指标列名,实际数据里叫dmdlleading就改成dmdlleading。
5.2 fit(search=20) 解决局部最优
马尔可夫区制转换模型的似然函数有很多局部极大值,直接用 BFGS 从默认参数出发,经常收敛到奇怪的地方。fit(search=20)会让 statsmodels 对起始参数向量做 20 次随机扰动,选最优的一组作为实际起始参数。这是 Filardo 复现里最关键的一步,不写search的话,时变转移概率的估计结果会很不稳定。
Codex 生成的对应代码大致是:
res_tvtp = mod_tvtp.fit(search=20) print(res_tvtp.summary()) smoothed_tvtp = res_tvtp.smoothed_marginal_probabilities # 时变模型的预期持续时间随 x_{t-1} 变化 expected_durations_tvtp = res_tvtp.expected_durations plt.plot(expected_durations_tvtp[0])expected_durations在时变模型里不再是一个常数,而是一条随x_{t-1}变化的曲线。把这条曲线画出来,能看到经济衰退期间低生产状态的预期持续时间明显抬升,这就是 Filardo 模型比 Hamilton 固定概率模型多出来的信息量。
5.3 对比两种模型的结果
固定概率模型给出的是一个平均意义上的持续期:衰退约 4 个季度,扩张约 10 个季度。时变模型则告诉你,这个持续时间不是均匀的——如果上一期的先行指标开得很差,那么当期进入低生产状态后,预期持续时间可能从 4 个季度拉长到 8 个甚至更久。
把 Hamilton 模型的平滑概率和 Filardo 模型的平滑概率画在同一张图上,最直观的差异是:Filardo 模型的区制切换点往往领先几个季度。这就是标题里「时变」两个字的实际价值,也是原文最后一张图想表达的东西。Codex 在这里的用法是:把两张图的数据都输出,然后让它用文字描述两个模型在衰退起点附近的差异,比你盯着图猜要快。
6. 在终端确认调用成功,并回到官网核对用量
6.1 怎么确认请求真的走通了
前面所有 Codex 交互,都是通过 TaoToken 通道发给大模型的。验证用量视角下,你需要确认请求确实成功,而不是本地缓存或错误回退。Codex 正常工作的标志是每次codex exec都有完整返回,没有任何 401 或 404。
如果 Codex 在运行中报了 401,先去检查TAOTOKEN_API_KEY这个环境变量是不是真的设上了:
echo $TAOTOKEN_API_KEY如果输出为空,回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 检查 Key 是否复制完整。如果报 404,大概率是base_url里多了/v1,把https://taotoken.net/api/v1改回https://taotoken.net/api就好。模型 ID 写错时的报错信息通常不是 401 也不是 404,而是一段「model not found」之类的 JSON,回去对一下模型广场的列表就行。
6.2 顺手记下来的三个注意点
第一,模型 ID 一定要从模型广场复制,不要凭记忆写。很多人图省事,让 Codex 直接猜一个模型名,结果请求发出去,服务端根本不认识这个 ID,报出来的错误既不是 401 也不是 404,而是模型不存在。第二,base_url永远只填https://taotoken.net/api,官网落地页的链接不要填进去。落地页负责注册、看模型、看用量,接口地址负责接收 Codex 的请求,两者各管各的。第三,Key 创建后只显示一次,但随时可以在控制台重新生成;重新生成后记得同步更新环境变量,否则终端里还挂着旧 Key,报错信息会误导你往模型方向上查。
6.3 打开控制台,核对这次调用的用量
最后一步验证用量视角:打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 进入控制台,看这次 Hamilton 和 Filardo 调试过程中发起了多少次请求、消耗了多少 token。这不是让你心疼额度,而是确认配置真实生效——你看到的请求数和本地实际操作次数对得上,说明 model_provider 指向完全正确。
以后遇到summary()读不懂、平滑概率对不上 NBER 衰退区间的情况,直接把输出贴给 Codex,让它按原文对照就行。Key 和 Base URL 配好一次,后面所有的模型访问都走同一条通道。这套「Codex 生成脚本 + 本地执行验证 + 控制台核对用量」的流程,比每次手动配环境变量再猜模型名要省心得多。