4. 数据清洗与预处理:处理缺失值、异常值,数据对齐与频率转换
各位同学,咱们直接进入正题。融资融券数据拿到手,第一件事不是跑策略,而是「洗数据」。
我见过太多人,数据没洗干净就往上怼模型,结果回测曲线漂亮得不行,实盘一跑就崩。为什么?因为数据里有坑。
这一章,咱们就把这些坑一个一个填上。
4.1 缺失值处理:别让「空」坑了你
融资融券数据里,缺失值很常见。比如某天没有交易,或者数据源漏了。我个人习惯,先看看缺失比例。
核心原则:缺失比例 < 5%,用填充;缺失比例 > 20%,直接删掉这个字段。
具体怎么填?我常用的三种方法:
- 前向填充(ffill):用上一个交易日的数据补。适合融资余额这种缓慢变化的指标。
- 后向填充(bfill):用下一个交易日的数据补。适合收盘后发布的指标。
- 插值法:线性插值,适合连续缺失。比如中间缺了三天,用前后值拉一条直线。
举个例子,我处理过某券商的融资数据,2015年股灾期间连续三天缺失。用ffill补?不行,那三天市场剧烈波动,前一天的余额根本不代表当天情况。最后我用了插值法,效果还行。
import pandas as pd
import numpy as np
# 模拟融资余额数据
dates = pd.date_range('2024-01-01', periods=10, freq='D')
balance = [100, 102, np.nan, np.nan, 108, 110, np.nan, 115, 118, 120]
df = pd.DataFrame({'date': dates, 'balance': balance})
df.set_index('date', inplace=True)
# 前向填充
df['balance_ffill'] = df['balance'].ffill()
# 线性插值
df['balance_interp'] = df['balance'].interpolate(method='linear')
print(df)
我的小技巧:别一股脑全用ffill。先画个图,看看缺失模式。如果缺失集中在某个时间段,那可能是数据源的问题,得回去查。
4.2 异常值检测:揪出那些「离谱」的数字
融资融券数据里,异常值很隐蔽。比如某天融资余额突然暴涨10倍,你以为是行情来了?其实可能是数据录入错误。
我常用的检测方法:
- 3σ原则:超过均值±3个标准差的值,标记为异常。适合正态分布的数据。
- IQR方法:四分位距法。低于Q1-1.5*IQR或高于Q3+1.5*IQR,算异常。更稳健,不受极端值影响。
- 业务规则:比如融资余额不能为负,融券余量不能超过总股本。这些是硬约束。
我曾经遇到过一个案例:某只股票的融券余额突然从500万跳到5个亿,第二天又跌回500万。用3σ检测,这个点被标记出来了。后来一查,是数据源把「万」和「亿」搞混了。嗯,这种坑,你不做异常检测根本发现不了。
# IQR方法检测异常值
Q1 = df['balance'].quantile(0.25)
Q3 = df['balance'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['balance'] < lower_bound) | (df['balance'] > upper_bound)]
print(f"检测到 {len(outliers)} 个异常值")
注意:别把「市场异动」当成异常值。比如2015年股灾期间,融资余额断崖式下跌,那是真实的市场行为,不是数据错误。处理前一定要结合业务背景判断。
4.3 数据对齐:让不同频率的数据「对上话」
融资融券数据是日频的,但你的择时信号可能是周频的,或者你要跟其他指标(比如北向资金、股指期货)做对比。这时候,数据对齐就很重要了。
说白了,就是让所有数据的时间戳保持一致。我常用的对齐方式:
- 按日期对齐:用merge或join,指定on='date'。注意日期格式要统一。
- 按交易日期对齐:A股只有交易日才有数据,非交易日要剔除。别把周末的数据硬凑进去。
- 按收盘时间对齐:融资融券数据是盘后发布的,跟盘中数据对不上。这个细节很多人忽略。
我记得有一次做策略回测,融资余额数据和股价数据差了整整一天。为什么?因为融资余额是T日收盘后发布,但股价是T日盘中数据。你想想看,用T日的融资余额去预测T日的股价,这不就是未来函数吗?
# 对齐融资余额和股价数据
balance_df = pd.read_csv('margin_balance.csv', parse_dates=['date'])
price_df = pd.read_csv('stock_price.csv', parse_dates=['date'])
# 内连接,只保留共同交易日
aligned_df = pd.merge(balance_df, price_df, on='date', how='inner')
# 检查对齐后的数据量
print(f"对齐前:balance {len(balance_df)} 行,price {len(price_df)} 行")
print(f"对齐后:{len(aligned_df)} 行")
避坑指南:我曾经用outer join对齐数据,结果多出来一堆NaN。后来才发现,是因为两个数据源的节假日定义不一样。一个用了上交所的日历,一个用了深交所的。从此以后,我统一用交易所的交易日历做对齐。
4.4 频率转换:从日频到周频
很多择时策略用周频信号,因为日频噪音太大。怎么把日频数据转成周频?
常用的聚合方式:
- 融资余额:取每周最后一个交易日的值。因为余额是存量数据,不是流量。
- 融资买入额:取每周总和。因为是流量数据,累计才有意义。
- 融券余量:取每周最后一个交易日的值。跟融资余额一样。
我个人习惯用resample方法,配合自定义的聚合函数。注意,A股的一周是从周一到周五,别用自然周。
# 日频转周频
df_weekly = df.resample('W-FRI').agg({
'balance': 'last', # 融资余额:取周五值
'buy_amount': 'sum', # 融资买入额:周累计
'short_volume': 'last' # 融券余量:取周五值
})
print(df_weekly.head())
关键点:频率转换时,一定要搞清楚每个指标的业务含义。存量指标用last,流量指标用sum。搞反了,信号就全错了。
4.5 本章知识体系
下面这张图,把数据清洗与预处理的整个流程串起来了。你照着这个流程走,基本不会出大问题。
数据清洗这一步,看着琐碎,但决定了你后面所有工作的质量。我见过太多人,策略逻辑写得天花乱坠,结果数据没洗干净,全白搭。
记住一句话:垃圾进,垃圾出。数据干净了,策略才有意义。