Pandas: 性能优化
最后更新:2026-08-26
Bob 的 apply 脚本跑了 10 分钟,Alice 改成向量化 30 秒——20 倍加速。这不是个例:Pandas 90% 的性能问题来自"用循环代替向量化"。本节覆盖从编码习惯到引擎优化的完整性能工具链,帮你写出又快又省内存的 Pandas 代码。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。
1. 你将学到
- ❶ 向量化 vs apply vs 循环
- ❷ eval / query 表达式引擎
- ❸ category 内存优化
- ❹ downcast 数值压缩
- ❺ Copy-on-Write 与大数据策略
2. Bob 的 10 分钟 apply
(1) 痛点:逐行循环太慢
Bob 用 iterrows 处理 10 万行数据,跑了 10 分钟:
PYTHON
import pandas as pd
import numpy as np
# DON'T DO THIS — very slow!
# for i, row in df.iterrows():
# df.loc[i, 'new_col'] = row['a'] * row['b'] + row['c']
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 解法:向量化 30 秒
▶ 示例:向量化 vs 循环(难度⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(100000),
'b': np.random.randn(100000),
'c': np.random.randn(100000)
})
# ❌ Slow: iterrows (~300s for 100K rows)
# for i, row in df.iterrows():
# df.loc[i, 'result'] = row['a'] * row['b'] + row['c']
# ✅ Fast: vectorized (~0.03s)
df['result'] = df['a'] * df['b'] + df['c']
print(f"Result mean: {df['result'].mean():.4f}")
# ~30 seconds vs ~0.03 seconds — 1000x faster!
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
3. 性能排序:循环 → apply → 向量化
(1) 4 种方法对比
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:4 种方法速度对比(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(50000),
'b': np.random.randn(50000)
})
# Method 1: iterrows (slowest — NEVER use for computation)
# ~60 seconds for 50K rows
# Method 2: apply (slow)
result2 = df.apply(lambda row: row['a'] + row['b'], axis=1)
# Method 3: vectorized (fast)
result3 = df['a'] + df['b']
# Method 4: numpy (fastest)
result4 = (df['a'].values + df['b'].values)
# Verify same result
print(np.allclose(result2.values, result3.values)) # True
print(np.allclose(result3.values, result4)) # True
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 性能排序表
| 排序 | 方法 | 相对速度 | 适用 |
|---|---|---|---|
| 1 | NumPy 运算 | 最快 (1x) | 纯数值 |
| 2 | Pandas 向量化 | 快 (1-2x) | 标签+数值 |
| 3 | map (字典) | 中 (5-10x) | 一对一映射 |
| 4 | apply | 慢 (50-100x) | 复杂逻辑 |
| 5 | itertuples | 很慢 (200x) | 必须逐行 |
| 6 | iterrows | 最慢 (500x) | 永远别用 |
🔥 铁律:能用向量化就用向量化,不能用就用 apply,最后才用循环。iterrows 永远是最后的选择(或根本不该选)。
4. eval / query 表达式引擎
(1) eval 加速复杂运算
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:eval 表达式引擎(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(100000),
'b': np.random.randn(100000),
'c': np.random.randn(100000),
'd': np.random.randn(100000)
})
# Complex expression — eval avoids intermediate DataFrames
result1 = df['a'] * df['b'] + df['c'] / df['d'] - df['a'] ** 2
result2 = df.eval('a * b + c / d - a ** 2')
print(np.allclose(result1, result2)) # True
# eval is faster when:
# 1. Many columns in the expression (>4)
# 2. DataFrame is large (>10K rows)
# 3. Memory is limited (avoids intermediates)
# query for filtering
filtered = df.query('a > 0 and b < 0')
# Equivalent: df[(df['a'] > 0) & (df['b'] < 0)]
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) eval vs 普通操作
| 场景 | eval 优势 | eval 劣势 |
|---|---|---|
| 多列复杂表达式 | 减少中间变量 | 语法受限 |
| 大 DataFrame | 省内存 | 小数据无优势 |
| 简单运算 | 无优势 | 可读性差 |
| 条件筛选(query) | 可读性好 | 灵活性有限 |
5. category 内存优化
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:category 类型压缩(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
# Simulate: 100K rows, 10 unique cities
df = pd.DataFrame({
'city': np.random.choice(['NYC', 'LA', 'Chicago', 'Houston', 'Phoenix',
'Philly', 'San Antonio', 'San Diego', 'Dallas', 'Austin'], 100000),
'status': np.random.choice(['Active', 'Inactive', 'Pending', 'Churned'], 100000)
})
# Before: object type
mem_before = df.memory_usage(deep=True).sum()
print(f"Before (object): {mem_before / 1024:.1f} KB")
# After: category type
df['city'] = df['city'].astype('category')
df['status'] = df['status'].astype('category')
mem_after = df.memory_usage(deep=True).sum()
print(f"After (category): {mem_after / 1024:.1f} KB")
print(f"Savings: {(1 - mem_after / mem_before) * 100:.1f}%")
# Typically 80-95% memory reduction for low-cardinality strings!
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(1) category 何时用
| 条件 | 适用 | 不适用 |
|---|---|---|
| 唯一值 < 总行数 50% | ✅ | ❌ |
| 字符串列 | ✅ | — |
| 排序有意义 | ✅ | 无序随机字符串 |
| 需要字符串方法 | ❌(.str受限) | ✅(用object) |
| 频繁 groupby | ✅(加速) | — |
6. downcast 数值压缩
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:downcast 数值类型(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'id': np.arange(100000),
'age': np.random.randint(0, 120, 100000),
'score': np.random.randint(0, 100, 100000),
'price': np.round(np.random.uniform(0, 1000, 100000), 2),
'flag': np.random.choice([0, 1], 100000)
})
# Check current types and memory
print("Before:")
print(df.dtypes)
print(f"Memory: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
# Downcast integers
df['id'] = pd.to_numeric(df['id'], downcast='unsigned') # uint32 → uint32
df['age'] = pd.to_numeric(df['age'], downcast='unsigned') # int64 → uint8 (0-255)
df['score'] = pd.to_numeric(df['score'], downcast='unsigned')
df['flag'] = pd.to_numeric(df['flag'], downcast='unsigned') # int64 → uint8
# Downcast floats
df['price'] = pd.to_numeric(df['price'], downcast='float') # float64 → float32
print("\nAfter:")
print(df.dtypes)
print(f"Memory: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
# Typical savings: 50-75%
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
7. Copy-on-Write 与大数据策略
(1) Copy-on-Write (CoW)
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:CoW 行为(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# Pandas 3.x: CoW is default
pd.options.mode.copy_on_write = True
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
# With CoW: slice returns a view, modification creates a copy
subset = df[['a']] # no copy until modified
df.loc[0, 'a'] = 99 # modifies df, subset unchanged
print(subset) # still [1, 2, 3] — CoW protects
# Safe patterns under CoW:
df['c'] = df['a'] + df['b'] # ✅ assign new column
df = df.drop(columns='c') # ✅ reassign
# Avoid:
# subset['a'] = 10 # ❌ SettingWithCopyWarning (pre-CoW issue, fixed by CoW)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 大数据策略
| 数据规模 | 策略 |
|---|---|
| < 1GB | 正常 Pandas |
| 1-5GB | downcast + category + eval |
| 5-50GB | chunksize 分块处理 |
| > 50GB | 考虑 Dask / Polars / PySpark |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:chunksize 分块(难度⭐)
PYTHON
import pandas as pd
from io import StringIO
# Simulate large CSV
csv_data = "id,value\n" + "\n".join([f"{i},{i*10}" for i in range(1000)])
# Process in chunks
total = 0
for chunk in pd.read_csv(StringIO(csv_data), chunksize=200):
total += chunk['value'].sum()
print(f"Total: {total}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
8. 完整示例:百万行数据优化全流程
(6) ▶ 性能优化检查清单
graph TB
A[性能优化] --> B[1. 向量化替代循环]
A --> C[2. eval / query 加速]
A --> D[3. category 压缩字符串]
A --> E[4. downcast 数值压缩]
A --> F[5. chunksize 分块]
A --> G[6. Copy-on-Write]
B --> H[100-500x 加速]
C --> I[省内存免中间变量]
D --> J[80-95% 内存节省]
E --> K[50-75% 内存节省]
F --> L[处理超内存数据]
G --> M[切片零复制]
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:从慢到快全流程(难度⭐⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# ============================================
# Comprehensive example: 1M rows optimization
# iterrows → apply → vectorized → eval → category
# ============================================
np.random.seed(42)
df = pd.DataFrame({
'city': np.random.choice([f'City_{i}' for i in range(20)], 1000000),
'category': np.random.choice(['Electronics', 'Clothing', 'Home', 'Sports'], 1000000),
'price': np.round(np.random.uniform(10, 500, 1000000), 2),
'quantity': np.random.randint(1, 100, 1000000),
'discount': np.random.choice([0, 0.1, 0.2, 0.3], 1000000)
})
# Step 1: Baseline memory
mem1 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"Step 1 — Raw memory: {mem1:.1f} MB")
# Step 2: Downcast numerics
df['quantity'] = pd.to_numeric(df['quantity'], downcast='unsigned')
df['price'] = pd.to_numeric(df['price'], downcast='float')
df['discount'] = pd.to_numeric(df['discount'], downcast='float')
mem2 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"Step 2 — After downcast: {mem2:.1f} MB ({(1-mem2/mem1)*100:.0f}% saved)")
# Step 3: Category for strings
df['city'] = df['city'].astype('category')
df['category'] = df['category'].astype('category')
mem3 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"Step 3 — After category: {mem3:.1f} MB ({(1-mem3/mem1)*100:.0f}% saved total)")
# Step 4: Vectorized calculation (fast)
df['revenue'] = df['price'] * df['quantity'] * (1 - df['discount'])
print(f"Step 4 — Revenue calculated (vectorized, ~0.01s)")
# Step 5: eval alternative
df['revenue2'] = df.eval('price * quantity * (1 - discount)')
print(f"Step 5 — Revenue via eval")
# Verify
print(f"Results match: {np.allclose(df['revenue'], df['revenue2'])}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
❓ 常见问题
Q iterrows 和 itertuples 区别?
A iterrows 返回 (index, Series) 对,每次创建新 Series——极慢(~500x 向量化)。itertuples 返回 (index, namedtuple) 对,不创建 Series——快 5-10 倍但仍比向量化慢 50x。必须逐行时用 itertuples,但首先想想能不能向量化。
Q eval 什么时候快?
A 大数据(>10K 行)+ 多列复杂表达式时 eval 更快——它避免创建中间 DataFrame,用 numexpr 引擎加速。小数据或简单表达式 eval 反而慢(引擎启动开销)。经验:4 列以上复合运算 + 大数据才值得用 eval。
Q inplace 真的省内存吗?
A Pandas 2.x 的 inplace 通常不省内存——内部可能仍创建临时副本再赋值。Pandas 3.x Copy-on-Write 模式下,inplace 和非 inplace 内存开销基本相同。推荐:不用 inplace,用
df = df.method() 赋值写法,语义更清晰。Q Copy-on-Write 是什么?
A CoW 是 Pandas 3.x 的默认行为——数据共享底层内存,只有修改时才创建副本。好处:切片不复制(省内存),链式赋值安全(不会 SettingWithCopyWarning)。影响:某些 inplace 操作行为变化,需要重新赋值。
Q 超大文件怎么办?
A 3 层策略:① chunksize 分块读取+聚合(适合可分治的统计);② downcast/category 减少单行内存(让内存装下更多行);③ 换工具——Dask(Pandas API 分布式)、Polars(Rust 写的超快 DataFrame)、PySpark(集群级)。单机内存上限约是数据的 3-5 倍。
Q apply 一定比向量化慢吗?
A 大多数情况是的——apply 逐行调用 Python 函数,向量化在 C 层执行。少数例外:当向量化需要创建多个巨大中间数组时,apply 可能省内存(不省时间)。策略:优先向量化,性能不够再看 eval,最后才 apply。
Q memory_usage 怎么用?
A
df.memory_usage(deep=True) 返回每列字节数。deep=True 对 object 列计算实际字符串内存(否则只算指针大小)。总和:df.memory_usage(deep=True).sum()。对比优化前后用这个函数。category 列只存整数编码+字典,内存远小于 object。📖 小节
- 铁律:向量化 > map > apply > itertuples > iterrows
- eval/query 用 numexpr 引擎加速复杂表达式,大数据+多列时值得
- category 类型对低基数字符串列节省 80-95% 内存
- downcast 把 int64→uint8/uint16/uint32,float64→float32,节省 50-75%
- Copy-on-Write 让切片零复制、赋值安全
- 大数据策略:downcast+category → chunksize → Dask/Polars
📝 作业
- 基础题(难度⭐):创建 10K 行 DataFrame,分别用 apply 和向量化计算新列,对比 memory_usage。
- 进阶题(难度⭐⭐):创建含 object 列的 100K 行 DataFrame,用 category + downcast 优化内存,计算节省百分比。
- 挑战题(难度⭐⭐⭐):创建 100 万行 DataFrame(含 category/float/int 列),完成:向量化计算 → eval 计算 → 对比结果 → memory_usage 全程追踪 → 总优化报告。