为什么分类类型能省内存?
在 Pandas 中,object 类型(字符串)每元素存储的是 Python 对象的引用,占用 8 字节指针 + 字符串对象本身,而 category 类型内部使用整数编码 + 一个唯一的映射表。当一列有大量重复值时,用整数替代字符串能大幅压缩内存。比如性别列只有 'M'/'F' 两种值,100 万行 object 列约占用 80 MB,而 category 列仅约 8 MB(整数编码 8 字节 + 映射表)------节省 90%。
最小可运行示例:从字符串到分类
下面代码演示核心转换过程。先创建一个含重复字符串的 DataFrame,然后使用 astype('category') 转换,并对比内存占用。
import pandas as pd
import numpy as np
# 模拟 10 万行重复类别数据
n = 100000
categories = ['apple', 'banana', 'orange', 'grape']
df = pd.DataFrame({'fruit': np.random.choice(categories, n)})
print('object 内存:', df['fruit'].memory_usage(deep=True))
# 输出约 8000000 字节
df['fruit_cat'] = df['fruit'].astype('category')
print('category 内存:', df['fruit_cat'].memory_usage(deep=True))
# 输出约 4000 字节(不含映射表)
# 查看分类编码
print(df['fruit_cat'].cat.codes.head())
print(df['fruit_cat'].cat.categories)
注意:memory_usage(deep=True) 会计算对象内部引用的真实内存,而 category 的映射表存储在 .cat.categories 中,计算总内存需加上它。
实战:批量处理 CSV 时的内存优化
当用 pd.read_csv() 读取大量文本列时,可以指定 dtype 参数为 'category',或者读取后用 astype 转换。更聪明的做法是让 Pandas 自动推断:使用 pd.read_csv(..., dtype='category') 会强制所有列都变 category,但可能过度。更好的策略是:先读取前 1000 行推断每列的唯一值比例,再决定哪些列转 category。
- 读取样本:
sample = pd.read_csv('large.csv', nrows=1000) - 计算每列唯一值占比:
unique_ratio = sample.nunique() / len(sample) - 对占比 < 0.5 的列转换:
convert_cols = unique_ratio[unique_ratio < 0.5].index - 正式读取时:
df = pd.read_csv('large.csv', dtype={col: 'category' for col in convert_cols})
这样既能减少内存,又不会损失数值列的精度。
注意事项与陷阱
- 排序问题 :category 列默认按字母排序,如需自定义顺序可用
pd.Categorical(..., categories=[...], ordered=True)。 - 缺失值 :NaN 会被视为一个特殊类别,
cat.codes中缺失值编码为 -1。 - 性能影响:groupby 和 value_counts 在 category 列上通常更快,但某些操作(如字符串方法)需要先转换回 object。
- 内存计算 :category 的总内存 = 编码数组(int8/16/32/64,根据类别数自动选择)+ 类别映射表,用
df.memory_usage(deep=True)查看更准确。
总结
分类类型是 Pandas 优化内存的利器,特别适合低基数(唯一值少)的字符串列。通过 astype('category') 或 read_csv 的 dtype 参数,可以轻松将内存占用降低一个数量级,同时保持代码简洁。下次处理大数据集时,不妨先检查重复列,用分类类型换取内存和速度的双赢。