Learn Vectorized Thinking in Python Through Examples
TL;DR · AI 摘要
NumPy向量化操作可替代Python循环,利用C后端实现数值计算加速,提升效率达百倍以上。
核心要点
- Python动态类型导致循环效率低,NumPy的C后端避免了这种开销
- 布尔掩码和广播机制可消除90%的循环需求
- NumPy函数支持多条件分支和轴聚合,无需编写复杂循环
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 向量化计算
- Python循环问题
- 动态类型开销
- 逐元素处理
- NumPy优势
- C后端加速
- 内存优化
- 应用技术
- 布尔掩码
- 广播机制
- 轴聚合
金句 / Highlights
值得收藏与分享的关键句。
Python循环处理百万级数据时,NumPy速度可提升100倍以上
NumPy数组存储为连续内存块,避免Python对象创建开销
广播机制自动对齐不同维度数组,消除循环需求
通过示例学习 Python 的向量化思维 - MachineLearningMastery.com
通过示例学习 Python 的向量化思维
By
Bala Priya C
on
August 17, 2026
in
实用机器学习
0
分享
文章
在本文中,你将学习如何使用 NumPy 以向量化操作的方式思考,用高效的数组级计算替代缓慢的 Python 循环。
我们将涵盖的主题包括:
- 为什么 Python 循环在处理数值数据时速度较慢,以及 NumPy 的基于 C 的引擎如何解决这个问题。
- 如何通过逐元素操作、布尔掩码和广播机制消除常见的循环模式。
- 如何完全使用 NumPy 函数处理多条件分支和基于轴的聚合。
引言
你已经知道如何在 Python 中使用循环。循环简单、可读性强,且能准确执行所描述的操作。问题是当处理大规模数据时,Python 循环会变得过于缓慢。在某个时刻,每个处理数值数据的开发人员都会开始寻找更好的方法。
NumPy 的向量化操作提供了这种替代方案。你不再需要告诉 Python 如何逐个元素地执行操作,而是可以在数组层面描述转换过程,让 NumPy 的基于 C 的引擎高效地应用于所有元素。
本文通过一系列示例教授向量化思维。你将看到基于循环的版本、其向量化等价版本,以及相互转换的原理。
你可以在 GitHub 上找到这些示例的完整代码。
理解为什么 Python 中的循环速度较慢
要开始理解为什么你要替换的循环速度较慢很有帮助。
Python 是动态类型语言。每次你在循环中编写类似 x * 2 的操作时,Python 必须确定 x 的类型,找到正确的乘法方法,执行它,并为结果创建一个新的 Python 对象。
当处理少量元素时,这种开销可以忽略不计。但当相同操作在数百万个值上运行时,这些重复的 Python 级操作会迅速累积。
NumPy 数组的工作方式不同。它们将元素作为原始数字存储在连续的内存块中,类似于 C 中数组的存储方式。当你编写 arr * 2 时,NumPy 会将整个数组传递给一个编译好的 C 函数,该函数在不产生 Python 开销的情况下对每个元素应用操作。
这种计算速度更接近编译代码的速度,而非解释型 Python 的速度。
import numpy as np prices = np.array([12.99, 45.00, 7.49, 129.99, 3.25, 89.50]) taxed = np.round(prices * 1.12, 2) print(taxed)
import
numpy
as
np
array
prices *
[ 14.55 50.4 8.39 145.59 3.64 100.24]
输出结果相同,但向量化方法的扩展性要好得多。对于包含数百万价格的大数组,向量化版本的执行速度可能比基于循环的版本快很多。
重要的思维转变是从:
“对每个价格执行此计算。”
转变为:
“将此转换应用于整个价格数组。”
数组成为计算的基本单位,而不是单个元素。
使用布尔掩码实现条件逻辑
循环中经常包含检查每个值的if语句。向量化的等价方法是布尔掩码:通过比较生成的True和False值组成的数组。
随后可以使用布尔掩码来过滤值或更新选定元素,而无需编写循环。
考虑一个记录每小时温度的天气监测系统。你希望将所有高于38°C的读数标记为高温警报。
循环方法会检查每个温度值并构建单独的警报标志列表。
readings = [34.1, 38.5, 37.2, 39.0, 36.8, 40.1, 35.5] alerts = [] for temp in readings: alerts.append(temp > 38.0) print(alerts)
readings
34.1
38.5
37.2
39.0
36.8
40.1
35.5
alerts
temp
38.0
[False, True, False, True, False, True, False]
False
True
使用NumPy时,直接比较数组会自动生成布尔掩码。不需要显式循环,也不需要重复的append()操作。
import numpy as np readings = np.array([34.1, 38.5, 37.2, 39.0, 36.8, 40.1, 35.5]) alerts = readings > 38.0 print(alerts) print("Alert readings:", readings[alerts])
8
"Alert readings:"
[False True False True False True False] Alert readings: [38.5 39. 40.1]
Alert
39.
掩码可以立即索引回原始数组,仅返回匹配条件的值。
这种模式是向量化编程中最重要的理念之一:
计算掩码,然后使用该掩码选择或修改值。
它取代了通常在循环中编写的许多条件检查。
对于条件赋值,np.where()提供了更简洁的替代方案。例如,以下操作将高温值设置为38.0,同时保持其他值不变:
np.where(readings > 38.0, 38.0, readings)
where
在不同形状的数组之间进行广播
广播是NumPy用于在不同形状的数组之间进行操作而无需创建冗余副本的机制。
起初这可能显得更抽象,但它消除了许多原本需要手动对齐数据结构的嵌套循环。
考虑一个实际例子。假设你有五个营销活动在三个渠道(电子邮件、社交、搜索)的点击率数据。你希望按列归一化每个渠道,方法是将值除以该列的最大值。
基于循环的方法会单独处理每列。
import numpy as np # rows = campaigns, columns = channels (email, social, search) ctr = np.array([ [0.042, 0.031, 0.078], [0.019, 0.055, 0.091], [0.033, 0.047, 0.063], [0.061, 0.028, 0.085], [0.025, 0.039, 0.070], ]) # Loop version: normalize each column separately normalized_loop = np.zeros_like(ctr) for col in range(ctr.shape[1]): col_max = ctr[:, col].max() normalized_loop[:, col] = ctr[:, col] / col_max print(normalized_loop)
9
10
11
12
13
14
15
16
17
18
19
行 = 活动,列 = 渠道(电子邮件、社交、搜索)
ctr
0.042
0.031
0.078
0.019
0.055
0.091
0.033
0.047
0.063
0.061
0.028
0.085
0.025
0.039
0.070
循环版本:分别对每列进行归一化
normalized_loop
zeros_like
col
range
shape
col_max
max
/
[[0.68852459 0.56363636 0.85714286] [0.31147541 1. 1. ] [0.54098361 0.85454545 0.69230769] [1. 0.50909091 0.93406593] [0.40983607 0.70909091 0.76923077]]
0.68852459
0.56363636
0.85714286
0.31147541
1.
0.54098361
0.85454545
0.69230769
0.50909091
0.93406593
0.40983607
0.70909091
0.76923077
结果是正确的,但逻辑需要遍历所有列。
广播方法将列最大值计算为一维数组,并通过单次操作对整个矩阵进行除法运算。
col_maxima = ctr.max(axis=0) normalized = ctr / col_maxima print(normalized)
col_maxima
axis
normalized
NumPy 会将 (5, 3) 数组除以 (3,) 数组,并自动对齐形状。概念上,一维数组被视为行向量,并应用于所有五列。
不会实际创建副本。NumPy 在其编译层中高效处理该操作。
一般规则很简单:当循环仅用于使数组形状对齐时,广播通常是更简洁的解决方案。
沿轴聚合数据
许多数据任务涉及汇总矩阵的行或列。NumPy 的约减函数(如 sum()、mean()、max() 和 std())包含一个 axis 参数,用于确定约减方向。
axis 参数告诉 NumPy 要折叠的维度:
- axis=0 折叠行,返回每列一个值。
- axis=1 折叠列,返回每行一个值。
- 不指定 axis 会将整个数组缩减为单个值。
继续使用上一个示例中的点击率数据,可以在不编写任何循环的情况下计算每个渠道和每个活动的平均表现。
channel_avg = ctr.mean(axis=0) campaign_avg = ctr.mean(axis=1) print("Channel averages:", np.round(channel_avg, 4)) print("Campaign averages:", np.round(campaign_avg, 4))
channel_avg
mean
campaign_avg
"Channel averages:"
"Campaign averages:"
Channel averages: [0.036 0.04 0.0774] Campaign averages: [0.0503 0.055 0.0477 0.058 0.0447]
Channel
averages
0.036
0.04
0.0774
Campaign
0.0503
0.0477
0.058
0.0447
输出仅用两行就提供了两个汇总结果。基于循环的方法需要分别迭代计算行和列的平均值。
使用 NumPy 时,axis 参数可以直接表达操作意图。
替代多条件循环
数据处理经常需要将多个条件与计算结合。当循环包含处理不同情况的分支逻辑时,向量化变得特别有价值。
考虑一个薪资处理示例。你有员工工时和时薪,并需要计算工资,其中超过 40 小时的部分按正常时薪的 1.5 倍支付加班费。
循环版本会单独检查每个员工并应用正确的计算。
hours = np.array([38, 45, 40, 52, 33, 41]) rate = np.array([22.50, 18.00, 31.00, 15.50, 27.00, 19.75]) pay_loop = [] for h, r in zip(hours, rate): if h <= 40: pay_loop.append(h * r) else: regular = 40 * r overtime = (h - 40) * r * 1.5 pay_loop.append(regular + overtime) print([round(p, 2) for p in pay_loop])
hours
38
45
40
52
33
41
费率
22.50
18.00
31.00
15.50
27.00
19.75
pay_loop
h
r
zip
if
<=
h *
else
regular
*
overtime
-
r *
1.5
+
p
[np.float64(855.0), np.float64(855.0), np.float64(1240.0), np.float64(899.0), np.float64(891.0), np.float64(819.62)]
float64
855.0
1240.0
899.0
891.0
819.62
向量化方法将计算分解为数组操作。常规工资适用于前40小时,而加班工资仅适用于超过该阈值的小时数。
regular_pay = np.minimum(hours, 40) * rate overtime_pay = np.maximum(hours - 40, 0) * rate * 1.5 gross_pay = np.round(regular_pay + overtime_pay, 2) print(gross_pay)
regular_pay
minimum
overtime_pay
maximum
rate *
gross_pay
[ 855. 855. 1240. 853.25 891. 839.38]
855.
1240.
853.25
891.
839.38
np.minimum() 函数将每个值限制在40以内,自动处理未加班的员工。
np.maximum() 函数通过减去40并用零替换负值来计算加班小时数,确保未加班的员工不会对加班计算产生贡献。
关键的思维转变是用逐元素操作取代if/else分支,使每个值同时产生正确结果。
培养向量化思维的习惯
向量化思维是一种需要实践培养的技能。主要挑战是改变思维方式,从描述Python应该如何迭代,转变为描述数组应该变成什么样子。
当你看到处理数值数据的循环时,请使用这个检查清单:
- 操作是否对每个元素应用相同的公式?使用数组运算。
- 是否根据条件过滤值?使用布尔掩码。
- 是否对行或列进行汇总?使用带有axis参数的np.sum()、np.mean()等函数。
- 是否处理形状不同的数组?检查广播是否能替代循环。
但你不必在代码中消除所有循环。有些问题本质上是迭代的,强制向量化可能会使代码更难理解。你的目标应该是识别数组本身是否能代表完整的计算。
从这里开始,下一步是探索np.vectorize(),用于无法自然映射到内置数组操作的函数。
你还可以学习在pandas中向量化操作,pandas在NumPy数组基础上构建了面向列的数据结构,并将相同的向量化模型扩展到带标签的混合类型数据集。
关于此主题的更多内容
- 机器学习中像设计师一样思考的5个步骤
- 通过实例培养贝叶斯定理的直觉
- 如何通过实例培养概率的直觉
- 在...中使用统计方法的10个例子
- 机器学习中线性代数的10个例子
- 为LSTM设计的10个简单序列预测问题示例
/.entry /think