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 本章知识体系

下面这张图,把数据清洗与预处理的整个流程串起来了。你照着这个流程走,基本不会出大问题。

数据清洗与预处理流程 原始融资融券数据 缺失值处理(ffill/bfill/插值) 异常值检测(3σ/IQR/业务规则) 数据对齐(按日期/交易日/收盘时间) 频率转换(日频→周频) 存量指标:last 流量指标:sum 缺失<5%:填充 缺失>20%:删除 异常值:标记+人工复核 对齐后检查数据量

数据清洗这一步,看着琐碎,但决定了你后面所有工作的质量。我见过太多人,策略逻辑写得天花乱坠,结果数据没洗干净,全白搭。

记住一句话:垃圾进,垃圾出。数据干净了,策略才有意义。

专注资料整理