Pandas: 缺失值处理

最后更新:2026-08-26

缺失值(NaN)是数据清洗的头号敌人——真实数据从来不会完美。传感器故障导致温度缺失,用户跳过选填项导致年龄缺失,系统合并导致 ID 缺失。Pandas 提供了完整的缺失值检测、删除、填充和插值工具链。本节带你逐一掌握,并理解 Copy-on-Write 下的安全操作。

⚠️ 注意: 以下代码需在本地 Python 环境中运行。

1. 你将学到


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(大写开头如 Int64booleanstring)用 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) ▶ 缺失值处理决策树

100%
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 表示缺失,保留整数语义。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建含 NaN 的 DataFrame,用 isnull().sum() 统计各列缺失数,用 dropna() 删除缺失行,对比行数变化。
  2. 进阶题(难度⭐⭐):创建时序 DataFrame(10 行含 3 个 NaN),分别用 ffill() / bfill() / interpolate(method='linear') 填充,对比三者的结果差异。
  3. 挑战题(难度⭐⭐⭐):模拟 50 行客户数据(age/salary/category 各 15% 缺失),完成完整清洗流程:缺失分析→ dropna(subset=[关键列]) → 均值填充 age → 插值填充 salary → 众数填充 category → 验证零缺失。

← 上一课:数据读写 · 下一课:重复数据处理 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