6.1 大规模数据与内存优化


6.1 大规模数据与内存优化

本节摘要:数据从十万行涨到几千万行时,第一道墙是内存——read_csv 直接读爆内存是最常见的现场。本节给出四板斧:分块读取、类型瘦身、category 优化、Dask 分布式,并讲清每招的适用边界,让你面对大文件不再怵。

你能学到什么

阅读完本节,你应当能够:

  1. 用 chunksize 分块读取超大 CSV
  2. 通过 dtype 瘦身大幅降低内存占用
  3. 用 category 类型优化低基数类别列
  4. 说出 Dask 与 Pandas 的关系及适用场景
  5. 用 memory_usage 量化内存优化效果

一、问题与直觉

"文件 8 个 G,read_csv 一跑,内存直接爆掉,电脑卡死。"这是数据量上规模后第一个绕不开的坎。原因不复杂:Pandas 默认把每列都按最宽类型读,字符串按对象存储,一个 8G 的 CSV 读进内存可能膨胀成 30G。好在大部分膨胀都是可以避免的——读之前想清楚每列是什么类型,内存能省下一大半。

二、核心原理

2.1 内存从哪来

内存优化的四板斧按"成本从低到高"排列,先易后难:

Pandas 每列按 dtype 分配内存:float64 每元素 8 字节、int64 每元素 8 字节、object(字符串)每元素指向一个 Python 对象,开销远大于数值。优化思路就一条:让每列用最窄、最合适的类型。整数够用就别用 float,类别有限就别存成字符串。

2.2 分块读取的本质

chunksize 让 read_csv 一次只读一部分行,处理完即释放,内存占用恒定为"一块"的大小。代价是不能一次拿到全表做跨块操作——但很多统计(求和、计数)可以边读边累加,这正是大数据处理的常见模式。

三、工程实践要点

3.1 第一步:看看到底占多少内存

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) 才包含字符串对象的真实开销。先量化问题,再动手优化。

3.2 类型瘦身

df["金额"] = df["金额"].astype("float32") # float64 → float32,内存减半 df["订单量"] = df["订单量"].astype("int16") # 范围够用就降级 df["日期"] = pd.to_datetime(df["日期"]) # 字符串日期转 datetime 更省

💡 关键直觉:降级前确认数值范围。订单量最大才几千,int16(上限 32767)绰绰有余;日期字符串每个都是独立对象,转成 datetime 类型内存立减。

3.3 category:低基数类别列

# 门店只有几十种取值,却有上千万行 df["门店"] = df["门店"].astype("category") print(df["门店"].memory_usage(deep=True))

category 类型只存一份类别字典,每行存整数编码——低基数(唯一值远小于行数)字符串列用 category,内存可降一个量级。

⚠️ 常见坑:高基数列(如订单号,每行几乎都不同)转 category 反而更占内存,因为要多存一份字典。先看 nunique() 再决定。

3.4 分块读取

total = 0.0 for chunk in pd.read_csv("big_orders.csv", chunksize=100_000): total += chunk["金额"].sum() print("总销售额:", total)

边读边算,峰值内存只有 10 万行的量级。分块还能配合类型参数,两层优化叠加效果更好。

3.5 Dask:超大数据的分身术

当单块优化也不够时,Dask 可以把 Pandas 操作并行化到多核甚至集群:接口与 Pandas 高度相似,dask.dataframe 读入后按块计算,最终 compute() 时才真正执行。适用场景是"数据大到单机内存装不下且计算要并行",代价是引入新依赖与学习成本——小数据用 Dask 纯属自找麻烦。

3.6 优化策略速查

场景 策略 效果
文件超大、只需汇总统计 chunksize 分块 内存恒定
float64 精度过剩 降 float32 减半
整数范围小 降 int16/int32 大幅
低基数字符串列 category 量级下降
字符串日期 to_datetime 明显
单机装不下且要并行 Dask 上规模

3.7 实战:一次完整优化

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")

读入时就把类型定好,比读进来再转省一倍的临时内存。

3.8 内存优化的效果估算

动手优化前,先用估算决定投入产出比:

# 估算各列的合理占用 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,那必须做。

3.9 常见内存问题排查

现象 原因 解法
read_csv 直接卡死 文件过大全量载入 chunksize 分块
内存缓慢增长 循环里反复拼接 一次 concat 或列表收集
字符串列占内存高 高基数 object category 或哈希映射
处理后内存不降 副本未释放 del df + 手动 gc
Jupyter 内存爆 输出保留过多 清空输出、重启内核

💡 关键直觉:内存问题的大头通常是字符串列副本堆积。前者用 category 解决,后者记住"Pandas 操作大多产生新对象,旧对象要释放"——写长流程时定期 del 掉不再用的中间表。

3.10 分块与全量的选择

分块不是万能钥匙:需要跨块操作(全表排序、全表去重)时分块会很别扭。选型建议——纯汇总统计(求和、计数、均值)用分块;需要全局操作时,优先把数据量压到内存能装下(采样、按需过滤),实在不行再上 Dask 或数据库。先想清楚"这个分析必须全量吗",再决定技术路线

要点速记

  • 要点一:内存优化 = 让每列用最窄合适的类型,先 memory_usage 量化再动手
  • 要点二:float64 降 float32、整数降级、字符串日期转 datetime,三板斧立竿见影
  • 要点三:低基数字符串列用 category,内存量级下降;高基数别用
  • 要点四:chunksize 分块边读边算,峰值内存恒定
  • 要点五:超大且需并行用 Dask,小数据别折腾
  • 要点六:读入时定好 dtype,比读进来再改省更多临时内存

内存这关过了,数据里往往还藏着时间这个维度——下一节专门处理时间序列数据。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U