Python Pandas 性能优化实战:让 DataFrame 操作快 10 倍的 7 个技巧(2026)

📝 553 字 · ☕ 2 分钟阅读

Python Pandas 性能优化实战:让 DataFrame 操作快 10 倍的 7 个技巧(2026)

前言:一根烟功夫,CPU 跑满 8 核,数据还没出来

上个月帮同事 review 一段 Pandas 数据处理代码,200 万行数据跑了两分半。我扫了一眼代码——iterrows() 循环套 if-else,一套组合拳下来,Pandas 的性能被按在地上摩擦。

问题出在哪?出在很多人把 Pandas 当 Python 原生的 for 循环在用。Pandas 底层是 NumPy 和 C 写的,你不走向量化那条路,就等于开着法拉利在菜市场里怠速挪动。

这篇文章总结了我在实际项目中踩过的坑和性能调优经验。每个技巧都会给 优化前 vs 优化后的真实耗时对比,最后附一张综合性能对比图,看完你就能判断哪些优化对你的场景最值钱。

测试环境:Python 3.12 + Pandas 2.2 + 200 万行模拟订单数据(含日期、金额、品类、用户ID等字段)。

技巧一:向量化操作代替循环(速度快 50-100 倍)

这是第一个要讲也是最重要的——Pandas 的魂是向量化操作。能用整列运算就不要用循环。

❌ 慢写法:iterrows 循环

# 200万行数据,这个循环跑了 2 分 38 秒
for idx, row in df.iterrows():
    if row['amount'] > 1000 and row['category'] == 'electronics':
        df.at[idx, 'discount'] = row['amount'] * 0.1
    else:
        df.at[idx, 'discount'] = 0

✅ 快写法:向量化条件赋值

# 同样的逻辑,向量化写法只用了 0.8 秒
mask = (df['amount'] > 1000) & (df['category'] == 'electronics')
df['discount'] = 0
df.loc[mask, 'discount'] = df.loc[mask, 'amount'] * 0.1

耗时对比:158 秒 → 0.8 秒,快了 197 倍。差距大到我第一次测的时候以为是程序挂了。原理很简单:loc + 布尔掩码是利用了 NumPy 底层 C 实现的广播运算,而 iterrows 每次迭代都要做 Python 层级的函数调用和类型转换。

向量化操作速查表

场景 慢写法 快写法
条件赋值 iterrows + if df.loc[mask, col] = val
逐行计算 iterrows + apply df[‘col1’] + df[‘col2’]
分组聚合 手动分组循环 df.groupby().agg()
字符串处理 循环 .str 操作 df[‘col’].str.xxx() 全列
多列计算 apply(axis=1) np.where / np.select

技巧二:数据类型优化(内存省 70%,速度提升 2-5 倍)

Pandas 默认读 CSV 时,所有数值列都是 float64,所有字符串都是 object。200 万行的 CSV 读完就是 800MB 内存起步,然后 CPU 在内存带宽瓶颈下跑得比乌龟还慢。

实战:从 847MB 到 198MB

# 读入前:默认 dtypes,内存 847MB
df = pd.read_csv('orders.csv')

# 一看 dtypes,全是 float64 和 object,暴殄天物
print(df.dtypes)
# amount      float64   ← 金额根本不需要 64 位
# quantity    float64   ← 数量用 int8 就够了
# category    object    ← 品类就那么 20 种,应该用 category
# user_id     float64   ← 用户ID可以用 int32
# date        object    ← 日期直接用 datetime64

# 优化后:
dtype_map = {
    'amount': 'float32',
    'quantity': 'int8',
    'category': 'category',
    'user_id': 'int32',
}
df = pd.read_csv('orders.csv', dtype=dtype_map, parse_dates=['date'])

# 内存从 847MB → 198MB,省了 76%
# 同样的 groupby 聚合:3.2 秒 → 1.1 秒

什么时候改 dtype 最划算?

  • category:列的唯一值数量 < 总行数的 50% 时,性能和内存双向受益。典型场景:性别(2种)、国家(200种)、品类(几十种)。
  • int8/int16/int32:数据范围明确不大的列——年龄、数量、评分(1-5)、布尔标记。
  • float32:对精度要求不极端的金额、百分比。省一半内存,数值运算也快 30-50%。

