批量处理数据文件时,你有没有遇到过这种事:脚本一行报错都没有,跑出来的数字"看着差不多,但就是对不上"?等你翻遍公式也找不出问题——因为错根本不在公式里,而在一个不起眼的下标。
现象
一个基于"距区间低点涨幅"的统计指标,算出来的数值系统性偏小,明显低于正确值,导致大量对象被错误分档。脚本没有任何异常,日志一切正常。类似的情况还包括:
区间最低价统计: 12.4% # 凭经验这个量级应该在 30% 上下
数字不是随机错的,而是整体、同方向地偏小——这正是"系统性错误"最典型的特征:不是某几条数据出问题,而是所有人一起错。
根因
问题出在数据记录的字段顺序上。这些文件的每一行是一个元组,约定的顺序是:
(日期, 收盘, 开盘, 最高, 最低, 成交量)
而脚本却用 min(记录[3]) 去求"区间最低价"。记录[3] 是最高价字段,所以这段代码算出来的其实是"区间内最高的那个最高价"。
关键在于:"最低的最高价"永远大于等于"最低的最低价"——这是一个恒成立的不等式。所以只要下标取错,结果必然是偏小的,而且偏得"有规律",不会离奇到让你一眼看出问题。反向的错法同样常见:拿开盘价字段当最高价用。最严重的是整文件错法——把最低价字段当成收盘价用,那你算出来的一整套指标全是错的。
下标错位之所以危险,是因为它"看起来太正常了"。记录[3] 不会报 IndexError,它有值、有类型、能参与运算,只是在语义上完全不是你想要的字段。
解决
第一步,统一索引约定,并且把它固化进数据读取函数的文档字符串里:
def load_kline(row):
"""字段顺序: (日期, 收盘, 开盘, 最高, 最低, 成交量)
低点统计取 row[4]; 高点统计取 row[3]; 收盘取 row[1]。
"""
date, close, open_, high, low, vol = row
return dict(date=date, close=close, open=open_,
high=high, low=low, vol=vol)
用解包赋值(date, close, open_, high, low, vol = row)而不是裸下标,是防止这类错误最直接的写法——字段名本身就变成了文档。
第二步,做量纲自检。找一把"尺子":用一个已知量级的指标做校验。比如用收盘价口径算出的某个统计量,正常情况下应当在 30% 附近,一旦算出来只有三分之一,你立刻就能嗅到"整文件口径错了"。批量修正时,这一次一共在 28 个文件里找出了 43 处同类问题。
# 量纲自检:用已知量级的指标当尺子
expected = 0.30
actual = compute_stat(records, field="low")
assert abs(actual - expected) < 0.10, f"口径可疑: {actual:.3f}"
第三步,回归验证。修完之后重跑,看偏小的数字是否回到合理区间——如果只是"变大了一点"但仍不对,说明还有别的地方也取错了字段。
延伸与预防
批量脚本里最容易出错的,几乎总是"第几个字段"。要根治它,可以从三个层面下手:用命名元组或字典代替裸下标(collections.namedtuple 或 dataclass),让字段自带名字;在读取层做一次解包校验,字段数不对直接报错;给每个统计口径配一个量纲断言,让错误在离数据最近的地方暴露。
还有一个更朴素的经验:凡是"所有结果都朝同一个方向偏"的现象,先怀疑字段口径,而不是去改公式。公式错通常是随机的、离散的,而口径错一定是系统性的。