自学教程 学编程从这里开始

Pandas 性能优化

Pandas 处理小数据时感受不到差异,十万、百万行以上数据,写法不同速度差距可达几十上百倍。本文从原理、写法选型、数据类型、内存、向量化、循环替换、IO、分块、并行、避坑全套讲解,延续销售明细数据集。
前置导入

import pandas as pd
import numpy as np
import time

一、示例测试数据

# 构造大数据用于测试
n = 100000
df = pd.DataFrame({
    "商品": np.random.choice(["键盘","鼠标","显示器"], size=n),
    "渠道": np.random.choice(["线上","线下"], size=n),
    "数量": np.random.randint(1, 10, size=n),
    "单价": np.random.randint(40, 1500, size=n),
})
df["金额"] = df["数量"] * df["单价"]

测速工具模板

def timer(func, *args, **kwargs):
    s = time.time()
    res = func(*args, **kwargs)
    print(f"耗时:{time.time() - s:.4f} s")
    return res

二、核心原则:优先向量化,拒绝循环

Pandas 底层是 NumPy,向量化运算在C层执行;Python 循环、apply、iterrows 是在Python层逐行跑,速度极慢。

执行优先级(从快到慢)

  1. 内置向量化运算 / str / dt 访问器
  2. eval / query
  3. groupby + agg / transform
  4. 列表推导式
  5. apply(Series)
  6. apply(axis=1,逐行)
  7. iterrows() / itertuples()(严禁大数据用)

❌ 反面:iterrows 最慢

# 不推荐,大数据卡死
for idx, row in df.iterrows():
    pass

✅ 推荐:直接列运算

df["金额_9折"] = df["金额"] * 0.9

补充:itertuples 比 iterrows 快一点,但依然远慢于向量化,只适合极小数据集临时调试。

三、数据类型优化(内存优化,性价比最高)

百万级数据集,类型优化经常直接砍掉一半以上内存。
查看内存:

df.memory_usage(deep=True)
df.info(memory_usage="deep")

3.1 分类类型 category

重复值多的字段(商品、渠道、状态)首选

df["商品"] = df["商品"].astype("category")

优点:内存大幅下降,groupby、筛选提速明显。

3.2 可空新类型(推荐 Pandas 1.0+)

原生numpy整数、布尔遇到空值会自动上浮成float;新类型支持NA,同时节省空间

# 支持空值的整数
df["数量"] = df["数量"].astype(pd.Int64Dtype())
# 支持空值字符串,替代object
df["商品"] = df["商品"].astype(pd.StringDtype())
# 支持空值布尔
df["flag"] = pd.Series([True, None, False], dtype=pd.BooleanDtype())

3.3 数值降级 downcast

数值范围小,向下转换精度,减少字节

df["数量"] = pd.to_numeric(df["数量"], downcast="integer")
df["金额"] = pd.to_numeric(df["金额"], downcast="float")

3.4 读文件时直接指定 dtype(最佳时机)

不要读完再转换,读取阶段直接约束类型

df = pd.read_excel(
    "data.xlsx",
    dtype={
        "商品": "category",
        "渠道": "category"
    }
)

四、条件逻辑优化:抛弃 apply 写if

不要用 apply(lambda x: ...) 做分支判断,优先:

  1. np.where 三目条件
  2. np.select 多分支
  3. df.where / df.mask
# 单条件
df["等级"] = np.where(df["金额"] > 500, "大单", "小单")

# 多条件分支
conds = [
    df["金额"] > 1000,
    df["金额"] > 300,
    df["金额"] > 100
]
choices = ["A","B","C"]
df["档位"] = np.select(conds, choices, default="D")

五、query / eval 加速筛选与计算

底层用Numexpr,大数据筛选比布尔索引更快,代码简洁

# 筛选
df_sub = df.query("金额 > 500 and 渠道 == '线上'")
# 新增列
df["折扣价"] = df.eval("金额 * 0.85")

适合:复杂多条件筛选;小数据提升不明显,百万行以上优势明显。

六、Groupby 优化

  1. 只对需要的列分组,不要全表groupby
# 好:只取参与聚合的列
df[["商品","金额"]].groupby("商品")["金额"].sum()
  1. 优先 agg 内置函数,不要在groupby里自定义apply
# 快:内置聚合
df.groupby("商品")["金额"].agg(["sum","mean","max"])
# 慢:自定义函数apply
df.groupby("商品").apply(lambda x: x["金额"].sum())
  1. 分组键提前转category,groupby提速
  2. 避免分组内rolling + apply,尽量使用原生rolling

七、IO读写优化(读取导出是高频瓶颈)

