数据清洗实战:缺失值处理、异常值检测、数据对齐与重采样、内存优化技巧
数据清洗,说白了就是给原始数据「洗澡」。
我见过太多人,因子算得飞起,结果一查,数据里全是坑。缺失值、异常值、时间戳对不齐……这些坑不填,后面所有分析都是白搭。
今天咱们就把这四块硬骨头啃下来。嗯,都是实战里天天碰到的活。
核心要点:数据清洗不是「洗得越干净越好」,而是「洗得适合你的策略」。
一、缺失值处理:别让空值毁了你的因子
我个人习惯,拿到数据第一件事就是看缺失率。
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('stock_data.csv')
# 看一眼缺失情况
missing_ratio = df.isnull().sum() / len(df)
print(missing_ratio[missing_ratio > 0])
缺失率低于5%的,我一般直接删行。高于30%的,这列基本废了,建议直接删列。
中间那档怎么办?我常用三种方法:
- 向前填充(ffill):适合时间序列,比如停牌日的数据用前一天的补上
- 向后填充(bfill):适合收盘后补数据
- 中位数填充:适合截面数据,比如某只股票某天缺失了市盈率
# 向前填充
df['close'].fillna(method='ffill', inplace=True)
# 中位数填充
df['pe_ratio'].fillna(df['pe_ratio'].median(), inplace=True)
我的经验:千万别用均值填充金融数据。金融数据分布偏得厉害,均值容易被极端值带跑。中位数稳得多。
二、异常值检测:3σ法则 vs IQR
异常值检测,我把它分成两类:
1. 3σ法则(正态分布假设)
这个适合收益率、波动率这类近似正态的数据。
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['return_outlier'] = detect_outliers_3sigma(df['daily_return'])
注意:金融数据很多不是正态的。比如成交量,分布长尾得很。这时候3σ法则会误杀很多正常值。
2. IQR法则(稳健方法)
这个我更喜欢。它不受极端值影响,适合大部分金融数据。
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)
# 标记异常
df['volume_outlier'] = detect_outliers_iqr(df['volume'])
避坑指南:我曾经在回测里没处理异常值,结果一个涨停板的成交量被当成正常值,导致因子权重全偏了。后来我养成了习惯——异常值先标记,再决定是截尾还是缩尾。
处理方式有两种:
- 截尾(删除):直接删掉异常行,适合数据量大的情况
- 缩尾(Winsorize):把异常值拉到边界值,适合不想丢失样本的情况
from scipy.stats.mstats import winsorize
# 缩尾处理
df['volume_winsorized'] = winsorize(df['volume'], limits=[0.01, 0.01])
三、数据对齐与重采样:时间戳对不齐?那是常态
做多因子时,最头疼的就是数据对齐。
A股有停牌,期货有夜盘,不同数据源的时间戳还不一样。你想想看,如果不对齐,算出来的相关性全是错的。
1. 时间戳对齐
# 确保时间戳是datetime类型
df['date'] = pd.to_datetime(df['date'])
# 按日期对齐
df.set_index('date', inplace=True)
df_aligned = df.reindex(pd.date_range(start='2020-01-01', end='2023-12-31', freq='D'))
2. 重采样
日频数据转周频、月频,是因子挖掘的常规操作。
# 日频转周频:取每周最后一个交易日
weekly_df = df.resample('W').last()
# 日频转月频:取每月均值
monthly_df = df.resample('M').mean()
我的习惯:重采样时,价格用last,成交量用sum,收益率用prod(复利)。别搞混了。
四、内存优化技巧:别让数据撑爆你的电脑
做量化的人,谁没被内存爆过?
我记得有一次,加载全A股10年数据,直接卡死。从那以后,我养成了几个习惯:
1. 数据类型优化
pandas默认用float64,但很多数据用float32就够了。
# 查看内存占用
print(df.memory_usage(deep=True))
# 优化数据类型
df['close'] = df['close'].astype('float32')
df['volume'] = df['volume'].astype('int32')
2. 分类数据用category
股票代码、行业分类这些重复值多的列,用category能省一大半内存。
df['stock_code'] = df['stock_code'].astype('category')
df['industry'] = df['industry'].astype('category')
3. 分块读取
文件太大?分块读。
chunk_size = 10000
for chunk in pd.read_csv('big_file.csv', chunksize=chunk_size):
# 处理每个chunk
process(chunk)
避坑指南:我曾经为了省内存,把所有列都转成float32,结果某只股票的价格精度不够,导致计算收益率时出现0值。记住:价格列用float64,成交量用int64,别省这点内存。
知识体系总览
下面这张图,把数据清洗的四个核心模块串起来了。你可以把它当成操作手册,遇到问题直接查。
数据清洗这件事,说白了就是「先看再动手」。
我个人习惯,每次清洗前先跑一遍describe()和info(),心里有个数。别上来就咔咔删数据,删完发现有用的信息也没了。
嗯,今天就到这儿。记住:干净的数据是因子挖掘的基石,这块省时间,后面全是坑。