金融数据清洗那些坑:我的数据处理经验与避坑指南
一、金融数据清洗的特殊性
金融数据具有高噪声、强时序性、幸存者偏差三大特征。我曾处理过某私募基金的股票因子数据,原始数据中竟存在停牌期间异常交易记录(因数据源接口BUG导致),直接导致回测收益率虚高12%。以下是关键处理流程与代码实现。
二、数据获取与初步处理
1. 数据获取(以股票数据为例)
import pandas as pd
import akshare as ak
# 获取A股日线数据(含复权因子)
def get_stock_data(code, start_date):
df = ak.stock_zh_a_hist(symbol=code, adjust="hfq", start_date=start_date)
# 处理东方财富接口的异常日期格式
df['日期'] = pd.to_datetime(df['日期'], errors='coerce')
df = df.dropna(subset=['日期']).set_index('日期')
return df
# 示例:获取贵州茅台数据
df = get_stock_data("600519", "20200101")
避坑点:
- 必须使用复权数据(
adjust="hfq"),否则除权缺口会导致收益率计算失真 - 部分数据源存在节假日非停牌零交易记录,需用
df.query('成交量 > 0')过滤
三、六大核心问题处理方案
1. 缺失值处理(比普通数据复杂3倍!)
# 金融数据缺失的特殊场景:停牌、集合竞价无成交
def handle_missing(df):
# 价格缺失:用前复权价向前填充
price_cols = ['开盘', '最高', '最低', '收盘']
df[price_cols] = df[price_cols].fillna(method='ffill')
# 成交量缺失:填充0(停牌期间无交易)
df['成交量'] = df['成交量'].fillna(0)
return df
# 验证:检查停牌期间是否填充正确
assert df.loc['2023-02-01':'2023-02-10', '成交量'].sum() == 0, "停牌期成交量处理错误!"
避坑点:
- 不可直接删除停牌日数据,否则回测时会产生未来信息泄露
- 若多只股票数据混合,需按
groupby('代码').fillna(...)分组处理
2. 异常值检测(分位数法失效案例)
import numpy as np
def detect_outliers(s, n=5):
"""改进版标准差法,解决涨停板干扰"""
median = np.median(s)
mad = np.median(np.abs(s - median)) # 中位数绝对偏差
return ~((s > median + n*mad) | (s < median - n*mad))
# 应用:过滤日收益率异常值
daily_return = df['收盘'].pct_change()
valid_mask = detect_outliers(daily_return.dropna(), n=5)
clean_df = df[valid_mask.reindex(df.index, fill_value=True)]
避坑点:
- 传统分位数法会将连续涨停误判为异常值,需结合业务规则调整阈值
- 科创板/创业板需动态调整涨跌幅阈值(20%→10%的历史数据)
3. 重复数据(暗藏玄机)
# 识别真正需要去重的场景
duplicate_mask = df.duplicated(subset=['代码', '日期'], keep=False)
if duplicate_mask.sum() > 0:
# 优先保留收盘价最接近成交均价的记录
df['价差'] = (df['收盘'] - df['成交均价']).abs()
df = df.sort_values('价差').drop_duplicates(['代码', '日期'], keep='first')
避坑点:
- 同一标的同日数据可能来自不同交易所(如AH股),不可直接去重
- 做市商报价产生的重复数据可能包含隐藏流动性信息,需谨慎处理
4. 涨跌停特殊处理
def handle_limit_up_down(df):
# 识别涨停(close=high且收益率>=9.7%)
df['is_limit_up'] = (df['收盘'] == df['最高']) & (df['收盘'].pct_change() >= 0.097)
# 涨停日特征:用次日开盘价修正流动性偏差
df['修正收盘价'] = np.where(df['is_limit_up'].shift(1), df['开盘'], df['收盘'])
return df
避坑点:
- 涨停次日往往存在流动性折价,需在因子计算中特殊处理
- 新股上市首周数据需单独提取(
df[df['上市日期'] > df.index.min()])
5. 特征工程陷阱
from talib import RSI, BBANDS
# 计算技术指标(典型错误示范)
df['rsi'] = RSI(df['收盘']) # 错误!未处理停牌期
df['upper'], _, _ = BBANDS(df['收盘']) # 错误!窗口包含未来数据
# 正确做法:滚动窗口计算
df['rsi'] = df.groupby('代码', group_keys=False)['收盘'].apply(
lambda x: x.rolling(14, min_periods=5).apply(lambda x: RSI(x)[-1]))
避坑点:
- 所有滚动计算必须使用
groupby+rolling,防止标的间数据污染 - 特征存储需带计算日期标记,避免回测时用到未来数据
6. 数据存储规范
# 最佳存储格式(Parquet + 分区)
df.to_parquet(
path='stock_data/',
partition_cols=['date_part=YYYYMM'], # 按年月分区
schema_version='v1.2',
metadata={'author': 'Your Name', 'source': 'AKShare'}
)
避坑点:
- 避免使用csv格式(无法保存数据类型和缺失值标记)
- 添加数据版本控制,防止因子计算混乱
四、工具链推荐(亲测避坑)
- 数据获取:AKShare(免费) > WindPy(付费但稳定)
- 清洗工具:Pandas(灵活) > FineDataLink(适合非编程用户)
- 特征计算:TA-Lib(C++加速) > 自行实现(易出错)
- 存储方案:Apache Parquet(列存储) > HDF5(单机专用)
五、实战案例:基金净值数据清洗
# 处理某私募基金净值数据
raw_df = pd.read_excel("fund_data.xlsx", skiprows=3, parse_dates=['估值日期'])
clean_df = (
raw_df
.rename(columns={'估值日': 'date', '单位净值': 'nav'})
.pipe(handle_missing)
.pipe(handle_limit_up_down)
.query("nav > 0") # 剔除清算期异常值
.sort_values('date')
)
clean_df.to_feather("cleaned_fund_data.feather")
成果:成功修复因数据错误导致的年化波动率计算偏差(从38%降至22%)
数据清洗箴言:金融数据清洗不是简单的ETL,而是对市场微观结构的深度理解。每一条异常数据背后,都可能隐藏着套利机会或风险信号。