跳到正文
本地阅读 · 缺失、重复与类型清洗

缺失、重复与类型清洗

计算机基础 · pandas

前置知识:索引、筛选与赋值

本章目录

本章学习目标

  • 区分未知、零和无记录
  • 按业务键发现重复并记录规则
  • 使用类型转换和清洗断言

缺失不是一个统一业务含义

NaN、NaT 与 pd.NA 都表示缺失,但所在类型和运算行为可能不同。isna 用来统一检查缺失,不要用等号与 NaN 比较。缺失收盘价可能意味着数据采集失败,缺失订单用户可能表示匿名购买,没有订单则是用户与订单汇总连接后没有匹配。这三个情形不应使用同一个填充策略。零价格是一个具体数值,不能代表未知价格;无订单计数可以填零,因为计数的业务定义明确。

在清洗之前先统计缺失发生在哪些列、日期和对象,记录原始值及数量,再决定拒绝、排除、填充或保留。缺失是否随机影响后续偏差:仅删除价格缺失的股票,可能让困难样本系统性消失。统计时报告有效样本量,避免以为每个指标用了完全相同的记录。

类型转换必须有审计

to_numeric 和 to_datetime 可以将文本解析成数值和时间,errors='raise' 遇到错误停止,errors='coerce' 则产生缺失。后者适合初步收集坏值,却不是忽略坏值的许可证。比较转换前非空和转换后缺失的掩码,定位无法解析的文本;例如价格写成字符 O,不能把它当成零。用户编号可能为空,使用可空整数而非随意转成 int 后填零。

清洗金额时必须保留小数精度,避免先读成浮点再四舍五入就称为原始精确数值。字符串清洗要确认大小写、空白和编码属于噪声还是标识的一部分。真正的合法类别和键关系需要业务合同约束,而不是靠转换函数推断。

小数据:重复键与完整重复

股票 A、日期一出现两行,价格分别十和十一;股票 A、日期二出现一行,价格为空。按整行 drop_duplicates 无法删掉第一天的重复键,因为价格不同。按照 ['symbol','date'] 检查才能发现同一天多个候选值。duplicated(keep=False) 把所有属于重复键的行标记出来,适合生成审计表;keep='last' 只保留每个键在当前输入中的最后一行。

本课程故意包含重复和乱序,清洗规则固定为教学副本保留输入最后一条,然后按股票和日期排序。这个规则只用于演示,不把最后输入等同于真实最后发布。生产中应使用明确的更新时间、版本号和来源优先级,若无法决定则保留冲突供核对。先按日期排序再 arbitrary 去重可能改变保留记录,必须记录顺序依据。

填充与删除的风险

fillna 可以填常数或分组统计量,dropna 可以按关键列删除记录。若模型需要均值填充,应先划分训练、验证和测试,再用训练集均值拟合填充参数;从全部数据计算均值会把未来分布带入训练。股票面板不能把一只股票的价格向另一只股票传播,分组前向填充只在明确采用已有历史价格的业务情形下使用,并报告连续填充长度。

默认 sum 跳过缺失,全部缺失的一组可能给出零;sum(min_count=1) 让没有有效数值的组保持缺失。count 计算非缺失元素,size 计算行数,这与 SQL COUNT(列)、COUNT(*) 的差异相似。清洗后把两者一起展示,便于解释统计的分母。

从检查到可复现流水线

脚本只修改副本,保留原始 panel,输出清洗后的 CSV 和缺失统计。断言键唯一、日期非空、输出行数不超过输入,能够立即捕捉核心合同破坏。之后还应检查价格是否正数、成交量是否非负,以及各股票日期是否单调;这些规则如果失败应停止而不是机械填零。

清洗不是追求所有单元格都非空,目标是形成可解释的数据状态。练习答案应同时写清删除多少行、填充哪些列和保留了什么限制。空表、全空列和没有重复的表也是合法边界;代码不能假设总有一条重复记录可打印。完成清洗后重新检查 schema,别让一次填充把整数键悄悄变成混合 object。

版本与复现约定

