第 6 章 · 03 RUB 制裁与 CAD dollar effect:证伪案例 本节摘要:本节把第 02 节的方法套到另外两个候选石油货币上,结果双双证伪,但失败方式截然不同。俄罗斯卢布(RUB)对 Urals 原油——用逐步回归(stepwise regression)逐年算 R²,清晰看到 2014 年克里米亚 annexation 之后,西方制裁如何一年一年地侵蚀 Urals 对 RUB 的解释力:2017 年前 Urals 单变量 R² 超 80%,2017 年后跌破 30%、系数甚至转负(RUB 升值反而伴随油价跌)。结论粗暴——DON'T TRADE IT,政治议程始终碾压石油货币属性。
本节摘要:本节把第 02 节的方法套到另外两个候选石油货币上,结果双双证伪,但失败方式截然不同。俄罗斯卢布(RUB)对 Urals 原油——用逐步回归(stepwise regression)逐年算 R²,清晰看到 2014 年克里米亚 annexation 之后,西方制裁如何一年一年地侵蚀 Urals 对 RUB 的解释力:2017 年前 Urals 单变量 R² 超 80%,2017 年后跌破 30%、系数甚至转负(RUB 升值反而伴随油价跌)。结论粗暴——DON'T TRADE IT,政治议程始终碾压石油货币属性。加拿大元(CAD)对 WCS/Edmonton/WTI 则是另一副面孔——所有烃类单变量 R² 都不到 5%,市场所谓的"加元是石油货币"多半是 dollar effect(商品以美元计价的假象)。作者用 K-Means 聚类 + elbow + silhouette 双指标在 (CAD, WCS, date) 三维空间找到 2016-03-02 这个切分点,但分段后 R² 依然可怜(20%/10%),证伪成立。两个证伪案例合在一起,把"相关≠因果"这条戒律演示得淋漓尽致。
涉及源码:原项目
Oil Money RUB.py(约 215 行)、Oil Money CAD.py(约 381 行)。
⚠️ 注意:本节是全项目最有"反面教材"价值的一节。请记住:证伪比确认更难、也更值钱。一个能在样本外被证伪的命题,才是真正科学的命题。
俄罗斯是教科书式的石油经济体。README 引用维基百科:2012 年俄罗斯石油业务(主要是 Rosneft)占 GDP 的 16%、联邦预算收入的 52%、出口总额的 70% 以上。理论上 RUB 应该是最强的石油货币。
但俄罗斯同时是全球被制裁最严重的国家之一。从 2014 年克里米亚事件开始,到叙利亚军事干预,再到 2018 年英国索尔兹伯里神经毒剂事件,西方对俄制裁一波接一波。每一波制裁都会让 RUB 偏离"油价驱动"的轨道——因为汇率开始被资本外逃、外交对抗、央行干预这些政治变量主导,而不是被油价主导。
💡 核心心法:这就是为什么逐步回归在这里比一次性多元回归更有用。一次性回归给的是"全样本平均 R²",会被早年的强关系和晚年的弱关系互相抵消,看似还行。逐步回归逐年算 R²,才能看出解释力的衰减轨迹——而这条轨迹本身就是制裁效应的可视化。
加拿大元有个悖论。所有人都说"加元是石油货币",但加拿大 96% 的原油出口给美国,加拿大在美国石油市场之外存在感极低。当作者把 CAD 对 WCS、Edmonton、WTI 分别做单变量回归,所有烃类的 R² 都不到 5%——加元和它自家的原油几乎没关系!
作者的解释是 dollar effect(美元效应):
所以所谓"加元是石油货币",很可能是一个统计假象——它是美元这个共同因子制造出来的相关,而不是加拿大原油出口驱动的因果。要识破这个假象,只要像本项目这样把所有变量换算到 AUD(或 JPY)这种非美元基准下,R² 就原形毕露。
CAD 既然在全样本上不显著,会不会是不同时间段有不同的体制(regime)?这是典型的**结构断点(structural break)**问题。传统做法是 Chow 检验,但需要预先指定断点日期。作者用了一个更"无监督"的做法——K-Means 聚类找断点:
需要强调的是,K-Means 本是为离散变量设计的、不考虑时间序列的自相关。所以作者也坦承这个方法有局限——必须配合可视化做判断。好在 CAD 这个例子里,两个簇在 z 轴(日期)上有一道干净的平面分界,断点足够清晰。
Oil Money RUB.py:31-65 是核心。用 locals() 动态为每个回归元创建一个 list,逐年(2015/2016/2017/2018)做单变量 OLS,把每年的 R² 收集起来:
year = df.index.year.drop_duplicates().tolist() var = locals() for i in df.columns: if i != 'rub': var[i] = [] for j in year: x = sm.add_constant(df[i][str(j):str(j)]) # 当年单变量 y = df['rub'][str(j):str(j)] m = sm.OLS(y, x).fit() var[i].append(m.rsquared)
然后画成"分组柱状图"——每个回归元一组、每组 4 根柱子对应 4 年。这张图(oil-rub stepwise1.png)一眼就能看出 Urals 的柱子逐年变矮,而日韩货币的柱子反而变高(后者更多是巧合,作者也承认)。
Oil Money RUB.py:73-104 是另一个版本——累计年份的 R²(用 [:str(j)] 而非 [str(j):str(j)])。这个版本把"早期强、晚期弱"的趋势叠加得更明显。

