避免使用 iterrows 和 Python 循环
使用向量化列操作、np.where 和 pd.cut 替代逐行循环,实现 10 到 100 倍的加速。
避免使用 iterrows 和 Python 循环 是 CoddyKit 上的免费 Pandas & NumPy Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Pandas & NumPy Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Pandas & NumPy Academy 课程共包含 4 节课。
逐行迭代的代价
Pandas 构建于 NumPy 之上,后者使用编译后的 C 代码一次处理整个数组。当您使用 for _, row in df.iterrows() 逐行迭代时,就绕过了这种机制,退回到纯 Python 执行——每一行都会被提取为一个 Series 对象,而循环在 Python 解释器中运行,其成本大约是等效向量化操作的 100 到 1000 倍。对于包含 100 万行的 DataFrame,这可能意味着需要几分钟,而不是几毫秒。
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})
# Slow: iterrows loop
def loop_version(df):
result = []
for _, row in df.iterrows():
result.append(row['a'] + row['b'])
return pd.Series(result)
# Fast: vectorised
t_loop = timeit.timeit(lambda: loop_version(df), number=5)
t_vec = timeit.timeit(lambda: df['a'] + df['b'], number=500)
print(f'Loop (5 runs): {t_loop:.3f}s total')
print(f'Vectorised (500 runs): {t_vec:.3f}s total')
print(f'Speedup: ~{(t_loop/5)/(t_vec/500):.0f}x')使用 np.where 替换条件循环
np.where(condition, value_if_true, value_if_false) 是逐元素 if/else 的向量化等价形式。不要迭代各行并编写 if 语句,而应将条件以及两种结果值作为数组传入。np.where 会使用 C 一次处理整个列;对于大型 DataFrame,它的速度比等效 Python 循环快 50 到 200 倍。
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'price': np.random.uniform(10, 100, 100000)})
# Slow: iterrows
def label_loop(df):
labels = []
for _, row in df.iterrows():
if row['price'] > 50:
labels.append('expensive')
else:
labels.append('cheap')
return labels
# Fast: np.where
def label_vectorised(df):
return np.where(df['price'] > 50, 'expensive', 'cheap')
# Verify equivalence on a small subset
assert list(label_loop(df.head(100))) == list(label_vectorised(df.head(100)))
print('Results match!')
print('Fast version result sample:', label_vectorised(df)[:5])使用 np.select 处理多个条件
当条件达到三个或更多时,应使用 np.select(condlist, choicelist, default=),而不是嵌套的 np.where。传入一个布尔数组列表和一个对应的输出值列表。第一个匹配的条件决定输出结果;不匹配的行使用 default 值。这样可以用简洁的向量化表达式替代循环中复杂的 if/elif/else 链。
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'score': np.random.randint(0, 101, 20)})
conditions = [
df['score'] >= 90,
df['score'] >= 75,
df['score'] >= 60
]
choices = ['A', 'B', 'C']
df['grade'] = np.select(conditions, choices, default='F')
print(df.sort_values('score', ascending=False).head(10))通过 .str 进行向量化字符串操作
字符串处理是循环运行缓慢的常见原因。Pandas 的 .str 访问器提供了所有 Python 字符串方法的向量化等价形式,例如 .str.lower()、.str.strip()、.str.replace()、.str.contains() 等。使用 .str 方法比逐行迭代并手动调用 Python 字符串方法快 10 到 50 倍。
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
names = ['Alice Smith', 'BOB JONES', ' carol ', 'Dave Brown'] * 25000
df = pd.DataFrame({'name': names})
# Slow: loop
def clean_loop(df):
return [n.strip().title() for n in df['name']]
# Fast: .str accessor
def clean_vectorised(df):
return df['name'].str.strip().str.title()
t1 = timeit.timeit(lambda: clean_loop(df), number=10)
t2 = timeit.timeit(lambda: clean_vectorised(df), number=50)
print(f'Loop (10 runs): {t1:.3f}s')
print(f'.str (50 runs): {t2:.3f}s')
print(f'Speedup: {(t1/10)/(t2/50):.0f}x')
print('Sample:', clean_vectorised(df).head(4).tolist())使用 pd.cut 和 pd.qcut 替代循环
pd.cut() 和 pd.qcut() 可将通常通过循环和多个 if/elif 条件实现的分箱操作向量化。如果您发现自己正在行循环中编写 'if value < 18: age_group = child elif value < 65: age_group = adult',请将其替换为一次 pd.cut() 调用,让它以 C 的速度一次处理整个列。
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'age': np.random.randint(0, 90, 100000)})
# Slow: loop with conditionals
def categorise_loop(df):
result = []
for age in df['age']:
if age < 18:
result.append('child')
elif age < 65:
result.append('adult')
else:
result.append('senior')
return result
# Fast: pd.cut
def categorise_cut(df):
return pd.cut(df['age'], bins=[0, 18, 65, 100],
labels=['child', 'adult', 'senior'],
right=False)
assert list(categorise_loop(df.head(5))) == list(categorise_cut(df.head(5)).astype(str))
print('Fast version sample:', categorise_cut(df).head(5).tolist())apply() 并不总是最佳答案
许多教程建议使用 .apply(func) 替换循环,但 apply 在内部仍然是 Python 级别的循环——它只比 iterrows 略快,远慢于真正的向量化。只有在不存在向量化替代方案时(例如复杂的多列逻辑),才应使用 apply。如果 Pandas 或 NumPy 的内置函数能够表达该操作,应始终优先使用内置函数,而不是 apply。
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(100000)})
# These all do the same thing — compare performance
t1 = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=20)
t2 = timeit.timeit(lambda: df['x'] ** 2, number=200)
t3 = timeit.timeit(lambda: np.square(df['x']), number=200)
print(f'apply(v**2): {t1/20*1000:.2f} ms per run')
print(f'** operator: {t2/200*1000:.2f} ms per run')
print(f'np.square(): {t3/200*1000:.2f} ms per run')使用 agg 和 transform 替换 groupby 循环
一种常见模式是手动遍历各个组:for name, group in df.groupby('cat'): do_something(group)。由于与 iterrows 相同的原因,这种方式速度很慢。请使用 groupby().agg() 生成汇总统计数据,或者使用 groupby().transform() 将组级统计数据广播回原始行的位置。
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'cat': np.random.choice(['A','B','C','D'], 100000),
'val': np.random.randn(100000)
})
# Slow: manual loop to add group mean
def slow_group_mean(df):
means = {}
for name, group in df.groupby('cat'):
means[name] = group['val'].mean()
return df['cat'].map(means)
# Fast: transform
def fast_group_mean(df):
return df.groupby('cat')['val'].transform('mean')
t1 = timeit.timeit(lambda: slow_group_mean(df), number=10)
t2 = timeit.timeit(lambda: fast_group_mean(df), number=100)
print(f'Loop: {t1/10*1000:.1f} ms')
print(f'transform: {t2/100*1000:.1f} ms')
print(f'Speedup: {(t1/10)/(t2/100):.0f}x')何时可以使用 iterrows
尽管速度较慢,iterrows 和 itertuples 仍有合理的使用场景:打印或记录行中的信息(性能无关紧要);构造 API 请求载荷,其中每一行都会触发一次 HTTP 调用(网络延迟占主导地位);以及在具有复杂条件的情况下调试特定行。如果行数少于 1,000 且操作只运行一次,那么循环与向量化之间的差异只有几毫秒,不值得为此增加代码复杂度。
import pandas as pd
import numpy as np
df = pd.DataFrame({
'order_id': [101, 102, 103],
'product': ['Widget', 'Gadget', 'Doohickey'],
'amount': [29.99, 49.99, 9.99]
})
# Acceptable use: building a structured log (small data, one-time)
for _, row in df.iterrows():
print(f'Order {row["order_id"]}: {row["product"]} — ${row["amount"]:.2f}')itertuples 比 iterrows 更快
如果必须在 Python 中迭代各行(因为不存在向量化等价形式),请使用 itertuples(),而不是 iterrows()。它会将每一行作为命名元组返回,而不是 Pandas Series,从而避免构造 Series 的开销。因此,它通常比 iterrows 快 5 到 10 倍。请使用属性表示法访问值:row.column_name。如果列名包含空格,则不要使用此方法,因为这类列名不是有效的属性名。
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})
t1 = timeit.timeit(
lambda: [row.a + row.b for row in df.itertuples()], number=10)
t2 = timeit.timeit(
lambda: [row['a'] + row['b'] for _, row in df.iterrows()], number=10)
print(f'itertuples: {t1/10*1000:.1f} ms')
print(f'iterrows: {t2/10*1000:.1f} ms')
print(f'itertuples speedup: {t2/t1:.1f}x')向量化模式检查清单
在编写循环之前,请思考以下问题:
- 操作是否针对单列逐元素进行?→ 使用列算术表达式或 NumPy 通用函数。
- 是否涉及条件逻辑?→ 使用
np.where(2 个条件)或np.select(3 个及以上条件)。 - 是否要将值分到不同范围?→ 使用
pd.cut或pd.qcut。 - 是否要执行字符串操作?→ 使用
.str访问器。 - 是否要在组内进行聚合?→ 使用
groupby().agg()或groupby().transform()。
apply() 或 itertuples()。真实加速示例:价格计算
下面是一个常见业务计算的完整重构前后示例:根据订单数量应用分级折扣。循环版本易于理解,但在大型 DataFrame 上速度慢得无法接受。使用 np.select 的向量化版本能够得到相同结果,而耗时仅为原来的十分之一。
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'price': np.random.uniform(10, 200, 100000),
'qty': np.random.randint(1, 500, 100000)
})
# BEFORE: loop with conditionals
def slow_discount(df):
result = []
for _, row in df.iterrows():
if row['qty'] >= 100:
disc = 0.2
elif row['qty'] >= 50:
disc = 0.1
elif row['qty'] >= 10:
disc = 0.05
else:
disc = 0.0
result.append(row['price'] * (1 - disc))
return pd.Series(result)
# AFTER: np.select
def fast_discount(df):
conditions = [df['qty'] >= 100, df['qty'] >= 50, df['qty'] >= 10]
discounts = [0.20, 0.10, 0.05]
disc = np.select(conditions, discounts, default=0.0)
return df['price'] * (1 - disc)
assert slow_discount(df.head(200)).round(4).equals(fast_discount(df.head(200)).reset_index(drop=True).round(4))
print('Both versions agree!')快速检查
测试您对本课向量化内容的理解。
课程回顾
本课中您学到了:iterrows 慢 100 到 1000 倍,向量化操作可替代条件循环;np.where 和 np.select 可替代条件循环;.str 访问器可将字符串操作向量化;而 groupby().transform() 可替代手动遍历各组。当必须进行迭代时,应使用 itertuples 而不是 iterrows,从而提升 5 到 10 倍的速度。接下来,我们将学习高效的数据类型——对数值类型进行降级转换,并使用 Categorical 将内存占用最多降低 70%。
常见问题解答
「避免使用 iterrows 和 Python 循环」课时是免费的吗?
是的 — 「避免使用 iterrows 和 Python 循环」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Pandas & NumPy Academy 课程的其余内容,请升级到 CoddyKit PRO。 Pandas & NumPy Academy 课程共包含 4 节课。
「避免使用 iterrows 和 Python 循环」这节课中我会学到什么?
使用向量化列操作、np.where 和 pd.cut 替代逐行循环,实现 10 到 100 倍的加速。 你通过在浏览器中直接运行的动手代码来练习 Pandas & NumPy Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Pandas & NumPy Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Pandas & NumPy Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「避免使用 iterrows 和 Python 循环」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Pandas & NumPy Academy 课中编写并运行代码吗?
能。每节 Pandas & NumPy Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用 timeit 和 memory_profiler 进行性能分析
- 避免使用 iterrows 和 Python 循环
- 用于减少内存的高效数据类型
- 大文件的分块读取