本节摘要:数据从十万行涨到几千万行时,第一道墙是内存——read_csv 直接读爆内存是最常见的现场。本节给出四板斧:分块读取、类型瘦身、category 优化、Dask 分布式,并讲清每招的适用边界,让你面对大文件不再怵。
阅读完本节,你应当能够:
"文件 8 个 G,read_csv 一跑,内存直接爆掉,电脑卡死。"这是数据量上规模后第一个绕不开的坎。原因不复杂:Pandas 默认把每列都按最宽类型读,字符串按对象存储,一个 8G 的 CSV 读进内存可能膨胀成 30G。好在大部分膨胀都是可以避免的——读之前想清楚每列是什么类型,内存能省下一大半。
内存优化的四板斧按"成本从低到高"排列,先易后难:
Pandas 每列按 dtype 分配内存:float64 每元素 8 字节、int64 每元素 8 字节、object(字符串)每元素指向一个 Python 对象,开销远大于数值。优化思路就一条:让每列用最窄、最合适的类型。整数够用就别用 float,类别有限就别存成字符串。
chunksize 让 read_csv 一次只读一部分行,处理完即释放,内存占用恒定为"一块"的大小。代价是不能一次拿到全表做跨块操作——但很多统计(求和、计数)可以边读边累加,这正是大数据处理的常见模式。
import pandas as pd df = pd.read_csv("big_orders.csv") print(df.memory_usage(deep=True).sum() / 1024**2, "MB")
memory_usage(deep=True) 才包含字符串对象的真实开销。先量化问题,再动手优化。
df["金额"] = df["金额"].astype("float32") # float64 → float32,内存减半 df["订单量"] = df["订单量"].astype("int16") # 范围够用就降级 df["日期"] = pd.to_datetime(df["日期"]) # 字符串日期转 datetime 更省
💡 关键直觉:降级前确认数值范围。订单量最大才几千,int16(上限 32767)绰绰有余;日期字符串每个都是独立对象,转成 datetime 类型内存立减。
# 门店只有几十种取值,却有上千万行 df["门店"] = df["门店"].astype("category") print(df["门店"].memory_usage(deep=True))
category 类型只存一份类别字典,每行存整数编码——低基数(唯一值远小于行数)字符串列用 category,内存可降一个量级。
⚠️ 常见坑:高基数列(如订单号,每行几乎都不同)转 category 反而更占内存,因为要多存一份字典。先看
nunique()再决定。
total = 0.0 for chunk in pd.read_csv("big_orders.csv", chunksize=100_000): total += chunk["金额"].sum() print("总销售额:", total)
边读边算,峰值内存只有 10 万行的量级。分块还能配合类型参数,两层优化叠加效果更好。
当单块优化也不够时,Dask 可以把 Pandas 操作并行化到多核甚至集群:接口与 Pandas 高度相似,dask.dataframe 读入后按块计算,最终 compute() 时才真正执行。适用场景是"数据大到单机内存装不下且计算要并行",代价是引入新依赖与学习成本——小数据用 Dask 纯属自找麻烦。
| 场景 | 策略 | 效果 |
|---|---|---|
| 文件超大、只需汇总统计 | chunksize 分块 | 内存恒定 |
| float64 精度过剩 | 降 float32 | 减半 |
| 整数范围小 | 降 int16/int32 | 大幅 |
| 低基数字符串列 | category | 量级下降 |
| 字符串日期 | to_datetime | 明显 |
| 单机装不下且要并行 | Dask | 上规模 |
import pandas as pd df = pd.read_csv( "big_orders.csv", dtype={"门店": "category", "订单量": "int16"}, parse_dates=["日期"], ) before = df.memory_usage(deep=True).sum() / 1024**2 df["金额"] = df["金额"].astype("float32") after = df.memory_usage(deep=True).sum() / 1024**2 print(f"优化前 {before:.0f} MB → 优化后 {after:.0f} MB")
读入时就把类型定好,比读进来再转省一倍的临时内存。
动手优化前,先用估算决定投入产出比:
# 估算各列的合理占用 def estimate(rows, cols): """按行数粗估 DataFrame 内存(MB)。""" per_row = sum(8 if c in ("float64", "int64") else 1 for c in cols) return rows * per_row / 1024**2
经验法则:int64/float64 每元素 8 字节、int32/float32 4 字节、category 约 1-2 字节。先算账再动手——如果只省几十 MB,就不值得为类型优化折腾;如果省几个 G,那必须做。
| 现象 | 原因 | 解法 |
|---|---|---|
| read_csv 直接卡死 | 文件过大全量载入 | chunksize 分块 |
| 内存缓慢增长 | 循环里反复拼接 | 一次 concat 或列表收集 |
| 字符串列占内存高 | 高基数 object | category 或哈希映射 |
| 处理后内存不降 | 副本未释放 | del df + 手动 gc |
| Jupyter 内存爆 | 输出保留过多 | 清空输出、重启内核 |
💡 关键直觉:内存问题的大头通常是字符串列和副本堆积。前者用 category 解决,后者记住"Pandas 操作大多产生新对象,旧对象要释放"——写长流程时定期
del掉不再用的中间表。
分块不是万能钥匙:需要跨块操作(全表排序、全表去重)时分块会很别扭。选型建议——纯汇总统计(求和、计数、均值)用分块;需要全局操作时,优先把数据量压到内存能装下(采样、按需过滤),实在不行再上 Dask 或数据库。先想清楚"这个分析必须全量吗",再决定技术路线。
内存这关过了,数据里往往还藏着时间这个维度——下一节专门处理时间序列数据。