4、数据清洗基础:缺失值处理(删除、填充、插值),重复值检测与去重

数据清洗,说白了就是给数据「洗澡」。

我做了这么多年量化,见过最离谱的数据集,缺失率高达 40%,还有一堆重复的行情记录。你想想看,拿这种数据去跑因子,结果能信吗?

今天我们就来聊聊数据清洗里最基础、也最核心的两个动作:缺失值处理重复值处理

核心观点:清洗不是越狠越好,而是「对症下药」。缺失值怎么补、重复值怎么删,取决于你的数据场景和业务逻辑。

数据清洗核心流程 原始数据 数据质量检查 缺失值处理 重复值处理 删除 填充 插值 完全去重 部分去重 业务去重

一、缺失值处理:三种策略

缺失值在金融数据里太常见了。停牌、节假日、数据源故障,都会导致空值。我个人习惯,拿到数据第一件事就是 df.isnull().sum(),先摸清底细。

1. 删除缺失值

最粗暴,但也最有效——前提是你丢得起。

import pandas as pd

# 删除包含任何缺失值的行
df_clean = df.dropna()

# 删除某列缺失的行
df_clean = df.dropna(subset=['close', 'volume'])

# 删除缺失比例超过 50% 的列
df_clean = df.dropna(thresh=len(df) * 0.5, axis=1)

⚠️ 注意:删除操作会丢失信息。如果缺失率超过 30%,我建议你慎重考虑。我曾经在回测中删掉了 40% 的数据,结果因子表现虚高——因为删掉的都是熊市数据。

2. 填充缺失值

填充比删除更「温柔」。常用的填充方式有几种:

填充方式 适用场景 代码示例
固定值填充 缺失值有明确业务含义(如 0) df.fillna(0)
均值/中位数填充 数值型特征,分布较稳定 df.fillna(df.mean())
前向/后向填充 时间序列数据(如股票价格) df.fillna(method='ffill')
分组填充 不同组别差异大(如不同股票) df.groupby('code').fillna(method='ffill')

举个例子。处理股票数据时,停牌日的价格是缺失的。用前向填充(ffill)最合理——停牌期间价格不变嘛。

# 按股票代码分组,前向填充停牌日价格
df['close'] = df.groupby('code')['close'].fillna(method='ffill')

💡 我的经验:千万别一股脑全用均值填充。金融数据有很强的时序特性,均值填充会破坏序列的连续性。我见过有人把收益率缺失值填 0,结果夏普比率直接翻倍——这完全是假象。

3. 插值法

插值比简单填充更「聪明」。它利用已知数据点之间的趋势来估算缺失值。

# 线性插值
df['close'] = df['close'].interpolate(method='linear')

# 时间插值(适用于时间序列)
df['close'] = df['close'].interpolate(method='time')

# 多项式插值(高阶,慎用)
df['close'] = df['close'].interpolate(method='polynomial', order=2)

为什么会用插值?说白了,金融数据很多时候是连续的。比如 5 分钟 K 线,中间缺了一根,用前后两根线性插值,比用均值填充合理得多。

关键区别:填充是「猜一个值放进去」,插值是「根据趋势算一个值放进去」。插值更尊重数据本身的形态。

二、重复值检测与去重

重复值比缺失值更隐蔽。缺失值一眼就能看到 NaN,重复值可能藏在数据里,悄无声息地放大你的统计量。

1. 检测重复值

# 检查完全重复的行
df.duplicated().sum()

# 检查某几列是否重复
df.duplicated(subset=['code', 'date', 'close']).sum()

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

我遇到过最离谱的一次,某只股票一天内被记录了 5 次相同的行情数据。原因是数据源重复推送了。如果不处理,那天的收益率计算就会出问题。

2. 去重策略

策略 说明 代码
保留第一个 默认行为,保留首次出现的行 df.drop_duplicates()
保留最后一个 适合时间序列,保留最新记录 df.drop_duplicates(keep='last')
全部删除 只要重复就全删,一个不留 df.drop_duplicates(keep=False)
按列去重 只根据关键列判断重复 df.drop_duplicates(subset=['code', 'date'])
# 按股票代码和日期去重,保留最新一条
df_clean = df.sort_values('date').drop_duplicates(
    subset=['code', 'date'], 
    keep='last'
)

⚠️ 避坑指南:我曾经在合并多个数据源时,忘了去重就直接算因子。结果因子值被重复计算了两次,相关性分析完全跑偏。后来我养成了一个习惯:任何数据合并操作之后,第一件事就是检查重复值。

三、实战中的注意事项

  • 先处理缺失值还是重复值?我个人习惯先处理重复值。因为重复行会影响缺失值的统计量(比如均值),先去掉重复,缺失值处理更准确。
  • 不要盲目删除。金融数据里,缺失本身可能包含信息。比如某只股票长期缺失交易数据,说明它流动性差。直接删掉,反而会高估组合的流动性。
  • 记录清洗日志。我每次清洗都会记录删了多少行、填了多少值。这样回测出问题时,能快速定位是数据问题还是策略问题。
# 一个完整的清洗流程示例
def clean_data(df):
    # 1. 去重
    before = len(df)
    df = df.drop_duplicates(subset=['code', 'date'], keep='last')
    print(f"去重删除 {before - len(df)} 行")
    
    # 2. 处理缺失值
    df['close'] = df.groupby('code')['close'].fillna(method='ffill')
    df['volume'] = df.groupby('code')['volume'].fillna(0)
    
    # 3. 删除仍然缺失的行(比如开头几行)
    df = df.dropna(subset=['close'])
    
    return df

💡 最后说一句:数据清洗没有银弹。你花 80% 的时间在清洗上,一点都不夸张。但正是这 80% 的时间,决定了你后面 20% 的建模工作能不能站得住脚。

专注资料整理