本课程 pandas 示例以 2.3.3 为验证基线,页面运行记录给出实际版本。pandas 3.x 默认使用 Copy-on-Write:从表中取出的对象发生修改时,不据此假定原表同步变化。跨版本都应使用一次完整的 loc 赋值表达式;需要独立工作表时显式 copy。阅读网上的旧示例时,先核对版本、输入类型和索引,再判断结果能否复现。

完整示例与实际输出

from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
panel = load("panel").copy()
key = ["symbol","date"]
duplicates = panel.duplicated(key,keep=False)
print("重复键记录:",int(duplicates.sum()))
clean = panel.drop_duplicates(key,keep="last").copy() # 仅教学副本按输入最后一条保留
clean["date"] = pd.to_datetime(clean["date"],errors="raise")
clean["close"] = pd.to_numeric(clean["close"],errors="raise")
clean = clean.sort_values(key).reset_index(drop=True)
assert not clean.duplicated(key).any()
assert len(clean)<=len(panel)
assert clean["date"].notna().all()
print("清洗前/后:",len(panel),len(clean))
print(clean.isna().sum().to_string())
clean.to_csv(OUT / "pd_clean_panel.csv",index=False)
s=pd.Series([1,None,3],dtype="Float64")
assert s.count()==2 and len(s)==3
assert s.sum(min_count=3) is pd.NA

实际运行通过CPU Python execution.

重复键记录: 2
清洗前/后: 144 143
symbol      0
date        0
open        0
close       1
volume      0
industry    0

五道自测:先回答,再核对

01 · NaN 能用 == 检测吗?

查看提示

使用统一缺失接口。

查看参考答案

不能可靠这样检测;用 isna。

02 · 按整行去重能保证股票日期唯一吗?

查看提示

价格可能不同。

查看参考答案

不能,需要指定业务键 ['symbol','date']。

03 · keep='last' 是最新发布时间吗?

查看提示

last 根据什么顺序?

查看参考答案

不是,指当前输入顺序最后出现的一条。

04 · count 与 size 差别是什么?

查看提示

缺失行是否算入。

查看参考答案

count 数非缺失值,size 数全部行。

05 · 全部缺失的一组求和如何保持未知?

查看提示

min_count 参数。

查看参考答案

使用 sum(min_count=1),不要把未知误当成零。

两项动手任务

任务 1 · 按指定规则清洗行情副本并保留审计统计。

提示

检查业务键,去重后排序。

完整参考解答与检查方式
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
panel = load("panel").copy()
key = ["symbol","date"]
duplicates = panel.duplicated(key,keep=False)
print("重复键记录:",int(duplicates.sum()))
clean = panel.drop_duplicates(key,keep="last").copy() # 仅教学副本按输入最后一条保留
clean["date"] = pd.to_datetime(clean["date"],errors="raise")
clean["close"] = pd.to_numeric(clean["close"],errors="raise")
clean = clean.sort_values(key).reset_index(drop=True)
assert not clean.duplicated(key).any()
assert len(clean)<=len(panel)
assert clean["date"].notna().all()
print("清洗前/后:",len(panel),len(clean))
print(clean.isna().sum().to_string())
clean.to_csv(OUT / "pd_clean_panel.csv",index=False)
s=pd.Series([1,None,3],dtype="Float64")
assert s.count()==2 and len(s)==3
assert s.sum(min_count=3) is pd.NA

任务 2 · 把无法解析的价格文本定位出来,不能静默丢失。

提示

比较转换前后缺失掩码。

完整参考解答与检查方式
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
raw=pd.Series(["10.5","bad",None,"0"],dtype="string")
parsed=pd.to_numeric(raw,errors="coerce")
bad=raw.notna() & parsed.isna()
assert raw[bad].tolist()==["bad"]
print("坏值:",raw[bad].tolist())
print("合法零:",parsed.eq(0).fillna(False).sum())

官方资料与掌握状态

完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。

动手任务记录

掌握状态与复习

关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。

记录尽量保存在当前浏览器,建议定期导出备份。