pandas.crosstab用于快速生成二维交叉频数表,支持行/列/整体归一化及边际汇总;三维及以上需用pivot_table;分类变量转category可提速5–10倍,NaN需显式填充才参与统计。

用 pandas.crosstab 快速生成二维交叉表
直接调用 pandas.crosstab 是最常用方式,它默认对两个分类变量做频数汇总。比如有 df['gender'] 和 df['region'],执行 pd.crosstab(df['gender'], df['region']) 就返回行列分别为 gender 和 region 的频数矩阵。
注意:输入必须是 Series 或长度一致的数组,传入 DataFrame 列时别加方括号套成二维(如 df[['gender']] 会报错 ValueError: arrays must all be same length)。
常见需求可直接通过参数满足:
-
normalize='index'计算行百分比(每行和为 1) -
normalize='columns'计算列百分比 -
normalize=True计算整体比例(所有单元格和为 1) -
margins=True自动添加行/列合计(即边际频率),结果中新增All行和列
扩展到三维及以上:用 pd.pivot_table 替代 crosstab
pandas.crosstab 最多只支持两个 index 变量(index 和 columns 参数),无法直接处理三个及以上维度。此时应切换到 pd.pivot_table,它更灵活且原生支持多维分组。
立即学习“Python免费学习笔记(深入)”;
例如要按 gender、region、age_group 三重分组统计人数:
pd.pivot_table(df,
index=['gender', 'region'],
columns='age_group',
aggfunc='size',
fill_value=0)
关键点:
-
aggfunc='size'等价于计数;若需其他统计量(如均值),可换为'mean'或自定义函数 -
fill_value=0避免出现NaN单元格(空组合默认不显式填充) - 想加边际合计?加上
margins=True—— 它会同时在行末、列末加All,但注意:三维 pivot_table 的 margins 是对最外层 index 和所有 columns 分别汇总,不是全维度总和
手动计算高维边际频率:用 groupby().size() + unstack()
当需要特定维度的边际(比如只要「gender × region」的合计,不管 age_group),靠 margins=True 可能不够精确,因为它的逻辑固定。更可控的方式是分步聚合:
先获取完整多维频数:
full = df.groupby(['gender', 'region', 'age_group']).size().unstack(fill_value=0)
再分别提取边际:
-
full.sum(level=['gender', 'region'])→ 压缩掉age_group维度,得二维边际 -
full.sum(level='gender')→ 按 gender 汇总所有 region 和 age_group -
full.sum().sum()→ 全局总计(标量)
这种方式优势在于:维度控制明确、可链式操作、兼容缺失组合(unstack(fill_value=0) 已处理),缺点是内存占用略高——尤其当原始数据稀疏但维度多时,unstack() 可能产生大量零值。
性能与类型陷阱:分类变量没设好会拖慢 5–10 倍
如果参与交叉分析的列是字符串类型且取值有限(如 status 仅有 'active'/'inactive'),务必提前转成 category 类型:
df['status'] = df['status'].astype('category')
原因:pandas 对 category 类型做 groupby/crosstab 时跳过排序和哈希推断,直接查表映射,实测在万级数据、5+分类水平下提速明显。反之,若留作 object 类型,不仅慢,还可能因隐式类型转换导致 crosstab 报 TypeError: unhashable type: 'list'(比如某单元格不小心存了 list)。
另一个易忽略点:crosstab 和 pivot_table 默认会自动排除 NaN,但如果你希望把缺失值当作一个独立类别统计,得先用 df.fillna({'col': 'MISSING'}) 显式替换,不能依赖 dropna=False —— 后者在 crosstab 中仅影响索引构建逻辑,不改变 NaN 的处理行为。
多维交叉本身不难,难的是让边际含义和实际业务口径对齐。别太依赖 margins=True 的“自动”结果,先手写一两条 groupby().size() 验证数值是否符合预期,再批量展开。


