Oil Money RUB.py:110-126 和 :132-147 分别对 2017 前后做 Urals 对 RUB 的单变量回归,并打印 summary、画拟合 vs 实际:
# post-2017 m = sm.OLS(y['2017':'2018'], x['2017':'2018']).fit() # pre-2017(加入 eur) m = sm.OLS(y[:'2016'], x[:'2016']).fit()
关键结论:
| 区间 | Urals 对 RUB 的 R² | Urals 系数符号 |
|---|---|---|
| pre-2017 | > 80% | 正(符合石油货币直觉) |
| post-2017 | < 30% | 负(彻底反直觉!) |
post-2017 系数转负是最致命的——它意味着"油价涨、RUB 反而跌",这和石油货币的定义完全相反。作者据此宣布:俄罗斯卢布的石油货币身份已被制裁摧毁,不要交易它。
⚠️ 重要说明:为什么系数转负?因为 post-2017 期间,油价在涨(2018 年初 Urals 飙升),但 RUB 在跌(制裁升级)。两个本应同向的变量变成了反向。任何还在把 RUB 当石油货币交易的模型,都会在这种"系数反转"里亏到吐血。

Oil Money CAD.py:107-139 对所有 12 个回归元分别对 CAD 做单变量 OLS,把 R² 收集到 var 里画柱状图。结果惊人——
| 回归元类别 | R² |
|---|---|
| 烃类(WCS/Edmonton/WTI) | < 5%(全部!) |
| GBP、CNY | ~35%(意外地高) |
| USD | 反而不是最高 |
烃类 R² 全军覆没,是 CAD 不是石油货币的最直接证据。而 GBP/CNY 的高 R² 反而引出另一个谜——脱欧前英镑和人民币居然同步?这跟石油无关,留给读者自己想。

