pandas 如何处理 category 类型列的内存优化与排序问题

category类型能省内存,但仅适用于唯一值占比低于50%的低基数字符串列,如性别、省份等,可省60%–90%内存;高基数列反而增加开销。

category 类型真能省内存?先看它适合什么场景

能省,但不是所有字符串列都适合。关键看「唯一值数量 / 总行数」是否明显小于 0.5(即重复度高)。比如性别、省份、订单状态这类低基数(low-cardinality)列,转 category 后内存常减少 60%–90%;而用户昵称、日志消息这种几乎每行都不同的列,转了反而多一层索引开销。

  • 唯一值占比 category
  • 唯一值占比 10%–50%:视数据量而定,百万行以上通常仍受益
  • 唯一值占比 > 50%:基本不省,还可能拖慢某些操作(如 str.contains()

用这行快速检查:

df['col'].nunique() / len(df)

别只看 df.info() 的 memory usage —— 它默认不 deep,得加 deep=True 才算真实字符串内存。

排序时 category 列为啥更快?但默认顺序可能不是你想要的

因为底层存的是整数编码(0, 1, 2…),排序实际是对整数数组操作,比逐个比较字符串快 3–5 倍。但注意:默认排序按「类别出现顺序」或「字典序」,不是你业务里的逻辑顺序(比如 “高” > “中” > “低”)。

  • 要自定义顺序,必须显式创建 CategoricalDtype
    from pandas.api.types import CategoricalDtype
    cat_type = CategoricalDtype(categories=['低', '中', '高'], ordered=True)
    df['level'] = df['level'].astype(cat_type)
  • 直接 df['level'].astype('category') 不带 ordered=True,后续 sort_values() 会按字典序排,且无法做大小比较(df['level'] > '中' 报错)
  • sort_values() 对已设 ordered=True 的列,自动按定义顺序升序;降序就加 ascending=False,无需额外映射

读取阶段就优化,别等加载完再转

pd.read_csv() 把整列读成 object 再转 category,中间已占用大量内存。应该在读取时直接指定 dtype:

  • 方法一(推荐):用 dtype 参数:
    df = pd.read_csv('data.csv', dtype={'status': 'category', 'region': 'category'})
  • 方法二:对数值列同步降级,避免 int64 浪费:
    dtype={'user_id': 'uint32', 'score': 'float32'}
  • 方法三:用 convert_dtypes() 做兜底(但它不会自动把字符串转 category,只转为 string 类型)

漏掉这步,100 万行含 3 个高重复字符串列的数据,可能多占 200MB+

内存,且后续所有 groupbysort_values 都更慢。

容易被忽略的坑:category 列参与 merge 或 filter 时的行为
  • merge 时若左右 DataFrame 的 category 列 categories 不一致(比如左有 ['A','B'],右有 ['B','C']),结果列会自动转回 object,内存和性能优势瞬间消失
  • filter 时写 df[df['cat_col'] == 'X'] 没问题,但用 isin() 要小心:df[df['cat_col'].isin(['X','Y'])] 若 'Y' 不在 categories 中,会静默返回空 —— 不报错,但结果不对
  • 保存为 parquet 时默认保留 category 结构,但存 CSV 会变回字符串;重读 CSV 若没指定 dtype,又回到起点

真正省内存,靠的不是“转一次”,而是从读入、处理到输出全程保持类型意识。一个列转了 category,不代表它就永远安全了——任何隐式类型推断操作(比如 pd.concat()merge()、甚至某些 apply())都可能把它悄悄打回原形。