如何让 pandas crosstab 保留所有分类级别(包括未出现的类别)

`pd.crosstab` 默认会丢弃未在数据中实际出现的分类级别,需显式设置 `dropna=false` 才能完整显示 categorical 的全部 categories。

在使用 pandas.crosstab 对分类数据(Categorical)进行交叉表统计时,一个常见误区是认为只要输入列是 CategoricalDtype 类型(含预定义的完整 categories),输出就自动包含所有级别。实际上,默认行为仍会过滤掉未在观测数据中出现的类别——这与 value_counts() 等方法的行为不同。

关键在于:crosstab 的 dropna 参数默认为 True,它不仅影响缺失值(NaN)的处理,也控制是否保留空分类级别。只有当 dropna=False 时,crosstab 才会严格遵循输入 Categorical 的 categories 属性,将所有预设类别(包括零频次的)纳入行/列索引,并用 0 填充对应单元格。

以下是一个完整示例:

import pandas as pd
from pandas.api.types import CategoricalDtype

# 定义包含 "a", "b", "c" 的完整类别
ctype = CategoricalDtype(categories=["a", "b", "c"])
df = pd.DataFrame({
    "x": ["a", "a", "a"],
    "y": ["b", "a", "b"]
}, dtype=ctype)

# ❌ 默认 dropna=True → 仅显示实际出现的类别
print("默认 dropna=True:")
print(pd.crosstab(df["x"], df["y"]))
# 输出:
# y  a  b
# x
# a  1  2

# ✅ 显式设置 dropna=False → 补全所有 categories
print("\n设置 dropna=False:")
print(pd.crosstab(df["x"], df["y"], dropna=False))
# 输出:
# y  a  b  c
# x         
# a  1  2  0
# b  0  0  0
# c  0  0  0

⚠️ 注意事项:

  • dropna=False 是必要且充分条件:仅声明 CategoricalDtype 不够,必须配合该参数;
  • 若 index 或 columns 中任一输入为 Categorical,则 dropna=False 会同时作用于二者的所有预设类别;
  • 对非 Categorical 输入(如普通字符串 Series),dropna=False 仅影响 NaN 处理,不会引入新标签
  • 该行为适用于 pd.crosstab 所有版本(≥1.0.0),但旧版文档曾表述模糊,建议以官方最新文档为准。

总结:要确保交叉表完整呈现分类变量的语义结构(尤其在建模前特征对齐、报表一致性等场景),务必在调用 crosstab 时显式指定 dropna=False。