本节摘要:真实数据几乎总有缺失,处理不当会让后续统计全部失真。本节先讲清缺失值的检测、删除与填充三类手段及各自适用场景,再覆盖列的新增修改删除、排序、去重等日常操作,让你对 DataFrame 的增删改查心中有数。
阅读完本节,你应当能够:
拿到订单表,info() 一跑发现"备注"列 500 行里空了 300 行,"金额"列有 2 个空值。这时候怎么处理?一股脑删掉,可能把有用的行也删了;一股脑填零,均值直接被拉低。缺失值处理没有放之四海皆准的答案,核心是搞清楚"为什么缺"——是系统没采集、用户没填,还是数据在传输中丢了?原因决定策略。
Pandas 用 NaN(Not a Number)表示缺失。它有几个特性:NaN == NaN 为 False;任何数值运算碰到 NaN 结果都是 NaN;sum()、mean() 默认会跳过 NaN。所以缺失值不处理,统计结果可能是错的——比如两列相加,任何一列有空,结果就是空。
删除有信息损失的风险,填充有引入偏差的风险。原则是:缺失少且随机,删;缺失多但可推断,填;缺失多且无规律,整列删。
import pandas as pd import numpy as np df = pd.DataFrame({ "订单号": ["A001", "A002", "A003", "A004"], "金额": [199, np.nan, 89, 1299], "门店": ["北京", "上海", np.nan, "广州"], }) df.isnull() # 布尔表,True 即缺失 df.isnull().sum() # 每列缺失数量 df["金额"].isna().sum() # 单列缺失数量
💡 关键直觉:
isnull().sum()是缺失体检的标准组合——先知道"哪列缺、缺多少",再决定怎么办。
df.dropna() # 删除含任何缺失的行 df.dropna(how="all") # 只删全为缺失的行 df.dropna(subset=["金额"]) # 只按指定列判空 df.dropna(axis=1) # 删除含缺失的列
df["金额"].fillna(0) # 填固定值(慎用:会把均值拉低) df["金额"].fillna(df["金额"].mean()) # 填均值 df["金额"].fillna(df["金额"].median()) # 填中位数(抗异常值) df["门店"].fillna("未知") # 类别填特殊值 # 时序前向/后向填充 df["金额"].fillna(method="ffill") # 用上一个值填 df["金额"].fillna(method="bfill") # 用下一个值填
⚠️ 常见坑:fillna 默认返回新对象,不修改原表。想原地改要写
df["金额"] = df["金额"].fillna(...)或加inplace=True。新手常犯"填了但表没变"的错。
df["折扣后金额"] = df["金额"] * 0.9 # 新增列 df["金额"] = df["金额"].fillna(0) # 修改列 df.drop(columns=["备注"], inplace=True) # 删除列 df.rename(columns={"金额": "销售额"}) # 列改名
df.sort_values("金额", ascending=False) # 按金额降序 df.sort_values(["门店", "金额"], ascending=[True, False]) # 多列排序 df.drop_duplicates() # 整行去重 df.drop_duplicates(subset=["订单号"]) # 按指定列去重
# map:Series 的逐元素映射 df["门店"].map({"北京": "华北", "上海": "华东", "广州": "华南"}) # apply:按行或列应用函数 df["金额"].apply(lambda x: "大单" if x > 300 else "普通单") # 整列类型转换 df["金额"] = df["金额"].astype(float)
💡 关键直觉:
map适合"查字典式"替换,apply适合"套函数式"计算。能用向量化表达式(如df["金额"] > 300)就不必 apply——向量化更快,代码也更短。
| 缺失情况 | 推荐做法 | 一句话理由 |
|---|---|---|
| 随机缺失、比例小 | dropna 删行 | 损失可忽略 |
| 数值列、分布右偏 | fillna 中位数 | 不被大单带偏 |
| 时间序列、顺序相关 | fillna ffill | 用历史值补最合理 |
| 类别列、缺得不多 | fillna 众数 | 保持分布 |
| 整列缺失超八成 | 删整列 | 已无信息量 |
"一刀切"填充常不满足业务。比如不同门店的客单价差异大,用全局中位数填不如用每个门店自己的中位数填:
# 按门店分组,用各组中位数填充 df["客单价"] = df.groupby("门店")["客单价"].transform( lambda x: x.fillna(x.median()) )
💡 关键直觉:
groupby + transform把填充粒度从"全表"细化到"每组",是处理"组间差异明显"数据的标准技巧。同样的思路也适用于均值、众数——先分组,再填充。
缺失值最隐蔽的危害是"连锁污染":df["金额"] + df["运费"] 只要任一列有缺失,结果就是 NaN;groupby 聚合时 sum() 默认跳过 NaN 会让小样本组的统计失真;建模时算法直接报错或隐含丢弃。所以清洗阶段的缺失处理,本质上是在为下游所有环节排雷。
| 操作 | 代码 |
|---|---|
| 新增列(按条件) | df["档位"] = np.where(df["金额"] > 300, "大", "小") |
| 删除行 | df.drop(index=[0, 1]) |
| 删除列 | df.drop(columns=["备注"]) |
| 列改名 | df.rename(columns={"旧": "新"}) |
| 重置索引 | df.reset_index(drop=True) |
| 按列排序 | df.sort_values(by="金额", ascending=False) |
| 前 N 大 | df.nlargest(3, "金额") |
| 行数统计 | df.shape[0] / len(df) |
nlargest 取"金额最高的前几单",比"排序再切片"语义更清晰,也是高频操作。
把本节知识串成一次完整演练,用一份带多种问题的订单表:
import pandas as pd import numpy as np df = pd.DataFrame({ "订单号": ["A001", "A002", "A003", "A004", "A005"], "门店": ["北京", "上海", "北京", np.nan, "广州"], "金额": [199, np.nan, 89, 1299, 450], "日期": ["2026-01-01", "2026-01-01", "2026-01-02", "2026-01-02", None], }) # 1. 体检 print("缺失情况:\n", df.isnull().sum()) # 2. 金额按中位数填 df["金额"] = df["金额"].fillna(df["金额"].median()) # 3. 门店缺失填"未知"(系统性缺失:该记录没采集门店) df["门店"] = df["门店"].fillna("未知") # 4. 日期缺失行删除(信息太少,无规律可填) df = df.dropna(subset=["日期"]) print("\n处理后缺失:", df.isnull().sum().sum()) print(df)
四种处理手段各用一次:中位数填充、特殊值填充、按列删除、最终验证。每一种都对应前面讲过的策略——体检、判断、处理、验证四步闭环。
如果某列缺失比例高且填充后仍觉得不对,别硬填——回数据源头查原因。是采集接口漏了字段?是导出程序 bug?是上游表没关联上?源头修复远好过下游补救。清洗的终极目标不是"填得漂亮",而是"让数据不再缺"。
isnull().sum() 是缺失体检标准组合,先看缺多少单表的增删改查都齐了,下一节是 Pandas 的重头戏——分组聚合与多表合并。