Pandas 性能优化技巧

随着数据量的增长,Pandas 代码的运行速度和内存占用变得至关重要。通过遵循一些核心原则和技巧,你可以显著提升代码性能,从几分钟缩短到几秒钟。本章将介绍最实用的 Pandas 性能优化方法,助你高效处理大规模数据。

🎯 核心原则:向量化

Pandas 和 NumPy 的设计核心就是向量化操作——对整个数组或 Series 执行操作,而不是逐个元素循环。向量化操作在底层使用 C 语言循环,速度远超 Python 级别的循环。

慢:显式循环
for i in range(len(df)):
    df.loc[i, 'new'] = df.loc[i, 'a'] + df.loc[i, 'b']
快:向量化 ~100x
df['new'] = df['a'] + df['b']

📊 1. 使用向量化操作

几乎所有的数学运算、比较、字符串操作都可以向量化。

import pandas as pd
import numpy as np

df = pd.DataFrame({'a': np.random.randn(1000000), 'b': np.random.randn(1000000)})

# 向量化加法
df['c'] = df['a'] + df['b']

# 向量化条件
df['d'] = np.where(df['a'] > 0, 1, 0)

# 向量化字符串(需要 .str 访问器,仍比循环快)
df['e'] = df['a'].astype(str).str[:5]

🧮 2. 避免 apply 和 transform(除非必要)

apply 虽然比显式循环快,但仍是 Python 级别的循环,对性能有影响。尽可能用内置聚合函数或向量化方法替代。

# 慢:用 apply 计算两列最大值
df['max'] = df.apply(lambda row: max(row['a'], row['b']), axis=1)

# 快:直接用 numpy 或 pandas 方法
df['max'] = np.maximum(df['a'], df['b'])

💾 3. 优化数据类型

Pandas 默认使用 int64 和 float64,但对于数值范围小的列,可以降级为 int8/16/32 或 float32,大幅节省内存。对于重复值多的字符串列,使用 category 类型。

# 查看当前内存
print(df.memory_usage(deep=True))

# 转换数据类型
df['small_int'] = df['small_int'].astype('int8')
df['float_col'] = df['float_col'].astype('float32')
df['category_col'] = df['category_col'].astype('category')

# 对于数值列,可以使用 pd.to_numeric 并 downcast
df['col'] = pd.to_numeric(df['col'], downcast='integer')  # 或 'float'

📂 4. 高效读取文件

读取大文件时,指定数据类型、只读必要列、分块读取可以显著减少内存和加快速度。

# 指定 dtypes 和 usecols
dtypes = {'user_id': 'int32', 'age': 'int8', 'gender': 'category'}
df = pd.read_csv('large_file.csv', usecols=['user_id', 'age', 'gender'], dtype=dtypes)

# 分块读取
chunk_iter = pd.read_csv('huge_file.csv', chunksize=100000)
for chunk in chunk_iter:
    process(chunk)  # 逐块处理

🔗 5. 使用 merge 和 concat 的优化

  • 合并前确保连接键的数据类型一致。
  • 对于大表,先过滤再合并。
  • 使用 join 参数控制连接方式,避免不必要的数据复制。
# 先筛选再合并
filtered = df1[df1['date'] > '2024-01-01']
result = filtered.merge(df2, on='key')

⚡ 6. 使用 eval() 和 query() 进行高速表达式求值

pd.eval()df.query() 使用 numexpr 引擎,对大型 DataFrame 的复杂表达式求值比纯 Python 快。

# 使用 query 筛选
df_filtered = df.query('a > 0 and b < 0')

# 使用 eval 创建新列
df.eval('c = a + b * 2', inplace=True)

🧩 7. 避免链式索引,使用 .loc 和 .iloc

链式索引(如 df[df.a>0]['b'])不仅可能产生视图/副本问题,而且通常较慢。应使用 .loc 进行赋值和选择。

# 慢且危险
df[df.a > 0]['b'] = 1

# 快且安全
df.loc[df.a > 0, 'b'] = 1

🔄 8. 使用 inplace 操作谨慎

inplace=True 可以避免复制,但很多函数(如 drop())在 inplace 时并不会节省内存,因为内部仍需复制。推荐使用赋值方式,更清晰。

# 通常不推荐 inplace
df.drop('col', axis=1, inplace=True)

# 推荐赋值
df = df.drop('col', axis=1)

📈 9. 使用并行化库

对于无法向量化的复杂操作,可以考虑使用并行库加速,例如:

  • swifter:自动判断是否向量化,否则并行 apply
  • pandarallel:简单的并行 apply
  • dask:分布式计算,处理超大数据集
# 安装 pip install swifter
import swifter
df['new'] = df.swifter.apply(lambda x: complex_func(x), axis=1)

🔍 10. 性能分析工具

在优化前,先用工具找出瓶颈。

  • %timeit(Jupyter 中)测试单行代码。
  • df.info()df.memory_usage() 查看内存。
  • line_profiler 分析函数逐行耗时。
  • pandas_profiling 生成数据报告。
# 在 Jupyter 中
%timeit df['a'] + df['b']

🧪 综合对比示例

import pandas as pd
import numpy as np

# 生成 100 万行数据
df = pd.DataFrame({
    'group': np.random.choice(['A','B','C','D'], 1_000_000),
    'value1': np.random.randn(1_000_000),
    'value2': np.random.randn(1_000_000),
    'flag': np.random.randint(0, 2, 1_000_000)
})

# 慢方法:循环
def slow_way(df):
    result = []
    for i in range(len(df)):
        if df.loc[i, 'flag'] == 1:
            result.append(df.loc[i, 'value1'] + df.loc[i, 'value2'])
        else:
            result.append(df.loc[i, 'value1'])
    df['result'] = result

# 较快:apply
def apply_way(df):
    df['result'] = df.apply(lambda row: row['value1'] + row['value2'] if row['flag'] == 1 else row['value1'], axis=1)

# 最快:向量化
def vectorized_way(df):
    df['result'] = np.where(df['flag'] == 1, df['value1'] + df['value2'], df['value1'])

# 测试性能(Jupyter 中)
# %timeit slow_way(df.copy())      # ~ 几秒
# %timeit apply_way(df.copy())      # ~ 几百毫秒
# %timeit vectorized_way(df.copy()) # ~ 几毫秒

📌 优化清单

  • 优先使用向量化操作
  • 避免循环和 apply,除非无法替代
  • 数据类型用最小精度(int8/float32)
  • 字符串列用 category 类型(如果唯一值少)
  • 读取文件时指定 dtype 和 usecols
  • 大文件分块处理
  • 使用 query/eval 加速复杂表达式
  • 合并前过滤数据
  • 用 .loc 进行选择赋值,避免链式索引
  • 必要时使用并行库
终极建议:优化是一个持续的过程。先写出正确代码,然后通过分析工具找到瓶颈,最后应用上述技巧。不要过早优化,但也不要等到数据爆炸才动手。