Python入门到高级(知识点六)

76 阅读14分钟

📚 Python入门到高级(知识点六)


💡 关键知识点

一、Pandas中的缺失值是什么

1.1 三种空值表现形式

Pandas中,缺失值主要有三种表现形式:

类型说明示例
NonePython原生的空值,类型为 NoneTypeNone
np.nanNumPy中的空值,类型为 floatnp.nan
pd.NAPandas 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(默认)自动将常见缺失值(空值、NULLnullNoneNAnan等)填充为 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相同

四种写法对比总结

写法代码返回类型特点
写法1df.groupby('year').lifeExp.mean()Series最简洁,点语法
写法2df.groupby('year')['lifeExp'].mean()Series括号语法,适合列名有空格
写法3df.groupby('year')[['lifeExp']].mean()DataFrame双层括号,返回DataFrame
写法4df.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 指定聚合函数
返回类型DataFrameDataFrame
适用场景简单分组聚合类似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() 通常更快