如果只允许你记住本书一个词,那就是「向量化」(vectorization)。第 4 章教你把「对每个元素做循环」的思维,替换成「对整块数据发指令」。这不只是优雅,是 10~100 倍的速度差。
⚡ 先看差距:为什么不用循环
import numpy as np
my_arr = np.arange(1_000_000)
my_list = list(range(1_000_000))
%timeit my_arr * 2 # 715 μs
%timeit [x * 2 for x in my_list] # 48.8 ms —— 慢约 68 倍!
差距的来源:NumPy 数组是同类型的连续内存块,运算直接由底层 C 代码批量执行;而 Python 列表是「指针数组」,每次操作都要解释一个完整对象。NumPy 的算法通常快 10~100 倍、内存更省。
🏗️ ndarray:形状与类型
data = np.array([[1.5, -0.1, 3.0], [0.0, -3.0, 6.5]])
data.shape # (2, 3) 两行三列
data.dtype # dtype('float64') 全体元素同类型
data.ndim # 2
# 常用创建函数
np.zeros(10); np.ones((3, 6)); np.empty((2, 2)) # empty 只分配不清零
np.arange(15).reshape(3, 5) # 0..14 排成 3×5
np.full((2,2), 7); np.eye(3) # 全 7 矩阵 / 单位阵
dtype 是门派规矩:一个数组只能有一种类型。int64、float64、bool、字符串等,arr.astype(np.float64) 显式转型(总是返回新数组)。
🚨 最重要的坑:切片是视图
arr = np.arange(10)
s = arr[5:8]
s[:] = 99 # 改切片……
arr # array([0,1,2,3,4,99,99,99,99,9]) 原数组也变了!
NumPy 刻意让切片返回视图(同一块内存的窗口)——大数据时省内存、速度快。想要独立副本,必须 arr[5:8].copy()。这个特性和 Python 列表(切片即拷贝)相反,是无数 bug 的根源。
🎯 三种高级索引
布尔索引——按条件筛,数据分析使用率最高:
names = np.array(["Bob", "Joe", "Will", "Bob"])
arr = np.arange(8).reshape(4, 2)
arr[names == "Bob"] # 选出 Bob 的行
arr[(names == "Bob") | (names == "Will")] # | 或 &(不能用 and/or!)
arr[arr[:, 1] < 4] = 0 # 条件赋值
花式索引——用整数列表按任意顺序取行:
arr[[3, 0, 1]] # 第3、0、1行,按这个顺序
arr[[-1, -3]] # 负数从尾倒数
转置与轴交换:arr.T、arr.swapaxes(0,1),线性代数的入场券:np.dot(arr.T, arr)。
🎉 广播(Broadcasting):不同形状也能算
arr = np.arange(4).reshape(2, 2) # [[0,1],[2,3]]
arr + np.array([10, 20]) # 每行加 [10,20]
# (2,2) 与 (2,) 相加:(2,1)×(2,) → 广播成 (2,2)
规则口诀:从右往左对齐维度,两边长度相等或有一边为 1 即可广播。如 (4,3) + (1,3) ✅、(4,3) + (4,) ❌(要从右对齐,右边 3≠4)。广播让「每个减去列均值」「归一化」这类操作一行写完。
🛠️ ufunc 与条件逻辑
ufunc(通用函数)是逐元素运算的 C 加速版:np.sqrt、np.exp、np.maximum(x, y)、np.add……二元 ufunc 返回数组,一元 ufunc 吃数组吐数组。
np.where 是向量化版的三元表达式:
xarr, yarr = np.array([1.1, 2.2]), np.array([3.3, 4.4])
cond = np.array([True, False])
np.where(cond, xarr, yarr) # [1.1, 4.4]
# 经典应用:把正数换成 2、负数换成 -2
np.where(arr > 0, 2, -2)
# 或「替换 + 保底」:>0 取 arr 本身,否则取 -2
np.where(arr > 0, arr, -2)
统计方法全部是「轴感知」的:
rng = np.random.default_rng(seed=12345) # 第3版推荐的新随机数API
arr = rng.standard_normal((5, 4))
arr.mean(); arr.sum(); arr.std()
arr.mean(axis=0) # 沿行方向压 → 每列的均值
arr.cumsum(axis=1) # 逐行累加
(arr > 0).sum() # 正数个数(布尔数组求和)
(arr > 0).any() # 有没有正数
(arr > 0).all() # 是不是全是正数
np.unique(names) # 排序去重,等价于 set 但保序输出
np.in1d / np.isin(x, ["Bob", "Will"]) # 成员判断的向量化版
🎲 实战:随机游走的两种写法
书里的招牌例子——模拟「抛硬币走步」:
# 单次游走:1000 步
rng = np.random.default_rng(seed=12345)
draws = rng.integers(0, 2, size=1000) # 0/1
steps = np.where(draws > 0, 1, -1) # 向量化三元
walk = steps.cumsum() # 累积位置
walk.min(), walk.max() # 最远到过哪
(np.abs(walk) >= 10).argmax() # 首次到达 ±10 的时间
注意那个漂亮的技巧:布尔数组 .argmax() 返回第一个 True 的位置——「第一次发生某事是什么时候」的向量化答案。
然后是「同时模拟 5000 次游走」:把 1000 步 × 5000 次的随机数做成 (5000, 1000) 矩阵,cumsum(axis=1) 一次性算完,hits30 = (np.abs(walks) >= 30).any(axis=1) 统计触界比例——纯 Python 循环要几秒,NumPy 一瞬间。
💾 其他实用件
arr = np.arange(10)
np.save("some_array.npy", arr) # 二进制存(推荐,快)
np.load("some_array.npy")
from numpy.linalg import inv, qr # 线性代数子模块
X = rng.standard_normal((5, 3))
inv(X.T @ X) # @ 是矩阵乘法
numpy.random 的老 API(np.random.seed/randn)在书里被标注为旧式;新代码统一用 default_rng() 生成 Generator 对象,统计性质更好、更安全。
📌 本章小结:向量化 = 对整块数据发指令;切片是视图,要副本用 .copy();布尔索引(
&/|)和花式索引是筛选利器;np.where是向量化三元;axis参数决定沿哪个方向压;随机数用default_rng()。数组思维练成后,pandas 的向量化操作就是换了个标签的同一件事。