Python数据清洗:系统识别所有形态的逻辑缺失值
1. 为什么“检查 NaN”这件事,远比你想象的更危险、更值得深挖
在真实的数据清洗现场,我见过太多人把 df.isna().sum() 一跑,看到几行 True 就心安理得地敲下 .dropna() —— 结果模型上线三天后业务方打电话来问:“为什么上个月的销售额预测全崩了?”查了一整天,最后发现是某张表里一个本该是整数的字段,混进了字符串 'N/A' ,而 isna() 对它完全免疫。它不是 NaN,它只是“看起来像缺失”,但 Python 根本不认识它。
这就是我要说的第一件事: NaN 不是缺失值的全部,它只是 IEEE 754 浮点标准下的一个特定幽灵 。它只存在于 float 类型的世界里,对 str 、 int 、 bool 、甚至 pd.Categorical 都是隐形的。而现实中的脏数据,90% 的“缺失”根本不是 NaN —— 它们是空字符串、是 'NULL' 、是 '-' 、是 'TBD' 、是 None (注意,是 Python 原生的 None ,不是 np.nan ),甚至是带空格的 ' ' 。如果你只盯着 np.isnan() 或 pd.isna() 看,等于在雾中开车,只开了前照灯,却关掉了后视镜和雷达。
所以这篇笔记不叫“4 种检查 NaN 的方法”,它的真实名字是: 《在 Python 数据流中,如何系统性地识别、归因、并最终驯服所有形态的‘逻辑缺失’》 。关键词里的 None 不是配角,它是主角之一 —— 因为 None 和 np.nan 在 Pandas 里被统一处理,但在底层行为、内存表现、序列化兼容性上,它们是两个物种。我用三年时间踩过所有坑才明白: isna() 是个好用的锤子,但你得先确认眼前的问题是不是钉子。
这篇文章适合三类人:刚学 Pandas 的新手(别再死记 isna 和 isnull 的区别了,它们本质一样);每天和 Excel、CSV、API 返回数据搏斗的业务分析师(你会看到为什么 read_csv(dtype=str) 能救你一命);还有正在写数据校验模块的工程师(最后一节的“混合缺失检测协议”直接能抄进你的 data_quality.py )。下面进入正题,我们不讲概念,只讲我在生产环境里反复验证过的实操逻辑。
2. 四种方法的本质拆解:不是“怎么用”,而是“为什么必须这样用”
2.1 np.isnan() :浮点世界的原生哨兵,但有致命盲区
np.isnan() 是 NumPy 底层 C 实现的函数,它直接调用 CPU 的 x87 指令集判断一个 float64 值是否符合 IEEE 754 的 NaN 编码规范。它的速度极快,单次判断耗时约 30 纳秒,比 Python 层任何判断都快一个数量级。但它的设计哲学非常纯粹: 只服务于数值计算,且只认 float 类型。
来看一组残酷的对比实验:
import numpy as np
# 这些都会返回 True —— 它们的二进制编码就是 NaN
print(np.isnan(np.nan)) # True
print(np.isnan(float('nan'))) # True
print(np.isnan(np.float64('nan')))# True
# 这些全会报错!因为类型不匹配
try:
print(np.isnan(None)) # TypeError: ufunc 'isnan' not supported for the input types
except Exception as e:
print(f"None -> {type(e).__name__}")
try:
print(np.isnan('')) # TypeError: ufunc 'isnan' not supported...
except Exception as e:
print(f"'' -> {type(e).__name__}")
try:
print(np.isnan(0)) # False(正确),但注意:0 是 int,会被隐式转为 float
print(np.isnan(np.int64(0))) # False(正确)
except Exception as e:
print(f"int -> {type(e).__name__}")
关键点来了: np.isnan() 对 int 、 str 、 None 、 list 等非浮点类型直接抛出 TypeError 。这意味着,如果你在一个混合类型的 Pandas Series 里强行用 np.isnan(series) ,结果不是 False ,而是整个程序崩溃。我曾经在某个 ETL 任务里,因为上游数据突然多了一列全是字符串的 ID 字段, np.isnan() 直接让调度器卡死,日志里只有一行 TypeError ,排查了六小时。
提示:
np.isnan()的唯一安全使用场景,是当你 100% 确认输入是np.ndarray且dtype为float32/64,或单个float值。对 Pandas 对象,请永远优先用pd.isna()。
2.2 pd.isna() :Pandas 的智能翻译官,统一抽象层的胜利
pd.isna() 是 Pandas 为解决 np.isnan() 的狭隘性而设计的“通用缺失探测器”。它的源码逻辑是:先尝试用 np.isnan() 判断,如果失败(比如遇到 None 或字符串),就退回到 Python 层的类型检查 —— 对 None 返回 True ,对字符串则检查是否为空或匹配常见缺失标识(如 'NULL' , 'N/A' 等,但默认不启用,需手动配置)。
我们用真实数据验证它的鲁棒性:
import pandas as pd
import numpy as np
# 构建一个典型的“脏数据”Series
dirty_series = pd.Series([
1.0, # float,正常
np.nan, # 标准 NaN
None, # Python None
'', # 空字符串
'N/A', # 文本型缺失
float('nan'), # 手动构造 NaN
0, # 整数 0(不是缺失!)
])
print("原始数据:", dirty_series.tolist())
print("pd.isna() 结果:", pd.isna(dirty_series).tolist())
# 输出: [False, True, True, False, False, True, False]
看到没? pd.isna() 正确识别了 np.nan 和 None ,但对 '' 和 'N/A' 返回 False —— 这不是 bug,是 design choice。Pandas 认为,空字符串和 'N/A' 是有效字符串值,除非你明确告诉它“这些也算缺失”。这个设计背后是数据语义的严谨性:一个电商订单的 shipping_address 字段如果是空字符串,可能表示“地址待填”,而 'N/A' 可能表示“无需配送”,它们的业务含义和 np.nan (数据未采集)完全不同。
注意:
pd.isna()和pd.isnull()是完全等价的,源码里isnull就是isna的别名。官方文档建议用isna(),因为语义更清晰(“is not available” vs “is null”)。
2.3 DataFrame 的 .isna() 方法:向量化操作的威力与陷阱
当 pd.isna() 作用于 DataFrame 时,它返回一个同形状的布尔 DataFrame,每个单元格是 True/False 。这是向量化操作的典范,但新手常犯两个致命错误:
错误一:用 df.isna().sum() 后直接 .dropna() ,忽略数据类型分布
import pandas as pd
import numpy as np
df = pd.DataFrame({
'sales': [100.0, 200.0, np.nan],
'region': ['North', 'South', None], # 注意:region 是 object 类型
'date': pd.to_datetime(['2023-01-01', '2023-01-02', None])
})
print("各列缺失统计:")
print(df.isna().sum())
# sales 1
# region 1
# date 1
# 看似三列各缺一个,但 dropna() 行为完全不同:
print("\n默认 dropna():")
print(df.dropna().shape) # (2, 3) —— 删除了第2行和第3行?不对,是删除了所有含缺失的行!
# 正确做法:按列策略处理
print("\n按列处理示例:")
df_clean = df.copy()
df_clean['sales'] = df_clean['sales'].fillna(0) # 数值列用均值/0填充
df_clean['region'] = df_clean['region'].fillna('Unknown') # 分类列用占位符
df_clean['date'] = df_clean['date'].fillna(pd.Timestamp('1970-01-01')) # 时间列特殊处理
错误二:对 object 类型列误用 np.isnan() 导致静默失败
# 危险操作!
# df['region'].apply(np.isnan) # 会报错,因为 np.isnan 无法处理字符串
# 正确操作:
df['region'].apply(lambda x: pd.isna(x)) # 安全,返回布尔 Series
# 或更高效:
pd.isna(df['region']) # 向量化,无需 apply
DataFrame 的 .isna() 方法真正的价值,在于它能和 any() / all() 配合做行/列级决策:
# 找出“整行都是缺失”的记录(极端脏数据)
full_missing_rows = df[df.isna().all(axis=1)]
print("全空行索引:", full_missing_rows.index.tolist())
# 找出“缺失率 > 50%”的列,考虑删除
missing_ratio = df.isna().mean()
high_missing_cols = missing_ratio[missing_ratio > 0.5].index.tolist()
print("高缺失率列:", high_missing_cols)
2.4 math.isnan() :纯 Python 的轻量级方案,但仅限单值
math.isnan() 是 Python 标准库函数,它和 np.isnan() 一样,只接受 float 类型参数。但它有一个关键优势: 不依赖 NumPy,启动快,内存占用极小 。在嵌入式环境、CLI 工具或需要最小依赖的脚本中,它是首选。
import math
# 安全检查:先确保是 float
def safe_math_isnan(x):
if isinstance(x, float):
return math.isnan(x)
elif isinstance(x, (int, str)):
try:
return math.isnan(float(x))
except (ValueError, TypeError):
return False
else:
return False
# 测试
print(safe_math_isnan(np.nan)) # True
print(safe_math_isnan('nan')) # True(因为 float('nan') 成功)
print(safe_math_isnan('hello')) # False(捕获 ValueError)
print(safe_math_isnan(None)) # False(isinstance 失败)
但请注意: math.isnan() 对 np.float64 会报错,因为它严格要求 float 类型(Python 内置类型),而 np.float64 是 NumPy 自定义类型:
x = np.float64('nan')
print(type(x)) # <class 'numpy.float64'>
print(math.isnan(x)) # TypeError: must be real number, not numpy.float64
print(np.isnan(x)) # True(正确)
所以 math.isnan() 的适用场景非常明确: 你在写一个不引入 NumPy 的纯 Python 脚本,且输入确定是 float 或可安全转为 float 的字符串。 其他情况,一律用 pd.isna() 。
3. 实操全流程:从读取数据到生成缺失报告,一套可复用的工业级方案
3.1 第一步:数据加载阶段的缺失预埋(防患于未然)
90% 的后续清洗痛苦,源于加载时的草率。 pandas.read_csv() 有四个关键参数,能让你在源头就控制缺失语义:
import pandas as pd
# 示例:一份真实的销售数据 CSV,包含多种缺失形态
# sales_data.csv 内容:
# product_id,sales,region,notes
# P001,100.0,North,"Good"
# P002,,South,"N/A"
# P003,200.0,,
# P004,,"",Out of stock
# ❌ 危险加载(默认行为)
df_bad = pd.read_csv('sales_data.csv')
print("默认加载 - region 列类型:", df_bad['region'].dtype) # object
print("默认加载 - sales 列缺失:", df_bad['sales'].isna().sum()) # 2(P002 和 P004 的空格被转为 NaN)
# ✅ 推荐加载(显式声明缺失标识)
df_good = pd.read_csv(
'sales_data.csv',
na_values=['', 'N/A', 'NULL', 'Out of stock'], # 显式声明哪些字符串算缺失
keep_default_na=True, # 保留默认的 ['', '#N/A', 'NULL'...],与自定义合并
dtype={'product_id': 'string'} # 强制 string 类型,避免数字 ID 被转为 float
)
print("\n优化加载 - region 列缺失:", df_good['region'].isna().sum()) # 1(P003 的空字符串被识别)
print("优化加载 - notes 列缺失:", df_good['notes'].isna().sum()) # 1('Out of stock' 被识别)
print("优化加载 - sales 列缺失:", df_good['sales'].isna().sum()) # 2(同上,但更可控)
关键参数解析:
na_values: 接收列表或字典({'column_name': ['val1', 'val2']}),定义业务语义上的缺失值。keep_default_na: 设为False可禁用所有默认缺失标识,完全由你控制。dtype: 对object列指定string类型(Pandas 1.3+),避免混合类型导致的object列无法向量化操作。converters: 对特定列用函数预处理,例如{'notes': lambda x: x.strip().upper() if isinstance(x, str) else x}。
实操心得:我维护的每个 ETL 项目,
read_csv()调用都封装在一个load_data()函数里,na_values参数从 YAML 配置文件读取,确保不同数据源的缺失规则可配置、可审计。
3.2 第二步:缺失探查与可视化(用代码代替肉眼)
光看 df.isna().sum() 是低效的。我们需要结构化报告:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
def generate_missing_report(df, title="Missing Data Report"):
"""生成完整的缺失数据探查报告"""
# 1. 基础统计
total_cells = np.product(df.shape)
total_missing = df.isna().sum().sum()
missing_percentage = (total_missing / total_cells) * 100
print(f"=== {title} ===")
print(f"数据形状: {df.shape}")
print(f"总单元格数: {total_cells}")
print(f"缺失单元格数: {total_missing} ({missing_percentage:.2f}%)")
# 2. 按列详细统计
missing_stats = pd.DataFrame({
'missing_count': df.isna().sum(),
'missing_pct': df.isna().mean() * 100,
'dtype': df.dtypes,
'unique_count': df.nunique(dropna=False), # 包含 NaN 的去重计数
'sample_values': df.apply(lambda x: x.dropna().head(3).tolist() if len(x.dropna()) > 0 else ['<all missing>'])
}).sort_values('missing_pct', ascending=False)
print("\n--- 按列缺失详情(降序)---")
print(missing_stats.round(2))
# 3. 可视化:缺失矩阵图(热力图)
plt.figure(figsize=(12, 6))
sns.heatmap(df.isna(), cbar=False, yticklabels=False, cmap='viridis_r')
plt.title(f'{title}: Missing Value Matrix')
plt.xlabel('Columns')
plt.ylabel('Rows')
plt.tight_layout()
plt.show()
# 4. 关联分析:缺失是否集中在某些行?
rows_with_missing = df.isna().any(axis=1).sum()
print(f"\n--- 行级分析 ---")
print(f"含缺失的行数: {rows_with_missing} ({rows_with_missing/len(df)*100:.2f}%)")
# 5. 组合缺失模式(找规律)
if len(df.columns) <= 10: # 列数太多时跳过,避免组合爆炸
from itertools import combinations
patterns = {}
for r in range(2, min(4, len(df.columns)+1)): # 检查 2-3 列组合
for combo in combinations(df.columns, r):
mask = df[list(combo)].isna().all(axis=1)
if mask.sum() > 0:
patterns[str(combo)] = mask.sum()
if patterns:
print("\n--- 常见缺失组合模式(出现频次 > 0)---")
for pattern, count in sorted(patterns.items(), key=lambda x: x[1], reverse=True)[:5]:
print(f"{pattern}: {count} 行")
# 使用示例
df_sample = pd.DataFrame({
'A': [1, 2, np.nan, 4],
'B': [np.nan, 2, np.nan, 4],
'C': [1, np.nan, 3, np.nan],
'D': ['x', 'y', 'z', 'w']
})
generate_missing_report(df_sample, "Sample Dataset")
这个报告函数输出五层信息:全局概览、列级明细、可视化热力图、行级覆盖、组合模式。其中“组合模式”能发现业务逻辑漏洞 —— 比如 customer_id 和 order_date 总是一起缺失,说明可能是测试数据或爬虫注入。
3.3 第三步:缺失归因与分类(决定如何处理)
不是所有缺失都该被填充或删除。我按业务影响将缺失分为四类:
| 类型 | 特征 | 处理建议 | 实操代码示例 |
|---|---|---|---|
| 随机缺失 (MCAR) | 缺失与任何变量无关,纯随机 | 可安全删除或均值填充 | df.dropna(thresh=len(df)*0.8) (保留至少80%非空的行) |
| 机制缺失 (MAR) | 缺失与观测到的其他变量相关(如:高收入用户更不愿填年龄) | 用回归/插补模型预测填充 | from sklearn.impute import KNNImputer; imputer = KNNImputer(n_neighbors=5) |
| 本质缺失 (MNAR) | 缺失与自身值相关(如:病情越重,越不愿填症状) | 必须建模为新特征(如: age_missing_flag = df['age'].isna().astype(int) ) |
df['sales_missing_flag'] = df['sales'].isna().astype(int) |
| 语义缺失 | 业务上明确的占位符(如: status='pending' 时 completion_date 必为空) |
用业务规则填充,而非统计方法 | df.loc[df['status']=='pending', 'completion_date'] = pd.NaT |
# 示例:自动归因缺失类型(简化版)
def diagnose_missing_type(df, target_col, observed_cols=None):
"""
简易缺失机制诊断
target_col: 待诊断的列名
observed_cols: 用于判断 MAR 的其他列(默认为除 target_col 外的所有列)
"""
if observed_cols is None:
observed_cols = [c for c in df.columns if c != target_col]
# 1. 检查是否 MCAR:缺失是否与任何观测列无关?
missing_mask = df[target_col].isna()
if len(observed_cols) == 0:
return "MCAR (no observed variables)"
# 计算每列与缺失掩码的相关性(分类列用卡方,数值列用 point-biserial)
correlations = {}
for col in observed_cols:
if pd.api.types.is_numeric_dtype(df[col]):
# 数值列:计算 point-biserial 相关系数
from scipy.stats import pointbiserialr
corr, _ = pointbiserialr(missing_mask.astype(int), df[col].dropna())
correlations[col] = abs(corr)
else:
# 分类列:计算卡方检验 p 值(越小越相关)
from scipy.stats import chi2_contingency
contingency = pd.crosstab(missing_mask, df[col])
_, p, _, _ = chi2_contingency(contingency)
correlations[col] = 1 - p # 转换为“相关强度”,便于排序
# 找出最强相关列
if correlations:
strongest_col = max(correlations, key=correlations.get)
if correlations[strongest_col] > 0.3:
return f"Likely MAR (driven by {strongest_col})"
return "MCAR or MNAR (requires domain knowledge)"
# 使用
print(diagnose_missing_type(df_sample, 'A'))
3.4 第四步:工业级缺失处理流水线(可部署代码)
以下是一个生产环境使用的缺失处理类,已通过 pytest 覆盖:
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Union, Callable
class MissingDataHandler:
"""工业级缺失数据处理器"""
def __init__(self, strategy_config: Dict[str, Dict]):
"""
初始化处理器
strategy_config 示例:
{
'sales': {'method': 'mean', 'groupby': ['region']},
'region': {'method': 'mode', 'fill_value': 'Unknown'},
'notes': {'method': 'constant', 'fill_value': 'No notes'}
}
"""
self.config = strategy_config
def fit(self, df: pd.DataFrame) -> 'MissingDataHandler':
"""拟合填充参数(如均值、众数)"""
self.stats_ = {}
for col, config in self.config.items():
if col not in df.columns:
continue
if config['method'] == 'mean':
if config.get('groupby'):
grouped = df.groupby(config['groupby'])[col]
self.stats_[col] = grouped.mean()
else:
self.stats_[col] = df[col].mean()
elif config['method'] == 'mode':
if config.get('groupby'):
grouped = df.groupby(config['groupby'])[col]
self.stats_[col] = grouped.apply(lambda x: x.mode().iloc[0] if not x.mode().empty else None)
else:
mode_val = df[col].mode()
self.stats_[col] = mode_val.iloc[0] if not mode_val.empty else None
return self
def transform(self, df: pd.DataFrame) -> pd.DataFrame:
"""应用填充"""
df_filled = df.copy()
for col, config in self.config.items():
if col not in df_filled.columns:
continue
mask = df_filled[col].isna()
if not mask.any():
continue
if config['method'] == 'mean':
if config.get('groupby'):
# 按组填充
for idx, row in df_filled[mask].iterrows():
group_key = tuple(row[config['groupby']])
fill_val = self.stats_.get(col, {}).get(group_key, np.nan)
df_filled.loc[idx, col] = fill_val if pd.notna(fill_val) else 0
else:
df_filled.loc[mask, col] = self.stats_.get(col, 0)
elif config['method'] == 'mode':
fill_val = self.stats_.get(col, 'Unknown')
df_filled.loc[mask, col] = fill_val
elif config['method'] == 'constant':
df_filled.loc[mask, col] = config.get('fill_value', 'MISSING')
return df_filled
def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:
return self.fit(df).transform(df)
# 使用示例
df_real = pd.DataFrame({
'sales': [100, 200, np.nan, 150, np.nan, 300],
'region': ['North', 'North', 'South', 'South', 'North', 'South'],
'category': ['A', 'B', np.nan, 'A', 'B', 'A']
})
handler = MissingDataHandler({
'sales': {'method': 'mean', 'groupby': ['region']},
'category': {'method': 'mode'}
})
df_clean = handler.fit_transform(df_real)
print(df_clean)
这个类的核心价值在于: 分离了“学习填充规则”和“应用规则”两个阶段 ,完美适配训练/预测分离的机器学习 pipeline。 fit() 在训练集上学习均值/众数, transform() 在测试集或新数据上应用,避免数据泄露。
4. 深度避坑指南:那些文档不会写的血泪教训
4.1 None vs np.nan :内存、序列化、比较的三重幻觉
这是最常被误解的概念。表面上 pd.isna(None) 和 pd.isna(np.nan) 都返回 True ,但它们在底层是截然不同的存在:
import pandas as pd
import numpy as np
import json
# 1. 内存表现
none_val = None
nan_val = np.nan
print("None 的内存地址:", id(none_val)) # 固定,所有 None 共享同一内存
print("np.nan 的内存地址:", id(nan_val)) # 每次创建新对象
# 2. JSON 序列化
try:
json.dumps({'val': none_val}) # ✅ 成功,转为 null
except Exception as e:
print("None JSON:", e)
try:
json.dumps({'val': nan_val}) # ❌ 失败!JSON 不支持 NaN
except Exception as e:
print("np.nan JSON:", type(e).__name__) # TypeError
# 3. 比较行为(最危险!)
print("None == None:", None == None) # True
print("np.nan == np.nan:", np.nan == np.nan) # False!IEEE 754 规定
print("pd.isna(np.nan):", pd.isna(np.nan)) # True(正确方式)
print("np.nan is np.nan:", np.nan is np.nan) # True(同一对象)
致命陷阱 :用 == 比较 NaN 会永远返回 False ,导致条件判断失效:
# ❌ 危险代码
if my_value == np.nan: # 永远不成立!
handle_missing()
# ✅ 正确代码
if pd.isna(my_value): # 安全,覆盖 None 和 np.nan
handle_missing()
4.2 pd.isna() 的隐藏参数: pd.options.mode.use_inf_as_na
Pandas 有一个全局开关,能改变 isna() 的行为:
import pandas as pd
import numpy as np
# 默认行为:inf 不被视为缺失
print(pd.isna(np.inf)) # False
# 开启后:inf 和 -inf 也被视为缺失
pd.set_option('mode.use_inf_as_na', True)
print(pd.isna(np.inf)) # True
# ⚠️ 警告:此选项会影响所有后续 isna() 调用,包括第三方库!
# 生产环境强烈建议显式控制,而非全局修改
pd.reset_option('mode.use_inf_as_na') # 恢复
这个选项在金融风控场景很有用(无穷大收益率明显异常),但会破坏与其他库的兼容性。我的建议是:在特定函数内临时设置:
def check_financial_anomaly(series):
original_setting = pd.get_option('mode.use_inf_as_na')
try:
pd.set_option('mode.use_inf_as_na', True)
return series.isna().sum() > 0
finally:
pd.set_option('mode.use_inf_as_na', original_setting)
4.3 混合缺失检测协议:应对真实世界的混沌
最后分享一个我压箱底的工具函数,它能一次性检测所有常见缺失形态:
import re
import pandas as pd
import numpy as np
def comprehensive_isna(x, custom_na_strings: List[str] = None) -> bool:
"""
综合缺失检测协议
检测:np.nan, None, '', ' ', 'N/A', 'NULL', 'NaN', 'nan', 'None', 'null'
"""
if custom_na_strings is None:
custom_na_strings = ['N/A', 'NULL', 'NaN', 'nan', 'None', 'null', 'nil', 'tbd', 'TBD']
# 1. 原生检测
if pd.isna(x):
return True
# 2. 字符串标准化检测
if isinstance(x, str):
stripped = x.strip()
if stripped == '':
return True
if stripped.lower() in [s.lower() for s in custom_na_strings]:
return True
# 3. 数值边界检测(可选:极小/极大值视为异常缺失)
if isinstance(x, (int, float, np.number)):
if np.isinf(x):
return True
if abs(x) < 1e-15 and x != 0: # 接近零的浮点数(有时是计算误差)
return True
return False
# 向量化版本(用于 Series)
def vectorized_comprehensive_isna(series: pd.Series, custom_na_strings=None) -> pd.Series:
return series.apply(lambda x: comprehensive_isna(x, custom_na_strings))
# 测试
test_values = [np.nan, None, '', ' ', 'N/A', '123', 0, 1e-20, np.inf]
for v in test_values:
print(f"{repr(v):>12} -> {comprehensive_isna(v)}")
这个函数是我处理政府开放数据、医疗问卷、电商评论时的标配。它把 pd.isna() 作为第一道防线,再用字符串规则兜底,最后加一层数值异常检测。你可以根据业务需求调整 custom_na_strings 列表。
5. 常见问题速查表与终极建议
| 问题现象 | 根本原因 | 解决方案 | 我的实操备注 |
|---|---|---|---|
df.isna().sum() 显示某列有缺失,但 df[col].unique() 里看不到 nan |
该列是 object 类型,缺失被存储为字符串 'nan' 或 'None' ,而非 np.nan |
用 df[col] = df[col].replace({'nan': np.nan, 'None': np.nan}) 清洗,或加载时用 na_values |
我在所有数据接入点加了 preprocess_object_columns() 钩子函数 |
pd.isna() 对某列返回全 False ,但直觉觉得有缺失 |
该列是 int 类型,Pandas 用 pd.NA (pandas 1.0+)或 None (旧版)表示缺失,但 int 列不能存 None ,所以被转为 float 并用 np.nan 存储 —— 但你的列可能被强制设为 Int64 (nullable integer) |
检查 df[col].dtype ,若为 Int64 ,用 df[col].isna() ;若为 int64 ,缺失值不可能存在(除非你用了 pd.NA ) |
Int64 是 Pandas 的杀手级特性,我所有新项目默认用 dtype={'id': 'Int64'} |
在 groupby().agg() 中,含缺失的组被自动丢弃 |
agg() 默认 skipna=True ,但某些函数(如 first() )对缺失行为不一致 |
显式指定 skipna 参数,或用 groupby(..., dropna=False) 保留缺失组 |
dropna=False 是救命参数,尤其在做分组统计时 |
用 json.dumps(df.to_dict()) 报错 Object of type float32 is not JSON serializable |
np.float32 不能被 JSON 序列化,而 df.isna() 返回的布尔数组是 np.bool_ ,某些旧版 Pandas 会触发此错误 |
在序列化前,用 df.replace({np.nan: None}).to_dict() ,或用 df.astype(object).where(pd.notna(df), None).to_dict() |
我的 api_response() 函数第一行永远是 df = df.replace({np.nan: None}) |
pd.isna() 在大型 DataFrame 上很慢 |
isna() 对 object 列需逐元素 Python 层判断,无法向量化 |
将 object 列转换为 string 类型(Pandas 1.3+),或对高频列预计算 isna_mask = df[col].isna() 缓存 |
df.astype({'col': 'string'}) 是性能优化核弹 |
最后分享一个小技巧: 永远在 Jupyter Notebook 的第一个 cell 里运行这段代码 :
# 全局缺失检测快捷键
def show_missing(df, top_n=10):
"""快速显示缺失最多的 top_n 列"""
missing = df.isna().sum().sort_values(ascending=False)
display(missing.head(top_n))
return missing
# 绑定到变量
_ = show_missing # 这样输入 `_` 就能快速调用
这个习惯让我在每次打开新数据集时,3 秒内就能掌握缺失全景。数据清洗不是苦力活,而是侦探工作 —— 你得先看清敌人长什么样,才能制定战术。而 isna() ,只是你手中的第一把匕首,不是万能钥匙。
我在实际使用中发现,真正决定数据质量的,从来不是用了多少高级插补算法,而是**在数据加载那一刻,你有没有勇气写下那行 na_values=['', 'N/A', 'NULL']
更多推荐


所有评论(0)