4. 数据质量初探:缺失值、异常值、重复值、不一致数据的识别与处理
各位同学,欢迎来到第四章。前面几章我们聊了怎么找数据、怎么存数据,但说实话,那些都只是开胃菜。真正让量化研究员头疼的,是数据本身的质量问题。
我经常跟团队里的新人说一句话:「垃圾进,垃圾出」。你模型再牛,策略再精巧,如果喂进去的数据全是坑,那结果就是一场灾难。我自己就吃过这个亏——有一次跑回测,夏普比率漂亮得吓人,结果一查,是因为某只股票的复权因子算错了,凭空多出来20%的收益。嗯,从那以后,我对数据清洗这件事,再也不敢马虎。
这一章,我们就来系统地看看数据质量的四个老大难问题:缺失值、异常值、重复值、不一致数据。说白了,就是帮你练就一双火眼金睛,把那些「脏数据」揪出来。
核心观点:数据清洗不是一次性工作,而是贯穿整个量化研究流程的「日常保洁」。你花在清洗上的每一分钟,都会在后续建模和回测中得到回报。
4.1 缺失值:数据里的「黑洞」
缺失值,说白了就是数据表格里那些空着的格子。在量化领域,这太常见了——某只股票停牌了、某个财务数据没披露、或者数据源本身就有问题。
我个人习惯把缺失值分成三类:
- 完全随机缺失(MCAR):缺失纯属偶然,比如传输过程中丢包了。这种最好处理,直接删掉也不会有太大偏差。
- 随机缺失(MAR):缺失跟其他变量有关,但跟自身无关。比如小市值公司更容易缺失分析师覆盖数据。
- 非随机缺失(NMAR):缺失跟自身取值有关。比如亏损的公司可能故意不披露某些数据。这种最麻烦,需要格外小心。
我的经验:在金融时间序列里,我见过最多的缺失是「停牌」导致的。千万别直接填充成0,那会把你的因子搞得一塌糊涂。我一般用前向填充(ffill),或者干脆把停牌期间的样本剔除掉。
来看看代码怎么处理:
import pandas as pd
import numpy as np
# 模拟一份有缺失的数据
df = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=10, freq='D'),
'close': [100, 102, np.nan, 105, 107, np.nan, 110, 112, 115, np.nan],
'volume': [1e6, 1.2e6, 0.9e6, np.nan, 1.1e6, 1.3e6, 1.5e6, np.nan, 1.4e6, 1.6e6]
})
# 1. 查看缺失情况
print(df.isnull().sum())
# 2. 删除缺失(慎用!)
df_drop = df.dropna()
# 3. 前向填充(金融数据常用)
df_ffill = df.fillna(method='ffill')
# 4. 线性插值(适合连续型变量)
df_interp = df.interpolate(method='linear')
print("前向填充结果:\n", df_ffill)
你想想看,如果某只股票停牌了3天,你用均值填充,那这3天的价格就变成了一个「假价格」。但用前向填充,至少保持了停牌前的价格水平,逻辑上更说得通。
4.2 异常值:藏在数据里的「刺客」
异常值,就是那些明显偏离正常范围的数据点。在量化里,异常值可能是数据录入错误,也可能是真实的极端事件(比如2008年金融危机)。
我曾经在清洗某只妖股的数据时,发现一天之内涨了5000%。一开始以为是数据错误,后来一查,原来是那天发生了极端缩股。嗯,这种「异常」其实是真实的,不能随便删。
常用的检测方法:
- 3σ原则:超出均值±3倍标准差的值,视为异常。适合正态分布的数据。
- IQR(四分位距):低于Q1-1.5×IQR或高于Q3+1.5×IQR的值,视为异常。更稳健,不受极端值影响。
- Z-score:标准化后的分数,绝对值大于3的通常视为异常。
# 用IQR方法检测异常值
def detect_outliers_iqr(data, column):
Q1 = data[column].quantile(0.25)
Q3 = data[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
return outliers, lower_bound, upper_bound
# 模拟数据
np.random.seed(42)
df['returns'] = np.random.normal(0, 0.02, 10)
df.loc[3, 'returns'] = 0.15 # 人为制造一个异常值
outliers, lb, ub = detect_outliers_iqr(df, 'returns')
print(f"检测到 {len(outliers)} 个异常值")
print(f"下界: {lb:.4f}, 上界: {ub:.4f}")
注意:千万别机械地删除所有异常值!在金融数据里,异常值往往蕴含着重要信息。比如某只股票突然暴跌,可能是基本面出了问题,也可能是市场情绪极端化。我建议先标记出来,结合业务逻辑判断,再决定是删除、截尾还是保留。
4.3 重复值:数据里的「双胞胎」
重复值,就是一模一样或者几乎一模一样的数据行。这通常是因为数据源重复推送、或者合并多个数据源时没去重。
我记得有一次做因子回测,发现某个月的收益率特别高。查了半天,原来是数据里有一整月的重复记录,相当于把同一个月的收益算了两次。这种错误很隐蔽,但破坏力极大。
处理重复值,分两种情况:
- 完全重复:所有列都一样。直接drop_duplicates()就行。
- 部分重复:关键字段一样,但其他字段有细微差异。比如同一只股票同一天有两条记录,但成交量不同。这种需要你根据业务逻辑决定保留哪一条。
# 完全重复处理
df_clean = df.drop_duplicates()
# 部分重复处理:按日期和股票代码去重,保留第一条
df_partial = df.drop_duplicates(subset=['date', 'stock_code'], keep='first')
# 更精细的做法:保留成交量最大的那条
df_max_volume = df.sort_values('volume', ascending=False).drop_duplicates(subset=['date', 'stock_code'])
我的习惯:在合并多个数据源之后,我一定会做一次全字段的去重检查。另外,我还会检查「隐式重复」——比如同一天、同一只股票,但价格完全一样,成交量却不同。这种往往是数据源之间的冲突,需要人工介入判断。
4.4 不一致数据:数据里的「方言」
不一致数据,指的是同一个含义的数据,在不同地方用了不同的表达方式。比如股票代码有的是6位数字,有的是带后缀的;日期格式有的是YYYY-MM-DD,有的是YYYY/MM/DD。
这玩意儿在实战中特别烦人。我见过最夸张的一次,同一个数据供应商,不同年份的数据文件里,股票代码的编码规则都不一样。你想想看,光是把这些代码统一起来,就花了我整整两天时间。
常见的不一致类型:
- 格式不一致:日期格式、数字格式(带不带千分位)、字符串大小写
- 单位不一致:有的用元,有的用万元;有的用百分比,有的用小数
- 编码不一致:同一只股票,有的用600001.SH,有的用600001
- 逻辑不一致:比如开盘价高于最高价,这种明显违反市场规则的数据
# 日期格式统一
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
# 股票代码标准化(统一为6位数字+后缀)
def standardize_code(code):
code = str(code).strip()
if len(code) == 6:
if code.startswith('6'):
return code + '.SH'
elif code.startswith('0') or code.startswith('3'):
return code + '.SZ'
return code
df['stock_code'] = df['stock_code'].apply(standardize_code)
# 逻辑校验:检查价格是否合理
assert (df['high'] >= df['low']).all(), "存在最高价低于最低价的异常数据!"
assert (df['close'] >= 0).all(), "存在负价格!"
避坑指南:我曾经在合并沪深300成分股数据时,发现某只股票在A数据源里叫「贵州茅台」,在B数据源里叫「贵州茅台酒股份有限公司」。如果不做名称标准化,这两条记录会被当成两只不同的股票。所以,我建议你从一开始就建立一套统一的编码体系,比如都用Wind代码或者ISIN代码。
4.5 实战建议:建立你的数据清洗流水线
说了这么多,你可能会觉得数据清洗很繁琐。确实如此。但好消息是,你可以把这些步骤自动化,做成一条流水线。
我个人习惯的做法是:
- 先做概览:用df.info()、df.describe()快速了解数据全貌
- 再查缺失:用isnull().sum() + 可视化(热力图)
- 接着查异常:用IQR或Z-score,结合业务逻辑判断
- 然后去重:先完全去重,再处理部分重复
- 最后统一:格式、单位、编码全部标准化
每一步做完,都保存一份中间结果。这样万一后面发现问题,还能回溯到之前的步骤。
一个小技巧:在清洗过程中,我会专门维护一个「数据质量日志」,记录每次清洗发现了多少问题、怎么处理的。这样下次遇到类似数据,直接复用经验,效率能提升不少。
好了,这一章的内容就到这里。数据清洗这件事,说白了就是「慢工出细活」。你前期花的时间越多,后面建模和回测就越省心。别嫌麻烦,这是每个量化研究员必须跨过的坎。
公众号:蓝海数据掘金营,微信deep3321