【Python系列课程】Pandas(二):数据清洗与缺失值处理
19 Pandas(二):数据清洗与缺失值处理——真实数据的"洗澡"指南
现实世界的数据永远是脏的:缺失值、重复行、格式不统一、异常值……据业界统计,数据分析师80%的时间都花在数据清洗上。Pandas提供了一整套数据清洗工具,让你用几行代码完成原本需要几小时的手工清理。
本篇讲透:缺失值处理、重复值处理、数据类型转换、字符串清洗。
一、缺失值(NaN)是什么?
在Pandas里,缺失值用 NaN(Not a Number)表示,它来自 numpy.nan。
import pandas as pd
import numpy as np
df = pd.DataFrame({
'name': ['Tom', np.nan, 'Bob'],
'age': [18, 19, np.nan],
'height': [167, 177, 178]
}, index=['n1', 'n2', 'n3'])
print(df)
# name age height
# n1 Tom 18.0 167.0
# n2 NaN 19.0 177.0
# n3 Bob NaN 178.0

1.1 检测缺失值:isnull() 和 notnull()
df = pd.DataFrame({
'name': ['Tom', np.nan, 'Bob'],
'age': [18, 19, np.nan],
'height': [167, 177, 178]
})
# isnull():是NaN的地方返回True
print(df.isnull())
# name age height
# n1 False False False
# n2 True False False
# n3 False True False
# notnull():不是NaN的地方返回True(isnull的倒数)
print(df.notnull())
# name age height
# n1 True True True
# n2 False True True
# n3 True False True
实用技巧:结合 sum() 快速统计每列有多少缺失值:
missing_count = df.isnull().sum()
print(missing_count)
# name 1
# age 1
# height 0
# dtype: int64
📸 [图2:用isnull()+sum()统计各列缺失值数量——柱状图展示]
二、删除缺失值:dropna()
dropna() 是最直接的缺失值处理方式——把有NaN的行或列直接删掉。
2.1 基础用法
df = pd.DataFrame({
'name': ['Tom', np.nan, 'Bob'],
'age': [np.nan, np.nan, 19],
'height': [177, 182, 179]
})
print(df)
# name age height
# n1 Tom NaN 177.0
# n2 NaN NaN 182.0
# n3 Bob 19.0 179.0
# 默认:删除任何含有NaN的行(axis=0, how='any')
print(df.dropna())
# name age height
# n3 Bob 19.0 179.0 ← 只有第3行没有任何NaN
2.2 关键参数详解
| 参数 | 可选值 | 说明 |
|---|---|---|
axis |
0(默认)或 1 |
0=删行,1=删列 |
how |
'any'(默认)或 'all' |
'any'=有NaN就删,'all'=全为NaN才删 |
thresh |
整数 | 至少保留有N个非NaN值的行/列 |
subset |
列名列表(当axis=0时) | 只检查指定列的NaN |
inplace |
True/False |
是否原地修改 |
df = pd.DataFrame({
'name': ['Tom', np.nan, 'Bob'],
'age': [np.nan, np.nan, 19],
'height': [177, 182, 179]
})
# axis=1:删除含有NaN的列
print(df.dropna(axis=1))
# height
# n1 177.0
# n2 182.0
# n3 179.0
# how='all':只有整行/整列全是NaN才删
df2 = pd.DataFrame({
'A': [np.nan, np.nan],
'B': [1, np.nan]
})
print(df2.dropna(how='all')) # 两行都不满足"全为NaN",所以不删任何行
# thresh=2:至少保留有2个非NaN值的行
df3 = pd.DataFrame({
'A': [1, np.nan, np.nan],
'B': [np.nan, 2, np.nan],
'C': [np.nan, np.nan, 3]
})
print(df3.dropna(thresh=2)) # 没有一行有≥2个非NaN,所以全删了
# 修改一下:
df3.loc['n1', 'B'] = 2
print(df3.dropna(thresh=2))
# A B C
# n1 1.0 2.0 NaN ← 有2个非NaN,保留
# subset:只根据指定列判断NaN
df = pd.DataFrame({
'name': ['Tom', np.nan, 'Bob'],
'age': [18, 19, np.nan],
'height': [167, 177, 178]
})
# 只根据'name'和'height'列的NaN来删除行
print(df.dropna(subset=['name', 'height']))
# name age height
# n1 Tom 18.0 167.0 ← n2的name是NaN,被删;n3的age是NaN但不在subset里,保留
# n3 Bob NaN 178.0

