Pandas: 性能优化

最后更新:2026-08-26

Bob 的 apply 脚本跑了 10 分钟,Alice 改成向量化 30 秒——20 倍加速。这不是个例:Pandas 90% 的性能问题来自"用循环代替向量化"。本节覆盖从编码习惯到引擎优化的完整性能工具链,帮你写出又快又省内存的 Pandas 代码。

⚠️ 注意: 以下代码需在本地 Python 环境中运行。

1. 你将学到


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) ▶ 性能优化检查清单

100%
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。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建 10K 行 DataFrame,分别用 apply 和向量化计算新列,对比 memory_usage。
  2. 进阶题(难度⭐⭐):创建含 object 列的 100K 行 DataFrame,用 category + downcast 优化内存,计算节省百分比。
  3. 挑战题(难度⭐⭐⭐):创建 100 万行 DataFrame(含 category/float/int 列),完成:向量化计算 → eval 计算 → 对比结果 → memory_usage 全程追踪 → 总优化报告。

← 上一课:可视化 · 下一课:风格化输出 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