数据清洗实战:缺失值处理、异常值检测、数据对齐与重采样、内存优化技巧

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

我见过太多人,因子算得飞起,结果一查,数据里全是坑。缺失值、异常值、时间戳对不齐……这些坑不填,后面所有分析都是白搭。

今天咱们就把这四块硬骨头啃下来。嗯,都是实战里天天碰到的活。

核心要点:数据清洗不是「洗得越干净越好」,而是「洗得适合你的策略」。

一、缺失值处理:别让空值毁了你的因子

我个人习惯,拿到数据第一件事就是看缺失率。

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,别省这点内存。

知识体系总览

下面这张图,把数据清洗的四个核心模块串起来了。你可以把它当成操作手册,遇到问题直接查。

数据清洗实战知识体系 缺失值处理 删除行/列(低/高缺失率) ffill / bfill / 中位数填充 异常值检测 3σ法则(正态分布) IQR法则(稳健方法) 截尾 vs 缩尾处理 对齐与重采样 时间戳对齐(reindex) 日频→周频/月频(resample) 内存优化 float32 / int32 / category 分块读取(chunksize) 原始数据

数据清洗这件事,说白了就是「先看再动手」。

我个人习惯,每次清洗前先跑一遍describe()和info(),心里有个数。别上来就咔咔删数据,删完发现有用的信息也没了。

嗯,今天就到这儿。记住:干净的数据是因子挖掘的基石,这块省时间,后面全是坑。


蓝海数据掘金营,专注资料整理