第三章 数据清洗与预处理:缺失值处理、异常值检测、数据标准化与归一化
各位做量化投资的朋友,尤其是搞碳因子的,应该都有体会——数据质量决定了模型的天花板。你模型再花哨,数据是垃圾,结果就是垃圾。今天咱们聊聊数据清洗与预处理,这是整个碳因子挖掘流程中最枯燥、但也是最关键的一环。
核心观点:数据预处理不是「锦上添花」,而是「地基工程」。地基没打好,后面盖的楼越高,摔得越惨。
3.1 缺失值处理:别让「空」坑了你
碳数据里缺失值特别常见。比如某个控排企业某月没上报排放数据,或者某个碳价指数某天没更新。我刚开始做碳因子研究时,遇到缺失值就直接删掉,结果回测曲线漂亮得不行,实盘一跑就崩。后来才明白——缺失值本身可能就携带信息。
3.1.1 缺失值类型
我个人习惯把缺失值分成三类:
- 完全随机缺失(MCAR): 缺失跟数据本身没关系。比如传感器偶尔抽风,这种最安全,直接删掉影响不大。
- 随机缺失(MAR): 缺失跟其他变量有关。比如某企业排放数据缺失,可能是因为它规模小、监测设备差。这种就要小心了。
- 非随机缺失(MNAR): 缺失跟变量本身有关。比如碳价大跌时,有些交易所干脆不报数据。这种最麻烦,需要专门建模处理。
我的经验:碳数据里大部分缺失属于MAR。比如企业碳强度数据,小企业往往缺失率高。如果你直接删掉,模型就会偏向大企业,产生选择偏差。
3.1.2 处理方法
我常用的方法有这几种,按推荐程度排序:
- 删除法: 缺失率低于5%且随机时可用。但碳数据里我很少用,因为样本本来就少。
- 均值/中位数填充: 简单粗暴,但会降低方差。我一般只在快速验证时用。
- 前向/后向填充: 时间序列数据首选。比如碳价日数据,用前一天的值填充,逻辑上说得通。
- 插值法: 线性插值、样条插值。我个人偏好,尤其是处理排放量这种有趋势的数据。
- 模型预测填充: 用随机森林或KNN预测缺失值。精度最高,但计算量大。我一般在关键因子构建时才用。
# 示例:碳价数据缺失值处理
import pandas as pd
import numpy as np
# 模拟碳价数据
dates = pd.date_range('2023-01-01', periods=100, freq='D')
prices = np.random.randn(100) * 5 + 50
prices[10:15] = np.nan # 人为制造缺失
df = pd.DataFrame({'date': dates, 'price': prices})
df.set_index('date', inplace=True)
# 前向填充(碳价常用)
df['price_ffill'] = df['price'].fillna(method='ffill')
# 线性插值(我个人推荐)
df['price_interp'] = df['price'].interpolate(method='linear')
print(df.head(20))
注意:千万不要在回测中使用未来数据填充!比如用后一天的值填充前一天,这叫「未来函数」,回测会严重失真。我曾经见过有人犯这个错,回测年化50%,实盘直接腰斩。
3.2 异常值检测:揪出那些「捣乱」的数据点
碳数据里的异常值特别有迷惑性。比如某天碳价突然暴涨20%,可能是政策利好,也可能是数据录入错误。怎么区分?嗯,这里就要靠方法了。
3.2.1 常用检测方法
| 方法 | 适用场景 | 我的评价 |
|---|---|---|
| Z-score | 正态分布数据 | 简单但碳数据很少正态 |
| IQR(四分位距) | 偏态分布 | 碳因子常用,稳健 |
| 孤立森林 | 高维数据 | 效果好,但解释性差 |
| DBSCAN | 密度聚类 | 适合发现局部异常 |
我个人最常用的是IQR方法。为什么?因为碳数据往往有厚尾特征,Z-score会把很多正常值误判为异常。举个例子,碳价在政策发布日波动大,但那是真实的市场行为,不是异常。
# IQR异常值检测
def detect_outliers_iqr(data, factor=1.5):
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - factor * IQR
upper_bound = Q3 + factor * IQR
return (data < lower_bound) | (data > upper_bound)
# 应用到碳价数据
outliers = detect_outliers_iqr(df['price_interp'].dropna())
print(f"检测到 {outliers.sum()} 个异常值")
避坑指南:我曾经在构建碳动量因子时,用3倍标准差检测异常值,结果把几次重要的政策冲击都剔除了。后来改用IQR + 领域知识判断,效果好了很多。记住:统计方法只是工具,最终判断要靠你对碳市场的理解。
3.3 数据标准化与归一化:让不同量纲的数据「对话」
碳因子数据量纲差异巨大。碳价可能是几十元,企业排放量可能是几百万吨,GDP可能是几万亿。如果不做标准化,模型会天然偏向数值大的特征。说白了,就是「大数欺负小数」。
3.3.1 标准化 vs 归一化
这两个概念经常被混淆,我简单说清楚:
- 标准化(Standardization): 减去均值,除以标准差。结果服从均值为0、方差为1的分布。适合数据有异常值的情况。
- 归一化(Normalization): 缩放到[0,1]区间。适合数据有明确边界的情况。
我个人在碳因子构建中,90%的情况用标准化。为什么?因为碳数据经常有极端值,归一化会把极端值压缩到0或1,丢失信息。而标准化虽然也会受影响,但相对稳健。
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 模拟碳因子数据
factors = pd.DataFrame({
'carbon_price': np.random.randn(100) * 10 + 50,
'emission_volume': np.random.randn(100) * 100 + 1000,
'gdp_growth': np.random.randn(100) * 2 + 3
})
# 标准化(推荐)
scaler_std = StandardScaler()
factors_std = pd.DataFrame(
scaler_std.fit_transform(factors),
columns=factors.columns
)
# 归一化
scaler_mm = MinMaxScaler()
factors_mm = pd.DataFrame(
scaler_mm.fit_transform(factors),
columns=factors.columns
)
print("标准化后均值:", factors_std.mean().round(2))
print("归一化后范围:", factors_mm.min().values, "~", factors_mm.max().values)
3.3.2 什么时候用归一化?
你想想看,如果因子本身有物理意义的上限,比如碳配额分配比例(0%~100%),那归一化就合适。或者用神经网络时,输入层通常需要归一化。但如果是线性模型、树模型,标准化更稳妥。
我的原则:不确定时先用标准化。回测对比两种方案,选效果好的。别纠结理论,实践出真知。
3.4 知识体系总览
下面这张图是我自己总结的数据预处理流程,每次做碳因子项目都会参考:
3.5 实战中的坑与建议
最后分享几个我踩过的坑:
- 坑一: 先做标准化再做训练测试集划分。正确做法是先划分,再用训练集的参数标准化测试集。否则会有数据泄露。
- 坑二: 碳因子数据有季节性。比如排放量冬季高夏季低。处理缺失值时,最好用同期数据填充,而不是简单的前向填充。
- 坑三: 异常值不一定要剔除。有时候异常值就是信号。比如碳价突破历史新高,可能意味着趋势变化。我一般会单独标记异常值,而不是直接删除。
我的习惯:每次做数据预处理,我都会保存一份原始数据副本,再建一个预处理日志文件。记录每一步做了什么、为什么这么做。这样回测出问题时,能快速定位是数据问题还是模型问题。
好了,数据清洗与预处理就聊到这儿。记住一句话:花在数据预处理上的时间,永远不会白费。你在这上面多花一小时,后面建模就能省十小时。