7.6 缺失值与异常值处理
课前导读
本节目标
你将学会:识别 DataFrame 里的缺失值和异常值,理解为什么不能随便填补或删除,并建立清洗记录习惯。
适合基础:已经学习 DataFrame、读取保存数据和时间序列处理,知道 A 股数据会受交易日、停牌和披露时间影响。
预计用时:约 38 分钟。
本节边界:只讲数据清洗的基础判断,不展开完整回测系统、因子检验或自动化数据工程。
知识正文
1. 把缺失值和异常值想成“表格里的空格与离谱数字”
你拿到一张体检表。
有些格子是空的,比如血压没有填。
有些格子看起来离谱,比如身高写成 18 米。
你会马上把空格填成平均值吗?
你会直接把 18 米这一行删掉吗?
当然不会。
你会先问:为什么空了?为什么离谱?是没测、录错、单位错,还是确实发生了特殊情况?
A 股数据也是一样。
缺失值是表格里的空格。
异常值是看起来不合理的数字。
它们不是简单的“脏东西”。
它们可能代表停牌、新股未上市、公告未披露、数据源导出失败、复权口径错误或单位混淆。
清洗数据不是把表格擦得越平滑越好。
清洗数据的目标,是让研究尽量接近当时真实可见的投资事实。
2. 什么是缺失值
在 pandas 里,缺失值常见表现是 NaN。
你可以把它理解为“这里没有有效数据”。
下面是一张手写的 A 股样例表。
import pandas as pd
data = {
"日期": ["2026-01-05", "2026-01-06", "2026-01-07", "2026-01-08"],
"股票代码": ["600000", "600000", "600000", "600000"],
"收盘价": [10.20, None, 10.45, 10.30],
"成交量": [1200000, None, 1500000, 1100000],
"备注": ["正常交易", "疑似停牌或缺数据", "正常交易", "正常交易"],
}
df = pd.DataFrame(data)
print(df)这里的 None 会在 pandas 中表现为缺失。
2026-01-06 的收盘价和成交量都没有值。
但我们不能立刻说“用前一天价格补上”。
也不能立刻说“把这一行删掉”。
要先判断缺失原因。
3. A 股中缺失值可能来自哪里
| 缺失场景 | 通俗解释 | 处理前要问的问题 |
|---|---|---|
| 停牌 | 股票当天没有正常交易 | 数据源是否保留停牌日期 |
| 新股未上市 | 股票在某日期还不存在交易记录 | 是否不应纳入当时样本 |
| 财报未披露 | 财务字段当时还不可见 | 是否会提前使用未来信息 |
| 数据导出失败 | 文件下载或保存过程出错 | 能否回到来源重新核对 |
| 字段口径变化 | 数据源改了字段含义或单位 | 是否需要分版本处理 |
缺失值不是一个单一问题。
同样是空格,原因不同,处理方式就不同。
停牌造成的缺失,和数据下载失败造成的缺失,不应该被同样对待。
财务数据未披露造成的缺失,更不能用未来财报倒填。
风险提醒
用未来才知道的数据填补过去的空格,会制造“偷看未来”的研究结果。
4. 先统计缺失,再查看具体行
拿到数据后,先做两步检查。
第一,统计每列缺失数量。
第二,把含缺失的行单独看出来。
print(df.isna().sum())
missing_rows = df[df["收盘价"].isna() | df["成交量"].isna()]
print(missing_rows)isna() 会判断每个格子是否缺失。
sum() 会把 True 当成 1 来统计。
| 表示“或者”。
这段代码会找出收盘价缺失或成交量缺失的行。
注意,这只是发现问题。
发现问题不等于立刻处理问题。
专业的数据清洗一定要先观察,再决策。
5. 不要随便向前填充
有一种常见做法叫向前填充。
意思是用前一个有效值填补当前空格。
df["前值填充收盘价"] = df["收盘价"].ffill()
print(df[["日期", "收盘价", "前值填充收盘价"]])ffill() 有时有用。
例如某些静态信息在下一次更新前保持不变。
但对收盘价要非常谨慎。
如果某天停牌,没有真实成交,把前一天收盘价填进去,可能会让程序误以为当天发生了一次“价格不变的交易日”。
这会影响收益率、成交量、持有天数和重采样结果。
所以对行情价格,填补前要写清楚原因。
新手阶段宁可先保留缺失并标记,也不要为了表格整齐而盲目填平。
6. 计算收益率时如何看待缺失
缺失值会影响收益率。
如果中间某天价格缺失,直接计算可能产生你没有意识到的跳跃。
可以先让 pandas 不自动填补。
df["日期"] = pd.to_datetime(df["日期"])
df = df.set_index("日期").sort_index()
df["日收益率"] = df["收盘价"].pct_change(fill_method=None)
print(df[["收盘价", "日收益率", "备注"]])fill_method=None 表示不要在计算前自动填补缺失。
这样结果可能会多出空值。
这些空值是在提醒你:这里的数据链条断了。
对于投资研究,这种提醒比假装连续更有价值。
7. 什么是异常值
异常值不是“亏损”或“上涨”。
异常值是与常识、规则或数据口径明显不一致的数字。
例如收盘价为负数。
例如成交量为负数。
例如某只主板股票在普通交易日单日涨幅显示为 80%。
例如成交量单位本应是股,却被误当成手。
这些都需要核对。
但要注意,A 股也会出现真实的大幅变化。
新股上市初期、复牌、重大公告后跳空,都可能让价格变化很大。
所以异常值的第一处理动作是“标记和核查”,不是马上删除。
8. 用简单规则标记疑似异常
下面扩展一个手写样例。
sample = pd.DataFrame(
{
"日期": ["2026-01-05", "2026-01-06", "2026-01-07", "2026-01-08"],
"股票代码": ["600000", "600000", "600000", "600000"],
"收盘价": [10.20, 10.10, 30.30, 10.30],
"成交量": [1200000, 980000, -500, 1100000],
}
)
sample["日期"] = pd.to_datetime(sample["日期"])
sample = sample.set_index("日期").sort_index()
sample["日收益率"] = sample["收盘价"].pct_change(fill_method=None)
sample["疑似价格异常"] = sample["日收益率"].abs() > 0.22
sample["疑似成交量异常"] = sample["成交量"] < 0
print(sample)这里用 22% 作为一个非常粗的提示线。
为什么不是 10%?
因为 A 股不同板块、不同阶段涨跌幅限制不同,新股早期也有特殊情况。
本节不展开所有制度细节。
我们只用这个阈值提醒自己:超过常见范围时要核查。
负成交量通常不符合常识,更需要检查来源。
9. 清洗决策表
| 问题类型 | 先做什么 | 可能处理 | 不建议做法 |
|---|---|---|---|
| 收盘价缺失 | 查原因:停牌、未上市还是数据失败 | 标记原因,必要时排除该日收益计算 | 无说明地用前值填充 |
| 成交量缺失 | 查是否停牌或字段导出失败 | 保留缺失并记录,或回源重取 | 随便填 0 |
| 成交量为负 | 核对数据源和单位 | 标记为错误,回源修正或设为缺失 | 当成真实交易 |
| 单日涨跌幅极端 | 查板块、日期、复牌和公告 | 标记疑似异常,确认后保留或修正 | 直接删除所有大涨大跌 |
| 财务字段缺失 | 查披露时间 | 未披露前保持不可用 | 用未来财报倒填 |
这张表不是死规则。
它的作用是提醒你:先问原因,再选处理方式。
量化开发面对数据问题时,要像医生看病历。
不能只看到一个数字异常,就立刻“开药”。
10. 记录清洗过程
清洗数据必须留痕。
否则你三天后可能忘记为什么删掉某些行。
一个最小清洗记录可以这样写。
| 日期 | 股票代码 | 问题 | 判断 | 处理 | 备注 |
|---|---|---|---|---|---|
| 2026-01-06 | 600000 | 收盘价缺失 | 疑似停牌或样例缺数据 | 保留缺失,不计算当日收益 | 待核对来源 |
| 2026-01-07 | 600000 | 收盘价跳变过大 | 疑似数据录入错误 | 标记异常,不直接删除 | 检查原始文件 |
| 2026-01-07 | 600000 | 成交量为负 | 不符合交易常识 | 设为缺失并回源核对 | 不能当真实成交 |
记录不是形式主义。
它能让你复盘研究过程。
当策略结果突然变化时,你可以回头检查是不是清洗规则变了。
当别人质疑你的结论时,你也能说明每一步如何处理。
关键心得
好清洗不是把数据变好看,而是把每个处理决定变得可解释、可复查。
11. 删除、填补、保留,哪个更好
没有永远正确的统一答案。
删除会减少样本。
填补会引入假设。
保留缺失会让后续计算更麻烦。
所以决策要服务于研究问题。
如果你只是学习 DataFrame,保留缺失并标记原因最安全。
如果你计算单日收益率,缺失日不应被悄悄填平。
如果你处理静态行业分类,前值填充可能比价格字段更合理。
如果你遇到负成交量,通常应视为数据错误并回源核对。
专业研究不是追求“表里没有 NaN”。
专业研究是知道哪些 NaN 代表不可用,哪些异常代表真实风险,哪些异常代表数据错误。
12. A 股语境下最容易忽视的三件事
第一,停牌不是普通休息日。
停牌期间没有正常成交,不能简单当成价格横盘。
第二,涨跌幅限制不是所有股票所有时期都一样。
主板、创业板、科创板、新股早期、风险警示股票,规则可能不同。
新手阶段不必背完所有细则,但看到极端涨跌要先核查背景。
第三,复权口径会改变历史价格。
如果一段价格看起来突然断裂,可能是分红送转后的口径问题。
这些问题都说明:数据清洗离不开 A 股制度理解。
交互实验室
练习 1:统计缺失值
运行本节第一段 DataFrame 示例。
使用 df.isna().sum() 统计每列缺失数量。
请写下哪几列存在缺失。
再把含缺失的行打印出来。
不要直接填补,先在笔记里写下你猜测的原因。
练习 2:比较填补前后
对“收盘价”执行 ffill()。
观察填补前后 2026-01-06 的变化。
请回答:如果这一天其实是停牌,前值填充会让研究产生什么误解?
练习 3:标记异常而不是删除异常
运行“疑似价格异常”和“疑似成交量异常”的示例。
请找出被标记的日期。
然后写一句清洗记录。
示例:“2026-01-07 收盘价跳变过大,先标记为疑似异常,等待核对原始来源。”
练习 4:写自己的清洗原则
请把下面三句话补完整。
当价格缺失时,我先检查________。
当成交量为负时,我先________。
当单日涨跌幅极端时,我不会马上删除,而是先________。
如果你能写出这三句,说明你已经开始从“写代码”转向“做可靠研究”。
核心复盘
知识清单:
- [ ] 缺失值是表格里的空格,可能来自停牌、未上市、未披露或数据错误。
- [ ] 异常值是看起来不合理的数字,但可能是真实事件,也可能是数据问题。
- [ ]
isna().sum()可以快速统计缺失,筛选缺失行可以帮助定位问题。 - [ ] 对行情价格不能随便前值填充,尤其要注意停牌和不可交易日期。
- [ ] 异常值应先标记和核查,不应机械删除所有极端变化。
- [ ] 每次清洗都要记录原因、处理方式和保留假设。
金句总结
数据清洗不是把表格擦干净,而是把每个空格和离谱数字问清楚。
风险提醒
随意填补缺失、删除极端涨跌或用未来数据倒填过去,会让量化研究产生虚假的稳定性。任何清洗结论都不构成投资建议。