Oil Money CAD.py:185-201 是 dollar effect 的可视化灵魂。同一对(CAD vs WCS),分别画两次双轴图:
# 用 AUD 计价 dual_axis_plot(df.index, df['cad'], df['wcs'], ...) # 用 USD 计价(把两者都除以 usd) dual_axis_plot(df.index, np.divide(df['cad'], df['usd']), np.divide(df['wcs'], df['usd']), ...)
两张图对比看:在 AUD 计价下 CAD 与 WCS 各走各的;换到 USD 计价后,两者突然像同步了——这就是 dollar effect 制造的假象。所谓"CAD 跟 WCS 同步",其实是"两者都跟美元反向"。
Oil Money CAD.py:206-228 实现 elbow method。对 K=1~7 分别跑 KMeans,收集 SSE(kmeans.inertia_),然后额外算每个 K 到"首尾连线"的垂直距离作为 elbow 选取的辅助:
sse = [] for i in range(1, 8): kmeans = KMeans(n_clusters=i) kmeans.fit(x) sse.append(kmeans.inertia_/10000) # 首尾两点连线,算每个点到这条线的垂直距离 a, b = get_line_params(0, sse[0], len(sse)-1, sse[-1]) distance = [get_distance(i, sse[i], a, b) for i in range(len(sse))]
💡 核心心法:为什么不只是看 SSE 拐点?因为 SSE 曲线的"elbow"经常很模糊,不同人能读出不同的 K。作者用"到首尾连线的垂直距离"这个几何量来自动化地找最远点——距离最大的那个 K 就是 elbow。这是 elbow method 的一个常用变体,比凭眼睛看更客观。
Oil Money CAD.py:235-258 实现 silhouette score。对 K=2~7 跑 KMeans,用 silhouette_score(x, projection) 算每个 K 的轮廓系数,选最大值对应的 K:
sil = [] for n in range(2, 8): clf = KMeans(n).fit(x) projection = clf.predict(x) sil.append(silhouette_score(x, projection))
两个指标都指向 K=2。Oil Money CAD.py:265-267 跑 K=2 的最终聚类,自动找出断点:
clf = KMeans(n_clusters=2).fit(x) df['class'] = clf.predict(x) threshold = df[df['class']==0].index[-1] # 类 0 的最后一天 = 断点
threshold 解出来正好是 2016-03-02。

Oil Money CAD.py:321-334 在断点前后分别做 WCS 对 CAD 的单变量 OLS,画 R² 对比柱状图:
| 区间 | WCS 对 CAD 的 R² |
|---|---|
| Before 2016-03-02 | ~20% |
| After 2016-03-02 | ~10% |
两段都远远达不到 70% 的有效性门槛。即使做了结构断点切分,CAD 的石油货币身份依然站不住脚。作者正式拒绝原假设:CAD 不是石油货币。

Oil Money CAD.py:274-296 用 3D 散点图(mpl_toolkits.mplot3d)可视化聚类结果:三个轴是 WCS、CAD、date,两个簇用不同颜色,断点前后清晰分开。这是无监督学习结果的标准可视化方式——尤其在时间序列聚类里,3D 加上时间轴能直观看出"断点是不是干净"。
逐步回归是看"解释力衰减"的最佳工具。一次性回归给的是全样本平均,会掩盖衰减;逐年逐步回归把衰减轨迹画出来,政治事件(制裁)对模型的破坏一目了然。RUB 的案例就是教科书。
系数符号反转是最强的证伪信号。R² 下降还能勉强解释成"噪声变大",但系数从正变负(RUB post-2017)直接否定因果方向。看到系数反转,基本可以宣布模型死了。
dollar effect 的识别招式:换计价货币。同一个"商品 vs 货币"的关系,在 USD 计价下显著、在 AUD/JPY 计价下消失,基本就是 dollar effect。这是识别统计假象的低成本验招。
elbow + silhouette 双指标确定 K。两个指标都指向同一个 K(K=2)时,聚类数才算稳。只看一个容易踩坑——SSE 拐点经常模糊,silhouette 在某些数据分布下也会失真。两个互为交叉验证。
"到首尾连线垂直距离"自动化找 elbow。比凭眼睛看 SSE 曲线更客观。这是一个小巧但实用的工程技巧,值得收藏。
KMeans + 时间轴维度。把 date 作为一个聚类维度,能让聚类天然带时间连续性,断点就是簇边界。但要警惕——KMeans 不假设时间自相关,必须配合可视化做判断,否则可能切出"_checkerboard"式的散乱簇。
证伪的价值。CAD 的全流程(单变量证伪 → dollar effect 解释 → KMeans 找断点 → 分段后仍证伪 → 拒绝原假设)是本教程最完整的证伪案例。它的教学价值远高于"我又找到一个 0.7 R² 的因子"。
下一节(第 04 节)进入项目的最后一个货币——哥伦比亚比索 COP。它是唯一被正面确认的石油货币,也是参数网格搜索的总结篇。