三、填充缺失值:fillna()
实际工作中,删除缺失值要慎重(会丢失数据)。更常见的做法是填充(impute)。
3.1 用标量填充
df = pd.DataFrame({
'A': [np.nan, 2, np.nan, 0],
'B': [ 4, 5, np.nan, 1],
'C': [ 0, 0, np.nan, np.nan],
'D': [ 1, 0, 0, 1]
})
# 所有NaN填充为0
print(df.fillna(0))
# A B C D
# 0 0.0 4.0 0.0 1
# 1 2.0 5.0 0.0 0
# 2 0.0 0.0 0.0 0
# 3 0.0 1.0 0.0 1
3.2 用字典按列填充不同值
# A列填充6,C列填充7
print(df.fillna({'A': 6, 'C': 7}))
# A B C D
# 0 6.0 4.0 0.0 1
# 1 2.0 5.0 0.0 0
# 2 6.0 0.0 7.0 0
# 3 0.0 1.0 7.0 1
3.3 用前后值填充(向前填充/向后填充)
这是时间序列数据中最常用的技巧!
# method='ffill'(pad):用前一个非NaN值填充
print(df.fillna(method='ffill'))
# A B C D
# 0 NaN 4.0 0.0 1 ← A列第0行前面没有值,还是NaN
# 1 2.0 5.0 0.0 0
# 2 2.0 5.0 0.0 0 ← C列第2行:前面最近的非NaN是0.0
# 3 0.0 1.0 0.0 1
# method='bfill'(backfill):用后一个非NaN值填充
print(df.fillna(method='bfill'))
# A B C D
# 0 2.0 4.0 0.0 1
# 1 2.0 5.0 0.0 0
# 2 0.0 1.0 NaN 0 ← C列第2行后面没有非NaN,还是NaN
# 3 0.0 1.0 NaN 1
沿列方向填充(axis=1):
print(df.fillna(method='ffill', axis=1)) # 向左填充
print(df.fillna(method='bfill', axis=1)) # 向右填充
3.4 限制填充数量
# limit=2:每列最多填充2个NaN
print(df.fillna(method='ffill', limit=2))

四、删除重复值:drop_duplicates()
重复数据在爬虫、日志分析、用户行为数据中极其常见。
4.1 基础用法
df = pd.DataFrame({
'A': [1, 3, 3, 1],
'B': [0, 2, 5, 0],
'C': [4, 0, 4, 4],
'D': [1, 0, 0, 1]
})
print(df)
# A B C D
# 0 1 0 4 1
# 1 3 2 0 0
# 2 3 5 4 0
# 3 1 0 4 1 ← 和第0行完全重复
# 默认:删除重复行,保留第一次出现的(keep='first')
print(df.drop_duplicates())
# A B C D
# 0 1 0 4 1
# 1 3 2 0 0
# 2 3 5 4 0
4.2 关键参数详解
| 参数 | 可选值 | 说明 |
|---|---|---|
subset |
列名列表 | 只根据指定列判断是否重复 |
keep |
'first'(默认)、'last'、False |
保留第一次/最后一次/删除所有重复 |
inplace |
True/False |
是否原地修改 |
# keep='last':保留最后一次出现的重复项
print(df.drop_duplicates(keep='last'))
# A B C D
# 1 3 2 0 0
# 2 3 5 4 0
# 3 1 0 4 1 ← 第0行被删,第3行(重复的最后一次)保留
# keep=False:所有重复行都删掉(一个不留!)
print(df.drop_duplicates(keep=False))
# A B C D
# 1 3 2 0 0
# 2 3 5 4 0 ← 第0行和第3行都删了
# subset:只根据指定列判断是否重复
print(df.drop_duplicates(subset=['A', 'D']))
# A B C D
# 0 1 0 4 1
# 1 3 2 0 0
# 2 3 5 4 0 ← 第0行和第3行的A、D列相同,但第3行被删了(keep='first')