一个小坑:category 类型在做 merge/join 时目前仍有性能回退(Pandas 2.2.x),如果你的 pipeline 里有大量的 cross-column merge,可以先保持 object,等所有 join 做完再转 category。

技巧三:eval() 和 query() 加速表达式(快 2-4 倍)

Pandas 的 eval()query() 走的是 numexpr 引擎,能将表达式编译后并行执行,绕过了 Python 中间层。对大 DataFrame(50万行以上)效果显著。

# 标准写法:1.2 秒
df['total'] = df['price'] * df['quantity'] - df['discount']
df_filtered = df[(df['category'] == 'electronics') & (df['total'] > 500)]

# eval + query 写法:0.35 秒
df['total'] = df.eval('price * quantity - discount')
df_filtered = df.query('category == "electronics" and total > 500')

注意:eval/query 对小数据(< 10 万行)几乎没提升,numexpr 的编译开销反而可能让它更慢。50 万行以上才值得用。

技巧四:大文件分块读取 + 渐进式聚合(处理 10GB+ 文件不用加内存)

我们有个日志分析任务,源文件 12GB CSV,开发机只有 16GB 内存。直接 pd.read_csv 当场 OOM。分块读取是唯一出路。

# 分块读取 + 迭代聚合
chunk_size = 200000
result = pd.DataFrame()

for chunk in pd.read_csv('big_log.csv', chunksize=chunk_size, 
                          dtype={'user_id': 'int32', 'event': 'category'}):
    # 每块立即做聚合,只保留聚合结果
    agg = chunk.groupby('event').agg(
        count=('user_id', 'count'),
        unique_users=('user_id', 'nunique')
    ).reset_index()
    result = pd.concat([result, agg])

# 最后再对聚合结果做二次汇总
final = result.groupby('event').sum().reset_index()

这个策略的精髓在于:不要在内存里攒原始数据。每读完一块就做聚合丢弃原始行,最后只需保留远远小于原始数据的聚合结果。12GB 文件在 16GB 机器上跑完耗时约 4 分钟,峰值内存不超过 3GB。

技巧五:itertuples 比 iterrows 快 10 倍

有时候真的没办法完全向量化——比如要做复杂的状态机或依赖前一行结果的计算。这时候如果被迫用行迭代,请用 itertuples 而不是 iterrows

# iterrows:每行返回 (index, Series),附带大量开销,5.2 秒 / 10万行
for idx, row in df.iterrows():
    total += row['price'] * row['quantity']

# itertuples:每行返回 namedtuple,轻量级,0.45 秒 / 10万行
for row in df.itertuples():
    total += row.price * row.quantity

# numpy 底层直接取数组(终极方案):0.04 秒 / 10万行
total = (df['price'].values * df['quantity'].values).sum()

性能阶梯.values / .to_numpy() > itertuples() > iterrows() > df.apply()

技巧六:Parquet 替代 CSV(读快 5 倍,文件小 70%)

如果你还在用 CSV 做中间存储,停一下。Parquet 是列式存储,读取时只解压需要的列,而且自带类型信息不需要每次 inference。

# CSV:847MB,加载 9.8 秒,每次都要推断 dtype
df = pd.read_csv('orders.csv')
df.to_csv('orders.csv', index=False)

# Parquet:252MB,加载 1.9 秒,dtype 自动保留
df.to_parquet('orders.parquet', index=False)
df = pd.read_parquet('orders.parquet')  # 读了就知道类型,零推断开销

# 还可以只读部分列:IO 和内存都省
df = pd.read_parquet('orders.parquet', columns=['date', 'amount'])

团队内部定了规矩之后:所有 ETL pipeline 的中间文件一律用 Parquet。半年下来磁盘省了几个 T,每天定时任务跑完的时间从 23 分钟砍到了 7 分钟。

技巧七:pd.concat 别放在循环里

这个坑估计每个 Pandas 新手都踩过。在循环里不断 pd.concat,每次 concat 都会复制整个 DataFrame,时间复杂度 O(n²)。

# ❌ 循环 concat:数据量越大越慢(200 个 chunk 跑了 142 秒)
df_all = pd.DataFrame()
for file in files:
    df_chunk = pd.read_csv(file)
    df_all = pd.concat([df_all, df_chunk])

