# 10000h·Day2:NumPy —— 数组、索引、广播与线性代数
所属专栏: 凯尔森的10000h刻意练习
Slogan: 一个36岁程序员, 研究生(学硕),前世界500强top20软件开发Java工程师,用10000小时深耕AI, 目标Top10 AI资深工程师, 全程日更记录。
tips: 文章末尾附有jupyter完整代码
# 一、今日定位
NumPy是整个Python数据科学栈的地基:Pandas建在它之上,sklearn的输入输出是它的数组。今天一天走完NumPy,解决六个问题:数组怎么造、怎么取、怎么算、形状不同怎么算、矩阵怎么乘、随机数与统计怎么用。
数据分析的四个真实需求:
| 需求 | 对应工具 |
|---|---|
| 几何/线性变换(机器学习的数学底座) | linalg 线性代数 |
| 模拟数据、随机采样、打乱数据集 | np.random |
| 拼接/拆分数据块、调整维度 | vstack / hsplit / newaxis |
| 汇总统计(均值、方差、分位数) | sum / mean / std / argmax |
Jupyter在macOS26中几个常用快捷键:
| 功能 | 操作 |
|---|---|
| 运行当前单元格,停在原地 | ⌘ + ↩(⌃ + ↩ 亦可) |
| 转为Code / Markdown / Raw | 先按Esc,Y / M / R |
| 上下移动选中 | 先按Esc,K J或 ↑ ↓ |
| 上方 / 下方插入单元格 | 先按Esc,A / B |
# 二、ndarray是什么:一次讲透
Python列表存的是"指针数组"——每个元素指向堆上的一个对象,遍历一次要经历N次指针跳转和类型检查。NumPy的 ndarray 则是一块连续内存里的同类型原始数据,加上一段描述元信息(shape、dtype)。
list: [ptr] → [PyObject int 1] [ptr] → [PyObject int 2] ... (离散、带类型开销)
ndarray: [1][2][3][4][5][6] ... (连续、纯数值)
2
这带来三个直接后果:
| 特性 | list | ndarray |
|---|---|---|
| 元素类型 | 可混装 | 必须同质(dtype) |
运算 a * 2 | 重复元素 | 逐元素乘2 |
| 速度 | 慢 | 快(向量化 + C循环) |
记住一个准则:数据分析里凡是能用NumPy数组表达的数据,就不要用list装着做循环。
# 三、数组的创建与属性
# 3.1 六种常用创建方式
import numpy as np
a = np.array([1, 2, 3]) # 从列表创建
b = np.array([[1, 2], [3, 4]]) # 二维(嵌套列表)
z = np.zeros((2, 3)) # 全0,传形状元组
o = np.ones((3, 2)) # 全1
f = np.full((2, 2), 7) # 全部填充指定值
e = np.eye(3) # 单位矩阵(对角线为1)
r = np.arange(0, 10, 2) # 等差序列(同range):[0 2 4 6 8]
l = np.linspace(0, 1, 5) # 等分点(含两端):[0. 0.25 0.5 0.75 1.]
2
3
4
5
6
7
8
9
10
11
arange 与 linspace 的区别是高频考点:arange 用步长,linspace 用份数。需要"把0到1均匀切成几份"时用后者,浮点步长的累积误差问题就不存在了。
# 3.2 dtype:内存账本
x = np.array([1, 2, 3], dtype=np.float32)
print(x.dtype) # float32
2
常用类型:int8/16/32/64、uint8、float16/32/64、bool。一个1000万行的float64列占80MB,换成float32直接省一半——dtype选择是内存优化的第一杠杆。注意 int64 在Windows上可能显示为 int32,只影响显示不影响使用。
# 3.3 数组属性速查
a = np.array([[1, 2, 3], [4, 5, 6]])
print(a.ndim) # 2维度数
print(a.shape) # (2, 3) 形状
print(a.size) # 6元素总数
print(a.dtype) # int64元素类型
print(a.itemsize) # 8每元素字节数
print(a.nbytes) # 48总字节数 = size * itemsize
2
3
4
5
6
7
# 四、形状操作:reshape与转置
a = np.arange(6) # [0 1 2 3 4 5]
b = a.reshape(2, 3) # 变2行3列
c = a.reshape(3, -1) # -1表示"这一维自动算"(6/3=2)
t = b.T # 转置(行列互换)
2
3
4
5
reshape返回视图(共享内存),改b会影响a;需要独立副本时用b = a.reshape(2, 3).copy()。- 元素总数必须能整除:
reshape(2, 3)要求size=6,否则报错。 -1只能占一个维度,让NumPy自己算。
# 五、索引与切片:取数的四种姿势
# 5.1 基础索引(与list同规则)
a = np.array([0, 1, 2, 3, 4, 5])
a[2] # 2
a[-1] # 5(负索引从尾数)
a[1:4] # [1 2 3]
a[::2] # [0 2 4](步长2)
2
3
4
5
# 5.2 多维索引(逗号分隔各轴)
m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
m[0, 1] # 2:第0行第1列
m[1] # [4 5 6]:整个第1行
m[:, 1] # [2 5 8]:所有行的第1列(冒号=全部)
m[0:2, 1:3] # 前两行、第1~2列的子块
2
3
4
5
# 5.3 布尔索引:条件筛选的主力
a = np.array([1, 2, 3, 4, 5])
mask = a > 2
print(mask) # [False False True True True]
print(a[mask]) # [3 4 5]——mask为True的位置才被取出
2
3
4
布尔索引是数据分析里"筛选行"的祖先,Pandas的 df[df.age > 25] 就是它。mask的长度必须与数组对应轴等长。
# 5.4 花式索引:按位置列表取数
a = np.arange(10) ** 2
idx = np.array([1, 3, 5])
a[idx] # [1 9 25]:一次取出多个任意位置
m = np.array([[1, 2], [3, 4], [5, 6]])
m[np.array([0, 2]), np.array([1, 0])] # [2 5]:取 (0,1) 和 (2,0) 两个坐标
2
3
4
5
6
注意花式索引与切片的组合语义:m[rows, cols] 是配对取(坐标一一对应),不是笛卡尔积。
# 六、数组运算:向量化思维
# 6.1 算术与比较
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
a + b # [5 7 9] 逐元素
a * b # [4 10 18] 逐元素乘(不是点积!)
a ** 2 # [1 4 9]
a > 2 # [False False True]
2
3
4
5
6
7
# 6.2 通用函数(ufunc)
np.sqrt / np.exp / np.sin / np.log 等都是ufunc:对每个元素施加同一个数学函数,底层是C循环。
np.sqrt(a) # [1. 1.41421356 1.73205081]
np.exp(a) # [2.71828183 7.3890561 20.08553692]
np.sin(a) # [0.84147098 0.90929743 0.14112001]
p.log(a) # [0. 0.69314718 1.09861229]
2
3
4
对比:对list做 [math.sqrt(x) for x in a] 慢一个数量级以上。见到for循环做数值计算,先想能不能换成ufunc。
# 七、广播机制:形状不同也能运算
核心规则:两个数组形状右对齐,从右往左逐维比;相等或有一边是1就能广播(为1的维度沿该方向复制值),否则报错。
案例:4名学生 × 3门课成绩表,形状 (4, 3)
scores = np.array([
[80, 90, 70], # 小明
[60, 75, 88], # 小红
[95, 85, 92], # 小刚
[70, 60, 65] # 小丽
])
# scores.shape = (4,3)
2
3
4
5
6
7
# 小tips:一维数组与补1规则
这是所有广播报错的根源,先彻底讲透 (3,)、(1, 3)、(4,)、(1, 4) 的区别与联系。
四种形状的本质差异
# ===== 一维数组:只有长度,没有「行/列」概念 =====
bonus = np.array([5, 0, 2])
bonus.shape # (3,) → 一维,3个元素,就是“一横排数”
row_mean = np.array([80, 74.33, 90.67, 65])
row_mean.shape # (4,) → 一维,4个元素
# ===== 二维数组:有明确的「行、列」维度 =====
bonus_2d = np.array([[5, 0, 2]])
bonus_2d.shape # (1, 3) → 二维,1行3列,是“一张1行的表”
mean_2d = np.array([[80, 74.33, 90.67, 65]])
mean_2d.shape # (1, 4) → 二维,1行4列,是“一张1行的表”
2
3
4
5
6
7
8
9
10
11
12
13
直观对比:
(3,) → [5, 0, 2] 纯一维,不分行列
(1, 3) → [[5, 0, 2]] 二维,是「1行3列」的行向量
(4,) → [80, 74.33, 90.67, 65] 纯一维
(1, 4) → [[80, 74.33, 90.67, 65]] 二维,是「1行4列」的行向量
2
3
4
5
广播的「左补1」铁则
广播第一条规则:维度数不同时,只在小数组形状的「左边」补1,直到两边维度数相同。
这是铁律,没有例外:
- 一维
(3,)升二维 → 自动补成(1, 3)(行向量形式) - 一维
(4,)升二维 → 自动补成(1, 4)(行向量形式)
NumPy 永远不会自动补成列向量(比如
(3, 1)、(4, 1))。想变成列向量,必须手动用keepdims=True或.reshape(-1, 1)指定。
# 7.1 整表 + 一行:每门课加不同的分
bonus = np.array([5, 0, 2]) # shape (3,)
scores + bonus
2
广播过程:
- 维度对齐:
scores是二维(4, 3),bonus是一维(3,)→ 左边补1 →bonus变成(1, 3) - 逐维校验(右对齐):
- 第1维:3 vs 3 → 相等,通过
- 第0维:4 vs 1 → 有1,通过;把这1行沿行方向复制4份
- 运算得到结果
(4, 3)
scores: (4, 3) → [[80, 90, 70],
[60, 75, 88],
[95, 85, 92],
[70, 60, 65]]
bonus: (3,) → 左补1 → (1, 3)
→ [[5, 0, 2]]
沿行复制4份 → [[5, 0, 2],
[5, 0, 2],
[5, 0, 2],
[5, 0, 2]]
结果: (4, 3) → [[85, 90, 72],
[65, 75, 90],
[100, 85, 94],
[75, 60, 67]]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NumPy不会真的复制内存,只是修改读取步长(stride=0),所以和加一整张表一样快。
# 7.2 整表 − 一列:每人减去自己的平均分
错误写法
row_mean = scores.mean(axis=1) # shape (4,)
scores - row_mean # 报错!
2
报错原因(核心就是左补1):
- 维度对齐:
scores是二维(4, 3),row_mean是一维(4,)→ 左边补1 →row_mean变成(1, 4) - 逐维校验(右对齐):
- 第1维:3 vs 4 → 既不相等也不为1 → 广播失败
scores: (4, 3)
row_mean: (4,) → 左补1 → (1, 4)
右对齐逐维比:
第1维: 3 vs 4 → ✗ 不匹配,报错
2
3
4
5
(4,) 被自动理解成「1行4列」,像是要给4门课加分,但表里只有3门课,所以对不上。
正确写法(推荐keepdims)
手动把一维 (4,) 变成二维列向量 (4, 1),就能正常广播。
row_mean = scores.mean(axis=1, keepdims=True) # shape (4, 1)
scores - row_mean
2
广播过程:
- 维度对齐:都是二维,无需补1
- 逐维校验(右对齐):
- 第1维:3 vs 1 → 有1,通过;把这1列沿列方向复制3份
- 第0维:4 vs 4 → 相等,通过
- 运算得到结果
(4, 3)
scores: (4, 3) → [[80, 90, 70],
[60, 75, 88],
[95, 85, 92],
[70, 60, 65]]
row_mean: (4, 1) → [[80.00],
[74.33],
[90.67],
[65.00]]
沿列复制3份 → [[80.00, 80.00, 80.00],
[74.33, 74.33, 74.33],
[90.67, 90.67, 90.67],
[65.00, 65.00, 65.00]]
结果: (4, 3) → [[ 0.00, 10.00, -10.00],
[-14.33, 0.67, 13.67],
[ 4.33, -5.67, 1.33],
[ 5.00, -5.00, 0.00]]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
心法总结:
axis=n是「被压掉的轴」。求每行均值用axis=1,结果天然是横向的(4,)要做行方向广播,必须用
keepdims=True把它竖成(4, 1)求每列均值用
axis=0,结果是(3,),左补1后是(1, 3),和(4, 3)天然匹配,不用额外处理:scores - scores.mean(axis=0) # (4,3) - (3,) → 每列减列均值,直接生效1
# 7.3 两个都广播:造一张"价格表"(外积)
3种会员折扣 × 4件商品原价,一次算出所有组合的实付价。
discount = np.array([[0.9], [0.8], [0.7]]) # shape (3, 1) 列向量
price = np.array([100, 200, 300, 400]) # shape (4,) 一维
discount * price
2
3
4
广播过程:
- 维度对齐:
discount二维(3, 1),price一维(4,)→ 左边补1 →price变成(1, 4) - 逐维校验:
- 第0维:3 vs 1 → 有1,折扣列向右复制4列
- 第1维:1 vs 4 → 有1,价格行向下复制3行
- 运算得到结果
(3, 4)
discount: (3, 1) → [[0.9],
[0.8],
[0.7]]
沿列复制4份 → [[0.9, 0.9, 0.9, 0.9],
[0.8, 0.8, 0.8, 0.8],
[0.7, 0.7, 0.7, 0.7]]
price: (4,) → 左补1 → (1, 4)
沿行复制3份 → [[100, 200, 300, 400],
[100, 200, 300, 400],
[100, 200, 300, 400]]
结果: (3, 4) → [[ 90., 180., 270., 360.],
[ 80., 160., 240., 320.],
[ 70., 140., 210., 280.]]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
万能诀窍:想让数组在第k维起作用,就把第k维写成实际长度、其余维写成1。
- 管行 →
(-1, 1) - 管列 →
(1, -1)
配合 np.newaxis,两个一维向量直接造表:
discount_1d = np.array([0.9, 0.8, 0.7])
price_1d = np.array([100, 200, 300, 400])
discount_1d[:, None] * price_1d[None, :] # (3,1) * (1,4) → (3,4)
2
3
4
# 7.4 广播速查表
| 操作 | 写法 | 形状变化 |
|---|---|---|
| 每列减列均值 | x - x.mean(0) | (4,3) − (3,) |
| 每行减行均值 | x - x.mean(1, keepdims=True) | (4,3) − (4,1) |
| 每列标准化 | (x - x.mean(0)) / x.std(0) | 两次都是 (3,) |
| 每行加不同偏置 | x + b.reshape(-1, 1) | (4,3) + (4,1) |
| 批量算两点距离 | np.sum((A[:,None,:] - B[None,:,:])**2, -1) | (m,1,d) − (1,n,d) → (m,n) |
广播本质:不是“把小数组变大”,而是告诉NumPy——这个维度上只有一个值,请沿这个维度反复读同一个数。想通这点,axis、keepdims、reshape(-1,1) 就全串起来了。
# 八、线性代数:机器学习的数学底座
# 8.1 两种乘法,一字之差
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
a * b # 逐元素乘:[[ 5 12] [21 32]]
a @ b # 矩阵乘法:[[19 22] [43 50]](或np.dot(a, b))
2
3
4
5
* 是对应位置相乘,@ 是"行乘列再求和"。深度学习里权重矩阵 W @ x 用的是后者——把 * 当矩阵乘法用是新手第一大坑。
# 8.2 linalg四件套
from numpy.linalg import inv, det, eig, solve
A = np.array([[1, 2], [3, 4]])
inv(A) # 逆矩阵
det(A) # 行列式:-2.0
eig(A) # 特征值与特征向量(PCA的数学核心)
# 解线性方程组Ax = b
A = np.array([[3, 1], [1, 2]])
b = np.array([9, 8])
x = solve(A, b) # [2. 3.]
2
3
4
5
6
7
8
9
10
11
实践建议:解方程永远用 solve(A, b),而不要 inv(A) @ b——前者更快且数值更稳定。
# 九、随机数:可复现是实验的生命线
np.random.seed(42) # 固定种子,结果可复现
np.random.rand(3, 2) # [0,1) 均匀分布,形状 (3,2)
np.random.randn(3, 2) # 标准正态分布(均值0方差1)
np.random.normal(47, 12, 100) # (mu, sigma, size)指定均值mu,方差sigma,大小为size
np.random.randint(0, 10, size=(2, 3)) # 整数,左闭右开
np.random.choice([1, 2, 3], size=5, p=[0.2, 0.5, 0.3]) # 带权重抽样
arr = np.arange(10)
np.random.shuffle(arr) # 原地打乱
2
3
4
5
6
7
8
9
三个工程要点:
- 先
seed再生成,同一seed产生同一序列——实验可复现、bug可重放的唯一办法; rand与randn一个字母之差:均匀vs正态,模拟真实世界噪声基本都用randn;choice的p参数能按概率抽样本,做加权模拟(如按销量占比抽店铺)时非常顺手。
# 十、合并、分割与维度变形
# 10.1 合并三兄弟
a = np.array([[1, 2], [3, 4]])
np.vstack((a, [[5, 6]])) # 纵向堆(加行):→ (3,2)
np.hstack((a, [[5], [6]])) # 横向堆(加列):→ (2,3)
d = np.array([1, 2, 3])
e = np.array([4, 5, 6])
np.stack((d, e), axis=1) # 沿新轴堆:→ [[1,4],[2,5],[3,6]] (3,2)
2
3
4
5
6
7
8
vstack/hstack 不改变维度数,stack 会升一维——把两个等长向量配对成二维表时用 stack(axis=1)。
# 10.2 分割
a = np.arange(12).reshape(3, 4)
np.hsplit(a, 2) # 按列切成2块,每块 (3,2)
np.vsplit(a, 3) # 按行切成3块,每块 (1,4)
2
3
# 10.3 维度增删:newaxis与squeeze
a = np.array([1, 2, 3]) # (3,)
col = a[:, np.newaxis] # (3,1) 列向量
row = a[np.newaxis, :] # (1,3) 行向量
back = np.squeeze(col) # (3,) 去掉长度为1的维度
2
3
4
5
newaxis 是配合上文广播的利器:a[:, None] + b[None, :] 就是外积。读别人代码时,None 与 np.newaxis 等价。
# 十一、统计函数:吃透axis
a = np.array([[1, 2, 3], [4, 5, 6]])
np.sum(a) # 21全部元素
np.sum(a, axis=0) # [5 7 9] 沿行压缩 → 每列的和
np.sum(a, axis=1) # [6 15] 沿列压缩 → 每行的和
np.mean(a) # 3.5
np.median(a) # 3.5
np.std(a) # 1.7078(标准差)
np.var(a) # 2.9167(方差)
np.min(a) # 1
np.max(a, axis=1) # [3 6]
np.argmin(a) # 0 (最小值的扁平索引)
np.argmax(a, axis=0) # [1 1 1](每列最大值所在行号)
np.cumsum(a) # [1 3 6 10 15 21] 累积和
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
axis心法:axis指定的是被消灭的轴。axis=0 把行压没,留下每列的结果。argmax 返回的是位置而不是值——"TopN商品"这类需求靠它。
# 十二、文件IO:数组的存取
# 二进制 .npy:保留dtype与shape,首选
a = np.array([[1, 2, 3], [4, 5, 6]])
np.save('my_array.npy', a)
b = np.load('my_array.npy') # 完整还原
np.savez('multi.npz', x=a, y=a * 2) # 多数组打包
z = np.load('multi.npz')
print(z['x'])
# 文本格式:与外部系统交换时用
np.savetxt('my_array.txt', a, delimiter=',', fmt='%d')
c = np.loadtxt('my_array.txt', delimiter=',')
2
3
4
5
6
7
8
9
10
11
12
选型:.npy 用于中间结果存档(快且无损);.txt/csv 用于跨工具交换(Excel、R等)。更大的表格数据交给明天的Pandas。
# 十三、课后习题
- 生成一个
(5, 4)的随机整数矩阵(值域 (0~100],取出:第2行、最后一列、左上角(2,2)子块。 - 用布尔索引把矩阵中所有大于50的元素替换为50(提示:赋值也可以用布尔索引)。
- 不用循环,构造一个
(4, 4)矩阵:第i行第j列的值等于i * j(提示:np.arange(4)[:, None] * np.arange(4))。 - 解释为什么
np.arange(3) + np.arange(4)报错,而np.arange(3).reshape(3,1) + np.arange(4)可以。写出结果形状。 - 求解方程组
3x + y = 9, x + 2y = 8,并用A @ x验证结果。 - 模拟掷骰子10000次(
randint(1, 7, 10000)),统计每个点数的频率,与理论值1/6对比。 - 生成50名学生的3门课成绩(正态分布,均值75/70/80,标准差10),计算每人的总分、每科的最高分及最高分学生序号。
- 把上题的
(50,3)成绩矩阵与一个(50,1)的学号列hstack,保存为.npy再读回验证。
# 十四、要点速查表
| 操作 | 写法 | 关键点 |
|---|---|---|
| 创建 | np.array / zeros / ones / full / eye / arange / linspace | arange 用步长,linspace 用份数 |
| 形状 | reshape(2, -1) | 视图共享内存;-1 自动推断 |
| 转置 | a.T | 行列互换 |
| 取数 | a[i, j]、a[:, 1] | 逗号分轴 |
| 条件筛选 | a[a > 2] | mask等长按位置取 |
| 花式索引 | a[[1, 3, 5]] | 配对取 |
| 数学函数 | np.sqrt / exp / sin / log | ufunc, 替代循环 |
| 广播 | (4,3)+(3,) / (4,3)−(4,1) / (3,1)×(1,4) | 右对齐逐位比;竖用加keepdims |
| 矩阵乘法 | a @ b | 与 a * b 完全不同 |
| 解方程 | solve(A, b) | 优于 inv(A) @ b |
| 可复现 | np.random.seed(42) | 必须在生成前设置 |
| 随机数 | rand / randn / randint / choice | 均匀 / 正态 / 整数 / 加权 |
| 合并 | vstack / hstack / stack | stack 升维 |
| 增删维 | a[:, None] / np.squeeze | 配合广播 |
| 统计 | mean / std / argmax / cumsum | axis是被压掉的轴 |
| 存取 | np.save / load / savetxt | npy无损,txt通用 |
明天Day3见!See you tomorrow!