五、数据排序:sort_values()
数据分析中经常需要按某列排序(比如按成绩从高到低)。
df = pd.DataFrame({
'col1': [4, 1, 2, np.nan, 5, 2],
'col2': [2, 1, 9, 8, 7, 6],
'col3': [0, 1, 9, 4, 2, 3]
})
# 按col1升序排列(默认ascending=True)
print(df.sort_values(by='col1'))
# col1 col2 col3
# 1 1.0 1 1
# 2 2.0 9 9
# 5 2.0 6 3 ← col1都是2,按其他列保持相对稳定
# 0 4.0 2 0
# 4 5.0 7 2
# 3 NaN 8 4 ← NaN默认排在最后
# 按col1降序
print(df.sort_values(by='col1', ascending=False))
# 按多列排序:先按col1,再按col2
print(df.sort_values(by=['col1', 'col2']))
# NaN排在最前面(na_position='first')
print(df.sort_values(by='col1', na_position='first'))
# 对列排序(沿axis=1)
print(df.sort_values(by=5, axis=1)) # 按第5行的值对列排序
原地排序:
df.sort_values(by='col1', inplace=True) # 直接修改原DataFrame

六、数据类型转换
有时候数据的类型不对(比如数字被读成了字符串),需要转换。
6.1 查看数据类型:dtypes
df = pd.DataFrame({
'name': ['Tom', 'Bob'],
'age': [18, 19],
'height': [167.5, 177.0]
})
print(df.dtypes)
# name object
# age int64
# height float64
# dtype: object
6.2 转换数据类型:astype()
# 把age列转成字符串
df['age'] = df['age'].astype(str)
print(df.dtypes)
# name object
# age object ← 现在是字符串了
# height float64
# dtype: object
# 把height列转成整数(会丢失小数位)
df['height'] = df['height'].astype(int)
print(df.dtypes)
# name object
# age object
# height int64
6.3 处理混合类型列
如果一列里既有数字又有字符串,astype() 会报错。这时可以用 pd.to_numeric() 的 errors 参数:
s = pd.Series(['1', '2', 'x', '4'])
# errors='raise'(默认):遇到不能转换的就报错
# print(pd.to_numeric(s)) # ❌ ValueError!
# errors='coerce':不能转换的变成NaN
print(pd.to_numeric(s, errors='coerce'))
# 0 1.0
# 1 2.0
# 2 NaN ← 'x'不能转成数字,变成NaN
# 3 4.0
# errors='ignore':不能转换的就保持原样
print(pd.to_numeric(s, errors='ignore'))
# 0 1
# 1 2
# 2 x ← 保持原样
# 3 4
七、字符串清洗:str属性
Pandas为Series提供了 .str 属性,可以对整列字符串进行批量操作(类似Python的字符串方法,但是向量化的)。
s = pd.Series([' Apple ', 'Banana', ' Cherry ', 'Date'])
# 去除两边空格
print(s.str.strip())
# 0 Apple
# 1 Banana
# 2 Cherry
# 3 Date
# 转大写/小写
print(s.str.upper())
print(s.str.lower())
# 判断是否包含某子串(返回布尔Series)
print(s.str.contains('a'))
# 0 False ('Apple'里没有'a',但有'A')
# 1 True ('Banana'里有'a')
# 2 True
# 3 False
# 替换字符串
print(s.str.replace('a', '@'))
# 0 Apple
# 1 B@n@n@
# 2 Cherry
# 3 D@te
# 拆分字符串
s2 = pd.Series(['Tom-18', 'Bob-19', 'Linda-20'])
print(s2.str.split('-', expand=True)) # expand=True:拆成多列
# 0 1
# 0 Tom 18
# 1 Bob 19
# 2 Linda 20

