4. 数据采集与清洗实战
做板块持续性统计,第一步不是算指标,而是把三类原始数据稳定地拿到手:板块指数日线(判断板块整体涨跌与量能)、成分股日线(判断内部广度与龙头结构)、资金流数据(判断增量资金是否持续流入)。这三类数据的时间轴、复权口径、成分口径各不相同,如果直接拼接统计,很容易把"成分股调整"误读成"资金异动",把"停牌"误读成"缩量"。本节按"取数—对齐—清洗—校验"的顺序,把这条数据链走通。
一、三类数据的取数口径
板块指数日线通常包含日期、开高低收、成交量、成交额、换手率等字段。取数时要明确两点:一是指数是否含加权方式差异(自由流通市值加权与等权指数的走势会明显不同,持续性统计应固定一种口径);二是指数本身的基期与发布规则,避免用早期回溯数据做样本外验证。
成分股数据是本节最容易出问题的地方。板块成分不是静态集合,交易所或指数公司会定期调整,也会因退市、并购、行业重分类发生临时变动。因此成分股数据必须带生效日期,形成"区间快照"而不是"当前名单"。用当前名单回溯历史,会产生严重的前视偏差:今天被纳入的热门股,在过去并不属于该板块,却被人为算进了历史涨幅。
资金流数据常见字段为主力净流入、超大单/大单/中单/小单净额、北向资金等。需要注意不同数据商的"主力"定义并不统一,有的按单笔成交金额分档,有的按委托规模估算。同一策略内应固定一家数据源,并在文档中记录字段定义,否则跨源拼接会出现方向性错误。
二、交易日对齐与缺失值处理
三类数据的第一道坎是交易日历不一致。板块指数按交易日发布,成分股可能因停牌在某日无成交,资金流数据在部分数据源中只覆盖有成交的个股。正确做法是以交易日历为主表,对成分股和资金流做左连接,缺失处留空而不是删除整行——删行会让后续的"上涨家数占比"分母变小,广度指标失真。
缺失值要分类处理,不能一刀切:
- 停牌导致的缺失:价格字段保留上一有效收盘价并打
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(同源时) |
其中"资金流对账"最容易被跳过。若板块资金流来自指数商直接发布,而成分股资金流来自另一数据商,两者加总必然有差异,此时不应强行对齐,而应二选一固定口径,并在统计脚本中注明来源。持续性统计比较的是同一口径下的时间序列变化,口径混用比数值偏差更致命。
完成本节后,应得到一张以"日期-股票"为粒度、带停牌标记与成分生效区间、资金流缺失显式保留的面板数据,以及一张按日汇总的板块指数与资金流序列。这张面板是后续计算板块内上涨家数占比、龙头集中度、资金流连续性的唯一输入,其口径一旦确定,整个统计周期内不应再改动。