NumPy实用函数实战:拆分、排序、统计与数据清洗技巧
如果你跟我一样是那种把 NumPy 当“数据仓库管理员”用的数据分析师通常最熟的就是 np.array、切片索引、reshape、广播机制、dot 矩阵乘法这些主线玩法。我带新人的时候发现一个普遍现象学完主干的教程之后教程停了但实际项目里的需求没停。比如按位置把数组拆成几份、去重后顺便统计每个元素出现几次、按条件快速找下标、对比两个用户 ID 集合之间的差异……这些零碎需求翻文档麻烦写循环又慢用错函数还会踩版本坑。这篇文章就是系列第 019 篇的完整实战记录。它的定位很直接把 NumPy 文档里那些“没人专门讲、但做 Python 数据分析项目时天天能用到”的“其他函数”集中过一遍包括数组拆分、排序搜索、集合运算、统计分位数、近似计算、随机种子、条件清洗等。适合三种人看一是学完 NumPy 基础但不知道怎么跟真实数据分析项目衔接的新手二是已经用 Pandas 比较多、想回去补 NumPy 底层函数的中级用户三是想系统排查“为什么我的代码报错”的老手——最后我整理了一份常见的报错和性能陷阱速查表都是实践里真实踩过的。1. 先搞清楚为什么还有“其他函数”值得单独写一章1.1 主线函数之外的“高频边角料”NumPy 文档大目录里占篇幅最大的一般是数组创建、索引、线性代数、随机抽样这些是教科书和网课最爱讲的部分。但真上了数据分析项目我自己的体感是每天最常敲的是另一批函数np.split、np.hstack、np.unique、np.argsort、np.where、np.percentile、np.clip、np.isnan、np.corrcoef。它们不算冷门但很少被当成一个“类别”系统讲所以很多人遇到需求时第一反应是写 Python 原生实现或者去 Pandas 里绕一圈绕完才发现 NumPy 一行就能解决。我单独开这一章就是想把它们从“其他分类”里捞出来按实际业务场景重新组织一遍。学习上的价值在于当你知道 NumPy 藏了哪些工具做数据处理时就会少写很多低效循环代码可读性也高一大截。1.2 这篇覆盖的函数清单和适用场景为了避免又是那种“按文档顺序罗列函数”的流水账我做了一张业务场景映射表你以后遇到类似需求直接对着找函数就行业务场景核心函数一句话记忆点把数据集切成训练集/验证集、按行拆批次np.split、np.array_splitsplit 必须整除array_split 自动找近似点把多个特征列合并、拼接数组np.concatenate、np.vstack、np.hstack先确认维度形状再选拼接方式求 TopN、找排序位置np.sort、np.argsort、np.partitionargsort 返回索引partition 不排序拿 TopN找最大最小值位置、按条件定位np.argmax、np.argmin、np.where配合 mask 做条件筛选最舒服去重、统计频次、集合对比np.unique、np.intersect1d、np.setdiff1d、np.in1d处理 ID、标签、分组问题必备均值、分位数、相关性np.nanmean、np.percentile、np.corrcoefnan 开头函数能“容忍”缺失值区别、梯度、近似积分np.diff、np.gradient、np.trapz处理时序和连续信号时常见采样复现、异常值截断np.random.seed、np.random.choice、np.clip随机实验要先固定种子再看结果这张表不是说要全背下来而是建议你把它当成索引。遇到卡壳的时候回来搜一下比翻几百页官方文档效率高得多。2. 数组拆分与合并预处理阶段最容易被忽略的硬功夫2.1 np.split 与 np.array_split怎么切、切完怎么用先说最常见的切分需求。我在真实项目里几乎每天都要做训练集和验证集的切分虽然 sklearn 里有 train_test_split但你做特征工程时经常需要按自定义比例、按某列分组、或者按时间切数据这时候 np.split 就是底层最顺手的工具。np.split 的用法非常固定把数组按某个轴切成几段传入一个整数表示“切几份”或者传入一个列表表示“在哪些位置切”。要注意的是如果传入整数个份数它要求数组在该轴上的长度能够被份数整除否则直接抛 ValueErrorimport numpy as np orders np.array([ [101, 25.8, 1], [102, 12.4, 2], [103, 33.1, 1], [104, 18.9, 3], [105, 27.5, 2], [106, 10.2, 3], [107, 42.8, 1], [108, 15.6, 2], ]) # 按行切成两份 a, b np.split(orders, 2, axis0) print(a.shape, b.shape) # (4, 3) (4, 3) # 按行切成 3 份就会报错8 不能被 3 整除 # a, b, c np.split(orders, 3, axis0)我更喜欢用的是传入节点列表的方式。比如你有 8 行数据想前 6 行做训练、后 2 行做临时验证集写法是 np.split(orders, [6], axis0)。它返回一个列表里面有两个数组训练集 6 行、验证集 2 行。这个方式比整数切分灵活得多因为你可以同时传多个位置比如 np.split(orders, [2, 5], axis0) 就能一次切成 [前2行、中间3行、最后3行] 三段。但如果你只是担心“数据量不是整数倍切不动怎么办”np.array_split 就是来救场的。它不要求整除会尽量按近似均分处理多余的放在靠前的片里。你不需要纠结它具体怎么分配只要知道“宁可让它自动分也不要自己写循环拆”效率差很远的。2.2 vstack、hstack、concatenate组合数据的正确姿势拆分之外就是合并。最常见的需求是把多个特征数组拼成一个特征矩阵或者把特征列和标签列拼到一起。这里有三兄弟np.vstack 垂直拼接、np.hstack 水平拼接、np.concatenate 通用拼接。它们的区别说穿了就一句话vstack 是把多个数组“上下叠”要求列数一致hstack 是“左右拼”要求行数一致concatenate 通过 axis 参数指定沿哪个轴拼本质上能覆盖前两者的功能。features np.array([[25.8, 11.0], [12.4, 5.2], [33.1, 18.3]]) labels np.array([[1], [2], [3]]) stacked np.hstack([features, labels]) print(stacked) # [[25.8 11. 1. ] # [12.4 5.2 2. ] # [33.1 18.3 3. ]]项目实战里最容易翻车的不是函数选错而是维度对不上。比如你要把两个 (1000,) 的一维数组水平拼成 (1000, 2)直接用 np.hstack([a, b]) 只会得到 (2000,) 的一维数组这是很多人踩过的暗坑。正确的做法是先 reshape或者用 np.column_stack([a, b])a np.array([1, 2, 3]) b np.array([4, 5, 6]) print(np.hstack([a, b])) # [1 2 3 4 5 6] print(np.column_stack([a, b])) # [[1 4] # [2 5] # [3 6]]我自己习惯的规则是一维数组想当“列”来拼用 np.column_stack二维矩阵想拼优先用 np.concatenate(axis...)不确定维度时先打印 .shape再决定函数。数据分析项目里维度错误占预处理报错的一半以上养成打印形状的习惯能省大量排查时间。2.3 一个订单数据切分的完整小案例把上面两个技能串起来就是一个很典型的预处理流程先把 8 条订单记录按 6:2 切成训练和临时验证集再把特征和城市编号合并成一个完整矩阵最后打印各块形状确认无误。orders np.array([ [101, 25.8, 1], [102, 12.4, 2], [103, 33.1, 1], [104, 18.9, 3], [105, 27.5, 2], [106, 10.2, 3], [107, 42.8, 1], [108, 15.6, 2], ]) train_set, valid_set np.split(orders, [6], axis0) X_train train_set[:, 1:3] # 金额和城市编号作为特征 y_train train_set[:, 0] # 订单 ID 当标签示例 # 把两个特征列和标签列拼成一个宽表 train_wide np.column_stack([train_set[:, 1:3], train_set[:, 0:1]]) print(train_wide.shape) # 验证集 valid_wide np.column_stack([valid_set[:, 1:3], valid_set[:, 0:1]]) print(valid_wide.shape)这段代码没有任何高深内容但它是很多项目流水线的雏形。数据从原始日志变成模型能吃的宽表中间经历的就是这种拆分合并操作。与其去背 API不如亲手跑一遍跑通了后面会顺很多。3. 排序与搜索让杂乱的数据快速变得有秩序3.1 sort vs argsort结果与路径同样重要排序是分析里最频繁的动作之一。np.sort 返回的是排序后的新数组不会修改原数组数组自带的 arr.sort() 则是原地排序直接改原数据。这个区别说出来都很简单但我见过太多人在项目里因为“排序后原顺序找不回来”而踩坑。更多时候数据分析要的并不是“排序后的值”而是“原数组中的位置”。比如你想知道订单金额最高的前几条记录对应哪些订单 ID直接 sort 金额会被丢弃位置信息这时候 np.argsort 就派上用场了它返回的是“如果排序原始下标按什么顺序排列”。amounts np.array([25.8, 12.4, 33.1, 18.9, 27.5, 10.2, 42.8, 15.6]) idx_asc np.argsort(amounts) print(idx_asc) # [5 1 7 3 0 4 2 6] top3_idx np.argsort(amounts)[-3:][::-1] print(top3_idx) # [6 2 4] print(amounts[top3_idx]) # [42.8 33.1 27.5]注意我用了 [::-1] 把降序位置反转因为 argsort 默认升序取最后三个是最大的三个但这三个内部的顺序是升序要得到“从大到小”再翻转一下。这个细节在输出 Top3 名单时很重要否则你拿到的城市排名顺序反了汇报时要闹笑话。3.2 partition不排完序也能拿 Top10有时候你只想拿 TopN不关心剩下几十万条具体怎么排。用 sort 或 argsort 会做完整排序数据量一大性能就吃亏。np.partition 可以在不全部排序的情况下把数组分成“左边小、右边大”两部分。它的经典用法是 np.partition(arr, kth-3)表示把第 3 大的元素放到倒数第 3 个位置它左边全是比它小的右边全是比它大的但左右两边内部不保证有序。np.random.seed(42) arr np.random.rand(1_000_000) top3_part np.partition(arr, -3)[-3:] top3_sort np.sort(arr)[-3:] # 两边的最大值一定相等但 partition 内部无序 print(np.sort(top3_part) np.sort(top3_sort)) # [ True True True]我的建议是当数组规模超过几十万、而且只取 TopN 时用 np.partition 能明显省时间。如果数组很小或者后面还要用完整排序结果那就别折腾sort 更省心。3.3 argmax、argmin 与 where定位比排序更常见排序解决“排名”但分析中更多时候是“找位置”。np.argmax 和 np.argmin 直接返回最大/最小元素的下标比你自己用 np.where(arr arr.max()) 快得多。配合业务场景最常见的例子是找到骑行时长最长的订单再看它发生在哪个城市。durations np.array([25.8, 12.4, 33.1, 18.9, 27.5, 10.2, 42.8, 15.6]) max_idx np.argmax(durations) min_idx np.argmin(durations) print(max_idx, durations[max_idx]) # 6 42.8 print(min_idx, durations[min_idx]) # 5 10.2np.where 则是“条件定位利器”它有两种典型用法。第一种只传一个条件它返回满足条件的位置第二种传三个参数 np.where(condition, x, y)相当于向量化的 if-else满足条件取 x否则取 y。第二种在特征工程里特别常用比如把“超过 30 元的订单标记为高价值”amounts np.array([25.8, 12.4, 33.1, 18.9, 27.5, 10.2, 42.8, 15.6]) high_value np.where(amounts 30, 1, 0) print(high_value) # [0 0 1 0 0 0 1 0]把 np.where 和 np.argmax 结合起来的场景也很多比如“找到第一个满足条件的位置”就先用 where 返回索引数组再取 [0]。这样不用写循环代码干净运行速度还快。4. 集合运算与去重处理ID、标签和分组问题4.1 unique一次搞定去重、排序和出现次数数据分析中数据集里的 ID、城市编号、商品编号经常有重复。np.unique 不只是去重它还默认对结果排序并且可以通过 return_countsTrue 拿到每个唯一值的出现次数。这两个功能合在一起做频次统计就特别舒服。city_ids np.array([1, 2, 1, 3, 2, 3, 1, 2]) uniq_cities, counts np.unique(city_ids, return_countsTrue) print(uniq_cities) # [1 2 3] print(counts) # [3 3 2]这个输出可以直接配合 np.column_stack 变成一张“城市-订单数”表再交给后面的排序或者可视化。我经常用它替代 Pandas 的 value_counts因为当数据只是 NumPy 数组时没必要为了一个频次统计专门引入 Pandas 重量级依赖。4.2 集合运算四件套交集、并集、差集、成员判断处理用户 ID、标签集合时最常见的就是两拨数据之间做对比这波用户和上一波有多少重合这波新增了谁流失了谁NumPy 的集合函数全部沿用数学集合语义名称也非常直白函数含义典型业务场景np.intersect1d(a, b)交集两波订单中都出现的用户np.union1d(a, b)并集两波用户去重后的总人数np.setdiff1d(a, b)在 a 但不在 b 的差集本波新增用户、流失用户np.in1d(a, b)或 np.isina 中的元素是否在 b 中圈定某批 ID 后做布尔过滤np.isin 和 np.in1d 功能基本一致后者在旧版本里更常见新版本推荐 np.isin语义更好记。返回的是布尔数组可以直接当作 mask 去筛原数组。4.3 实操案例用集合运算对比两批次用户我拿两个批次的订单用户 ID 来做一次完整的对比batch1 np.array([101, 102, 103, 104, 105]) batch2 np.array([103, 104, 105, 106, 107]) # 老用户两批都有 retained np.intersect1d(batch1, batch2) # 新增用户只在 batch2 里出现 new_users np.setdiff1d(batch2, batch1) # 潜在流失只在 batch1 里出现 churned np.setdiff1d(batch1, batch2) all_users np.union1d(batch1, batch2) print(重合用户:, retained) print(新增用户:, new_users) print(潜在流失:, churned) print(总用户数:, len(all_users))这段代码跑出来的结果可以直接写进日报里。更重要的是这几个函数在处理百万级 ID 时速度极快因为它们底层是排序后做有序比较复杂度远低于 Python 原生列表的嵌套循环。5. 统计、分位数与近似计算数据分析的底层计算力5.1 nan安全的统计函数别让一个缺失值毁掉整个均值真实数据里几乎一定有缺失值。普通 np.mean、np.std 碰到 np.nan结果直接变 nan导致整个统计流程崩掉。NumPy 提供了 nan 开头的一整套安全版本np.nanmean、np.nanstd、np.nanmin、np.nanmax、np.nansum 等它们会忽略缺失值再计算。data np.array([25.8, np.nan, 33.1, 18.9, np.nan, 10.2, 42.8, 15.6]) print(np.mean(data)) # nan print(np.nanmean(data)) # 24.4 print(np.nanstd(data)) # 11.01用 nanmean 并不意味着你可以不处理缺失值但至少能快速估算整体水平而不至于被一个 nan 卡住。我自己的习惯是先用 np.isnan(data).sum() 统计缺失量再决定是填充还是删除而不是一上来就填 0那样会把均值拉偏。5.2 percentile与相关系数从均值到分布的进阶均值只能描述中心数据分析还需要知道分布。np.percentile 可以一次性算多个分位数比如常见的四分位数data np.array([25.8, 12.4, 33.1, 18.9, 27.5, 10.2, 42.8, 15.6]) q25, q50, q75 np.percentile(data, [25, 50, 75]) print(q25, q50, q75) # 14.625 22.35 29.1分位数在画箱线图、做异常值检测时是最底层的依据。比如把低于 Q1 - 1.5 * IQR 或高于 Q3 1.5 * IQR 的点定义为疑似异常。再看相关性。np.corrcoef 返回相关系数矩阵通常在两个变量之间做相关性分析时用到x np.array([1, 2, 3, 4, 5]) y np.array([2, 4, 6, 8, 10]) r np.corrcoef(x, y)[0, 1] print(r) # 1.0完全正相关注意一个细节np.corrcoef([x, y]) 的结果是 2x2 的矩阵对角线上是变量自身的相关系数恒为 1非对角线才是变量之间的相关系数。所以取 [0, 1] 或 [1, 0] 才是你真正想要的值。我第一次用的时候直接打印整个矩阵困惑了半天才反应过来。5.3 diff、gradient、trapz做数值近似计算的常用武器这组函数在处理时序数据、连续信号时特别有存在感。np.diff 计算相邻元素的差分等价于一阶离散导数np.gradient 计算梯度可以用于近似导数对边界处理也更聪明np.trapz 用梯形法则做近似积分在计算曲线下面积时非常有用。t np.linspace(0, 2 * np.pi, 100) v np.sin(t) velocity_approx np.diff(v) / np.diff(t) # 一阶导近似 acceleration_approx np.gradient(v, t) # 梯度方式更平滑 area_approx np.trapz(v, t) # 对 sin 在 0~2pi 积分约等于 0 print(area_approx)关于 np.trapz 要特别提醒一句在较新版本的 NumPy 里trapz 已经不在主命名空间直接调用可能报 module numpy has no attribute trapz。这个问题在后面的排查部分还会细说。如果你用的是新版本建议优先从 scipy.integrate 导入 trapezoid或者安装旧版兼容。数值计算方向的函数很多已经迁移到 SciPy这是生态发展的正常趋势不用慌。6. 随机数、条件逻辑与其他零碎函数6.1 random.seed、choice、permutation让采样可复现数据分析项目里做随机采样、划分数据集时如果不固定随机种子每次跑结果都不一样排查问题会非常痛苦。np.random.seed(42) 之后所有随机操作都变成可复现的这一点在写实验报告或者给同事复现 bug 时极其有用。np.random.seed(42) sample_idx np.random.choice(10, size3, replaceFalse) print(sample_idx) # 固定种子时每次运行结果完全相同np.random.choice 可以从数组中按概率抽取replaceFalse 表示不重复抽样。np.random.permutation 则是打乱顺序后返回新数组常用于打乱数据集索引然后再配合切片切出训练集和验证集。6.2 clip、isnan、isfinite、any/all数据清洗的零碎利器np.clip 可以把所有值限制在上下限之间超过上限的直接变成上限低于下限的变成下限。这个在异常值截断时特别常用。比如订单金额明显异常大的值直接 clip 到 99 分位数比直接删掉更保守适合需要保留样本量的场景。amounts np.array([25.8, 12.4, 300.0, 18.9, 27.5, 10.2, 42.8, 15.6]) clipped np.clip(amounts, 0, 60) print(clipped) # [25.8 12.4 60. 18.9 27.5 10.2 42.8 15.6]np.isnan 用于定位缺失值np.isfinite 则一次性排除 nan 和 inf更全。加上 np.any、np.all 这两个逻辑函数可以快速判断“是否存在缺失值”、“是否全部满足某个条件”这在数据质量检查里是最高频的操作之一。6.3 零散函数组合拳清理异常值的完整流程把上面几个函数串起来就是一个很完整的数据清洗流水线raw np.array([25.8, np.nan, 33.1, 300.0, 27.5, 10.2, np.inf, 15.6]) # 1. 看看有多少非法值 print(np.isnan(raw).sum()) # 1 print(np.isfinite(raw).sum()) # 5只有 5 个可用值 # 2. 先把非有限值替换成 nan统一处理 clean np.where(np.isfinite(raw), raw, np.nan) # 3. 用 nanmean 之类的函数继续统计或者填充 filled np.nan_to_num(clean, nannp.nanmean(clean)) print(filled)这里的关键思想是先把所有非法值统一成 np.nan再做集中处理而不是写一堆 if 分支去判断。数据清洗最忌讳的就是每种异常各写一套逻辑最后代码成一团乱麻。7. 实战复盘常见报错与性能陷阱附排查速查表7.1 版本差异NumPy 2.x 中“老函数”消失怎么办这是最近很多同学问得最多的问题。如果你代码里写了 np.trapz而且 NumPy 版本比较新会直接遇到“AttributeError: module numpy has no attribute trapz”。原因很简单NumPy 2.x 里一部分和 SciPy 重叠的数值分析函数被彻底移除了官方建议使用 scipy.integrate 里的 trapezoid。解决方法也简单# 方案一如果用 scipy直接换 from scipy.integrate import trapezoid # 方案二先查版本 import numpy as np print(np.__version__) x np.linspace(0, np.pi, 100) y np.sin(x) area trapezoid(y, x) print(area)我建议所有依赖数值积分、信号处理的代码尽早把相关函数切换到 SciPy而不是继续依赖 NumPy 旧版。否则以后升级环境或者队友的新环境跑同一套代码就会因为版本不一致而崩溃。7.2 四个高频踩坑点与排查思路把实践过程中遇到的典型问题整理成一张速查表排查时直接对号入座典型报错出现原因解决方案ValueError: array split does not result in an equal divisionnp.split 整数份数无法整除改用 np.array_split或传入位置列表拼接后形状和预期不一致一维数组直接 hstack 导致变长用 np.column_stack 或 reshape 后再拼排序后拿不到原始下标使用了 np.sort 而不是 argsort需要索引时用 np.argsort统计结果全是 nan数据里有缺失值用了普通 mean/std先检查 np.isnan再用 nanmean/nanstd记忆这些坑记住一句话先看形状再谈操作先查缺失再谈统计先定版本再找函数。7.3 性能对比为什么原生Python循环跑不过向量化最后聊一个老生常谈但一定要自己跑一遍的问题。NumPy 的底层是 C 实现的向量化操作能规避 Python 解释器的逐元素开销。同样是一百万个随机数求总和原生 Python 循环慢了不是一点半点import time import numpy as np n 1_000_000 arr np.random.rand(n) start time.time() total 0 for v in arr: total v loop_time time.time() - start start time.time() total2 arr.sum() numpy_time time.time() - start print(fPython loop: {loop_time:.4f}s) print(fNumPy sum: {numpy_time:.4f}s)我自己实测下来循环版本和向量化版本经常差几十倍。所以当你想写 for 循环去处理一个数组时先停下来想想这个操作能不能转成 NumPy 的向量化写法大部分情况下能而且代码更短、更不易出错。8. 这章之后我常用的几个小习惯个人体会收尾写到这里正好把个人经验里最想分享的几个习惯整理出来算是给这篇“其他函数”做个实战落地的收尾。第一个习惯处理数组之前一定先花十秒钟打印 .shape 和 dtype。大多数新手出问题不是函数不会用而是数据形状和自己想的不一样后面每一步都被带偏。这个习惯看着简单真正坚持下来的都是老手。第二个习惯随机实验开跑前先固定 np.random.seed。不管是做抽样、切分还是随机初始化不固定种子等于允许每次实验结果不可复现。数据分析面试和实际交付中“你的结果能不能复现”是非常重要的专业素养。第三个习惯遇到“要不要为了一个功能引入 Pandas”这种问题时先翻一翻 NumPy 已有函数。像 np.unique、np.where、np.clip、np.percentile 这组函数很多时候你只需要一个轻量级的解决方案引入 Pandas 反而增加了依赖和代码复杂度。先用 NumPy 解决问题不行再升级工具是我一直推荐的决策顺序。第四个习惯勤查版本变更记录。像 trapz 被移除这种问题靠死记硬背函数名是躲不掉的环境升级后代码随时可能出问题。我每次升级 NumPy、Pandas 这类核心库都会花五分钟把项目里的核心脚本跑一遍回归测试比出问题了再排查省事太多。NumPy 的“其他函数”章节本质上就是一份“工具库的边角料”但它们在实际数据分析项目里的出现频率一点都不比主线函数低。希望这篇第 019 期的实战笔记能让你在下次遇到这些零碎需求时想到的不是写循环而是“NumPy 里是不是有内置方案”。这也是我这个系列想一直传递的价值用最轻量、最 Pythonic 的方式解决真实数据问题。