分类数据在分组、聚合、透视等统计场景中有天然优势。本节介绍相关用法。


1. 频次统计:value_counts()

s = pd.Series(['a', 'b', 'a', 'c', 'b']).astype('category')
s.value_counts()
# a    2
# b    2
# c    1
# Name: count, dtype: int64

参数

参数说明
normalize是否返回比例(默认 False)
sort是否按频次排序
ascending是否升序
dropna是否排除缺失值(默认 True)
s.value_counts(normalize=True)  # 比例
s.value_counts(ascending=True)  # 升序

分类数据的 value_counts 默认 包含所有类别(即使频次为 0),与普通列不同。


2. 分组聚合:groupby()

分类列可以直接作为 groupby 键:

df = pd.DataFrame({
    '城市': ['北京', '上海', '北京', '广州', '上海'],
    '销售额': [100, 200, 150, 300, 250]
})
df['城市'] = df['城市'].astype('category')
 
# 分组求和
df.groupby('城市')['销售额'].sum()
# 城市
# 北京    250
# 上海    450
# 广州    300
# Name: 销售额, dtype: int64

groupby + 有序分类

df['等级'] = pd.Categorical(['高', '低', '中', '高', '低'],
                             categories=['低', '中', '高'], ordered=True)
 
# 按等级分组并按顺序输出
df.groupby('等级')['销售额'].sum()
# 等级
# 低    450
# 中    150
# 高    400
# Name: 销售额, dtype: int64

有序分类在 groupby 后会自动按类别顺序排列,无需额外排序。

observed 参数(重要)

pandas 2.x 中,分组时若 categorical 列存在未观察到的类别,默认 observed=False 会包含这些类别:

df['城市'] = df['城市'].cat.add_categories(['深圳'])
df.groupby('城市')['销售额'].sum(observed=False)   # 包含深圳(0 销售额)
df.groupby('城市')['销售额'].sum(observed=True)    # 只包含出现的类别
observed行为
False(默认)包含所有类别(含空组)
True只包含实际出现的类别

注意

pandas 2.x 中 observed 参数在 groupby/pivot_table/crosstab 中默认变为 False,可能导致输出中出现空组。


3. 透视表:pivot_table()

df = pd.DataFrame({
    '地区': ['华东', '华北', '华东', '华北'],
    '季度': ['Q1', 'Q1', 'Q2', 'Q2'],
    '销售额': [100, 200, 150, 180]
})
df['地区'] = df['地区'].astype('category')
df['季度'] = df['季度'].astype('category')
 
pd.pivot_table(
    df,
    values='销售额',
    index='地区',
    columns='季度',
    aggfunc='sum',
    fill_value=0
)

4. 交叉表:crosstab()

import pandas as pd
 
df = pd.DataFrame({
    '性别': ['男', '女', '男', '女'],
    '城市': ['北京', '上海', '北京', '广州']
})
df['性别'] = df['性别'].astype('category')
df['城市'] = df['城市'].astype('category')
 
pd.crosstab(df['性别'], df['城市'])
# 城市  北京  上海  广州
# 性别
# 男     2    0    0
# 女     0    1    1

crosstab 同样有 observed 参数。


5. 描述性统计

s = pd.Series(['低', '中', '高', '中']).astype(
    pd.CategoricalDtype(categories=['低', '中', '高'], ordered=True)
)
 
s.describe()
# count     4
# unique    3
# top       中
# freq      2
# Name: count, dtype: object
 
# 有序分类支持 min/max
s.min()   # '低'
s.max()   # '高'

注意

无序分类不支持 min() / max(),会抛出 TypeError。


6. 与 get_dummies 结合

直接生成哑变量

df = pd.DataFrame({'城市': ['北京', '上海', '北京', '广州']})
df['城市'] = df['城市'].astype('category')
 
pd.get_dummies(df['城市'], prefix='城市')
#    城市_北京  城市_广州  城市_上海
# 0       1       0       0
# 1       0       0       1
# 2       1       0       0
# 3       0       1       0

对 DataFrame 操作

df = pd.DataFrame({
    '城市': ['北京', '上海'],
    '级别': ['A', 'B']
})
df = df.astype('category')
 
pd.get_dummies(df, columns=['城市', '级别'])

分类数据在 get_dummies 中的优势

  • 类别顺序固定,哑变量列顺序稳定
  • 未出现的类别也能生成列,保证结构一致

7. 与排名 / 分位数结合

scores = pd.Series([88, 92, 70, 60, 99])
 
# 用 qcut 生成等级
grades = pd.qcut(scores, q=3, labels=['C', 'B', 'A'])
print(grades)
# 0    B
# 1    A
# 2    C
# 3    C
# 4    A
# Categories (3, object): ['C' < 'B' < 'A']
 
# 与原始数据组合并分组统计
df = pd.DataFrame({'分数': scores, '等级': grades})
df.groupby('等级')['分数'].agg(['mean', 'count'])

小结

  • value_counts() 包含全部类别
  • groupby 支持有序分类自动排序
  • observed 参数控制是否显示空组
  • pivot_table / crosstab 适合多维统计
  • ordered 分类支持 min/max 等比较
  • get_dummies 生成稳定哑变量