第 3.3 节 常见问题#
这一节收集了初学者在「洗数据」环节最常卡的 10 个问题。每个问题都给出直接答案,能跑代码的都附上完整示例。
怎么用这一节 不用通读。清洗时遇到问题用 Ctrl+F 搜关键词,直接跳到对应那条。
准备一下数据,下面每题都会用到:
import pandas as pd
import numpy as np
data = {
"订单号": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
"下单日期": ["2024-01-05", "2024-01-08", "2024-01-12", "2024-02-03",
"2024-02-15", "2024-03-02", "2024-03-20", "2024-04-08"],
"城市": ["北京", "上海", "北京", "广州", "上海", "北京", "深圳", "广州"],
"商品": ["手机", "电脑", "耳机", "手机", "键盘", "显示器", "耳机", "手机"],
"销量": [2, 1, 3, 2, 5, 1, 4, 2],
"单价": [3999.0, 7999.0, 599.0, 3999.0, 299.0, 1299.0, 599.0, 3999.0],
"金额": [7998.0, 7999.0, 1797.0, 7998.0, 1495.0, 1299.0, 2396.0, 7998.0],
"客户评分": [4.5, 4.0, 4.8, 3.9, 4.2, 4.6, 4.1, 4.9],
}
df = pd.DataFrame(data)
dirty = df.copy()
dirty.loc[1, "金额"] = np.nan
dirty.loc[5, "客户评分"] = np.nan
dirty = pd.concat([dirty, dirty.iloc[[0]]], ignore_index=True)Q1:如何统计每列的缺失值数量?#
答: isna().sum()。
print(dirty.isna().sum())订单号 0
下单日期 0
城市 0
商品 0
销量 0
单价 0
金额 1
客户评分 1
dtype: int64几个常见的衍生问法:
dirty.isna().sum().sum() # 整张表一共缺几个:2
dirty.isna().any() # 每一列有没有缺失(返回布尔 Series)
dirty.isna().any().any() # 整张表有没有缺失:True
dirty.isna().all() # 每一列是不是全空(返回布尔 Series)算缺失比例(做数据质量报告时常用):
missing_ratio = dirty.isna().sum() / len(dirty) print(missing_ratio)缺失比例超过 30% 的列,通常要考虑「这一列还有没有分析价值」。
Q2:isna() 和 notna() 有什么区别?#
答: 正好相反。isna() 问「有没有空」,notna() 问「有没有值」。
dirty.isna() # True = 这里缺了
dirty.notna() # True = 这里有值
notna()最常见的用法是「只要有值的」:# 所有格子都有值的行 print(dirty[dirty.notna().all(axis=1)])
axis=1表示「沿着列的方向看每一行」,axis=0表示「沿着行的方向看每一列」。 这是 Pandas 里最容易搞混的轴参数,记法是:axis=1是横着看,作用在行上。
另外还有两个等价写法:
dirty.isna() # ✅ 推荐
dirty.isnull() # 同义,老写法
dirty[pd.isna(dirty)] # 也行,但啰嗦Q3:如何使用均值、中位数或固定值填充缺失值?#
答: 都用 fillna(),区别只是填什么。
t = dirty.copy()
# 固定值
t["金额"] = t["金额"].fillna(0)
# 均值
t["金额"] = t["金额"].fillna(t["金额"].mean()) # 4425.857...
# 中位数
t["金额"] = t["金额"].fillna(t["金额"].median()) # 2396.0
# 用前一个值填(时间序列常用)
t["金额"] = t["金额"].ffill()
# 用后一个值填
t["金额"] = t["金额"].bfill()一次填多列用字典:
dirty.fillna({"金额": 0, "客户评分": 5.0})均值还是中位数? 看数据有没有极端值。上面这个例子里:
- 均值 = 4425.86
- 中位数 = 2396.0
差别这么大,是因为金额分布是右偏的(大部分订单在 1500~8000,但有一两笔 7998 把均值往上拉)。 如果你的数据里有明显偏高的极端值,用中位数更稳。
Q4:dropna() 和 fillna() 分别适用于什么场景?#
答: 看缺失的严重程度和这行的信息还有没有用。
fillna() | dropna() | |
|---|---|---|
| 做什么 | 补上缺的格子 | 删掉整行 |
| 行数 | 不变 | 变少 |
| 什么时候用 | 缺失少、且那行其它信息有用 | 缺失多、或关键字段缺失 |
| 典型场景 | 评分缺失(不影响其它字段) | 金额缺失(金额没法补) |
print(len(dirty.dropna())) # 6 —— 任何一格空就删
print(len(dirty.dropna(subset=["金额"]))) # 7 —— 只因金额空而删
print(len(dirty.dropna(how="all"))) # 8 —— 只有整行全空才删实用策略:先试
subset,再试fillna# 优先只删关键字段缺失的行 clean = dirty.dropna(subset=["金额"]) # 剩下的小缺失再填 clean["客户评分"] = clean["客户评分"].fillna(clean["客户评分"].median())这样比「一上来全删」损失的数据少得多。
Q5:如何删除完全重复的行?#
答: drop_duplicates(),默认保留第一次出现的。
print(len(dirty)) # 9
print(len(dirty.drop_duplicates())) # 8想先看看哪些行重复,用 duplicated():
print(dirty.duplicated())[False False False False False False False False True]True 表示这一行和前面某一行完全一样。
drop_duplicates()返回新表,不改原表。dirty = dirty.drop_duplicates() # ✅ 接住返回值 dirty.drop_duplicates() # ❌ 白干
Q6:如何根据指定列判断重复记录?#
答: 用 subset 参数指定「拿哪几列来判断重复」。
# 只看「城市」和「商品」两列,这两列一样就算重复
print(len(dirty.drop_duplicates(subset=["城市", "商品"]))) # 7也可以先标记再处理,这样你能看清到底哪些行有问题:
print(dirty.duplicated(subset=["城市", "商品"]))[False False False False False False False True True]业务上该按哪几列判断重复? 选**「能唯一标识一条业务记录」的字段**:
- 订单表 →
订单号- 用户表 →
用户 ID- 日报表 →
日期(或日期 + 门店)不要图省事用整行去重。 如果同一笔订单的两行金额不一样(改过价),整行去重会把它们都留下,你就把这笔订单算了两遍。
Q7:drop_duplicates() 中的 keep 参数有什么作用?#
答: 它决定「一组重复记录里,保留哪一条」。
keep 取值 | 效果 |
|---|---|
"first"(默认) | 保留第一次出现的 |
"last" | 保留最后一次出现的 |
False | 全部删掉,一条不留 |
print(len(dirty.drop_duplicates(subset=["城市", "商品"]))) # 7
print(len(dirty.drop_duplicates(subset=["城市", "商品"], keep="last"))) # 7
print(len(dirty.drop_duplicates(subset=["城市", "商品"], keep=False))) # 5
keep="last"的实战场景 你的订单表里,同一个订单号出现了两次:一次是早上录的旧价,一次是下午改的新价。你知道后来的更对:df = df.drop_duplicates(subset=["订单号"], keep="last")反过来,如果你的数据是「不断追加的新记录」,用默认的
keep="first"。
Q8:如何批量重命名列名或修改列名格式?#
答: rename(columns=...),字典和函数都能用。
# 字典:指定改哪些
df.rename(columns={"订单号": "order_id", "城市": "city"}).columns.tolist()['order_id', '下单日期', 'city', '商品', '销量', '单价', '金额', '客户评分']# 函数:每一列都过一遍
df.rename(columns=lambda c: c + "_x").columns.tolist()['订单号_x', '下单日期_x', '城市_x', '商品_x', '销量_x', '单价_x', '金额_x', '客户评分_x']处理「列名带空格」这种脏数据,更推荐直接改 columns 属性:
d2 = pd.DataFrame([[1, 2]], columns=[" 订单号 ", "下 单 日 期"])
d2.columns = d2.columns.str.strip() # 去首尾空格
d2.columns = d2.columns.str.replace(" ", "") # 去掉所有空格
print(d2.columns.tolist())['订单号', '下单日期']
str后缀家族(处理列名格式的瑞士军刀):df.columns.str.strip() # 去首尾空格 df.columns.str.lower() # 转小写 df.columns.str.upper() # 转大写 df.columns.str.replace(" ", "_") # 替换 df.columns.str.title() # 每个词首字母大写
Q9:replace() 和条件筛选后赋值有什么区别?#
答: 两种都是「改值」,但适用场景不同。
# 方式 A:replace —— 值到值的映射
df["商品"].replace({"手机": "智能手机"})
# 方式 B:条件筛选后赋值 —— 满足规则才改
df.loc[df["商品"] == "手机", "商品"] = "智能手机"怎么选?
- 只是一对一或一对多的值映射 → 用
replace(),写起来最清楚; - 条件是业务规则(而不是简单的值相等)→ 用
.loc,比如:
# 销量超过 3 的订单,单价打八折
df.loc[df["销量"] > 3, "单价"] = df.loc[df["销量"] > 3, "单价"] * 0.8
# 金额超过 5000 的,标记为大单
df.loc[df["金额"] > 5000, "订单类型"] = "大单"用
.loc赋值前记得.copy()df = df.copy() # ✅ df.loc[df["销量"] > 3, "单价"] = ...不加的话 Pandas 2.x 会给
SettingWithCopyWarning警告,数据处理起来很脆弱。
顺便提一个坑:replace() 的正则匹配默认是关闭的。
df["城市"].replace({"北.*": "北方"}) # 不生效
df["城市"].replace({"北.*": "北方"}, regex=True) # 生效Q10:如何使用 astype()、to_numeric() 和 to_datetime() 转换数据类型,并处理转换失败的情况?#
答: 三个方法分工不同,失败时处理方式也不同。
astype() —— 类型明确、能保证转成功时用,失败直接报错:
df["订单号"].astype(str).head(2).tolist() # ['1001', '1002']
df["销量"].astype("int64")pd.Series(["1001", "abc", "1003"]).astype(int)
# ValueError: invalid literal for int() with base 10: 'abc'to_numeric() —— 字符串转数字,失败用 errors="coerce" 变 NaN:
s = pd.Series(["1001", "abc", "1003"])
pd.to_numeric(s, errors="coerce").tolist()[1001.0, nan, 1003.0]to_datetime() —— 字符串转日期,失败用 errors="coerce" 变 NaT:
pd.to_datetime(pd.Series(["2024-01-05", "不是日期"]), errors="coerce")0 2024-01-05
1 NaT
dtype: datetime64[ns]实战的标准三步走:
# 1. 先转换
df["金额"] = pd.to_numeric(df["金额"], errors="coerce")
df["下单日期"] = pd.to_datetime(df["下单日期"], errors="coerce")
# 2. 看看有多少转失败了
print(df["金额"].isna().sum())
print(df["下单日期"].isna().sum())
# 3. 根据数量决定:少就填掉,多就查源头
if df["金额"].isna().sum() < 5:
df["金额"] = df["金额"].fillna(0)
else:
raise ValueError("金额列脏数据太多,需要人工核对")
errors的另一个取值是"raise"(默认),意思是转换失败就抛异常。
"raise"—— 严格,出错就停(默认)"coerce"—— 宽容,转不了变 NaN"ignore"—— 直接放弃这一列,返回原样日常清洗基本都用
"coerce",然后自己处理那些 NaN。
顺便提一个细节:
astype("int64")是直接截断,不是四舍五入。df["客户评分"].astype("int64") # 4.8 → 4 df["客户评分"].round(1).astype("int64") # 4.8 → 5需要四舍五入就先
round()再转。
小结#
这一节的 10 个问题,其实都围绕五个动作展开:
- 体检——
isna().sum()看清问题,notna()找有值的部分; - 缺失值——
fillna()填(固定值 / 均值 / 中位数 / ffill),dropna(subset=...)删; - 重复值——
drop_duplicates(subset=..., keep=...),先duplicated()看清; - 列名和取值——
rename(columns=...)改列名,replace()/.loc[条件, 列] = 值改数据; - 类型——
astype()硬转,to_numeric()/to_datetime()配errors="coerce"软转。
下一章预告 数据洗干净了,该问业务问题了。「每个城市卖了多少」「哪种商品最赚钱」「北京和上海哪个客单价高」——第 4 章教你用
groupby()和pivot_table()把一张明细表变成一张汇总报表。