7.1 读取时只加载需要的列

df = pd.read_csv("data.csv", usecols=["商品","渠道","金额"])

7.2 分批读取(chunksize),超大文件不爆内存

chunk_iter = pd.read_csv("big.csv", chunksize=100000)
for chunk in chunk_iter:
    # 分片处理、入库、汇总
    pass

7.3 优先二进制格式:Parquet > Feather > CSV > Excel

  • Parquet:压缩、列式存储、自带schema,读写极快,大数据首选
  • Excel(xlsx)速度最慢,尽量减少大批量导出读取
# 写入
df.to_parquet("data.parquet", compression="snappy")
# 读取
df = pd.read_parquet("data.parquet")

7.4 CSV 优化参数

pd.read_csv("data.csv", dtype={}, parse_dates=[], low_memory=False)

八、字符串处理优化

Series.str 是向量化实现,比逐行lambda快很多

# ✅ 推荐
df["商品"].str.contains("盘", na=False)
# ❌ 不推荐
df["商品"].apply(lambda s: "盘" in s)

复杂正则同样优先str系列方法。

九、索引优化

  1. 高频筛选、关联的字段设置为索引 set_index
df = df.set_index("订单号")

loc 基于索引查询 O(1);普通布尔筛选 O(n)
2. 避免频繁 reset_index、sort_index
3. 多表merge前,把关联键设为索引,使用 join 替代merge,速度更快

df1.join(df2, on="key")

十、避免链式赋值、不必要副本

# 每次[]切片默认生成副本,消耗内存
df_sub = df[["商品","金额"]].copy()

链式赋值警告根源:视图 vs 副本。大量中间df会成倍占用内存,处理完及时释放:

del df_temp
import gc
gc.collect()

十一、进阶加速方案(原生Pandas不够用时)

11.1 Numba 加速自定义函数

适合必须写复杂逻辑、无法向量化的场景,给函数加装饰器JIT编译

from numba import jit

@jit(nopython=True)
def calc_score(arr):
    res = np.empty(len(arr))
    for i in range(len(arr)):
        res[i] = arr[i] * 0.8
    return res
df["score"] = calc_score(df["金额"].to_numpy())

11.2 Polars / Dask

  • Dask:API和Pandas几乎一致,适合超大数据,分块并行,不一次性加载到内存
  • Polars:新一代高性能DataFrame库,速度远超Pandas,语法接近,适合ETL

11.3 swifter 自动优化apply

swifter 自动判断能否向量化,不能则调用Dask并行,替代原生apply

import swifter
df["new"] = df["金额"].swifter.apply(lambda x: x**2 + 1)

十二、性能排查工具

  1. %timeit(jupyter)反复测速
  2. df.memory_usage(deep=True) 内存诊断
  3. line_profiler 逐行代码耗时分析
  4. py-spy 采样分析线上脚本

十三、避坑清单

  1. 不要在循环里不断拼接DataFrame(pd.concat 在循环内),先放列表最后一次性concat
# ❌ 坏写法
res = pd.DataFrame()
for x in lst:
    res = pd.concat([res, subdf])
# ✅ 好写法
tmp_list = []
for x in lst:
    tmp_list.append(subdf)
res = pd.concat(tmp_list)
  1. 不要频繁修改df列(循环内df[“col”] = xxx)
  2. 不要滥用 axis=1 的apply,百万行基本必卡
  3. category类型排序、分组快,但字符串匹配部分接口受限
  4. parquet压缩选snappy,平衡速度和压缩率
  5. 日期列提前转为datetime,不要用字符串做时间筛选

十四、优化流程模板(生产ETL标准流程)

def load_and_optimize(path):
    # 1. 读取时指定列、dtype,减少初始内存
    df = pd.read_csv(
        path,
        usecols=["商品","渠道","数量","单价","下单日期"],
        dtype={"商品":"category","渠道":"category"},
        parse_dates=["下单日期"]
    )
    # 2. 数值降级
    df["数量"] = pd.to_numeric(df["数量"], downcast="integer")
    # 3. 向量化计算
    df["金额"] = df["数量"] * df["单价"]
    return df

十五、优化速查表

场景慢写法高速替代
逐行计算apply(axis=1) / iterrows向量化 / np.where / np.select
字符串匹配apply判断str.contains / str.extract
大数据筛选df[cond] 复杂条件df.query
分组统计groupby + applygroupby + agg内置函数
重复分类字段object类型category
超大文件一次性read_csvchunksize分块 / parquet
循环拼接df循环内concat列表收集,最后一次concat
标签:

0 条笔记