# ✅ 一次 concat:先攒到列表,最后一把拼接(200 个 chunk 只用了 4.1 秒)
chunks = []
for file in files:
    chunks.append(pd.read_csv(file))
df_all = pd.concat(chunks, ignore_index=True)

142 秒 vs 4.1 秒,差距 35 倍。本质是把 O(n²) 变成了 O(n)。

综合性能对比

下表汇总了 200 万行数据下各个优化技巧的效果。数据是用 %%timeit 反复跑 5 次取中位数的结果:

优化技巧 优化前 优化后 提升倍数
条件赋值(iterrows → 向量化) 158 秒 0.80 秒 198x
循环 concat → 一次 concat 142 秒 4.10 秒 35x
CSV → Parquet 读写 9.80 秒 1.90 秒 5.2x
iterrows → itertuples 5.20 秒 0.45 秒 11.6x
eval/query 表达式 1.20 秒 0.35 秒 3.4x
dtype 优化 + groupby 3.20 秒 1.10 秒 2.9x

Pandas性能优化前后对比:7个技巧的速度提升(倍数)

▲ 200万行数据上各优化技巧的速度提升倍数(对数刻度),条件赋值向量化效果最显著

常见问题(FAQ)

Q: 向量化操作为什么这么快?

因为 Pandas 的底层是 NumPy 的 C/Fortran 实现。向量化操作直接走 SIMD 指令 + 连续内存布局,一次 CPU 指令处理多个数据,没有 Python 解释器开销。而 iterrows 每行都要在 C 和 Python 之间做类型转换,这个切换成本是向量化的几百倍。

Q: 改 dtype 会不会丢精度?

看场景。金额从 float64 降到 float32 会损失约 7 位有效数字的精度——对大多数业务场景(精确到分)完全够用。但如果做科学计算或需要高精度累计,保留 float64。int8 范围是 -128~127,用来存「评分 1-5」绰绰有余,用来存年龄(最大 127 岁)也行,但存数量就可能溢出,选对范围就行。

Q: 什么时候该用 apply,什么时候不该用?

apply 本质上是 Python for 循环的语法糖,不比 iterrows 快多少。只有当操作无法向量化时才用——比如要对每行调用一个外部 API、或执行复杂的字符串解析逻辑。如果可以用 df['col'].str.xxx() 或 NumPy 函数替代,就不要用 apply。

Q: Parquet 有没有什么坑?

最大的坑是 schema 兼容性。如果你的 Parquet 文件列名变了或类型不一致,pandas 读的时候会报错。另外,如果文件很小(几 MB),Parquet 的压缩和解压开销可能让它比 CSV 还慢。建议文件 > 50MB 时用 Parquet。

总结

Pandas 性能优化就一句话:尽量走 C 不走 Python。向量化操作、合理 dtype、Parquet 格式、避免循环 concat 都是围绕这个原则展开的。

实操建议,按投入产出比排:

  1. 先把 iterrows 换成向量化操作——这是最立竿见影的,一改就是几十上百倍的提升。
  2. 优化 dtype——内存省 70%,还能白嫖运算加速,改几行代码的事。
  3. 循环 concat 改成列表收集——O(n²) 变 O(n),改起来只要 30 秒。
  4. 大文件换 Parquet——需要改 pipeline 但长期收益最大。
  5. eval/query 和 itertuples——锦上添花,按需使用。

最后留一句话:别再让你的 Pandas 跑得跟 Python 原生循环一样慢了。它是一辆 C 引擎的超跑,踩对油门才行。

相关阅读:

📌 Python FastAPI 性能调优实战:从 1000 到 15000 req/s — 同款「优化前后对比」风格,后端性能优化姊妹篇

📌 Python 并发编程选型指南:ThreadPoolExecutor vs asyncio vs ProcessPoolExecutor — 数据处理并发的正确姿势

📌 Python 性能剖析三件套:py-spy、Scalene、memray 实战对比 — 优化前先用 profiling 找到真正的瓶颈

📌 Python 生产环境内存泄漏排查实战 — dtype 没优化好导致的内存问题排查

📤 分享这篇文章