今天聊的话题,我们在数据处理中,经常会遇到这样的场景:明明数据量不算 “天文数字”,但用 Pandas 加载后却变得异常卡顿,甚至出现 “内存不足” 的报错。这其实是因为 Pandas 在默认情况下,对数据的存储方式并不 “精打细算”—— 就像用大箱子装小物件,看似方便却浪费了大量空间。今天我们一起梳理Pandas 的内存压缩技术,通过简单的操作让数据处理效率大幅提升。
一、关于Pandas 的内存占用
凡事都需要从原理切入进来,当我们用 Pandas 处理数据时,所有数据都会被加载到计算机内存(RAM)中。如果数据占用的内存超过了系统可用内存,轻则程序运行变慢(需要频繁与硬盘交换数据),重则直接崩溃。比如处理 100 万行的用户数据时,若内存占用从 2GB 降到 200MB,不仅能避免 “内存溢出” 错误,还能让数据筛选、计算等操作度提升数倍。
简单来说:优化内存不是 “炫技”,而是让数据处理从 “卡到用不了” 变成 “流畅高效” 的关键。
1、 内存占用分析
在优化内存之前,我们首先需要知道:当前数据到底占用了多少内存?哪些字段是 “内存大户”?
Pandas 提供了一个简单却强大的工具:df.info(memory_usage='deep')。其中memory_usage='deep'参数会精确计算所有数据(包括字符串)的实际内存占用,而不是估算值。
我们用一个示例来演示。假设我们有一份包含 “用户 ID、年龄、性别、职业” 的数据集(模拟 10 万行数据):
import pandas as pd
import numpy as np
# 生成模拟数据(10万行)
data = {
"user_id": np.arange(100000), # 用户ID:0到99999的整数
"age": np.random.randint(0, 120, size=100000), # 年龄:0到120的整数
"gender": np.random.choice(["男", "女", "未知"], size=100000), # 性别:3种重复字符串
"occupation": np.random.choice(["学生", "教师", "工程师", "医生", "其他"], size=100000) # 职业:5种重复字符串
}
df = pd.DataFrame(data)
# 查看内存占用
print("原始数据内存占用:")
df.info(memory_usage='deep')
运行后会得到类似这样的结果:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100000 entries, 0 to 99999
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 100000 non-null int64
1 age 100000 non-null int64
2 gender 100000 non-null object
3 occupation 100000 non-null object
dtypes: int64(2), object(2)
memory usage: 26.6 MB
从结果中我们能看到:
- 整数类型(user_id、age)默认用int64存储;
- 字符串类型(gender、occupation)默认用object存储;
- 总内存占用约 26.6 MB。
接下来,我们就针对这两类数据,用不同的压缩技术 “瘦身”。
二、类型降级
Pandas 中,整数默认用int64(64 位)存储,浮点数默认用float64(64 位)。但很多时候,我们的数据根本用不到这么大的存储空间。
比如 “年龄” 字段:人类年龄最大一般不超过 120,用int8(范围 - 128 到 127)就足够存储;“用户 ID” 如果是 10 万以内的数字,int32(最大能存 21 亿)已经绰绰有余,甚至int16(最大 32767)如果 ID 范围更小也能使用。
这种 “用更小的类型存储数据” 的操作,就是类型降级,通过df.astype({'列名': '目标类型'})实现。
1、操作步骤与效果验证
我们针对示例中的user_id和age进行类型降级:
# 类型降级:根据数据范围选择合适的类型
df_compressed = df.copy()
# user_id最大99999,int32足够(int32范围:-2147483648到2147483647)
df_compressed['user_id'] = df_compressed['user_id'].astype('int32')
# age最大120,int8足够(int8范围:-128到127)
df_compressed['age'] = df_compressed['age'].astype('int8')
# 查看压缩后内存
print("\n类型降级后内存占用:")
df_compressed.info(memory_usage='deep')
运行后结果:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100000 entries, 0 to 99999
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 100000 non-null int32
1 age 100000 non-null int8
2 gender 100000 non-null object
3 occupation 100000 non-null object
dtypes: int32(1), int8(1), object(2)
memory usage: 18.6 MB
对比原始数据,仅类型降级就把内存从 26.6 MB 降到了 18.6 MB,减少了约 30%!
2、关于“数据溢出”
类型降级的核心是 “匹配数据范围”,如果选择的类型太小,会导致数据溢出(比如用int8存 128,结果会变成错误值 - 128)。因此需要先确认数据的最大 / 最小值:
# 查看数值列的范围,确定合适的类型
print("user_id范围:", df['user_id'].min(), "~", df['user_id'].max()) # 0 ~ 99999
print("age范围:", df['age'].min(), "~", df['age'].max()) # 0 ~ 119
常用数值类型的范围参考:
- int8:-128 ~ 127(适合年龄、评分等小范围整数)
- int16:-32768 ~ 32767(适合万级以内的整数)
- int32:-21 亿~21 亿(适合亿级以内的整数)
- int64:极大范围(仅当数据超过 int32 时使用)
三、用 Category 类型处理重复字符串
字符串是 Pandas 中的 “内存杀手”。默认情况下,Pandas 用object类型存储字符串,每个字符串都会单独占用内存。比如 “性别” 字段有 10 万行,但实际只有 “男”“女”“未知” 3 个值,却要存储 10 万个字符串副本,非常浪费。
category类型的原理很简单:把重复的字符串变成 “编号”。比如用 0 代表 “男”、1 代表 “女”、2 代表 “未知”,然后只存储编号和对应关系。这样 10 万行数据只需要存储 10 万个编号(整数),加上 3 个字符串的对应表,内存占用会大幅降低。
1、操作步骤与效果验证
我们对示例中的gender和occupation(都是重复率高的字符串)使用category类型:
# 对重复字符串列使用category类型
df_compressed['gender'] = df_compressed['gender'].astype('category')
df_compressed['occupation'] = df_compressed['occupation'].astype('category')
# 查看最终压缩后内存
print("\n类型降级+Category后内存占用:")
df_compressed.info(memory_usage='deep')
运行后结果:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100000 entries, 0 to 99999
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 100000 non-null int32
1 age 100000 non-null int8
2 gender 100000 non-null category
3 occupation 100000 non-null category
dtypes: category(2), int32(1), int8(1)
memory usage: 2.1 MB
惊人的变化!总内存从最初的 26.6 MB 降到了 2.1 MB,压缩率超过 90%!
2、哪些场景适合用 Category 类型?
category类型不是万能的,它的优势体现在重复率高的字符串:
- 适合:性别(2-3 个值)、职业(10 个以内)、省份(34 个)等重复率 > 50% 的字段;
- 不适合:随机字符串(如 UUID)、几乎无重复的文本(如评论内容)—— 这类数据转换后反而可能更占内存。
可以用nunique()查看字符串的唯一值数量,判断是否适合转换:
# 查看字符串列的唯一值数量
print("gender唯一值数量:", df['gender'].nunique()) # 3
print("occupation唯一值数量:", df['occupation'].nunique()) # 5
最后小结
通过上面的示例,我们能总结出 Pandas 内存压缩的核心逻辑:根据数据的实际特征选择存储方式,避免默认类型的冗余。
- 数值型数据:用 “刚好能装下” 的类型(如 int8 替代 int64);
- 字符串数据:重复率高就用 category(用编号替代重复副本)。
除了这两种方法,还有一些扩展思路可以结合使用:
- 对浮点数:类似整数,可用float32替代float64(精度要求不高时);
- 大文件处理:用chunksize分块加载数据,避免一次性占用大量内存;
- 存储格式优化:将数据保存为 Parquet、Feather 等二进制格式(比 CSV 更省空间,加载更快)。
我想这就是内存压缩的价值:不增加硬件成本,只通过 “精打细算” 提升数据处理能力。未完待续.......