3、北向资金数据清洗:缺失值处理、异常值检测、数据标准化与对齐
数据清洗这步,说实话,是整套策略里最枯燥但最要命的环节。我见过太多人拿着漂亮模型往脏数据上一跑,结果亏得底裤都不剩。北向资金数据尤其如此——交易所接口偶尔抽风、节假日导致断档、不同数据源时间戳对不上……这些问题你不提前处理,后面所有分析都是白搭。
今天咱们就把这块硬骨头啃下来。我会把我在实战中踩过的坑、试过的好方法,一股脑倒给你。
3.1 缺失值处理:别让空值毁了你的策略
北向资金数据最常见的缺失场景有三种:
- 交易日缺失:A股休市但港股通正常,或者反过来
- 单日数据缺失:接口超时、网络波动导致某天没拉到数据
- 字段缺失:比如只拿到了净买入额,没拿到成交额
我个人习惯先做一步「数据完整性检查」。写个脚本看看哪些日期是空的:
import pandas as pd
import numpy as np
# 读取北向资金日频数据
df = pd.read_csv('northbound_flow.csv', parse_dates=['date'])
df.set_index('date', inplace=True)
# 检查缺失日期
full_calendar = pd.date_range(start=df.index.min(), end=df.index.max(), freq='B')
missing_dates = full_calendar.difference(df.index)
print(f"缺失交易日数量: {len(missing_dates)}")
print(missing_dates[:10]) # 看看前10个缺失日期
嗯,这里要注意:freq='B' 只排除周末,但A股还有春节、国庆等法定假日。更稳妥的做法是用交易所公布的交易日历。
处理缺失值,我一般按优先级来:
- 前向填充(ffill):适合北向资金这种「存量数据」。比如昨天买了100亿,今天数据缺失,用昨天的值填充——逻辑上说得通,因为北向资金持仓变化是渐进的。
- 线性插值:如果连续缺失超过3天,前向填充误差会累积。这时候用插值更合理。
- 直接删除:缺失比例低于1%的,删掉也没影响。但别超过5%,否则样本偏差会让你后悔。
# 前向填充(默认策略)
df.fillna(method='ffill', inplace=True)
# 如果连续缺失超过3天,改用线性插值
df.interpolate(method='linear', limit_direction='both', inplace=True)
# 检查还有没有漏网之鱼
print(f"处理后缺失值数量: {df.isnull().sum().sum()}")
3.2 异常值检测:揪出那些离谱的数字
北向资金数据里,异常值通常长这样:
- 单日净买入突然飙到500亿(正常也就几十亿)
- 成交额突然变成0(可能是接口返回了空值)
- 连续5天净流入后突然巨量流出(可能是数据错误,也可能是真的大资金调仓)
怎么区分真假异常?我常用的方法是「3σ原则 + 滚动窗口」:
def detect_outliers(series, window=20, n_std=3):
rolling_mean = series.rolling(window=window).mean()
rolling_std = series.rolling(window=window).std()
upper_bound = rolling_mean + n_std * rolling_std
lower_bound = rolling_mean - n_std * rolling_std
outliers = (series > upper_bound) | (series < lower_bound)
return outliers
# 检测北向资金净买入的异常值
df['outlier'] = detect_outliers(df['net_buy'], window=20, n_std=3)
outlier_dates = df[df['outlier']].index
print(f"检测到 {len(outlier_dates)} 个异常日期")
print(outlier_dates[:5])
为什么用滚动窗口?因为北向资金的波动率本身就在变——2019年日均几十亿,2021年可能上百亿。固定阈值根本没法用。滚动窗口相当于给每个时间段「量身定制」了正常范围。
关键判断逻辑:
- 如果异常值出现在财报发布日、政策公布日——保留,这是真实的市场反应
- 如果异常值出现在普通交易日,且前后数据正常——标记为可疑,人工复核
- 如果异常值导致策略信号反转——我会回看原始数据源,确认是不是接口问题
处理异常值,我推荐「缩尾处理」而不是直接删除:
# 缩尾处理:将异常值替换为边界值
def winsorize(series, lower=0.01, upper=0.99):
q_low = series.quantile(lower)
q_high = series.quantile(upper)
return series.clip(lower=q_low, upper=q_high)
df['net_buy_clean'] = winsorize(df['net_buy'], lower=0.01, upper=0.99)
你想想看,直接删除异常值会导致时间序列出现「断点」,后续计算滚动指标时全乱套。缩尾处理既保留了数据量,又限制了极端值的影响。
3.3 数据标准化:让北向资金和指数站在同一起跑线
北向资金净买入是「绝对数值」,上证指数是「点位」,两者量纲完全不同。直接拿原始数据做回归?那结果肯定被指数点位主导。
标准化说白了就是「去量纲化」。我常用的方法有三种:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Z-score 标准化 | (x - μ) / σ | 数据近似正态分布时 |
| Min-Max 归一化 | (x - min) / (max - min) | 数据有明确上下界时 |
| 分位数标准化 | 映射到 [0,1] 均匀分布 | 数据分布偏态严重时 |
我个人偏好 Z-score,原因很简单——它保留了数据的相对大小关系。北向资金净买入从100亿降到50亿,Z-score能反映出「偏离均值多少倍标准差」,这对策略信号生成特别有用。
from sklearn.preprocessing import StandardScaler
# 对北向资金和指数分别做Z-score标准化
scaler = StandardScaler()
df[['net_buy_std', 'index_std']] = scaler.fit_transform(df[['net_buy_clean', 'index_close']])
# 标准化后,两者均值0,标准差1,可以直接比较
print(f"北向资金标准化后均值: {df['net_buy_std'].mean():.4f}")
print(f"指数标准化后均值: {df['index_std'].mean():.4f}")
3.4 数据对齐:让时间戳严丝合缝
北向资金数据和指数数据的时间戳,经常对不上。原因包括:
- 北向资金数据是「收盘后」发布的,指数数据是「盘中实时」的
- 港股通交易日和A股交易日不完全一致
- 不同数据源的时间格式不同(有的用'2024-01-15',有的用'2024/01/15')
对齐的核心原则就一条:以指数数据的时间戳为基准。因为我们的策略是「根据北向资金信号交易指数」,指数的时间轴才是主线。
# 读取指数数据
index_df = pd.read_csv('sh_index.csv', parse_dates=['date'])
index_df.set_index('date', inplace=True)
# 读取北向资金数据
north_df = pd.read_csv('northbound.csv', parse_dates=['date'])
north_df.set_index('date', inplace=True)
# 以指数日期为基准,左连接北向资金数据
aligned_df = index_df.join(north_df[['net_buy_clean']], how='left')
# 检查对齐后的缺失情况
print(f"对齐后缺失值数量: {aligned_df['net_buy_clean'].isnull().sum()}")
这里用 how='left' 的意思是:保留指数所有交易日,北向资金数据有就填,没有就空着。然后我们再走一遍缺失值处理流程。
how='inner' 做对齐,结果发现回测区间少了将近20个交易日——因为港股通休市日被全删了。这些日子指数明明在交易,策略却「空仓」,回测结果严重失真。记住:永远以指数为基准!
3.5 本章知识体系
下面这张图,把数据清洗的完整流程串起来了。我建议你保存下来,每次做数据预处理时对照着走一遍:
数据清洗这步,说白了就是「垃圾进,垃圾出」的防火墙。你花80%的时间把数据洗干净,后面20%的建模工作才能出真东西。我见过太多人急着跑模型,结果被脏数据坑得怀疑人生——别走那条路。
下一章咱们聊特征工程,到时候你会感谢今天认真清洗数据的自己。