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)
小技巧:向前填充适合时间序列数据。但要注意,如果连续缺失太多(比如超过3期),填充出来的值可能失真。我一般会设个阈值,超过就标记为「不可用」。

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')
避坑指南:errors='coerce'会把无法转换的值变成NaN。用之前一定要确认数据质量,否则可能悄无声息地丢失数据。

4.4 知识体系总览

下面这张图,把数据清洗的核心逻辑串起来了。你可以把它当作操作手册,每次处理数据前看一眼。

数据清洗核心流程 原始宏观因子数据 缺失值处理 异常值处理 数据类型转换 向前填充 线性插值 时间插值 3-Sigma法则 IQR方法 业务规则判断 日期类型转换 数值类型转换 分类变量编码 清洗后的干净数据

4.5 实战建议

说了这么多,总结几条我自己的经验:

  • 先看后动:拿到数据先describe()一下,看看缺失比例、分布情况。别上来就fillna()。
  • 保留原始列:我习惯在原始数据后面加一列,比如'CPI_clean',而不是直接覆盖。万一处理错了还能回滚。
  • 记录处理日志:哪些值被填充了,哪些被标记为异常了,都记下来。回测结果不对劲时,这些日志能救命。
  • 一致性检查:处理完数据后,检查一下时间索引是否连续、数值范围是否合理。我见过有人把CPI填充成负数,回测出来收益率高得离谱。
一个小习惯:每次清洗完数据,我都会画个分布图,跟原始数据对比一下。如果分布形状变化太大,说明处理方式可能有问题。肉眼检查永远是最快的。

数据清洗没有标准答案。不同因子、不同场景,处理方法都不一样。但核心原则就一条:让数据真实反映经济现实。你想想看,如果数据本身都是错的,再牛的模型也白搭。


专注资料整理