本文是「Python 量化数据工程实战」系列第 1 篇。做量化的人都有这样的经历:兴冲冲拉了一年的日线,跑回测时发现某只股票中间缺了 30 天------原来那段时间停牌了;又或者某天收益率突然跳到 +900%,查了半天发现是除权没处理。数据清洗是量化工程里"最不起眼、最耗时间、最容易出 bug"的环节。这篇把最常见的三类脏数据一次性讲清楚,代码可直接跑。
本文你将得到什么
- 用 Python SDK 批量拉取历史 K 线的正确姿势(含分页与字段说明)
- 识别并处理停牌日(volume=0)的两种策略:删除 vs 前向填充
- 从原始收盘价计算真实涨跌幅,避免把"昨收"当"涨跌幅"的坑
- 用 3σ 法则 + IQR 检测价格异常值,防止极端行情污染模型
- 涨停/跌停自动标记,区分正常波动与制度性价格边界
- 一份可直接套用的清洗后 DataFrame,回测前 copy-paste 即可
一、数据从哪来:批量拉取与字段避坑
很多新手第一步就踩坑:stock_history 返回的字段里,pc 不是涨跌幅,而是昨收 (pre_close)。如果你直接拿 pc 当涨跌幅算指标,所有结果都会错。
python
from mairui import Client
import pandas as pd
client = Client("LICENCE-66D8-9F96-0C7F0FBCD073") # 演示证书,返回演示数据
# 拉取单只股票近 120 日日线
rows = client.stock_history("000001", period="d", dividend="n", lt=120)
df = pd.DataFrame(rows)
print(df[["t", "o", "h", "l", "c", "pc", "v"]].head(3))
真实输出:
text
t o h l c pc v
0 2025-11-25 11.61 11.80 11.60 11.80 11.60 1028081
1 2025-11-26 11.81 11.69 11.60 11.69 11.80 821301
2 2025-11-27 11.66 11.71 11.69 11.71 11.69 1011847
注意 pc=11.60 接近 c=11.80,这是昨收价,不是涨跌幅百分比。正确涨跌幅要手动算:
python
df["c"] = pd.to_numeric(df["c"])
df["pc"] = pd.to_numeric(df["pc"])
df["ret_pct"] = (df["c"] - df["pc"]) / df["pc"] * 100
print(df[["t", "c", "pc", "ret_pct"]].head(3))
真实输出:
text
t c pc ret_pct
0 2025-11-25 11.80 11.60 1.724138
1 2025-11-26 11.69 11.80 -0.932203
2 2025-11-27 11.71 11.69 0.171086
这才是真实的日涨跌幅。很多量化框架直接假设接口返回了涨跌幅字段,结果模型输入全是错的比例------这个坑排查起来极其隐蔽。
二、停牌识别:缺数据不是 bug,是真实市场状态
A股每天都有股票停牌(重组、退市整理、重大事项)。如果你拉了一年数据,某只股票中间断了 20 天,那不是接口漏了,是那只股票确实没交易。
python
# 批量拉取 5 只股票
codes = ["000001", "000002", "600519", "600000", "000858"]
all_data = []
for code in codes:
rows = client.stock_history(code, period="d", dividend="n", lt=120)
df = pd.DataFrame(rows)
df["code"] = code
all_data.append(df)
df = pd.concat(all_data, ignore_index=True)
df["v"] = pd.to_numeric(df["v"], errors="coerce")
# 识别停牌日(成交量为 0)
suspension = df[df["v"] == 0]
print(f"停牌记录: {len(suspension)} 条")
处理策略有两种,看场景选:
| 场景 | 策略 | 代码 |
|---|---|---|
| 回测模型 | 删除停牌日 | df = df[df["v"] > 0] |
| 时序模型(LSTM/Transformer) | 前向填充 | df["c"] = df["c"].ffill() |
删除是最安全的做法------停牌日本就没有价格发现,硬填进去反而给模型喂噪声。只有在"时间轴必须连续"的深度学习场景下,才用前向填充,且要在特征里加一个 is_suspended 标记,告诉模型"这天价格是假的"。
三、异常值检测:3σ 与 IQR 双保险
即使是正常交易的股票,偶尔也会出现异常价格------比如闪崩、乌龙指、或者数据源的脏数据。这些极端值如果不清洗,会直接扭曲均值、标准差、相关系数等所有统计量。
python
# 为每只股票计算 3σ 异常值
df["ret_pct"] = (df["c"] - df["pc"]) / df["pc"] * 100
for code in df["code"].unique():
sub = df[df["code"] == code]["ret_pct"].dropna()
if len(sub) < 3:
continue
mean, std = sub.mean(), sub.std()
outliers = sub[(sub - mean).abs() > 3 * std]
if len(outliers):
print(f"{code}: 发现 {len(outliers)} 个 3σ 异常值")
3σ 的局限 :如果股票本身波动就极大(比如次新股、ST 股),3σ 会把正常波动也标成异常。更稳健的做法是再加一层 IQR(四分位距):
python
Q1 = sub.quantile(0.25)
Q3 = sub.quantile(0.75)
IQR = Q3 - Q1
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
iqr_outliers = sub[(sub < lower) | (sub > upper)]
实际工程中,我会同时用 3σ 和 IQR,只有两个方法都标为异常的行才删除------这能大幅降低误判率。
四、涨跌停标记:制度性边界不可忽视
A股的涨跌停是制度性价格边界,不是市场自发行为。涨停日的成交量、委买委卖结构、后续走势,都与自由交易日常态完全不同。很多策略如果不区分"涨停日",会把制度性约束误判为市场信号。
python
# 涨跌停标记(主板 ±10%,科创/创业板 ±20%,ST ±5%)
df["limit_up"] = df["ret_pct"] >= 9.9
df["limit_down"] = df["ret_pct"] <= -9.9
# 打印涨停记录
ups = df[df["limit_up"]]
print(f"涨停记录: {len(ups)} 条")
if len(ups):
print(ups[["code", "t", "ret_pct"]].head())
建议在特征工程阶段加入三个衍生字段:
is_limit_up:当日是否涨停is_limit_down:当日是否跌停days_since_limit:距离最近一次涨跌停的天数(用于判断"情绪冷却期")
五、完整清洗流水线
把上面四步拼起来,就是一份生产可用的清洗脚本:
python
import pandas as pd
from mairui import Client
client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")
def clean_kline(code, lt=120):
rows = client.stock_history(code, period="d", dividend="n", lt=lt)
df = pd.DataFrame(rows)
df["code"] = code
# 数值化
for col in ["o", "h", "l", "c", "v", "pc"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
# 1. 计算真实涨跌幅
df["ret_pct"] = (df["c"] - df["pc"]) / df["pc"] * 100
# 2. 去掉停牌日
df = df[df["v"] > 0].copy()
# 3. 3σ + IQR 异常值过滤
sub = df["ret_pct"].dropna()
mean, std = sub.mean(), sub.std()
Q1, Q3 = sub.quantile(0.25), sub.quantile(0.75)
IQR = Q3 - Q1
mask_sigma = (df["ret_pct"] - mean).abs() <= 3 * std
mask_iqr = (df["ret_pct"] >= Q1 - 1.5 * IQR) & (df["ret_pct"] <= Q3 + 1.5 * IQR)
df = df[mask_sigma | mask_iqr].copy() # 任一方法通过即保留(宽松策略)
# 4. 涨跌停标记
df["limit_up"] = df["ret_pct"] >= 9.9
df["limit_down"] = df["ret_pct"] <= -9.9
return df
# 批量清洗
codes = ["000001", "000002", "600519"]
cleaned = pd.concat([clean_kline(c) for c in codes], ignore_index=True)
print(f"清洗后: {len(cleaned)} 条")
cleaned.to_csv("cleaned_kline.csv", index=False, encoding="utf-8-sig")
六、小结
数据清洗没有"标准答案",只有"适合你的策略的答案"。但有几个原则通用:
- 先理解字段含义 ,别凭名字猜(
pc不是涨跌幅) - 停牌不是 bug,处理策略取决于模型类型
- 异常值要双保险,3σ + IQR 结合比单一方法更稳健
- 涨跌停是制度边界,标记出来比忽略它更明智
下一篇我们讲:把清洗后的数据,变成模型能吃的"特征"------动量、波动、资金流三因子实战。
注意:本文为技术分享,所有接口调用均使用官方演示证书返回的演示数据(需替换为自己的实际证书),不构成任何投资建议。市场有风险,决策需谨慎。
代码与文档 :更多 SDK 用法与接口文档可参考 github.com/MaiRuiApi