4. 数据采集与清洗实战

做板块持续性统计,第一步不是算指标,而是把三类原始数据稳定地拿到手:板块指数日线(判断板块整体涨跌与量能)、成分股日线(判断内部广度与龙头结构)、资金流数据(判断增量资金是否持续流入)。这三类数据的时间轴、复权口径、成分口径各不相同,如果直接拼接统计,很容易把"成分股调整"误读成"资金异动",把"停牌"误读成"缩量"。本节按"取数—对齐—清洗—校验"的顺序,把这条数据链走通。

一、三类数据的取数口径

板块指数日线通常包含日期、开高低收、成交量、成交额、换手率等字段。取数时要明确两点:一是指数是否含加权方式差异(自由流通市值加权与等权指数的走势会明显不同,持续性统计应固定一种口径);二是指数本身的基期与发布规则,避免用早期回溯数据做样本外验证。

成分股数据是本节最容易出问题的地方。板块成分不是静态集合,交易所或指数公司会定期调整,也会因退市、并购、行业重分类发生临时变动。因此成分股数据必须带生效日期,形成"区间快照"而不是"当前名单"。用当前名单回溯历史,会产生严重的前视偏差:今天被纳入的热门股,在过去并不属于该板块,却被人为算进了历史涨幅。

资金流数据常见字段为主力净流入、超大单/大单/中单/小单净额、北向资金等。需要注意不同数据商的"主力"定义并不统一,有的按单笔成交金额分档,有的按委托规模估算。同一策略内应固定一家数据源,并在文档中记录字段定义,否则跨源拼接会出现方向性错误。

三类原始数据 → 对齐 → 清洗 → 校验 板块指数日线 日期 / OHLC / 量额 换手率 · 固定加权口径 成分股日线 带生效日期的区间快照 前复权 · 停牌标记 资金流数据 主力/超大单/大单净额 固定数据源与字段定义 交易日对齐 以交易日历为基准 左连接 · 缺失留空 不向前填充价格 清洗规则 停牌:标记不删行 复权:统一前复权 成分调整:按生效日 资金流缺失:置空 异常值:阈值截断 去重:主键唯一 校验 行数守恒 日期连续 成分覆盖 资金流对账 关键原则 ① 成分股必须按生效日期回溯,禁止用当前名单回填历史;② 停牌保留行并打标记,避免成交量被误判为缩量;③ 资金流与价格同源同频,跨源拼接前先核对字段定义。

二、交易日对齐与缺失值处理

三类数据的第一道坎是交易日历不一致。板块指数按交易日发布,成分股可能因停牌在某日无成交,资金流数据在部分数据源中只覆盖有成交的个股。正确做法是以交易日历为主表,对成分股和资金流做左连接,缺失处留空而不是删除整行——删行会让后续的"上涨家数占比"分母变小,广度指标失真。

缺失值要分类处理,不能一刀切:

  • 停牌导致的缺失:价格字段保留上一有效收盘价并打 is_suspended=1 标记,成交量置 0,涨跌幅置 0。这样计算板块内涨跌家数时,停牌股不计入上涨也不计入下跌。
  • 资金流缺失:若某股当日无资金流记录,净流入置为 NaN,在汇总板块资金流时用 skipna 求和,并单独记录"覆盖股票数",避免用 0 填充把缺失当成"无流入"。
  • 指数数据缺失:多为节假日或数据源断档,应直接与交易日历比对,缺失日整行剔除并记录日志,不做插值。

下面是一段可复用的清洗骨架(pandas 示例,参数为教学假设):

import pandas as pd

# 1) 交易日历为主表
cal = pd.read_csv("trade_cal.csv", parse_dates=["date"])

# 2) 成分股区间快照:code, start_date, end_date
members = pd.read_csv("members.csv", parse_dates=["start_date", "end_date"])

