3、数据清洗与预处理:处理缺失值、异常值,时间序列对齐,计算散户仓位占比

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

我做了这么多年量化,见过太多人拿着脏数据跑回测,结果跑出来一条漂亮的曲线,实盘一上去就崩。嗯,这里要记住:数据质量决定了策略的天花板

3.1 缺失值处理:别让「空」坑了你

散户仓位数据来自各种渠道,比如券商研报、第三方数据商。这些数据经常会有缺失。为什么?因为节假日、数据源故障、或者干脆就是没采集到。

我个人习惯,先看看缺失比例。如果某个字段缺失超过30%,我会直接扔掉。如果只是零星缺失,那就用插值法补上。

核心原则:不要用均值填充!散户仓位是时间序列,均值填充会破坏时序结构。

我常用的方法有两种:

  • 前向填充(ffill):用上一个交易日的数据填充。适合短期缺失。
  • 线性插值:用前后两个点的连线推算中间值。适合中期缺失。
import pandas as pd
import numpy as np

# 模拟散户仓位数据
dates = pd.date_range('2024-01-01', periods=10, freq='D')
positions = [0.65, np.nan, 0.68, 0.70, np.nan, np.nan, 0.72, 0.71, 0.69, 0.67]
df = pd.DataFrame({'date': dates, 'retail_position': positions})

# 前向填充
df['ffill'] = df['retail_position'].ffill()

# 线性插值
df['linear'] = df['retail_position'].interpolate(method='linear')

print(df)

你看,前向填充适合「数据源突然断了一下」的情况。线性插值适合「中间几天确实没数据」的情况。我一般会先试试线性插值,效果不好再换前向填充。

小技巧:如果缺失值连续超过5个,建议直接删除这段数据。强行补上反而会引入噪声。

3.2 异常值处理:那些「离谱」的数据点

散户仓位占比,正常范围在0.3到0.9之间。但有时候你会看到0.05或者1.2这种值。这明显是数据录入错误。

我曾经遇到过一家数据商,把0.65录成了6.5。跑回测的时候,那个信号直接让策略满仓做多,结果第二天就暴跌。嗯,从那以后我养成了检查异常值的习惯。

常用的方法:

  • 3σ原则:超过均值±3倍标准差的数据,视为异常。
  • IQR方法:低于Q1-1.5*IQR或高于Q3+1.5*IQR的数据,视为异常。
  • 业务规则:直接设定上下限,比如0.2到0.9。
# 使用IQR方法检测异常值
Q1 = df['retail_position'].quantile(0.25)
Q3 = df['retail_position'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['retail_position'] < lower_bound) | 
              (df['retail_position'] > upper_bound)]
print(f"异常值数量: {len(outliers)}")

我个人更推荐用业务规则。因为散户仓位占比有明确的物理意义,0.2以下基本不可能(除非数据错了),0.9以上也不太现实(散户不会满仓到90%以上还持续很久)。

注意:不要一发现异常值就删除。先看看是不是市场极端情况导致的。比如2015年股灾期间,散户仓位确实出现过极端值。这时候删除反而会丢失重要信息。

3.3 时间序列对齐:让数据「对上节奏」

散户仓位数据通常是日频的,但不同数据源的交易日历可能不一样。有的用A股交易日历,有的用全球交易日历。如果不做对齐,你算出来的仓位占比就是错的。

我建议统一使用A股交易日历。因为散户仓位本身就是针对A股市场的。

# 生成完整的交易日历
trade_calendar = pd.bdate_range(start='2024-01-01', end='2024-01-31', 
                                freq='C', weekmask='Mon Tue Wed Thu Fri')

# 对齐数据
df_aligned = df.set_index('date').reindex(trade_calendar, method='ffill')
df_aligned.index.name = 'trade_date'

这里有个坑:reindex之后,一定要检查对齐后的数据长度。我遇到过因为节假日不同,导致数据少了十几天的。那段时间的策略信号全是错的。

对齐后的数据,必须满足:

  • 每个交易日都有数据
  • 非交易日的数据被前向填充或删除
  • 数据长度与回测周期一致

3.4 计算散户仓位占比:核心指标

散户仓位占比,公式很简单:

散户仓位占比 = 散户持仓市值 / 散户总资产

但实际计算时,数据源通常只提供「散户净买入额」和「散户总资产」。这时候需要做一下转换。

# 假设我们有散户净买入额和总资产数据
df['net_buy'] = [100, 150, -50, 200, -100]  # 单位:亿
df['total_asset'] = [1000, 1020, 1050, 1080, 1100]  # 单位:亿

# 计算仓位占比
# 仓位 = 前一日仓位 + 当日净买入 / 当日总资产
df['position_ratio'] = 0.0
df.loc[0, 'position_ratio'] = df.loc[0, 'net_buy'] / df.loc[0, 'total_asset']

for i in range(1, len(df)):
    df.loc[i, 'position_ratio'] = (
        df.loc[i-1, 'position_ratio'] * df.loc[i-1, 'total_asset'] + 
        df.loc[i, 'net_buy']
    ) / df.loc[i, 'total_asset']

print(df[['date', 'position_ratio']])

你想想看,这个计算其实是在模拟散户的持仓变化。每天净买入多少,总资产变化多少,仓位自然就变了。

避坑指南:我曾经直接用「净买入/总资产」作为当日仓位,结果算出来全是负值。后来才发现,仓位是累积的,不是当天的。这个错误让我浪费了整整两天时间。

3.5 本章知识体系

下面这张图,是我自己总结的数据清洗流程。你照着做,基本不会出大问题。

数据清洗与预处理流程 原始数据 缺失值处理(ffill / 线性插值) 异常值处理(3σ / IQR / 业务规则) 时间序列对齐(交易日历) 计算散户 仓位占比 净买入 / 总资产

这张图把整个流程串起来了。从原始数据开始,先处理缺失值,再处理异常值,然后对齐时间序列,最后计算仓位占比。每一步都不能跳过。

好了,数据清洗这部分就讲到这里。记住一句话:数据干净了,策略才能跑得稳


公众号:蓝海数据掘金营,微信deep3321