八、综合案例:清洗一份真实的学生成绩数据
把本篇学到的所有技巧串起来,做一个完整的数据清洗流程。
import pandas as pd
import numpy as np
# 模拟一份"脏"数据(从CSV读入的效果)
data = {
'姓名': ['张三', '李四', ' 王五 ', '张三', np.nan, '赵六'],
'年龄': [18, 19, 20, 18, 21, 22],
'成绩': [85, np.nan, 92, 85, 88, np.nan],
'城市': ['北京', '上海', '北京', '北京', '广州', '深圳'],
'备注': ['A', 'B', 'A', 'A', 'C', 'B']
}
df = pd.DataFrame(data)
print("=== 原始脏数据 ===")
print(df)
print(f"\n形状: {df.shape}")
# 步骤1:查看缺失值情况
print("\n=== 各列缺失值统计 ===")
print(df.isnull().sum())
# 步骤2:删除完全重复的行(基于所有列)
print("\n=== 删除完全重复的行 ===")
df = df.drop_duplicates()
print(df)
# 步骤3:填充姓名的NaN为"未知"
df['姓名'] = df['姓名'].fillna('未知')
# 步骤4:去除姓名列的两边空格
df['姓名'] = df['姓名'].str.strip()
# 步骤5:填充成绩的NaN为平均值
age_mean = df['成绩'].mean() # 注意:mean()会自动忽略NaN
df['成绩'] = df['成绩'].fillna(age_mean)
print(f"\n成绩平均值: {age_mean:.1f}")
# 步骤6:按成绩降序排列
df = df.sort_values(by='成绩', ascending=False)
# 步骤7:重置索引(因为删行和排序后索引乱了)
df = df.reset_index(drop=True)
print("\n=== 清洗后的干净数据 ===")
print(df)
print(f"\n最终形状: {df.shape}")
输出结果:
=== 原始脏数据 ===
姓名 年龄 成绩 城市 备注
0 张三 18 85.0 北京 A
1 李四 19 NaN 上海 B
2 王五 20 92.0 北京 A
3 张三 18 85.0 北京 A
4 NaN 21 88.0 广州 C
5 赵六 22 NaN 深圳 B
各列缺失值统计 ===
姓名 1
年龄 0
成绩 2
城市 0
备注 0
=== 删除完全重复的行 ===
(第3行和第0行重复,被删除)
成绩平均值: 88.3
=== 清洗后的干净数据 ===
姓名 年龄 成绩 城市 备注
0 王五 20 92.0 北京 A
1 Unknown 21 88.0 广州 C
2 张三 18 88.3 北京 A
3 李四 19 88.3 上海 B
4 赵六 22 88.3 深圳 B

九、本文总结
| 操作 | 函数 | 核心参数 |
|---|---|---|
| 检测缺失值 | isnull() / notnull() |
— |
| 删除缺失值 | dropna() |
axis, how, thresh, subset |
| 填充缺失值 | fillna() |
value, method='ffill'/'bfill' |
| 删除重复值 | drop_duplicates() |
subset, keep |
| 排序 | sort_values() |
by, ascending, na_position |
| 类型转换 | astype() |
目标类型字符串 |
| 安全转数字 | pd.to_numeric() |
errors='coerce'/'ignore' |
| 字符串清洗 | .str.xxx() |
同Python字符串方法 |
数据清洗的通用流程:
- 用
isnull().sum()查看缺失值 - 用
drop_duplicates()删除重复行 - 用
fillna()或dropna()处理缺失值 - 用
.str.xxx()清洗字符串列 - 用
astype()或pd.to_numeric()转换数据类型 - 用
sort_values()和reset_index()整理顺序
动手练习
-
创建一个包含NaN的DataFrame(3行4列),然后:(a)删除所有包含NaN的行;(b)只用前2列来判断是否有NaN并删除对应行;(c)用0填充所有NaN。
-
有一个DataFrame
df = pd.DataFrame({'A':[1,2,2,3], 'B':[4,5,5,6], 'C':[7,8,9,10]}),删除重复行,但保留最后一次出现的(即保留第2行而不是第1行)。 -
创建一个Series
s = pd.Series(['1','2','x','4','5']),把所有能转成数字的值转成int,不能转的填为0(提示:结合pd.to_numeric和fillna)。 -
有一个学生成绩DataFrame,包含
姓名、成绩两列,其中成绩列有部分NaN。请:(a)用同列的平均值填充NaN;(b)按成绩降序排列;(c)添加一个新列等级,成绩>=90为’A’,60-89为’B’,其余为’C’。
下一篇预告:《20 Pandas(三):分组聚合与合并连接——数据分析的核心操作》
更多推荐



所有评论(0)