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层逐行跑,速度极慢。
执行优先级(从快到慢)
- 内置向量化运算 /
str/dt访问器 eval/querygroupby+agg / transform- 列表推导式
apply(Series)apply(axis=1,逐行)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: ...) 做分支判断,优先:
np.where三目条件np.select多分支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 优化
- 只对需要的列分组,不要全表groupby
# 好:只取参与聚合的列
df[["商品","金额"]].groupby("商品")["金额"].sum()
- 优先
agg内置函数,不要在groupby里自定义apply
# 快:内置聚合
df.groupby("商品")["金额"].agg(["sum","mean","max"])
# 慢:自定义函数apply
df.groupby("商品").apply(lambda x: x["金额"].sum())
- 分组键提前转category,groupby提速
- 避免分组内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系列方法。
九、索引优化
- 高频筛选、关联的字段设置为索引
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)
十二、性能排查工具
%timeit(jupyter)反复测速df.memory_usage(deep=True)内存诊断- line_profiler 逐行代码耗时分析
- py-spy 采样分析线上脚本
十三、避坑清单
- 不要在循环里不断拼接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)
- 不要频繁修改df列(循环内df[“col”] = xxx)
- 不要滥用
axis=1的apply,百万行基本必卡 - category类型排序、分组快,但字符串匹配部分接口受限
- parquet压缩选snappy,平衡速度和压缩率
- 日期列提前转为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 + apply | groupby + agg内置函数 |
| 重复分类字段 | object类型 | category |
| 超大文件 | 一次性read_csv | chunksize分块 / parquet |
| 循环拼接df | 循环内concat | 列表收集,最后一次concat |
0 条笔记