大数据集与性能优化
本教程共 54 篇 · 第 53 篇 · 更新于 2026-08-11 · 约 7 分钟阅读
本节目标:学会压缩内存占用、分块处理大文件、用 PyArrow 后端和向量化提速,知道什么时候用 numba、query 和 eval。
先看内存:一切优化的起点
大数据集的第一道坎不是速度,是内存。pandas 是内存计算,数据装不下,后面都白搭。先摸清家底:
import pandas as pd
df = pd.DataFrame({
"id": range(10000),
"类别": ["甲", "乙", "丙"] * 3333 + ["甲"],
"数值": [i * 1.1 for i in range(10000)],
})
print(df.memory_usage(deep=True))
memory_usage(deep=True) 逐列报内存字节数,deep=True 才会把字符串内容算进去。哪列最大,就从哪列开刀。df.info() 底部也会汇总总内存。
少加载:能不带就不带
数据还在磁盘上时,先问自己”真的需要全部列吗”。read_csv 用 usecols 只读指定列,read_parquet 用 columns 参数。列少一半,内存就少一半:
import pandas as pd
df = pd.DataFrame({"id": range(5), "类别": ["甲"] * 5,
"数值": [1.0] * 5, "备注": ["x"] * 5})
df.to_csv("demo.csv", index=False)
# 只读需要的 3 列,跳过"备注"
df2 = pd.read_csv("demo.csv", usecols=["id", "类别", "数值"])
print(df2.head())
换类型:压缩数值与文本
pandas 默认 int64/float64 偏保守,实际数据往往用不了那么宽。用 pd.to_numeric 的 downcast 参数自动降档;低基数文本列转 category:
import pandas as pd
df = pd.DataFrame({"id": [1, 2, 3] * 1000, "城市": ["北京", "上海", "广州"] * 1000})
df["id"] = pd.to_numeric(df["id"], downcast="unsigned") # int64 降到能装下为止
df["城市"] = df["城市"].astype("category") # 3 种值,存整数编码
print(df.memory_usage(deep=True))
这两招配合,常见数据集能压到原来的 1/3 甚至更少。注意:唯一值特别多的列不适合转 category(§50 讲过),转之前先 value_counts() 看看。
分块:装不下就分批读
数据比内存大时,别一次性读进来。read_csv 的 chunksize 参数把文件切成一块块,循环里逐块处理,每次只占一块的内存:
import pandas as pd
import io
# 用 StringIO 模拟大文件,实际换成文件路径
data = "id,数值\n" + "\n".join(f"{i},{i * 1.5}" for i in range(1, 1001))
total = 0
count = 0
for chunk in pd.read_csv(io.StringIO(data), chunksize=100):
total += chunk["数值"].sum() # 每块算完就丢
count += len(chunk)
print(total / count) # 全局平均值
分块适合”块与块之间不需要配合”的统计(求和、计数、value_counts)。多个文件同理:循环里逐个读入、累积结果。要 groupby 这种跨块的复杂操作,手动累积很麻烦,这时考虑 Dask、Polars 这些专门处理超大数据集的库。
PyArrow 后端:类型更丰富更快
pandas 3.0 里,PyArrow 不只是字符串的存储层,还能当所有列的后端。装好 pyarrow 后,指定 dtype="float64[pyarrow]" 这类 Arrow 类型即可:
import pandas as pd
s = pd.Series([1.5, 2.5, None], dtype="float64[pyarrow]")
print(s)
# 0 1.5
# 1 2.5
# 2 <NA>
# dtype: double[pyarrow]
Arrow 后端的优势:所有类型都支持缺失值(NumPy 的 int 列做不到)、数值聚合和字符串运算走 Arrow 的 C++ 实现更快、还能和 Polars、DuckDB 等 Arrow 生态库零拷贝互转。读文件时也能直接指定:pd.read_csv("big.csv", dtype_backend="pyarrow")。
Tip升级 pandas 3.0 后如果发现字符串操作明显变快,多半是因为装过 pyarrow——新 StringDtype 自动用了它(§49)。没装的话
pip install pyarrow即可,pandas 检测到就自动启用。
向量化:别用循环
Python 循环逐行处理,每一行都要走一遍 Python 解释器,慢。pandas 的向量化操作整列一起算,底层是 C 代码,快几个数量级:
import pandas as pd
df = pd.DataFrame({"a": range(10000), "b": range(10000)})
# 慢:Python 循环
df["c"] = [x + y for x, y in zip(df["a"], df["b"])]
# 快:向量化,一行搞定
df["c"] = df["a"] + df["b"]
连 apply 都要谨慎——它本质还是逐行调 Python 函数。能写成向量化表达式的(加、减、乘、比较、where、clip),一律别用 apply。
query / eval:表达式引擎
df.query 用字符串写筛选条件,df.eval 用字符串写计算式。query 的语法(@ 变量、反引号列名、and/or 组合)在 §21 讲透了,这里只聊性能:它们不只是省代码,大 DataFrame 下可走 numexpr 引擎一次算完多个操作:
import pandas as pd
df = pd.DataFrame({"a": range(10000), "b": range(10000)})
# 表达式里直接创建新列(返回新 DataFrame)
df2 = df.eval("c = a + b")
df2 = df2.eval("d = c * 2")
print(df2.head())
eval 的注意点和 query 一样:局部变量必须加 @ 前缀,否则 pandas 找不到;表达式里的布尔运算用 and / or,不是平时布尔索引的 & / |。
Warningquery/eval 不是万能的。官方经验:几万行以下的小 DataFrame 用普通写法反而更快,eval 的解析开销摆在那。超过 10 万行、表达式又长又复杂时,才是它的主场。
numba:给自定义计算加速
循环无法向量化时(比如逐行依赖前面结果),还有一招:numba 即时编译,把 Python 函数编译成机器码,接近 C 的速度。两种用法:
import pandas as pd
import numpy as np
s = pd.Series(range(1000000))
# 用法一:rolling 窗口的 apply 指定 numba 引擎
def f(x):
return np.sum(x) + 5
print(s.rolling(10).apply(f, engine="numba", raw=True))
# 用法二:自己写 @jit 函数,传入 to_numpy() 的数组
import numba
@numba.jit(nopython=True)
def add_one(x):
return x + 1
print(add_one(s.to_numpy()))
numba 的代价:第一次调用要编译,慢得吓人,之后就走缓存飞快。数据量小(几万行以内)别用它,收益撑不起编译开销。numba 只擅长数值计算,字符串、日期这类操作不在它的加速范围内。
Note优化顺序记牢:1. 用
memory_usage(deep=True)找出内存大户;2. 用usecols/columns参数少加载列;3. 换类型压缩——category 收编低基数文本、downcast 压缩数值;4. 用向量化替代循环和 apply;5. 还不够再上 numba、query/eval,或分块处理。前三步通常已经解决了 90% 的问题。另外,3.0 默认开启的 Copy-on-Write(§9)让赋值语义更安全,也少了很多隐性复制开销。
小结
性能优化四板斧:memory_usage 找大户、usecols 少加载、换类型压缩、向量化替代循环。内存不够就分块,函数没法向量化就 numba,超大表达式用 query/eval。优化不是炫技,是让分析跑得完、跑得快。最后一节,看看 pandas 在数据工具江湖里的位置,和接下来往哪学。