背景:2GB CSV 为什么读得慢
一个 2GB 的 CSV,pd.read_csv() 卡 8 分钟、内存飙到 12GB,这是很多人处理稍大数据时的常见场景。问题通常不在 pandas 本身,而在默认参数偏保守:它会把每列都当成可能的混合类型,先全部读进来,再逐列推断。下面 6 个技巧按改动成本从低到高排列,目标是在不换工具的前提下同时降低耗时和内存。
1. 指定 dtype,别让 pandas 猜类型
这是收益最大、改动最小的一条。- # 慢:让 pandas 推断 30 列的类型
- df = pd.read_csv('big.csv')
- # 快:直接告诉它每列是什么
- df = pd.read_csv('big.csv', dtype={
- 'user_id': 'int64',
- 'city': 'category',
- 'amount': 'float32',
- })
复制代码 两个关键点:字符串列中重复值多的列(城市、状态、品类)用 category,内存能从几百 MB 降到几十 MB;浮点数在精度够用时用 float32 代替 float64,直接省约一半内存。不知道有哪些列时,先读小样本探查:- cols = pd.read_csv('big.csv', nrows=5).columns
- sample = pd.read_csv('big.csv', nrows=10000)
- print(sample.dtypes)
- print(sample.memory_usage(deep=True).sum() / 1024**2, 'MB')
复制代码
2. 用 usecols 只读需要的列
如果文件有 50 列而只用 6 列,usecols 可以直接绕过其余 44 列的解析成本:- df = pd.read_csv('big.csv', usecols=['user_id', 'amount', 'city', 'date'])
复制代码 按列位置读通常比按列名读略快,因为省掉一次名字匹配:- df = pd.read_csv('big.csv', usecols=[0, 3, 7, 11])
复制代码
3. 日期不要在读取时用 parse_dates 解析
parse_dates 很方便,但它是逐值调用日期解析器的,在大文件上非常昂贵。更快的做法是先当字符串读进来,再一次性转换:- # 慢
- df = pd.read_csv('big.csv', parse_dates=['date'])
- # 快:先读字符串,再一次性转换
- df = pd.read_csv('big.csv')
- df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
复制代码 务必带上 format。不指定格式时 pandas 会逐值猜测;指定格式后才有机会走向量化的快路径。
4. 内存不够时用 chunksize 分块处理
这是内存不足时的标准解法:读成分块迭代器,边算边丢。- total = 0.0
- for chunk in pd.read_csv('big.csv', chunksize=200_000,
- usecols=['amount', 'city'],
- dtype={'amount': 'float32', 'city': 'category'}):
- total += chunk.groupby('city', observed=True)['amount'].sum().sum()
- print(total)
复制代码 注意两点:chunksize 不是越小越好,太小会放大 Python 循环开销,20 万到 50 万行比较合适;groupby 加 observed=True,否则 category 类型会生成全组合的笛卡尔积行,白占内存。
5. 换 pyarrow 引擎
pandas 2.x 起支持 engine='pyarrow',多线程解析,在多核机器上提速明显:- df = pd.read_csv('big.csv', engine='pyarrow', dtype_backend='pyarrow')
复制代码 没装的话先安装:dtype_backend='pyarrow' 会使用 Arrow 的原生类型(string、int32 等),内存占用通常再降一档。缺点是部分老 API 不兼容;如果导入后要和 scikit-learn 打交道,建议再 .convert_dtypes() 或直接 .to_numpy()。
6. 反复读取同一文件:先把 CSV 转成 Parquet
如果要反复读同一个文件,就别每次都解析 CSV。一次转换后,后续读取通常快 5 到 10 倍:- # 一次性转换
- df = pd.read_csv('big.csv', dtype={...})
- df.to_parquet('big.parquet', compression='snappy')
- # 后续读取:列式存储,只加载需要的列
- df = pd.read_parquet('big.parquet', columns=['user_id', 'amount'])
复制代码 Parquet 的三个好处:列式存储让 columns= 能真正跳过不用的列,不用解析整个文件;自带 schema,不用每次推断 dtype;snappy 压缩后体积通常只有原 CSV 的 20% 到 30%。不过文件太小(小于 50MB)时,Parquet 的列式开销反而不划算,直接读 CSV 更快。
附:一个通用提速模板
把上面几条打包成一个函数,日常直接复用:- import pandas as pd
- def read_fast(path, usecols=None, dtypes=None, chunksize=None):
- kwargs = dict(
- usecols=usecols,
- dtype=dtypes,
- engine='pyarrow',
- )
- if chunksize:
- return pd.read_csv(path, chunksize=chunksize, **kwargs)
- return pd.read_csv(path, **kwargs)
- # 用法
- df = read_fast(
- 'big.csv',
- usecols=['user_id', 'amount', 'city', 'date'],
- dtypes={'user_id': 'int64', 'amount': 'float32', 'city': 'category'},
- )
- df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
复制代码
排错清单
报 MemoryError:先加 usecols + category,再上 chunksize,最后考虑换 Parquet。
dtype 指定后报类型冲突:说明该列里有脏值,比如数字列混进了 'NULL' 字符串,可加 na_values=['NULL', '', 'NA']。
转换 Parquet 后读取更慢:文件太小(小于 50MB)时 Parquet 的列式开销反而不划算,CSV 直接读更快。
chunksize 循环里 groupby 结果不对:分块会把同一个 key 切到不同块,需要把每块结果再 concat 后聚合一次,别直接对每块 sum() 后相加。
小结
优先级排序:usecols + dtype(零成本,收益最大)→ 延后日期解析 → 换 pyarrow 引擎 → 分块 → 转 Parquet。大多数 “pandas 太慢” 的场景,光做前两条就能解决,根本用不到换工具。真正需要上 Dask / Polars 的,是那种内存死活放不下的单机极限场景;在那之前,先把 read_csv 的默认参数调明白。 |