# 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] ...                                     (连续、纯数值)
1
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.]
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
1
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
1
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  # 转置(行列互换)
1
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)
1
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列的子块
1
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的位置才被取出
1
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) 两个坐标
1
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]
1
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]
1
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)
1
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行的表”
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列」的行向量
1
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
1
2

广播过程:

  1. 维度对齐:scores 是二维 (4, 3),bonus 是一维 (3,) → 左边补1 → bonus 变成 (1, 3)
  2. 逐维校验(右对齐):
    • 第1维:3 vs 3 → 相等,通过
    • 第0维:4 vs 1 → 有1,通过;把这1行沿行方向复制4份
  3. 运算得到结果 (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]]
1
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  # 报错!
1
2

报错原因(核心就是左补1):

  1. 维度对齐:scores 是二维 (4, 3),row_mean 是一维 (4,) → 左边补1 → row_mean 变成 (1, 4)
  2. 逐维校验(右对齐):
    • 第1维:3 vs 4 → 既不相等也不为1 → 广播失败
scores:    (4, 3)
row_mean:  (4,)  →  左补1 → (1, 4)

右对齐逐维比:
  第1维: 3 vs 4  →  ✗ 不匹配,报错
1
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
1
2

广播过程:

  1. 维度对齐:都是二维,无需补1
  2. 逐维校验(右对齐):
    • 第1维:3 vs 1 → 有1,通过;把这1列沿列方向复制3份
    • 第0维:4 vs 4 → 相等,通过
  3. 运算得到结果 (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]]
1
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
1
2
3
4

广播过程:

  1. 维度对齐:discount 二维 (3, 1),price 一维 (4,) → 左边补1 → price 变成 (1, 4)
  2. 逐维校验:
    • 第0维:3 vs 1 → 有1,折扣列向右复制4列
    • 第1维:1 vs 4 → 有1,价格行向下复制3行
  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.]]
1
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)
1
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))
1
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.]
1
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)  # 原地打乱
1
2
3
4
5
6
7
8
9

三个工程要点:

  1. 先 seed 再生成,同一seed产生同一序列——实验可复现、bug可重放的唯一办法;
  2. rand 与 randn 一个字母之差:均匀vs正态,模拟真实世界噪声基本都用 randn;
  3. 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)
1
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)
1
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的维度
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] 累积和
1
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=',')
1
2
3
4
5
6
7
8
9
10
11
12

选型:.npy 用于中间结果存档(快且无损);.txt/csv 用于跨工具交换(Excel、R等)。更大的表格数据交给明天的Pandas。

# 十三、课后习题

  1. 生成一个 (5, 4) 的随机整数矩阵(值域 (0~100],取出:第2行、最后一列、左上角 (2,2) 子块。
  2. 用布尔索引把矩阵中所有大于50的元素替换为50(提示:赋值也可以用布尔索引)。
  3. 不用循环,构造一个 (4, 4) 矩阵:第i行第j列的值等于 i * j(提示:np.arange(4)[:, None] * np.arange(4))。
  4. 解释为什么 np.arange(3) + np.arange(4) 报错,而 np.arange(3).reshape(3,1) + np.arange(4) 可以。写出结果形状。
  5. 求解方程组 3x + y = 9, x + 2y = 8,并用 A @ x 验证结果。
  6. 模拟掷骰子10000次(randint(1, 7, 10000)),统计每个点数的频率,与理论值1/6对比。
  7. 生成50名学生的3门课成绩(正态分布,均值75/70/80,标准差10),计算每人的总分、每科的最高分及最高分学生序号。
  8. 把上题的 (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!

最近更新: 2026/9/30
 |  Copyright © -Present Kelsen All Rights Reserved. | Author KelsenJing