Pandas: 缺失值处理
最后更新:2026-08-26
缺失值(NaN)是数据清洗的头号敌人——真实数据从来不会完美。传感器故障导致温度缺失,用户跳过选填项导致年龄缺失,系统合并导致 ID 缺失。Pandas 提供了完整的缺失值检测、删除、填充和插值工具链。本节带你逐一掌握,并理解 Copy-on-Write 下的安全操作。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。
1. 你将学到
- ❶ NaN 的本质与语义
- ❷ isnull / notnull 检测
- ❸ dropna 删除缺失行/列
- ❹ fillna 填充缺失值
- ❺ interpolate 插值方法
2. Carol 的运动数据缺口
(1) 痛点:30% 的心率数据缺失
Carol 的健身追踪器记录了 30 天心率数据,但运动时有 9 天信号丢失(NaN):
PYTHON
import pandas as pd
import numpy as np
hr = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=30),
'heart_rate': [72, 75, np.nan, 78, np.nan, 80, 82, np.nan,
85, 88, np.nan, 90, 87, np.nan, 83, 80,
np.nan, 78, 76, np.nan, 74, 72, np.nan, 70,
68, np.nan, 65, 63, np.nan, 60]
})
print(f"Missing: {hr['heart_rate'].isnull().sum()} / {len(hr)}")
# Missing: 9 / 30
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 解法:4 种策略各有适用场景
| 策略 | 方法 | 适用场景 |
|---|---|---|
| 检测 | isnull / notnull | 了解缺失分布 |
| 删除 | dropna | 缺失少、行不重要 |
| 填充 | fillna | 有合理默认值 |
| 插值 | interpolate | 时序数据、趋势明显 |
3. NaN 的本质
(1) NaN 不等于 NaN
▶ 示例:NaN 的特殊行为(难度⭐)
PYTHON
import numpy as np
# NaN is NOT equal to itself — this is IEEE 754 standard
print(np.nan == np.nan) # False!
print(np.nan != np.nan) # True!
# Use np.isnan() to detect NaN
print(np.isnan(np.nan)) # True
# Pandas handles this internally — isnull() works correctly
import pandas as pd
s = pd.Series([1, np.nan, 3, None, 5])
print(s.isnull())
# 0 False
# 1 True
# 2 False
# 3 True
# 4 False
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) Pandas 中的缺失值类型
| 类型 | 缺失表示 | 示例 |
|---|---|---|
| float64 | np.nan | 默认浮点缺失 |
| int64 | 无法原生表示 NaN → 自动转 float64 | [1, None, 3] → float64 |
| object | None / np.nan | 混合类型列 |
| string | pd.NA | Pandas 1.x+ nullable string |
| Int64 | pd.NA | nullable integer(大写 I) |
| datetime64 | NaT | 时间缺失 |
💡 提示: Pandas 2.x 的 Nullable dtypes(大写开头如
Int64、boolean、string)用 pd.NA 替代 np.nan,解决了整数列无法表示缺失的问题。转换:df['col'].astype('Int64')。
4. isnull / notnull 检测
(1) 基础检测
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:缺失值检测(难度⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Carol', 'David'],
'age': [28, np.nan, 25, 30, np.nan],
'salary': [75000, 92000, np.nan, 88000, 105000],
'department': ['Sales', 'Engineering', np.nan, 'Sales', 'Management']
})
# Per-element detection
print(df.isnull())
# name age salary department
# 0 False False False False
# 1 False True False False
# 2 False False True True
# 3 False False False False
# 4 False True False False
# Per-column count
print(df.isnull().sum())
# name 0
# age 2
# salary 1
# department 1
# Total missing
print(f"Total missing: {df.isnull().sum().sum()}") # 4
# Not-null (inverse)
print(df.notnull().sum()) # count of non-missing per column
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 缺失值可视化分析
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:缺失模式分析(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Carol', 'David', 'Eve'],
'age': [28, np.nan, 25, 30, np.nan, 27],
'salary': [75000, 92000, np.nan, 88000, 105000, np.nan],
'department': ['Sales', 'Engineering', np.nan, 'Sales', 'Management', 'HR']
})
# Which rows have ANY missing?
rows_with_missing = df[df.isnull().any(axis=1)]
print(f"Rows with missing values: {len(rows_with_missing)}")
# 3 rows (Bob, Charlie, David)
# Which columns have missing?
cols_with_missing = df.columns[df.isnull().any()].tolist()
print(f"Columns with missing: {cols_with_missing}")
# ['age', 'salary', 'department']
# Missing percentage per column
missing_pct = (df.isnull().sum() / len(df) * 100).round(1)
print(missing_pct[missing_pct > 0])
# age 33.3%
# salary 33.3%
# department 16.7%
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
5. dropna 删除缺失
(1) 基础删除
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:dropna 删除(难度⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Carol', 'David'],
'age': [28, np.nan, 25, 30, np.nan],
'salary': [75000, 92000, np.nan, 88000, 105000]
})
# Drop rows with ANY missing (default)
print(df.dropna())
# name age salary
# 0 Alice 28.0 75000.0
# 3 Carol 30.0 88000.0
# Drop rows where ALL values are missing
print(df.dropna(how='all'))
# Drop columns with any missing
print(df.dropna(axis=1))
# name
# 0 Alice
# 1 Bob
# ...
# Drop rows with threshold (keep rows with >= N non-missing)
print(df.dropna(thresh=2)) # at least 2 non-NaN values
# name age salary
# 0 Alice 28.0 75000.0
# 1 Bob NaN 92000.0
# 3 Carol 30.0 88000.0
# 4 David NaN 105000.0
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) dropna 参数详解
| 参数 | 默认 | 说明 |
|---|---|---|
| axis | 0 | 0=删行,1=删列 |
| how | 'any' | 'any'=有 NaN 就删,'all'=全 NaN 才删 |
| thresh | None | 保留至少 N 个非缺失值的行 |
| subset | None | 只检查指定列 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:dropna 按列检查(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Carol', 'David'],
'age': [28, np.nan, 25, 30, np.nan],
'salary': [75000, 92000, np.nan, 88000, 105000],
'department': ['Sales', 'Engineering', 'Marketing', 'Sales', 'Management']
})
# Only drop rows where 'salary' is missing
# (keep rows with missing 'age' — we can impute age)
print(df.dropna(subset=['salary']))
# name age salary department
# 0 Alice 28.0 75000.0 Sales
# 1 Bob NaN 92000.0 Engineering
# 3 Carol 30.0 88000.0 Sales
# 4 David NaN 105000.0 Management
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
6. fillna 填充缺失
(1) 常量填充
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:fillna 常量与统计填充(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Carol', 'David'],
'age': [28, np.nan, 25, 30, np.nan],
'salary': [75000, 92000, np.nan, 88000, 105000]
})
# Fill with constant
df_const = df.fillna({'age': 0, 'salary': 0})
print(df_const)
# Fill with mean (most common for numeric)
df_mean = df.copy()
df_mean['age'] = df_mean['age'].fillna(df_mean['age'].mean())
df_mean['salary'] = df_mean['salary'].fillna(df_mean['salary'].median())
print(df_mean)
# age: filled with 27.67, salary: filled with 88000.0
# Fill with forward fill (use previous value)
df_ffill = df.fillna(method='ffill')
print(df_ffill)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
⚠️ 注意: Pandas 2.x 中
fillna(method=...) 已弃用,请用 df.ffill() / df.bfill() 替代。
(2) 前向/后向填充
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:ffill 和 bfill(难度⭐)
PYTHON
import pandas as pd
import numpy as np
# Time-series data — ffill makes sense here
ts = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=8),
'temperature': [22.5, np.nan, np.nan, 23.1, np.nan, 24.0, np.nan, 24.5]
})
# Forward fill (propagate last valid value)
print(ts.assign(temperature_ffill=ts['temperature'].ffill()))
# NaN → use previous non-NaN value
# Backward fill (propagate next valid value)
print(ts.assign(temperature_bfill=ts['temperature'].bfill()))
# NaN → use next non-NaN value
# Limit: only fill N consecutive NaN
print(ts.assign(temperature_limited=ts['temperature'].ffill(limit=1)))
# Only fill 1 consecutive NaN, leave the rest
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
7. interpolate 插值
(1) 线性插值
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:interpolate 线性插值(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# Carol's heart rate data — linear interpolation is natural
hr = pd.DataFrame({
'time_min': list(range(10)),
'heart_rate': [72, 75, np.nan, 78, np.nan, 80, 82, np.nan, 85, 88]
})
# Linear interpolation (default)
hr['hr_linear'] = hr['heart_rate'].interpolate()
print(hr[['time_min', 'heart_rate', 'hr_linear']])
# time 2: (75+78)/2 = 76.5
# time 4: (78+80)/2 = 79.0
# time 7: (82+85)/2 = 83.5
# Index-based vs time-based interpolation
hr_ts = pd.DataFrame({
'heart_rate': [72, 75, np.nan, 78, np.nan, 80]
}, index=pd.to_timedelta([0, 5, 15, 20, 40, 45], unit='min'))
# Time-weighted interpolation
hr_ts['hr_time'] = hr_ts['heart_rate'].interpolate(method='time')
print(hr_ts)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 插值方法对比
| 方法 | method | 适用场景 |
|---|---|---|
| 线性 | 'linear' | 均匀采样,趋势平滑 |
| 时间加权 | 'time' | 非均匀时间间隔 |
| 索引加权 | 'index' | 非均匀索引 |
| 二次 | 'quadratic' | 曲线趋势 |
| 三次 | 'cubic' | 平滑曲线 |
| 最近邻 | 'nearest' | 离散/阶跃数据 |
| 样条 | 'spline' | 平滑插值(需 order 参数) |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:插值方法对比(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
s = pd.Series([0.0, np.nan, np.nan, np.nan, np.nan, 5.0, 10.0])
print("linear: ", s.interpolate(method='linear').tolist())
# [0.0, ~1.67, ~3.33, 5.0, ~6.67, 5.0, 10.0]
# 但这样包含 NaN 边界。
print("linear (trim):", s.dropna().interpolate(method='linear').tolist())
# 仅演示线性插值。
# quadratic 需要边界两侧各 1 个非 NaN,这里用 7 个数据
print("quadratic:", s.interpolate(method='quadratic').tolist())
# 拟合二次曲线,需至少 3 个非 NaN 点
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
8. Copy-on-Write 与缺失值操作
(1) CoW 下的安全写法
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:CoW 安全操作(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
pd.options.mode.copy_on_write = True # Pandas 3.x default
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'age': [28, np.nan, 25],
'salary': [75000, 92000, np.nan]
})
# ✅ Safe: assign result back
df['age'] = df['age'].fillna(df['age'].mean())
# ✅ Safe: use inplace (works with CoW)
df.fillna({'salary': 0}, inplace=True)
# ❌ Dangerous (pre-CoW): chained assignment
# subset = df[df['age'].notnull()]
# subset['salary'] = 100 # May not modify df!
# ✅ Safe with CoW: loc
df.loc[df['age'].notnull(), 'salary'] = df.loc[df['age'].notnull(), 'salary'] * 1.1
print(df)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
9. 完整示例:缺失值处理全流程
(5) ▶ 缺失值处理决策树
graph TB
A[检测缺失] --> B{缺失率?}
B -->|<5%| C[dropna 删除]
B -->|5-30%| D{数据类型?}
D -->|数值| E[fillna 均值/中位数]
D -->|类别| F[fillna 众数]
D -->|时序| G[interpolate 插值]
B -->|>50%| H[考虑删除该列]
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:缺失值清洗全流程(难度⭐⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# ============================================
# Comprehensive example: Full missing-value
# handling pipeline
# ============================================
# 1. Load data with missing values
np.random.seed(42)
df = pd.DataFrame({
'customer_id': range(1, 21),
'name': [f'Customer_{i}' for i in range(1, 21)],
'age': [25, np.nan, 35, 40, np.nan, 28, np.nan, 50, 33, np.nan,
29, np.nan, 45, 38, np.nan, 27, np.nan, 42, 31, np.nan],
'purchase_amount': [120, 85, np.nan, 200, 150, np.nan, 90, 310,
np.nan, 60, 175, np.nan, 250, 140, np.nan,
95, 180, np.nan, 110, 75],
'category': ['Electronics', np.nan, 'Home', 'Electronics', 'Clothing',
'Home', np.nan, 'Electronics', 'Clothing', np.nan',
'Home', 'Electronics', 'Clothing', np.nan, 'Home',
'Electronics', np.nan, 'Home', 'Clothing', 'Electronics']
})
# 2. Analyze missing pattern
print("=== Missing Analysis ===")
missing_report = pd.DataFrame({
'count': df.isnull().sum(),
'pct': (df.isnull().sum() / len(df) * 100).round(1)
})
print(missing_report[missing_report['count'] > 0])
# 3. Drop rows where critical fields are missing
df = df.dropna(subset=['customer_id', 'name'])
# 4. Fill numeric columns
df['age'] = df['age'].fillna(df['age'].median())
df['purchase_amount'] = df['purchase_amount'].interpolate(method='linear')
# 5. Fill categorical column
df['category'] = df['category'].fillna(df['category'].mode()[0])
# 6. Verify: no more missing
print(f"\n=== After Cleaning ===")
print(f"Remaining missing: {df.isnull().sum().sum()}")
print(df.head(10))
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
❓ 常见问题
Q NaN 和 None 有什么区别?
A NaN(np.nan)是 IEEE 754 浮点缺失值,NaN ≠ NaN。None 是 Python 对象缺失值,None == None。在 Pandas 中两者都会被 isnull() 检测到。整数列出现 NaN 时自动转 float64;用 nullable Int64(大写 I)可保留整数类型,缺失用 pd.NA 表示。
Q dropna 和 fillna 怎么选?
A 缺失 < 5% 且不重要→dropna 删除。缺失 5-30% 且有合理默认→fillna 填充(均值/中位数/众数)。时序数据→interpolate 插值。缺失 > 50%→考虑删除该列。关键原则:删除会丢信息,填充会引入偏差,插值需要假设趋势。
Q fillna(method='ffill') 为什么报错?
A Pandas 2.x 弃用了 fillna 的 method 参数。请改用 df.ffill()(前向填充)和 df.bfill()(后向填充)。ffill 适合时序数据(用上一个有效值),不适合随机顺序数据(可能传播错误值很远)。
Q interpolate 哪种方法好?
A 默认 linear 最通用。时间序列用 method='time'(按时间距离加权)。趋势明显用 quadratic/cubic。数据是阶跃变化用 nearest。spline 适合平滑但需调 order 参数。建议先用 linear,效果不够再换。
Q fillna(inplace=True) 还安全吗?
A Pandas 3.x 的 Copy-on-Write 模式下,inplace 仍然安全——它直接修改原 DataFrame。但更推荐的写法是
df = df.fillna(...) 赋值回变量,语义更清晰。CoW 保证两种写法结果一致。Q 如何检测某列是否含 NaN?
A 最快方式:
df['col'].isnull().any() 返回 True/False。计数:df['col'].isnull().sum() 返回缺失个数。百分比:df['col'].isnull().mean() * 100。全表检测:df.isnull().any().any() 判断 DataFrame 是否有任意 NaN。Q 为什么整数列出现 NaN 后变 float?
A NumPy 的 int64 无法表示 NaN(所有位都是有效数字)。Pandas 只好把整列转成 float64 来容纳 np.nan。解决:用 nullable 类型
df['col'].astype('Int64')(大写 I),它用 pd.NA 表示缺失,保留整数语义。📖 小节
- NaN 不等于自身(IEEE 754),必须用 isnull() 检测,不能用 ==
- isnull / notnull 检测缺失,.sum() 计数,.any() 快速判断
- dropna 删除缺失行/列,how='any'/'all',thresh 阈值,subset 按列检查
- fillna 填充:常量/均值/中位数/众数,Pandas 2.x 用 ffill()/bfill() 替代 method
- interpolate 插值:linear 默认,time 按时间加权,quadratic/cubic 曲线
- Copy-on-Write 下安全操作:赋值回变量或用 inplace
- 处理流程:检测分析→删除不可修复→填充/插值→验证零缺失
📝 作业
- 基础题(难度⭐):创建含 NaN 的 DataFrame,用 isnull().sum() 统计各列缺失数,用 dropna() 删除缺失行,对比行数变化。
- 进阶题(难度⭐⭐):创建时序 DataFrame(10 行含 3 个 NaN),分别用 ffill() / bfill() / interpolate(method='linear') 填充,对比三者的结果差异。
- 挑战题(难度⭐⭐⭐):模拟 50 行客户数据(age/salary/category 各 15% 缺失),完成完整清洗流程:缺失分析→ dropna(subset=[关键列]) → 均值填充 age → 插值填充 salary → 众数填充 category → 验证零缺失。