3. 数据获取与清洗实战
做流动性分组测试,第一步不是写分组逻辑,而是把“干净、对齐、可复现”的数据底座搭起来。本节以A股为对象,走一遍从真实数据源取数到形成可用面板的完整链路:日频行情(开高低收、成交量、成交额、复权因子、换手率)与财务数据(净利润、营业收入、净资产、总资产等),并完成缺失值处理、异常值修正与时间对齐。示例中的接口名、字段名与数值均为教学示例,实际使用时以所选数据源文档为准。
一、先明确数据契约:字段、频率与时间语义
在动手取数前,先把“数据契约”写清楚,否则后面清洗会反复返工。行情数据是日频、按交易日的截面数据,主键为(股票代码,交易日期);财务数据是报告期数据,主键为(股票代码,报告期),且存在“报告期—公告日”的时滞。流动性分组通常按月度或季度调仓,因此需要把日频行情聚合到调仓日,把财务数据按公告日而非报告期对齐到调仓日,避免使用未来信息。
关键约定先定死:股票代码统一为6位字符串并补零(如 000001),交易所后缀单独一列;日期统一为 YYYY-MM-DD;金额单位统一为元;收益率计算使用后复权价格,避免除权除息造成的假跳空。下面给出一个最小字段清单示例:
| 数据表 | 主键 | 核心字段 | 频率 |
|---|---|---|---|
| 日行情 | 代码+交易日 | open/high/low/close、volume、amount、turnover、adj_factor | 日 |
| 财务指标 | 代码+报告期 | net_profit、revenue、equity、total_asset、ann_date | 季 |
| 交易日历 | 交易日 | is_open | 日 |
| 股票基础信息 | 代码 | name、list_date、delist_date、board | 静态 |
二、取数:分批拉取与落盘
真实数据源通常按股票或按日期分页返回,直接一次性拉全市场容易超时或触发限流。稳妥做法是“按交易日循环、按批请求、逐日落盘”,并用交易日历驱动循环,避免把周末和节假日当成缺失。下面是一段示意性伪代码,重点在控制节奏与断点续传:
import pandas as pd, time, os
def fetch_daily(trade_dates, batch=200):
for d in trade_dates:
path = f"raw/daily/{d}.parquet"
if os.path.exists(path): # 断点续传
continue
frames = []
for i in range(0, len(codes), batch):
chunk = codes[i:i+batch]
df = api.get_daily(code_list=chunk, trade_date=d,
fields="open,high,low,close,vol,amount,turnover,adj_factor")
frames.append(df)
time.sleep(0.3) # 限流保护
pd.concat(frames).to_parquet(path, index=False)
财务数据按报告期拉取,务必同时取回 ann_date(公告日)。若数据源只给报告期,可用“报告期+固定滞后”近似,但要在文档中注明这是近似处理,不能当作真实公告日使用。
三、缺失值处理:先分类,再决定填不填
缺失不是一种东西,处理方式必须分开。常见三类:停牌缺失(当日无成交,行情行不存在)、字段缺失(某字段为空但行存在)、结构性缺失(新股上市前、退市后无数据)。停牌不应简单用前值填充价格后参与收益计算,正确做法是先按交易日历重建完整面板,标记 is_suspend,收益计算时跳过停牌区间;成交量、成交额在停牌日填0是合理的,价格则保留空值并前向填充仅用于展示。
财务字段缺失可用“同报告期行业中位数”或“上一报告期值”填补,但必须新增 is_imputed 标记列,便于后续敏感性检验。对于关键字段(如净资产为负或为空)无法合理填补的样本,直接在分组前剔除并记录剔除数量,保证样本可追溯。
四、异常值修正:价格、成交量与财务三线并查
异常值最容易被忽略,却最影响分组结果。价格线重点查:非正价格、单日涨跌幅超过板块限制(主板±10%、创业板/科创板±20%、ST±5%,需按板块与是否ST动态判断)、复权因子突变。成交量线查:成交量为负、成交额与成交量严重不匹配(如均价偏离收盘价数倍)。财务线查:净资产为负、营收为负、同一报告期重复记录。
修正策略要保守:明显录入错误(如价格多一个0)可按相邻交易日中位数回改并标记;涨跌幅超限但确属真实波动(如注册制新股首日)不应删除,应结合 list_date 判断;无法判定的异常先隔离到 quarantine 表,不直接进入主面板。下面给出一个按板块动态判断涨跌幅的示例:
limit_map = {"main": 0.10, "gem": 0.20, "star": 0.20}
def flag_limit(row):
lim = 0.05 if row["is_st"] else limit_map[row["board"]]
ret = row["close"] / row["pre_close"] - 1
return abs(ret) > lim * 1.02 # 留2%容差应对复权误差
五、时间对齐:把日频与季频拼成一张面板
对齐是本节的收口环节。行情侧:以交易日历为骨架,把日行情聚合到调仓日(如每月最后一个交易日),流动性指标用调仓日前N个交易日的平均成交额与平均换手率,避免只用单日数据受极端值影响。财务侧:对每个调仓日,取 ann_date <= 调仓日 中报告期最新的那条记录,这就是“按公告日对齐”,可有效防止前视偏差。
下图展示了一条完整的数据流:原始行情与财务数据分别经过清洗后,在调仓日汇合成统一面板,并输出流动性指标与财务特征,供后续分组使用。
六、结果检查:用少量断言守住质量底线
清洗完成后不要急着分组,先跑几条断言:面板行数应等于“有效股票数 × 调仓日数”;每个调仓日流动性指标非空比例应高于95%;财务字段的 ann_date 不得晚于对应调仓日;复权后收益率不应出现超过板块限制的极端值(除新股首日等已标记情形)。这些检查写成脚本,每次更新数据自动执行,比人工抽查可靠得多。
至此,一份可用于流动性分组测试的面板就准备好了:行情按交易日历重建并标记停牌,异常样本被隔离而非误删,财务数据按公告日对齐到调仓日,流动性指标用前N日均值平滑。后续分组、排序与回测都建立在这张面板之上,数据质量直接决定分组结论的可信度。