资讯动态

NumPy NaN处理全覆盖:检测、填充与聚合统计实战指南

发布时间:2026/10/16 22:08:58 来源:尧图企业网站定制
前两天有个刚开始接触数据分析的朋友问我NumPy 里的 NaN 到底该怎么处理他写了一句arr[arr np.nan]想筛出缺失值结果筛出来是空数组后面再一算平均值整片结果全是nan整个人直接懵了。NaN 在 NumPy 里确实是个很特别的存在它是一个浮点标准里定义好的“非数字”既不能跟任何值相等还会把一路计算全都带偏。这篇干脆针对 NumPy 里的 nan 和日常工程里最高频的一批方法做一次完整梳理从识别、处理到聚合统计最后再附一个可以直接照抄的清洗流程保证你读完能直接在项目里用起来。1. 认识 NaN它是怎么来的又有哪些怪脾气1.1 什么叫“非数”从一次 0/0 说起NaN 的全称是 Not a Number它在 IEEE 754 浮点标准里是一个专门表示“未定义或不可表示数值结果”的特殊值。比如 0 除以 0、正无穷减正无穷这类数学上根本没有定义的运算落到计算机里就是 NaNimport numpy as np print(0.0 / 0.0) # nan print(np.inf - np.inf) # nan print(np.sqrt(-1)) # nan同时会弹出 RuntimeWarning print(np.log(-3)) # nan同样有 Warning第一次接触 NaN 的人最容易犯的错就是把“找不到东西”和“数值溢出”混在一起。实际上 NaN 是浮点数的一种合法状态它不是说“这个位置没数据”而是说“这个位置的数据在数学上无法给出有效值”。理解这一点后面处理缺失数据的思路会清晰很多。1.2 实际业务中 NaN 的三个典型来源除了数学运算工程里最常见的 NaN 来源是数据读取和转换。用np.loadtxt或csv模块读入外部文件时如果某个字段是空字符串转成浮点数组时解析失败就很容易被填成 NaN。还有一种场景是业务含义上的“无定义值”比如实时监测设备在某些时刻没有返回读数这部分本来就应该作为缺失值处理而不是用 0 去硬凑。从模型或计算链路里也会冒出 NaN比如矩阵运算过程中发生数值溢出、日志概率计算时出现-inf的指数结果这类 NaN 往往代表上游计算有 bug处理之前必须先查根因而不是简单填充了事。所以拿到一份含 NaN 的数据第一步应该是问它是数据采集时丢的还是计算逻辑搞出来的不同来源对应完全不同的处理方法。1.3 三个必须记住的特性不自等、会传染、强制转浮点NaN 最反直觉的三个特性可以说每一项都是新手掉坑的重灾区不自等np.nan np.nan的结果是False。在普通浮点世界里每个数都等于自己但 NaN 偏偏不遵守这条规则。强传染任何包含 NaN 的算术运算结果几乎都是 NaN。比如np.array([1.0, np.nan]).mean()直接返回nan因为它会把 NaN 一路带进最终结果。强制转浮点如果你写np.array([1, np.nan])整个数组会被自动转成float64因为整数类型里根本存不了 NaN。arr np.array([1, np.nan, 2]) print(arr.dtype) # float64 print(arr np.nan) # [False False False]这行代码是个经典坑 print(np.isnan(arr)) # [False True False]这才是正确姿势这三点看起来简单实际排查问题时90% 的诡异现象都源于“不自等”和“强传染”。每次有人跟我说“我用判断 NaN 怎么没反应”或者“算着算着结果全变了”我基本都能猜到是踩了这两条。2. 检测 NaN 的几种姿势别再写 arr np.nan 了2.1 单值检测np.isnan 才是正确入口判断一个标量是不是 NaN直接用np.isnan就行print(np.isnan(np.nan)) # True print(np.isnan(3.14)) # False print(np.isnan(None)) # TypeErrorNone 不是 float注意np.isnan(None)会直接报错因为 None 是 Python 对象不是浮点数。这点需要单独区分当你处理的是 Python 原生的 None而不是 NumPy 的 NaN 时判空逻辑完全是另一套。如果数组里既混着 None 又混着 NaN一般先统一成 NaN 再批量处理别混合着用容易把 dtype 搞成 object性能会掉一大截。2.2 批量统计np.isnan(arr).sum(axis…)检测单个值没难度真正的实用场景是批量统计整个数组里有多少缺失值。因为np.isnan返回的是同样形状的布尔数组所以直接对它求和就是缺失数量data np.array([[1.0, np.nan, 3.0], [4.0, 5.0, np.nan]]) print(np.isnan(data).sum()) # 2全场 NaN 总数 print(np.isnan(data).sum(axis0)) # [0 1 1]按列统计 print(np.isnan(data).sum(axis1)) # [1 1]按行统计这里涉及一个贯穿整个 NumPy 的核心概念axis。简单理解axis0是沿着第一维方向做聚合对应二维表格里的“列方向”axis1是沿着第二维方向对应“行方向”。统计缺失值时按列看能快速定位哪个字段缺得最厉害按行看能发现哪些样本已经残废到直接删除更划算。2.3 条件判断与布尔索引的配合检测完缺失值之后最常见的动作就是“把非缺失的行筛出来”或者“把缺失的位置找出来”。布尔索引在这时候特别好用# 筛掉含任意 NaN 的行 clean_rows data[~np.isnan(data).any(axis1)] # 筛掉全部是 NaN 的列 valid_cols data[:, ~np.isnan(data).all(axis0)] # 找出所有 NaN 的坐标 rows, cols np.where(np.isnan(data)) print(list(zip(rows, cols))) # [(0, 1), (1, 2)]np.where在这种场景下价值很大它既能返回条件为 True 的索引坐标也能在后面提到的填充场景里做条件选择。检测阶段学会这三招基本就能应对大多数带缺失值的数组了。2.4 一个容易忽略的检测场景全 NaN 数组如果一个数组整块都是 NaNnp.isnan(data).all()会返回 True。这在统计函数里是个隐患因为很多 NaN 安全函数比如 np.nanmean对全 NaN 输入会给出 RuntimeWarning并返回 NaN。所以写统计代码之前习惯性先打印一下缺失率能省掉大量定位问题的时间。3. 处理 NaN 的三种主流手段删、填、忽略3.1 删除布尔索引与 np.delete 的取舍数据量足够大、缺失比例又低的时候直接把含 NaN 的行或列删除是最省事儿的方案。一维数组可以直接用布尔索引二维数组通常按行删# 一维数组去掉所有 NaN arr np.array([1.0, np.nan, 3.0, np.nan, 5.0]) arr_clean arr[~np.isnan(arr)] print(arr_clean) # [1. 3. 5.] # 二维数组删除任意位置含 NaN 的行 data np.array([[1.0, 2.0], [np.nan, 4.0], [5.0, np.nan]]) mask ~np.isnan(data).any(axis1) print(data[mask]) # [[1. 2.]]也可以用np.delete按索引删但布尔索引更直观也更好读。需要注意的是删除法会压缩样本量如果本来数据就不多或者缺失不是完全随机产生的删完可能导致样本偏差。所以删除之前先算一下缺失比例超过一定阈值我个人习惯是超过 30%就尽量别走删除路线。提示即使决定删行也建议先确认这些行是不是存在其他有效字段的价值。比如某行只有一列缺失但其他列在后续分析里很重要直接整行删掉会浪费信息。3.2 填充np.where、np.nan_to_num、np.copyto 各显神通填充的思路是给缺失位置补一个值保留整条样本。最基本的写法是用np.where做条件选择arr np.array([1.0, np.nan, 3.0]) arr_filled np.where(np.isnan(arr), 0.0, arr) print(arr_filled) # [1. 0. 3.]np.nan_to_num更简单一行就能把 NaN 替换成 0同时还能处理正负无穷arr np.array([1.0, np.nan, np.inf, -np.inf]) fixed np.nan_to_num(arr, nan0.0, posinf1e308, neginf-1e308) print(fixed) # [1.e000 0.e000 1.e308 -1.e308]np.copyto则适合“用另一个数组填充缺失位置”的场景比如有两组数据一组主数据缺失时想用另一组同类数据补位dst np.array([1.0, np.nan, 3.0]) src np.array([10.0, 20.0, 30.0]) np.copyto(dst, src, wherenp.isnan(dst)) print(dst) # [ 1. 20. 3.]三者怎么选我的原则是只替换 NaN 用np.where最灵活处理同时含 inf 和 NaN 的脏数组用np.nan_to_num最省事用另一份数值补缺失用np.copyto最清晰。3.3 忽略统计nan 前缀函数是聚合场景的救命稻草这是最容易踩的坑直接对含 NaN 的数组做sum()或mean()结果一次串烧全变 NaN。NumPy 专门准备了一套 NaN 安全版本的聚合函数凡是带nan前缀的都会自动跳过 NaN普通函数NaN 安全版作用np.sumnp.nansum求和np.meannp.nanmean平均值np.stdnp.nanstd标准差np.varnp.nanvar方差np.minnp.nanmin最小值np.maxnp.nanmax最大值arr np.array([1.0, np.nan, 3.0, 4.0]) print(np.nanmean(arr)) # 2.6666666666666665 print(np.nanstd(arr)) # 1.247219128924647要注意np.nanpercentile是没有的中位数可以用np.nanmedian。还有一种情况如果整块区域全是 NaNnp.nanmean也会弹警告并返回 NaN所以使用前先判断一下缺失率比较稳妥。3.4 填充值怎么选均值、中位数还是 0这是业务决策问题不是纯技术问题。我的经验是先用业务逻辑定方向缺失值对业务无含义比如监控点位根本没有读数可视情况填 0 或删。缺失是随机的、且数据分布接近正态时均值填充比较常见。数据存在明显离群值或偏态分布时中位数比均值更稳不会把极端值的影响带进缺失位置。时间序列邻接数据优先考虑前向填充或插值这个用 pandas 更方便NumPy 里可以借助np.copyto手动实现简单版本。填充值一旦选定建议在文档里留一句记录否则后期很难解释特征分布为什么偏移。4. 除了 nanNumPy 里真正高频的常用方法盘点4.1 创建数组从 np.array 到 np.random.default_rng处理数据第一步是造数组NumPy 的创建函数要成组记忆别一个个死记# 直接创建 a np.array([1, 2, 3]) # 全零/全一/指定值/空数组 b np.zeros((2, 3)) c np.ones((2, 3)) d np.full((2, 3), 7.0) e np.empty((2, 3)) # 注意内容随机取决于内存残余值 # 序列类 f np.arange(0, 10, 2) # [0 2 4 6 8] g np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ] # 单位矩阵 h np.eye(3)创建随机数据现在推荐用新风格 APInp.random.default_rng()它比旧的np.random.rand更好控制随机种子生成的随机序列可复现rng np.random.default_rng(42) data rng.normal(loc5.0, scale2.0, size(100, 4))用同一个rng对象连续调用时每次都会推进随机状态写实验代码不会因为重复调用随机函数得到完全相同的序列这一点对后续测试非常关键。4.2 运算与广播一次说清 ufuncNumPy 的数组运算之所以快核心是向量化和广播机制。所谓的 ufunc通用函数就是能对数组逐元素计算的函数比如np.add、np.multiply、np.power、np.sqrt、np.exp、np.log等arr np.array([1.0, 4.0, 9.0]) print(np.sqrt(arr)) # [1. 2. 3.] print(arr 10) # [11. 14. 19.]标量广播 print(np.exp(np.array([0, 1, 2]))) # [1. 2.71828183 7.3890561 ]广播规则可以简化成一句话从右往左对齐两个数组的维度每个维度要么大小相等要么其中一个为 1。比如A np.ones((3, 1)) B np.ones((1, 4)) C A * B print(C.shape) # (3, 4)这个机制能让很多循环场景直接消失。一个经典例子给每一行数据减去该行的均值不需要写 for 循环用广播和聚合组合就能一次搞定。4.3 聚合统计轴 axis 是第一道坎聚合函数是数据处理里最常用的包括np.sum、np.mean、np.std、np.var、np.min、np.max、np.ptp峰峰值、np.median、np.percentile等。关键点是axis参数没搞懂统计结果基本就是错的data np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) print(data.mean(axis0)) # [2.5 3.5 4.5]按列求平均 print(data.mean(axis1)) # [2. 5.]按行求平均 print(data.mean(axis0, keepdimsTrue)) # [[2.5 3.5 4.5]]保留维度keepdimsTrue在广播场景里非常实用。比如你想要每个样本减去自己那行的均值直接data - data.mean(axis1)会报广播错误因为data是 (2,3)而data.mean(axis1)是 (2,)加keepdimsTrue变成 (2,1) 才能正确广播。4.4 索引筛选排序np.where、np.argsort 用熟能省一半时间数据处理免不了条件筛选和排序。除了前面提到的布尔索引np.where可以做条件替换arr np.array([-3, 1, -2, 5]) relu_like np.where(arr 0, arr, 0) print(relu_like) # [0 1 0 5]排序相关的函数容易搞混np.sort返回排序后的新数组ndarray.sort()原地排序而np.argsort返回的是排序后的索引scores np.array([8, 3, 10, 1]) print(np.argsort(scores)) # [3 1 0 2]从最小到最大的位置 print(scores[np.argsort(scores)]) # [ 1 3 8 10]np.argsort在需要保留原数组、同时按另一个字段的排序结果重排数据时特别好用。比如有一组特征和一组标签想按特征值排序后同时重排标签直接用idx np.argsort(feature)再feature[idx]和label[idx]同步重排就行。5. 实战复现一份带 30% 缺失数据的清洗全流程5.1 模拟数据在 numpy 里造一份传感器读数为了把前面这些方法串起来我模拟了一个某跨平台系统的传感器数据清洗场景。先用随机种子构造 200 行 6 列的正常读数再按 30% 比例随机注入 NaNrng np.random.default_rng(2024) data rng.normal(loc25.0, scale4.0, size(200, 6)) # 随机生成缺失掩码缺失比例约 30% mask rng.random(data.shape) 0.3 data[mask] np.nan print(data.shape) # (200, 6) print(np.isnan(data).sum()) # 353 左右 print(np.isnan(data).sum(axis0)) # 每列缺失数量这一步先用rng.random生成 0~1 的随机矩阵再与 0.3 比较得到布尔掩码是构造带缺失数据最灵活的方式。你可以任意调整缺失率和缺失模式方便测试后面的清洗逻辑。5.2 清洗流程识别、替换、验证三步走第一步看缺失分布。直接算每列缺失率决定删除还是填充missing_count np.isnan(data).sum(axis0) missing_rate missing_count / data.shape[0] print(missing_rate)第二步选策略。假设业务判断缺失是随机采集丢包且分布近似正态我选择按列用中位数填充因为中位数对异常值不敏感在部分极端读数存在时更稳col_median np.nanmedian(data, axis0) print(col_median) # 用 np.where 按列填充 for i in range(data.shape[1]): col data[:, i] data[:, i] np.where(np.isnan(col), col_median[i], col)第三步验证。填充后确认没有 NaN 残留同时对比填充前后的均值和标准差检查数据分布有没有被明显改变print(np.isnan(data).sum()) # 0 print(np.nanmean(data, axis0)) # 填充前每列均值 print(data.mean(axis0)) # 填充后每列均值如果填充前后的均值差异过大说明填充值选得有问题需要回头检查缺失是否为非随机模式。5.3 一个更高效的填充写法np.where 的向量化替代上面的 for 循环按列填虽然直观但 6 列还好几千列就会慢。更高效的做法是把中位数数组广播到每个位置一次性完成填充data rng.normal(loc25.0, scale4.0, size(200, 6)) data[mask] np.nan col_median np.nanmedian(data, axis0) # shape (6,) data_filled np.where(np.isnan(data), col_median, data)这里col_median会广播成 (200, 6)和原来的 data 逐位置比较后选择填充值。整个过程没有 Python 层循环数据量大时差距非常明显。5.4 关于性能的一点体会NumPy 的优势体现在向量化。同样是计算 100 万个数求和用原生 Pythonfor循环明显更慢而arr.sum()底层走 C 实现速度快一两个数量级。处理 NaN 相关的统计时也一样能用 nan 前缀函数就不要自己写循环跳 NaN。在我实际测试中对 100 万级别的一维数组做np.isnan加布尔索引筛选耗时基本在毫秒级同样逻辑的 Python 循环则要慢上很多。这里不贴具体耗时数字因为机器不同差异大建议你自己实测一次心里会有更清晰的概念。6. 常见问题排查速查表与我的避坑心得6.1 问题速查表症状可能原因解决方案arr[arr np.nan]筛不出任何值用判断 NaN改用np.isnan(arr)数组求和或均值返回 nan聚合函数遇到 NaN 被污染用np.nansum、np.nanmean等 nan 前缀函数np.isnan(None)报 TypeError输入不是浮点数先统一转成 NaN 或过滤 Nonenp.nanmean返回 nan 并有警告整块数据全是 NaN先用np.isnan(arr).all()检查填充后数据分布偏移明显填充策略与业务含义不匹配改用中位数或按业务规则填充数组被转成 object 类型混入 None、字符串等非数值清洗前统一 dtype 为 floatnp.nan_to_num填充了不该填的 infinf 有业务含义显式设置posinf、neginf6.2 三条写在最后的避坑心得第一任何统计结果出现大片 nan 时先不要怀疑算法多半是数据里混了 NaN。用np.isnan(data).sum()看一眼分布很多时候定位时间只需要十秒。第二在处理缺失值之前先搞清楚业务含义。如果缺失是采集器没开机导致的那填充 0 反而会把“无数据”伪装成“真实低值”后续建模要么学不到规律要么学到错误规律。记录下来“缺失率多少、怎么填的、为什么填”比填完之后的效果更重要。第三尽量不要混用 None 和 NaN。我在实际项目里见过同时存在 None、np.nan、pd.NA 三种“空值”的数据集清洗时稍不注意就会把 dtype 整成 object后面所有向量化运算全部失效。建议读入数据后第一时间统一成 np.nan后续只跟一种“空”打交道。最后再分享一个小技巧如果数组里除了 NaN 还有 inf别急着删先用np.isfinite(arr)看哪些位置同时不满足有限数条件。np.isfinite(arr)会把 inf 也筛出来配合np.isnan用两步就能把数值异常位置全部锁定。这个习惯我保持了很多年排查数据问题一直很省心。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