NumPy 的实战价值集中在四件事:批量计算(向量化)、条件筛选(布尔掩码)、广播(形状对齐)、结构变换(拼接/拆分/重塑)。下面 6 个案例分别对应一个真实场景,全部不含绘图。
案例 1:成绩表分析 ------ 吃透 axis
场景:30 名学生、5 门课的成绩矩阵,一次算出每人总分、各科均分、及格率、排名、等级。
python
import numpy as np
rng = np.random.default_rng(42)
成绩 = rng.integers(50, 101, size=(30, 5)) # 30行(学生) × 5列(课程)
总分 = 成绩.sum(axis=1) # 横向压缩 → 每个学生的总分(长度30)
各科均分 = 成绩.mean(axis=0) # 纵向压缩 → 每门课的均分(长度5)
及格率 = (成绩 >= 60).mean(axis=0) # 布尔数组求均值 = 及格比例
最高分 = 成绩.max(axis=0)
排名 = np.argsort(-总分)[:3] # 降序后取前3名,得到的是行号
等级 = np.select(
[总分 >= 450, 总分 >= 400, 总分 >= 350],
["A", "B", "C"],
default="D"
)
axis 的记忆法:axis 指"被消掉的那个维度"。
text
科目1 科目2 科目3 科目4 科目5
学生1 [ 90 85 77 92 88 ] ← axis=1 沿行压缩:这一行变成一个总分
学生2 [ 60 72 68 81 55 ]
↑
axis=0 沿列压缩:这一列变成一个均分
所以 axis=1 出"每人一行一个数",axis=0 出"每科一个数"。布尔数组 (成绩 >= 60) 的 .mean() 直接就是及格率,这个技巧在统计里高频使用。
案例 2:数据清洗 ------ 缺失值 + 异常值
场景:一份体检数据里混着 NaN 和明显离谱的数值。
python
原始 = np.array([88, np.nan, 92, 120, 75, np.nan, 300, 81, 95, 70], dtype=float)
有效 = ~np.isnan(原始) # 布尔掩码(不能写 原始 != np.nan)
中位数 = np.nanmedian(原始) # 90.0,跳过 NaN 计算
填补 = np.where(np.isnan(原始), 中位数, 原始) # NaN 换成中位数
Q1, Q3 = np.percentile(填补, [25, 75]) # 82.75 / 94.25
IQR = Q3 - Q1 # 11.5
下界, 上界 = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR # 65.5 / 111.5
清洗后 = np.clip(填补, 下界, 上界) # 300 和 120 被压到 111.5
三个要点:
np.nanmedian/np.nanmean/np.nansum会自动忽略 NaN,普通mean会得到 NaN;np.where(条件, 真值, 假值)是三目运算的向量化版本,一次替换整组;np.clip是"截断式"异常值处理,不删除样本,只把极端值压回边界。
案例 3:销售时间序列 ------ 移动平均、环比、累计
场景:14 天日销量,做趋势平滑与累计业绩统计。
python
日销量 = np.array([120, 135, 128, 160, 172, 155, 168,
180, 175, 190, 205, 198, 210, 225])
七日移动平均 = np.convolve(日销量, np.ones(7) / 7, mode="valid") # 长度 14-7+1 = 8
日环比 = np.diff(日销量) # 后一天 - 前一天,长度13
累计销量 = np.cumsum(日销量) # 逐日累加
周环比 = 日销量[7:] / 日销量[:7] - 1 # 第二周 vs 第一周(同位置对比)
np.convolve 做的是"滑动窗口加权求和",卷积核取 np.ones(7)/7 就是 7 日简单移动平均;mode="valid" 表示只保留窗口完全落在数据内的部分,因此结果比原数组短 6 个。
案例 4:距离矩阵 ------ 广播的经典用法
场景:3 个客户要各自找最近的线下门店,店有 4 家。
python
门店 = np.array([[0, 0], [10, 0], [0, 15], [12, 18]]) # (4, 2)
客户 = np.array([[3, 4], [9, 1], [5, 12]]) # (3, 2)
# 广播:客户插一个轴,门店插一个轴,自动扩展成 (3, 4, 2) 的差值张量
差 = 客户[:, None, :] - 门店[None, :, :]
距离 = np.sqrt((差 ** 2).sum(axis=-1)) # (3, 4)
最近门店 = 距离.argmin(axis=1) # [0 1 2]
最近距离 = 距离.min(axis=1) # [5. 1.41 5.83]
text
客户(3,2) → 客户[:, None, :] 变成 (3, 1, 2)
门店(4,2) → 门店[None, :, :] 变成 (1, 4, 2)
相减广播 → (3, 4, 2) 即 每个客户 × 每家店 × 二维坐标
广播规则一句话:两个形状从右往左逐位比较,相等或其中一个是 1 就能对齐,否则报错。这段代码如果写成双重循环,速度会差两个数量级。
案例 5:表格结构操作 ------ 拼接、拆分、重塑、排序
场景:两个季度的销售记录要合并、加编号列、按月份重组、按业绩排序。
python
一季度 = np.array([[101, 88], [102, 92], [103, 79]])
二季度 = np.array([[201, 95], [202, 85]])
全量 = np.vstack([一季度, 二季度]) # 纵向堆叠:追加记录(行)
编号列 = np.arange(len(全量)).reshape(-1, 1) # 变成 (5,1) 的列向量
带编号 = np.hstack([编号列, 全量]) # 横向拼接:追加字段(列)
前三条, 剩余 = np.split(全量, [3]) # 在索引3处切一刀,切成两段
月份表 = np.arange(1, 13).reshape(3, 4) # 3个季度 × 4个月
季度合计 = 月份表.sum(axis=1) # 每行求和 → 3个季度
月度合计 = 月份表.sum(axis=0) # 每列求和 → 4个月
按业绩排序 = 全量[np.argsort(-全量[:, 1])] # 取第2列作为排序键
对照:vstack 竖着拼(加行)、hstack 横着拼(加列);np.split(数组, [切点]) 按位置切;reshape(-1, n) 里的 -1 表示"这一维自动推算"。
案例 6:向量化实战 ------ 批量折扣 + 蒙特卡洛估算 π
6.1 会员折扣 (np.where 嵌套实现多级判断):
python
金额 = np.array([88, 268, 520, 999, 45, 180])
折扣 = np.where(金额 >= 500, 0.8,
np.where(金额 >= 200, 0.9, 1.0)) # [1. 0.9 0.8 0.8 1. 1. ]
应付 = np.round(金额 * 折扣, 2) # [ 88. 241.2 416. 799.2 45. 180. ]
6.2 蒙特卡洛估算 π(100 万次模拟,不用任何循环):
python
rng = np.random.default_rng(2024)
n = 1_000_000
x, y = rng.random(n), rng.random(n)
在圆内 = (x ** 2 + y ** 2) <= 1
pi 估计 = 4 * 在圆内.mean() # ≈ 3.14
6.3 向量化 vs 循环的真实差距:
python
# 慢:逐元素循环
结果 = [金额[i] * 折扣[i] for i in range(len(金额))]
# 快:整数组一次算完(推荐)
结果 = 金额 * 折扣
100 万元素规模下,向量化通常比 Python 循环快 50~200 倍,因为循环在 C 层完成,且省掉了每个元素的解释器开销。
六个高频陷阱
| 陷阱 | 错误写法 | 正确做法 |
|---|---|---|
| NaN 判断 | arr == np.nan 永远 False |
np.isnan(arr) |
| 切片是视图 | sub = arr[:3]; sub[0] = 0 会改原数组 |
需要独立副本时 .copy() |
| axis 方向 | 凭感觉猜 0/1 | 记住"axis 是会被消掉的那一维" |
| 整数溢出 | 大数组 .sum() 在 int32 上溢出 |
指定 dtype=np.int64 或先转 float |
| 随机种子 | np.random.seed(42) 旧接口 |
np.random.default_rng(42) 新接口 |
| reshape 失败 | 元素总数对不上 | 元素总数必须相等,-1 只能出现一次 |