第 4 章的 NumPy 是「会用」,这个附录是「懂它」。内容偏底层,但每一节都对应真实的高性能场景——当你开始抱怨 pandas 慢的时候,这里的招式就是解药。
🏗️ dtype 层次与内存视图
NumPy 的 dtype 是一个家族树:浮点(float16/32/64)、整数(int8→int64)、布尔、字符串、object、还有特殊的 datetime64。而 astype 只是表面操作,底层事实是:每个 ndarray 都挂在一块连续内存上,view 和切片只是换了种方式「看」这块内存:
arr = np.arange(8)
floats = arr.astype(np.float64) # 真拷贝,新内存
view = arr[::2] # 视图,共享内存(步长变 2)
arr.view() 甚至能把同一块内存按不同 dtype 重新解读——危险但强大,理解它才算摸到了 NumPy 的地板。
📐 重塑与内存序:C 顺序 vs Fortran 顺序
reshape 不搬数据,只是改读内存的说明书。但按哪个方向读,分两大门派:
arr = np.arange(6).reshape(2, 3) # 默认 C 顺序:按行优先
arr.ravel(order="F") # Fortran 顺序:按列优先
arr.T.copy(order="C") # 转置后再按 C 序拷贝
C 顺序 = 行优先(一行读完再下一行),F 顺序 = 列优先。ravel() 不会真拷贝数据(只可能拷贝一份);操作非连续内存(比如转置后的数组)时,NumPy 可能被迫复制——这就是「为什么有时转置后变慢」的答案。
🧵 拼接、切分与重复
np.concatenate([a, b], axis=0) # 拼接
np.vstack / np.hstack # 竖拼 / 横拼
np.split(arr, [3, 5]) # 按位置切成三段
np.tile([0,1,2], 3) # 平铺:[0,1,2,0,1,2,0,1,2]
np.repeat([0,1,2], 3) # 重复:[0,0,0,1,1,1,2,2,2]
tile 是「整块复制」,repeat 是「逐元素重复」——groupby 展开数据、构造重复模式的常用件。
⚡ take/put:花式索引的极速版
第 4 章的花式索引 arr[[4,3,0]] 有个性能更猛的等价物:
idx = np.array([4, 3, 0])
arr.take(idx) # 与花式索引同结果,但更快
arr.put(idx, values) # 原地批量赋值
大规模逐行取数时(比如按索引对齐两张大表),take 比花式索引快一截。书里用它实现了「每行偏移不同的取数」——配合广播的花式索引堪称黑魔法:
arr[np.arange(2)[:, None], idx[None, :]] # 行偏移 + 列索引 组合取数
🎉 多维广播:把维度玩到极致
第 4 章讲了两数组相加的广播,这里升级到三维和「在任意轴上广播」:
arr3d = rng.standard_normal((2, 3, 4))
depth_means = arr3d.mean(axis=2) # (2,3)
demeaned = arr3d - depth_means[:, :, np.newaxis] # 手动补维度 → 广播成功
记一个万能的「广播专用函数」模板:
def demean_axis(arr, axis=0):
means = arr.mean(axis=axis)
indexer = [slice(None)] * arr.ndim # [slice(None),...] 长度=维数
indexer[axis] = np.newaxis
return arr - means[tuple(indexer)]
以及「用广播赋值」:arr[:] = arr.mean(axis=1)[:, np.newaxis] 把每行减去行均值,原数组就地更新。
🏷️ 结构化数组:一列多种类型
普通数组必须同类型,但 dtype 可以是复合结构——像 C 语言的 struct:
dtype = [("x", np.float64), ("y", np.int32), ("name", "U10")]
arr = np.zeros(3, dtype=dtype)
arr["x"] = [1.5, 2.5, 3.5] # 按字段名赋值
arr["name"] = ["a", "b", "c"]
适合「每条记录多个异构字段」的场景(传感器读数、日志行)。日常表格还是 pandas 舒服,但读懂别人代码里的结构化数组是必备技能。
🔀 高级排序:argsort、lexsort 与 searchsorted
arr = np.array([5, 0, 3, 3, 7, 9])
np.argsort(arr) # 返回「排序后的位置索引」[1,2,3,0,4,5]
np.sort(arr, kind="stable") # 稳定排序(mergesort 家族)
# 多列联合排序:先按姓、再按名(类似 SQL 的 ORDER BY a,b)
np.lexsort((first_names, last_names))
# 已排序数组上的二分查找:O(log n)
np.searchsorted(sorted_arr, 4) # 4 应该插到哪个位置才能保持有序
searchsorted 是「分桶归组」的经典底层:先 searchsorted 出箱号再分组,比逐个比较快得多。np.partition(arr, k) 则是 O(n) 的部分排序——只要前 k 小时的利器。
🚀 Numba 与内存映射:最后一公里
# Numba:JIT 编译,让纯 Python 循环跑出 C 的速度
import numba
@numba.jit(nopython=True)
def nb_mean_axis0(arr): # 手写按列均值
out = np.zeros(arr.shape[1])
for i in range(arr.shape[0]):
out += arr[i]
return out / arr.shape[0]
Numba 第一次调用时编译(有延迟),之后飞快——NumPy 没有现成 ufunc、又躲不开循环的场景专用。
超大数组用内存映射:np.memmap 把磁盘文件当成内存数组用,操作系统按需加载页,几 TB 数据也能切片处理,不需要全部读进 RAM。
📌 本章小结
| 需求 | 答案 |
|---|---|
| 重塑/展平 | reshape / ravel(注意 C/F 序) |
| 批量复制 | tile 整块、repeat 逐元素 |
| 高性能取数 | take / put |
| 任意轴广播 | means[..., np.newaxis] 补维模板 |
| 异构记录 | 结构化数组(复合 dtype) |
| 多键排序 | lexsort / argsort |
| 有序数组查找 | searchsorted(O(log n)) |
| 循环提不动 | Numba JIT / memmap |
深入 NumPy 的意义不在炫技,而在于:当你看懂
axis、视图、广播的底层规则,pandas 的一切行为突然都「有原因」了——这正是本书把 NumPy 放在最前面的原因。