4、数据清洗基础:处理缺失值、异常值与数据类型转换
数据清洗,说白了就是给原始数据「洗澡」。我做了这么多年量化,见过太多人一上来就建模型,结果跑出来的结果自己都不敢信。为什么?因为数据没洗干净。
宏观因子数据尤其脏。不同来源、不同频率、不同统计口径,凑在一起就是个大杂烩。今天我们就聊聊怎么把这锅杂烩炖成一道好菜。
4.1 缺失值处理:别让「空」坑了你
宏观数据里缺失值太常见了。GDP季度数据,有些国家晚发布一个月;CPI月度数据,偶尔某个月统计局放假。这些空值怎么填?
我个人习惯,先看缺失比例。超过30%的变量,直接扔掉。低于5%的,可以大胆补。中间那档,得看情况。
4.1.1 向前填充(Forward Fill)
这是我最常用的方法。说白了就是「用上一个已知值填当前空值」。比如昨天CPI是2.5%,今天没数据,那就还是2.5%。
为什么好用?宏观因子变化慢,相邻时间点的值通常很接近。我在处理中国PMI数据时,经常遇到春节月份数据缺失,向前填充效果就很好。
import pandas as pd
import numpy as np
# 模拟宏观因子数据
dates = pd.date_range('2020-01-01', periods=10, freq='M')
data = {
'CPI': [2.5, 2.6, np.nan, 2.7, 2.8, np.nan, 2.9, 3.0, np.nan, 3.1],
'GDP': [6.0, np.nan, 6.1, 6.2, np.nan, 6.3, 6.4, np.nan, 6.5, 6.6]
}
df = pd.DataFrame(data, index=dates)
# 向前填充
df_ffill = df.ffill()
print(df_ffill)
4.1.2 插值法(Interpolation)
向前填充太粗暴?那就试试插值。线性插值假设数据在两个点之间是线性变化的。比如1月CPI是2.5%,3月是2.7%,那2月就填2.6%。
我曾经处理过一组利率数据,缺失值刚好在政策转向的节点上。向前填充会忽略这个转折,但线性插值能捕捉到趋势变化。结果证明,插值后的回测效果好了不少。
# 线性插值
df_interp = df.interpolate(method='linear')
print(df_interp)
# 时间插值(考虑时间间隔)
df_time = df.interpolate(method='time')
print(df_time)
4.2 异常值处理:揪出那些「离谱」的数据
你想想看,GDP增长率突然蹦到15%,CPI一夜之间变成负数。这些数据你敢信吗?反正我不敢。
异常值有两种:一种是数据录入错误(比如多打了个0),另一种是真实但罕见的事件(比如金融危机)。处理方式不一样。
4.2.1 3-Sigma法则
这个方法简单粗暴。假设数据服从正态分布,超过均值±3倍标准差的值就是异常。概率上只有0.3%的可能性。
我在项目中遇到过,用3-sigma处理M2增速数据时,2008年金融危机期间的数据全被标记为异常。但那次确实是真实事件,不能简单剔除。所以我会先看业务背景,再决定是否处理。
def detect_outliers_3sigma(series):
mean = series.mean()
std = series.std()
lower = mean - 3 * std
upper = mean + 3 * std
return (series < lower) | (series > upper)
# 标记异常值
df['CPI_outlier'] = detect_outliers_3sigma(df['CPI'])
print(df[df['CPI_outlier']])
4.2.2 IQR方法(四分位距)
3-sigma对极端值敏感,IQR更稳健。它用中位数和四分位数来定义异常范围,不受极端值影响。
公式很简单:异常值 = 小于Q1-1.5*IQR 或 大于Q3+1.5*IQR。IQR = Q3 - Q1。
说白了,就是看数据离「大多数」有多远。我处理汇率数据时,IQR比3-sigma好用,因为汇率经常有厚尾分布。
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return (series < lower) | (series > upper)
# 用IQR检测
df['GDP_outlier'] = detect_outliers_iqr(df['GDP'])
print(df[df['GDP_outlier']])
4.3 数据类型转换:让计算机「看懂」你的数据
这个问题看似简单,但坑最多。你从数据库里读出来的数据,日期可能是字符串,数值可能是对象类型。不转换,后面所有计算都会报错。
我记得有一次跑回测,收益率算出来全是NaN。查了半天,发现收益率列是字符串类型,里面有百分号。pandas把它当文本处理了。
4.3.1 日期类型转换
宏观因子数据离不开时间。日期格式五花八门:'2020-01-01'、'2020/01/01'、'01-Jan-2020'。统一转成datetime类型。
# 字符串转日期
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
# 设置日期为索引
df.set_index('date', inplace=True)
4.3.2 数值类型转换
最常见的问题:数字里混了逗号、百分号、货币符号。比如'1,234.56'、'5.2%'、'$1000'。
处理方法:先清洗字符串,再转数值。
# 去除逗号和百分号
df['CPI'] = df['CPI'].str.replace(',', '').str.replace('%', '').astype(float)
# 或者用pd.to_numeric,自动处理错误
df['GDP'] = pd.to_numeric(df['GDP'], errors='coerce')
4.4 知识体系总览
下面这张图,把数据清洗的核心逻辑串起来了。你可以把它当作操作手册,每次处理数据前看一眼。
4.5 实战建议
说了这么多,总结几条我自己的经验:
- 先看后动:拿到数据先describe()一下,看看缺失比例、分布情况。别上来就fillna()。
- 保留原始列:我习惯在原始数据后面加一列,比如'CPI_clean',而不是直接覆盖。万一处理错了还能回滚。
- 记录处理日志:哪些值被填充了,哪些被标记为异常了,都记下来。回测结果不对劲时,这些日志能救命。
- 一致性检查:处理完数据后,检查一下时间索引是否连续、数值范围是否合理。我见过有人把CPI填充成负数,回测出来收益率高得离谱。
数据清洗没有标准答案。不同因子、不同场景,处理方法都不一样。但核心原则就一条:让数据真实反映经济现实。你想想看,如果数据本身都是错的,再牛的模型也白搭。