📚 Python入门到高级(知识点六)
💡 关键知识点
一、Pandas中的缺失值是什么
1.1 三种空值表现形式
Pandas中,缺失值主要有三种表现形式:
| 类型 |
说明 |
示例 |
None |
Python原生的空值,类型为 NoneType |
None |
np.nan |
NumPy中的空值,类型为 float |
np.nan |
pd.NA |
Pandas 1.0+ 引入的专属空值 |
pd.NA |
import numpy as np
import pandas as pd
print(None, type(None)) # None <class 'NoneType'>
print(np.nan, type(np.nan)) # nan <class 'float'>
print(pd.NA, type(pd.NA)) # <NA> <class 'pandas._libs.missing.NAType'>
1.2 注意:这些「看起来像空值」的其实不是
| 值 |
是否识别为空值 |
说明 |
'null' |
❌ |
只是一个普通字符串 |
''(空字符串) |
❌ |
Pandas认为空字符串是有内容的 |
np.inf(无穷大) |
❌ |
是有效数值 |
1.3 为什么不能用 == 判断空值?
在Python中,np.nan 有一个特殊性质------它不等于自身:
print(np.nan == np.nan) # False
print(np.nan is np.nan) # False
因此,判断空值必须使用Pandas提供的专用方法,而不是 == 运算符。
二、如何检测缺失值
2.1 核心检测方法
Pandas提供了两套功能完全等价的方法:
| 方法 |
含义 |
返回值 |
isna() / isnull() |
检测是否为缺失值 |
True表示缺失 |
notna() / notnull() |
检测是否不是缺失值 |
True表示非缺失 |
import pandas as pd
# 单个值测试
print(pd.isna(None)) # True
print(pd.isna(np.nan)) # True
print(pd.isna(pd.NA)) # True
print(pd.isna('null')) # False
print(pd.isna('')) # False
2.2 实战:检测DataFrame中的缺失值
import pandas as pd
import numpy as np
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六'],
'年龄': [25, np.nan, 30, 28],
'城市': ['北京', '上海', np.nan, '广州'],
'收入': [5000, 6000, 5500, np.nan]
})
print(df)
# 姓名 年龄 城市 收入
# 0 张三 25.0 北京 5000.0
# 1 李四 NaN 上海 6000.0
# 2 王五 30.0 NaN 5500.0
# 3 赵六 28.0 广州 NaN
# 检测缺失值
print(df.isnull())
# 姓名 年龄 城市 收入
# 0 False False False False
# 1 False True False False
# 2 False False True False
# 3 False False False True
2.3 快速统计缺失情况
# 每列缺失数量
print(df.isnull().sum())
# 姓名 0
# 年龄 1
# 城市 1
# 收入 1
# dtype: int64
# 每列缺失比例(%)
print(df.isnull().sum() / len(df) * 100)
# 姓名 0.0
# 年龄 25.0
# 城市 25.0
# 收入 25.0
# dtype: float64
# 总缺失数量
print(df.isnull().sum().sum()) # 3
2.4 关于 isna() 和 isnull() 的疑问
很多初学者会问:为什么有两套名字不同的方法,做的事情却完全一样?
官方的说法是:isnull() 是 isna() 的别名(alias),两者功能完全相同。
原因在于Pandas的设计借鉴了R语言。在R中,NA(Not Available)和 NULL 是两个不同的概念,而Python/Numpy中统一使用 NaN 表示缺失值。Pandas为了兼容不同背景的用户,同时保留了两套命名。
建议 :任选其一,在项目中保持风格统一即可。我个人习惯使用 isna() / notna(),因为更简短。
三、Pandas读取CSV时的缺失值处理参数总结
1. keep_default_na 参数
| 参数值 |
作用 |
True(默认) |
自动将常见缺失值(空值、NULL、null、None、NA、nan等)填充为 NaN |
False |
关闭自动识别,所有值保持原始内容,不填充 NaN |
2. na_values 参数
- 作用 :手动指定哪些值应被识别为缺失值并填充为
NaN
- 使用前提 :通常与
keep_default_na=False 配合使用
- 注意 :设置
na_values 后,只有列表中指定的值会被替换为 NaN
方式一:默认读取(自动填充)
import pandas as pd
import numpy as np
# keep_default_na=True(默认),自动填充常见缺失值为 NaN
df = pd.read_csv('data/survey_visited.csv')
方式二:关闭自动填充
import pandas as pd
import numpy as np
# keep_default_na=False,不填充任何值,保留原始数据
df = pd.read_csv('data/survey_visited.csv', keep_default_na=False)
方式三:关闭自动填充 + 自定义缺失值
import pandas as pd
import numpy as np
# keep_default_na=False 关闭自动填充 + na_values 手动指定哪些值算缺失
df = pd.read_csv('data/survey_visited.csv', keep_default_na=False, na_values=[None, np.nan, pd.NA])
四、Pandas删除缺失值详解
# 1. 导包
import pandas as pd
# 2. 加载数据
train = pd.read_csv('data/titanic_train.csv')
# 3. 了解数据
train.info()
print(train.shape) # (892, 12)
# ============================================================
# 4. 删除缺失值
# ============================================================
# 重要概念:
# dropna(inplace=True) → 直接在原数据上修改
# dropna() → 默认复制一份,原数据不变,返回新DataFrame
方式一:dropna() 默认删除任意缺失行
# 4.1 dropna() 默认 how='any',删除包含任何缺失值的行
new_train1 = train.dropna() # 默认 how='any'
print(new_train1.shape) # (183, 12)
# 示例数据理解:
# 原始数据 (892行, 12列)
# Age列缺失 178行,Cabin列缺失 688行,Embarked列缺失 3行
# dropna() 删除所有包含缺失值的行后,只剩下 183行
# 这183行是 Age、Cabin、Embarked 三列都没有缺失的乘客
方式二:dropna(how='all') 删除全为空的行
# 4.2 dropna(how='all') 只有整行都是缺失值才删除
new_train2 = train.dropna(how='all')
print(new_train2.shape) # (891, 12)
# 示例数据理解:
# 原始数据有 892行,删除了 1行(该行所有列都是NaN)
# 因为泰坦尼克数据中,只有极少数行可能全为空
# 所以结果几乎没变化:892 → 891
方式三:dropna(subset=[列名]) 指定列检测缺失
# 4.3 dropna(subset=[列名]) 只检测指定列是否有缺失
# 如果指定列有缺失,就删除该行
# 示例1:只检测 Age 列
new_train3 = train.dropna(subset=['Age'])
print(new_train3.shape) # (714, 12)
# 示例数据理解:
# Age列原来有 178个缺失值 → 删除178行
# 892 - 178 = 714 ✓
# 示例2:同时检测多列(Age 和 Cabin)
new_train3 = train.dropna(subset=['Age', 'Cabin'])
print(new_train3.shape) # (183, 12)
# 示例数据理解:
# 删除 Age 或 Cabin 任一列有缺失的行
# 结果和 dropna() 一样,因为 Cabin 缺失最多
# 892 - 688 = 183(Cabin非空的行数)✓
方式四:drop() 直接删除指定列
# 4.4 drop() 直接删除指定的列(不是删除行)
# axis=1 表示按列删除
# 删除 Cabin 列(缺失比例高达77%)
new_train4 = train.drop('Cabin', axis=1)
print(new_train4.shape) # (892, 11)
# 示例数据理解:
# 原始数据有 12列,删除了 'Cabin' 这一列
# 892行不变,列数 12 → 11
# 也可以删除多列
new_train5 = train.drop(['Cabin', 'Age'], axis=1)
print(new_train5.shape) # (892, 10)
五种方法对比总结
| 方法 |
代码 |
删除对象 |
结果 (行, 列) |
| 原始数据 |
train |
无 |
(892, 12) |
dropna() |
train.dropna() |
任意列有缺失的行 |
(183, 12) |
dropna(how='all') |
train.dropna(how='all') |
整行全为空的行 |
(891, 12) |
dropna(subset=['Age']) |
train.dropna(subset=['Age']) |
Age列有缺失的行 |
(714, 12) |
dropna(subset=['Age','Cabin']) |
train.dropna(subset=['Age','Cabin']) |
Age或Cabin有缺失的行 |
(183, 12) |
drop('Cabin', axis=1) |
train.drop('Cabin', axis=1) |
Cabin这一列 |
(892, 11) |
关键区别:dropna() vs drop()
| 方法 |
作用 |
axis参数 |
dropna() |
删除行(有缺失的行) |
默认 axis=0 |
drop() |
删除列(指定的列) |
需指定 axis=1 |
常用参数速查表
| 参数 |
作用 |
示例 |
how='any' |
只要有缺失就删除(默认) |
dropna(how='any') |
how='all' |
全部缺失才删除 |
dropna(how='all') |
subset=[列名] |
指定检测哪些列 |
dropna(subset=['Age','Cabin']) |
axis=0 |
删除行(默认) |
dropna(axis=0) |
axis=1 |
删除列 |
drop('列名', axis=1) |
inplace=True |
原地修改,不返回新对象 |
dropna(inplace=True) |
五、Pandas填充缺失值详解(带示例数据)
# 1. 导包
import pandas as pd
# 忽略所有警告
import warnings
warnings.filterwarnings('ignore')
# 2. 读取文件
df = pd.read_csv('data/students.csv')
print(df)
# ============================================================
# 3. fillna() 填充数据
# ============================================================
# 假设 students.csv 数据如下(示例):
# name age score gender
# 0 张三 18.0 90.0 男
# 1 李四 NaN 85.0 女
# 2 王五 20.0 NaN 男
# 3 赵六 22.0 88.0 NaN
# 4 孙七 NaN 92.0 男
# 5 周八 19.0 NaN 女
方式一:直接填充固定值(0)
# 方式1:直接填充0
new_df1 = df.fillna(0)
print(new_df1)
# 示例输出:
# name age score gender
# 0 张三 18.0 90.0 男
# 1 李四 0.0 85.0 女 ← age缺失,填充为0
# 2 王五 20.0 0.0 男 ← score缺失,填充为0
# 3 赵六 22.0 88.0 0 ← gender缺失,填充为0
# 4 孙七 0.0 92.0 男 ← age缺失,填充为0
# 5 周八 19.0 0.0 女 ← score缺失,填充为0
# ⚠️ 缺点:age填充0会导致年龄异常(成年学生变成0岁)
方式二:填充平均值
# 方式2:填充平均年龄
new_df2 = df.fillna(df['age'].mean())
print(new_df2)
# 示例输出(假设 age 平均值为 19.8):
# name age score gender
# 0 张三 18.0 90.0 男
# 1 李四 19.8 85.0 女 ← age缺失,填充为平均值19.8
# 2 王五 20.0 NaN 男
# 3 赵六 22.0 88.0 NaN
# 4 孙七 19.8 92.0 男 ← age缺失,填充为平均值19.8
# 5 周八 19.0 NaN 女
# ⚠️ 注意:只有age列被填充了,其他列(score、gender)的缺失值没有被填充
方式三:前向填充(ffill)
# 方式3:填充当前缺失值的前一个数据(向前填充)
new_df3 = df.fillna(method='ffill')
print(new_df3)
# 示例输出:
# name age score gender
# 0 张三 18.0 90.0 男
# 1 李四 18.0 85.0 女 ← age用上一行的18.0填充
# 2 王五 20.0 85.0 男 ← score用上一行的85.0填充
# 3 赵六 22.0 88.0 NaN ← gender用上一行的"男"填充
# 4 孙七 22.0 92.0 男 ← age用上一行的22.0填充
# 5 周八 19.0 92.0 女 ← score用上一行的92.0填充
# ✅ 适用场景:时间序列数据,用前一时刻的值填充
# ⚠️ 注意:第3行gender原本是NaN,前向填充时用了第2行的"男"
方式四:后向填充(bfill)
# 方式4:填充当前缺失值的后一个数据(向后填充)
new_df4 = df.fillna(method='bfill')
print(new_df4)
# 示例输出:
# name age score gender
# 0 张三 18.0 90.0 男
# 1 李四 20.0 85.0 女 ← age用下一行的20.0填充
# 2 王五 20.0 88.0 男 ← score用下一行的88.0填充
# 3 赵六 22.0 88.0 NaN ← gender用下一行的"男"填充
# 4 孙七 19.0 92.0 男 ← age用下一行的19.0填充
# 5 周八 19.0 NaN 女 ← score后面没有数据,无法填充(仍然NaN)
# ✅ 适用场景:时间序列数据,用后一时刻的值填充
# ⚠️ 注意:最后一行score缺失,后面没有数据,填充失败
四种方式对比总结
| 方式 |
代码 |
填充逻辑 |
适用场景 |
| 固定值 |
fillna(0) |
所有缺失填同一个值 |
临时占位 |
| 平均值 |
fillna(df['age'].mean()) |
用该列平均值填充 |
数值列,无明显异常值 |
| 前向填充 |
fillna(method='ffill') |
用上一行同列的值 |
时间序列,数据渐变 |
| 后向填充 |
fillna(method='bfill') |
用下一行同列的值 |
时间序列,数据渐变 |
常用填充方式汇总
# 1. 填充固定值
df.fillna(0)
# 2. 填充指定值
df.fillna('未知')
# 3. 填充平均值(数值列)
df['age'] = df['age'].fillna(df['age'].mean())
# 4. 填充中位数(更稳健,不受异常值影响)
df['age'] = df['age'].fillna(df['age'].median())
# 5. 填充众数(分类列)
df['gender'] = df['gender'].fillna(df['gender'].mode()[0])
# 6. 前向填充
df.fillna(method='ffill')
# 7. 后向填充
df.fillna(method='bfill')
# 8. 不同列填不同值
df.fillna({
'age': df['age'].median(),
'score': 0,
'gender': '未知'
})
# 9. 限制连续填充数量
df.fillna(method='ffill', limit=1) # 连续缺失只填1个
特殊知识点
1. 为什么 df.fillna(df['age'].mean()) 只填充了 age 列?
因为只传入了 df['age'].mean() 这一个值,Pandas 会将这个值应用到所有列的缺失值上。
- age 列是数值型,填充平均值 → 合理
- score 列也是数值型,填充 age 的平均值 → 错误!应该用 score 自己的平均值
- gender 列是字符串,填充数值 → 无法填充
正确做法:对不同列使用不同的填充值
# 分别填充
df['age'] = df['age'].fillna(df['age'].mean())
df['score'] = df['score'].fillna(df['score'].mean())
df['gender'] = df['gender'].fillna(df['gender'].mode()[0])
2. ffill 和 bfill 的局限性
- 如果开头就有缺失,
ffill 无法填充(前面没有数据)
- 如果结尾就有缺失,
bfill 无法填充(后面没有数据)
- 连续多个缺失时,
ffill 会用同一个值填充所有缺失
# 示例:连续缺失
# 原数据:10, NaN, NaN, 40
# ffill后:10, 10, 10, 40 ← 两个NaN都被填充为10
# bfill后:10, 40, 40, 40 ← 两个NaN都被填充为40
六、Pandas分组聚合(GroupBy)详解(带示例数据)
# 1. 导包
import numpy as np
import pandas as pd
# 2. 读取文件
# csv: 具有固定分隔符的结构化数据,都是csv格式,不用关注后缀名是不是.csv
df = pd.read_csv('data/gapminder.txt', sep='\t')
print(df.shape) # (1704, 6)
数据简介
gapminder.txt 数据包含以下列:
country:国家
year:年份
pop:人口
continent:大洲
lifeExp:平均寿命
gdpPercap:人均GDP
需求1:求每个年份的平均寿命
四种写法对比
# 写法1:df.列名(返回Series)
print(df.groupby('year').lifeExp.mean())
# 示例输出:
# year
# 1952 49.057620
# 1957 51.507401
# 1962 53.609247
# 1967 55.678290
# 1972 57.647386
# 1977 59.570157
# 1982 61.533197
# 1987 63.212613
# 1992 64.160338
# 1997 65.014676
# 2002 65.694923
# 2007 67.007423
# Name: lifeExp, dtype: float64
# 写法2:df['列名'](返回Series)
print(df.groupby('year')['lifeExp'].mean())
# 输出与写法1相同
# 写法3:df[['列名']](返回DataFrame)
print(df.groupby('year')[['lifeExp']].mean())
# 示例输出:
# lifeExp
# year
# 1952 49.057620
# 1957 51.507401
# 1962 53.609247
# 1967 55.678290
# 1972 57.647386
# 1977 59.570157
# 1982 61.533197
# 1987 63.212613
# 1992 64.160338
# 1997 65.014676
# 2002 65.694923
# 2007 67.007423
# 写法4:agg()方法(推荐,可扩展)
print(df.groupby('year').agg({"lifeExp": 'mean'}))
# 输出与写法3相同
print(df.groupby('year').agg({"lifeExp": np.mean}))
# 输出与写法3相同
四种写法对比总结
| 写法 |
代码 |
返回类型 |
特点 |
| 写法1 |
df.groupby('year').lifeExp.mean() |
Series |
最简洁,点语法 |
| 写法2 |
df.groupby('year')['lifeExp'].mean() |
Series |
括号语法,适合列名有空格 |
| 写法3 |
df.groupby('year')[['lifeExp']].mean() |
DataFrame |
双层括号,返回DataFrame |
| 写法4 |
df.groupby('year').agg({"lifeExp": 'mean'}) |
DataFrame |
最推荐,可多个字段多个函数 |
需求2:求每个年份、每个大洲的平均寿命和最高GDP
# 需求2:求每个年份,每个大洲的平均寿命和最高GDP
# 伪SQL:SELECT AVG(lifeExp), MAX(gdpPercap) FROM 表名 GROUP BY year, continent
print(df.groupby(['year', 'continent']).agg({"lifeExp": 'mean', "gdpPercap": 'max'}))
# 或
print(df.groupby(['year', 'continent']).agg({"lifeExp": np.mean, "gdpPercap": np.max}))
# 示例输出(部分):
# lifeExp gdpPercap
# year continent
# 1952 Africa 39.135500 4725.00000
# Americas 53.279840 12000.00000
# Asia 46.317394 10000.00000
# Europe 64.408500 15000.00000
# Oceania 69.255000 12000.00000
# 1957 Africa 41.266346 5489.00000
# Americas 55.960280 13000.00000
# ...(省略)
常用聚合函数
| 函数 |
作用 |
示例 |
mean() |
平均值 |
'mean' 或 np.mean |
max() |
最大值 |
'max' 或 np.max |
min() |
最小值 |
'min' 或 np.min |
sum() |
总和 |
'sum' 或 np.sum |
count() |
计数 |
'count' |
std() |
标准差 |
'std' 或 np.std |
var() |
方差 |
'var' 或 np.var |
median() |
中位数 |
'median' |
多个聚合函数同时使用
# 对同一列使用多个聚合函数
# 球每个年份的平均寿命的平均值、最大值、最小值、标准差
print(df.groupby('year')['lifeExp'].agg(['mean', 'max', 'min', 'std']))
# 示例输出(部分):
# mean max min std
# year
# 1952 49.057620 72.0 30.0 10.0000
# 1957 51.507401 73.0 31.0 10.0000
# 1962 53.609247 74.0 32.0 10.0000
# ...
# 对不同列使用不同聚合函数
print(df.groupby('year').agg({
'lifeExp': ['mean', 'max'],
'gdpPercap': ['mean', 'max'],
'pop': 'sum'
}))
关键知识点总结
1. groupby() 返回类型
# 分组后选择列的方式决定返回类型
df.groupby('year')['lifeExp'] # SeriesGroupBy
df.groupby('year')[['lifeExp']] # DataFrameGroupBy
df.groupby('year')[['lifeExp', 'gdpPercap']] # DataFrameGroupBy
2. 点语法 df.列名 和括号语法 df['列名']
| 语法 |
适用场景 |
df.lifeExp |
列名无空格、无特殊字符 |
df['lifeExp'] |
列名有空格、有特殊字符,或需要动态列名 |
df[['lifeExp']] |
需要返回DataFrame(单列也返回DataFrame) |
3. agg() 是万能方法
# 单个字段单个聚合
df.groupby('year').agg({"lifeExp": 'mean'})
# 单个字段多个聚合
df.groupby('year').agg({"lifeExp": ['mean', 'max', 'min']})
# 多个字段多个聚合
df.groupby('year').agg({
"lifeExp": ['mean', 'max'],
"gdpPercap": ['mean', 'max'],
"pop": 'sum'
})
# 重命名列名
df.groupby('year').agg(
平均寿命=('lifeExp', 'mean'),
最高GDP=('gdpPercap', 'max')
)
七、Pandas分组聚合:groupby() vs pivot_table()
# 1. 导包
import numpy as np
import pandas as pd
# 2. 读取文件
df = pd.read_csv('data/gapminder.txt', sep='\t')
print(df.shape) # (1704, 6)
# 3. 分析数据(分组聚合操作)
# 需求:求每个年份的平均寿命
# 方式1:groupby() + agg()
print(df.groupby('year').agg({"lifeExp": 'mean'}))
# 方式2:pivot_table() 透视表
result = df.pivot_table(index=['year'], values='lifeExp', aggfunc='mean')
print(result)
两种方式对比
| 对比项 |
groupby() + agg() |
pivot_table() |
| 语法 |
df.groupby('year').agg({"lifeExp": 'mean'}) |
df.pivot_table(index=['year'], values='lifeExp', aggfunc='mean') |
| 参数含义 |
groupby() 指定分组字段 agg() 指定聚合字段和函数 |
index 指定分组字段 values 指定聚合字段 aggfunc 指定聚合函数 |
| 返回类型 |
DataFrame |
DataFrame |
| 适用场景 |
简单分组聚合 |
类似Excel透视表习惯 |
参数说明
groupby() + agg() 写法
df.groupby('year').agg({"lifeExp": 'mean'})
# │ │ │ │
# │ │ │ └── 聚合函数
# │ │ └─────────── 聚合字段
# │ └──────────────── 聚合方法
# └─────────────────────────────── 分组字段
pivot_table() 写法
df.pivot_table(index=['year'], values='lifeExp', aggfunc='mean')
# │ │ │ │
# │ │ │ └── 聚合函数
# │ │ └───────────────── 聚合字段
# │ └─────────────────────────────── 分组字段
# └────────────────────────────────────────────── 透视表方法
两种方式等价关系
# 两者完全等价,输出结果相同
# 方式1
df.groupby('year').agg({"lifeExp": 'mean'})
# 方式2
df.pivot_table(index=['year'], values='lifeExp', aggfunc='mean')
多字段扩展写法
多分组字段
# groupby 写法
df.groupby(['year', 'continent']).agg({"lifeExp": 'mean'})
# pivot_table 写法
df.pivot_table(index=['year', 'continent'], values='lifeExp', aggfunc='mean')
多聚合字段
# groupby 写法
df.groupby('year').agg({
"lifeExp": 'mean',
"gdpPercap": 'max'
})
# pivot_table 写法
df.pivot_table(
index='year',
values=['lifeExp', 'gdpPercap'],
aggfunc={'lifeExp': 'mean', 'gdpPercap': 'max'}
)
多聚合函数
# groupby 写法
df.groupby('year').agg({"lifeExp": ['mean', 'max', 'min']})
# pivot_table 写法
df.pivot_table(
index='year',
values='lifeExp',
aggfunc=['mean', 'max', 'min']
)
选择建议
| 场景 |
推荐方式 |
| 简单分组聚合 |
groupby() + agg() |
| 习惯Excel透视表操作 |
pivot_table() |
| 需要将字段值转为列名 |
pivot_table()(加 columns 参数) |
| 追求性能 |
groupby() 通常更快 |