附录A封面

第 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 放在最前面的原因。