# 3) 按生效日展开为"日期-股票"长表
rows = []
for _, m in members.iterrows():
    days = cal[(cal.date >= m.start_date) & (cal.date <= m.end_date)]
    rows.append(pd.DataFrame({"date": days.date, "code": m.code}))
panel = pd.concat(rows, ignore_index=True)

# 4) 左连接行情与资金流,缺失留空
px = pd.read_csv("stock_daily.csv", parse_dates=["date"])
mf = pd.read_csv("moneyflow.csv", parse_dates=["date"])
panel = panel.merge(px, on=["date", "code"], how="left") \
             .merge(mf, on=["date", "code"], how="left")

# 5) 停牌标记:有成分但无成交
panel["is_suspended"] = panel["volume"].isna().astype(int)
panel.loc[panel.is_suspended == 1, ["volume", "amount"]] = 0
panel["pct_chg"] = panel["pct_chg"].fillna(0)

# 6) 资金流缺失保留 NaN,汇总时 skipna
board_mf = panel.groupby("date")["main_net_inflow"].sum(min_count=1)
coverage = panel.groupby("date")["main_net_inflow"].count()

注意第 6 步的 min_count=1:如果某日全部成分股都无资金流数据,求和结果应为 NaN 而不是 0,否则会在资金流曲线上制造一个假的"零流入"点。

三、停牌与成分股调整的处理细节

停牌对持续性统计的影响常被低估。假设某板块 50 只成分股中有 8 只停牌,若直接删除这 8 行,当日"上涨家数/总家数"的分母从 50 变成 42,广度会被系统性高估。正确口径是分母仍为 50,停牌股单列,输出"上涨、下跌、平盘、停牌"四类计数,这样广度指标才可比。

成分股调整更隐蔽。以某板块在 3 月 1 日调入 5 只新股为例:若用当前名单回溯 2 月数据,这 5 只股票在 2 月的涨幅会被错误计入板块。处理方式是维护一张成分变更表(变更日期、调入代码、调出代码、变更原因),在生成"日期-股票"面板时按生效日切分区间。对于调出股票,其历史区间保留到调出前一日,调出后不再参与板块汇总。

常见错误:用 ffill 对成分股价格做前向填充。停牌期间价格不变是事实,但复权因子可能因分红送转在复牌时跳变,前向填充会把复权跳变抹平,导致复牌首日涨跌幅计算错误。应保留原始价格与复权因子,在复牌日重新计算。

四、清洗后的校验清单

清洗完成不等于数据可用,必须做四项校验,任何一项不通过都要回到上游排查:

校验项检查方法通过标准(示例)
行数守恒面板行数 = Σ(每日成分股数)与成分快照逐日核对一致
日期连续面板日期集合 = 交易日历无缺失、无重复
成分覆盖每日有效成分数 / 快照成分数≥ 95%,停牌股单独计数
资金流对账板块资金流合计 vs 成分股加总差异为 0(同源时)

其中"资金流对账"最容易被跳过。若板块资金流来自指数商直接发布,而成分股资金流来自另一数据商,两者加总必然有差异,此时不应强行对齐,而应二选一固定口径,并在统计脚本中注明来源。持续性统计比较的是同一口径下的时间序列变化,口径混用比数值偏差更致命。

实操建议:把上述清洗流程封装为可重复执行的脚本,每次运行输出一份"数据质量报告"——包含日期范围、成分股数量变化、停牌比例、资金流覆盖率、异常值条数。这份报告本身就是后续复盘板块异动时的重要旁证:某日资金流覆盖率骤降,往往意味着数据源问题而非市场问题。

完成本节后,应得到一张以"日期-股票"为粒度、带停牌标记与成分生效区间、资金流缺失显式保留的面板数据,以及一张按日汇总的板块指数与资金流序列。这张面板是后续计算板块内上涨家数占比、龙头集中度、资金流连续性的唯一输入,其口径一旦确定,整个统计周期内不应再改动。