第4章:数据清洗实战——缺失值处理、重复值检测、异常值识别

数据清洗这活儿,说白了就是给数据「洗澡」。我做了这么多年量化,见过太多脏数据了——有的缺胳膊少腿,有的重复得离谱,还有的数值大得吓人。你想想看,如果拿这些数据直接建模,结果能靠谱吗?

今天咱们就聊聊数据清洗的三个核心环节:缺失值、重复值、异常值。每个环节我都会结合实战经验来讲,希望能帮你少踩几个坑。

4.1 缺失值处理:别急着删,先看看情况

缺失值是最常见的问题。我刚开始做量化时,遇到缺失值就习惯性删掉,后来发现这其实挺危险的。为什么?因为缺失可能不是随机的,它背后可能有规律。

4.1.1 先检测缺失情况

拿到数据第一步,先看看缺失值长什么样。我个人习惯用 isnull()info() 快速扫一眼:

import pandas as pd
import numpy as np

# 模拟一份金融数据
df = pd.DataFrame({
    '日期': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04'],
    '收盘价': [100.5, np.nan, 102.3, 101.8],
    '成交量': [10000, 12000, np.nan, 11000],
    '市盈率': [15.2, 15.5, np.nan, np.nan]
})

# 快速查看缺失情况
print(df.isnull().sum())
print(df.info())

嗯,这里要注意:info() 能告诉你每列的非空数量,如果某列非空数远小于总行数,那就要警惕了。

4.1.2 删除法:什么时候用?

删除法是最简单粗暴的。但我的建议是:只在缺失比例很小(比如低于5%)时用。我在项目中遇到过一只股票,某天因为停牌导致数据缺失,如果直接删掉那行,整个时间序列就断了,后面做回测会出问题。

# 删除含有缺失值的行
df_dropna = df.dropna()

# 删除某列缺失的行(比如只删'收盘价'缺失的)
df_drop_subset = df.dropna(subset=['收盘价'])
注意:删除法会损失样本量。如果缺失比例高,删完数据就剩一半了,那模型还怎么训练?

4.1.3 填充法:更聪明的做法

我个人更推荐填充法。常用的填充方式有几种:

  • 均值/中位数填充:适合数值型数据,比如用过去30天的平均收盘价填充缺失值
  • 前向填充/后向填充:时间序列数据特别适合,比如用前一天的数据填充
  • 插值法:线性插值、多项式插值,更平滑
# 均值填充
df['收盘价'].fillna(df['收盘价'].mean(), inplace=True)

# 前向填充(金融时间序列常用)
df['收盘价'].fillna(method='ffill', inplace=True)

# 线性插值
df['收盘价'].interpolate(method='linear', inplace=True)
实战技巧:我曾经处理过一只港股的数据,因为节假日不同导致连续缺失3天。用前向填充最合理,因为停牌期间股价理论上不变。但如果用均值填充,反而会引入偏差。

4.1.4 插值法:更精细的处理

插值法适合数据有趋势的情况。比如股价在上涨趋势中缺失了一天,线性插值能给出一个合理的估计值。

# 时间序列插值(按时间索引)
df['日期'] = pd.to_datetime(df['日期'])
df.set_index('日期', inplace=True)
df['收盘价'].interpolate(method='time', inplace=True)

你想想看,如果缺失值出现在趋势转折点,插值法可能会低估或高估。所以我的习惯是:先可视化看看趋势,再决定用哪种插值

4.2 重复值检测:别让数据「复制粘贴」

重复值在金融数据里也挺常见的。比如数据源重复推送、同一笔交易被记录两次。我见过最夸张的一次,某只股票的数据有30%是重复的,那回测结果简直没法看。

4.2.1 检测重复行

# 检测完全重复的行
duplicates = df.duplicated()
print(f"重复行数: {duplicates.sum()}")

# 查看重复的具体行
df[df.duplicated(keep=False)]

keep 参数很关键:keep='first' 保留第一个,keep='last' 保留最后一个,keep=False 标记所有重复。

4.2.2 处理重复值

# 删除重复行,保留第一个
df_clean = df.drop_duplicates()

# 按指定列判断重复(比如按'日期'去重)
df_clean = df.drop_duplicates(subset=['日期'])
避坑指南:我曾经在处理期货数据时,发现同一时间戳有两条记录,但价格不同。后来发现是数据源更新了价格,旧记录没删。这种情况下不能简单去重,得保留最新的那条。

4.3 异常值识别与处理:别让「黑天鹅」毁了模型

异常值在金融数据里特别棘手。一方面,它可能是数据录入错误;另一方面,它也可能是真实的极端行情(比如2020年原油期货跌到负值)。

4.3.1 常用识别方法

我常用的方法有三种:

方法 适用场景 原理
Z-score 正态分布数据 超过均值±3倍标准差
IQR(四分位距) 偏态分布数据 低于Q1-1.5IQR或高于Q3+1.5IQR
可视化法 所有数据 箱线图、散点图
# Z-score 方法
from scipy import stats
z_scores = np.abs(stats.zscore(df['收盘价'].dropna()))
outliers = df[z_scores > 3]

# IQR 方法
Q1 = df['收盘价'].quantile(0.25)
Q3 = df['收盘价'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = df[(df['收盘价'] < Q1 - 1.5*IQR) | (df['收盘价'] > Q3 + 1.5*IQR)]

4.3.2 处理策略

识别出异常值后,怎么处理?我的经验是分情况讨论:

  • 明显错误(比如股价为负数):直接删除或修正
  • 极端但真实(比如金融危机时的暴跌):保留,但做缩尾处理
  • 不确定:用中位数或前后值替换
# 缩尾处理(将异常值替换为边界值)
def winsorize_series(s, limits=[0.01, 0.01]):
    lower = s.quantile(limits[0])
    upper = s.quantile(1 - limits[1])
    return s.clip(lower, upper)

df['收盘价_缩尾'] = winsorize_series(df['收盘价'])
个人经验:做量化回测时,我建议对异常值做缩尾而不是直接删除。因为删除会破坏时间序列的连续性,而缩尾保留了数据点,只是把极端值拉回到合理范围。

4.4 知识体系总览

下面这张图总结了数据清洗的核心逻辑,你可以对照着梳理自己的流程:

数据清洗核心流程 原始金融数据 缺失值处理 重复值检测 异常值识别 处理方法 • 删除法(缺失<5%) • 填充法(均值/前向) • 插值法(线性/时间) 检测方法 • duplicated() 检测 • 按列去重 • 保留最新记录 识别方法 • Z-score(正态) • IQR(偏态) • 箱线图可视化 清洗后数据

4.5 实战建议

最后,我想分享几点实战中的体会:

  • 先备份原始数据:我每次清洗前都会复制一份原始数据,万一处理错了还能恢复
  • 记录处理日志:删了多少行、填充了什么值,都记下来。这在做报告时特别有用
  • 不要过度清洗:有时候「脏数据」反而包含信息。比如异常值可能预示着市场异动
核心原则:数据清洗没有标准答案,关键是要理解你的数据和业务场景。同样的缺失值,在回测中可能直接删掉就行,但在实盘交易中就必须谨慎处理。

好了,数据清洗这部分就聊到这儿。记住一句话:垃圾进,垃圾出。花在数据清洗上的时间,永远值得。

专注资料整理