Pandas 分类数据处理

分类数据(categorical)是 Pandas 中一种专门处理有限、离散取值的数据类型,例如性别、等级、地区等。使用 category 类型不仅能大幅节省内存,还能提高某些操作(如 groupby、排序)的性能,同时支持自定义顺序,方便统计分析和可视化。

📌 为什么需要分类类型?

  • 节省内存:对于重复值很多的列,用整数编码代替字符串存储,内存占用可减少数倍。
  • 性能提升:分组、排序等操作直接基于底层的整数编码,速度更快。
  • 逻辑顺序:可以为类别指定顺序(如 '小' < '中' < '大'),便于排序和比较。
  • 数据完整性:限制取值只能在预定义的类别中,防止脏数据。

📊 创建分类数据

Pandas 中可以通过多种方式将列转换为 category 类型:

使用 astype()
df['性别'] = df['性别'].astype('category')
使用 pd.Categorical()
cat = pd.Categorical(['a','b','a'], categories=['a','b','c'], ordered=False)
使用 pd.CategoricalDtype()
dtype = pd.CategoricalDtype(categories=['低','中','高'], ordered=True)
df['等级'] = df['等级'].astype(dtype)
import pandas as pd
import numpy as np

# 创建示例数据
df = pd.DataFrame({
    '姓名': ['张三', '李四', '王五', '赵六', '陈七'],
    '部门': ['技术', '销售', '技术', '管理', '销售'],
    '绩效': ['B', 'A', 'C', 'B', 'A']
})
print(df.dtypes)

# 转换为分类类型
df['部门'] = df['部门'].astype('category')
df['绩效'] = pd.Categorical(df['绩效'], categories=['A','B','C'], ordered=True)

print(df.dtypes)
print(df[['部门', '绩效']].head())

🔍 分类类型的属性

通过 .cat 访问器可以获取分类相关的属性和方法。

# 查看类别
print(df['部门'].cat.categories)

# 查看编码后的整数(每个类别对应的数字)
print(df['部门'].cat.codes)

# 是否有序
print(df['绩效'].cat.ordered)  # True

# 查看所有类别(即使数据中没有出现)
print(df['绩效'].cat.categories)

✏️ 重命名类别

使用 rename_categories() 可以批量重命名类别。

# 将部门英文改为中文(演示)
df['部门'] = df['部门'].cat.rename_categories({'技术': 'Engineering', '销售': 'Sales', '管理': 'Management'})
print(df['部门'].cat.categories)

# 传入列表(长度必须等于原类别数)
df['绩效'] = df['绩效'].cat.rename_categories(['优秀', '良好', '合格'])
print(df['绩效'].cat.categories)

➕ 添加/删除类别

如果新数据中出现未定义的类别,可以通过 add_categories()set_categories() 处理。

# 添加新类别
df['部门'] = df['部门'].cat.add_categories(['HR', 'Finance'])
print(df['部门'].cat.categories)

# 删除类别(注意:如果数据中有该类别,会变成 NaN)
df['部门'] = df['部门'].cat.remove_categories(['Finance'])  # 假设没有数据

# 直接设置新类别(可以同时增删,不在新类别中的原值变为 NaN)
df['部门'] = df['部门'].cat.set_categories(['Engineering', 'Sales', 'HR'])

🔢 有序分类

有序分类可以比较大小、排序。

# 创建有序分类
s = pd.Series(['B', 'A', 'C'], dtype=pd.CategoricalDtype(categories=['A','B','C'], ordered=True))
print(s > 'B')  # 输出: [False, False, True]

# 转换为有序/无序
df['绩效'] = df['绩效'].cat.as_ordered()   # 如果原本无序
df['绩效'] = df['绩效'].cat.as_unordered() # 转为无序

# 排序
df_sorted = df.sort_values('绩效')
print(df_sorted[['姓名', '绩效']])

🧠 内存优化示例

通过将对象类型转为分类,可以显著减少内存占用,特别是重复值多的列。

# 创建一个大型 DataFrame
n = 100000
df_large = pd.DataFrame({
    '部门': np.random.choice(['技术', '销售', '管理', '财务'], n),
    '评分': np.random.choice(['A','B','C','D'], n)
})

print("转换前内存使用:")
print(df_large.memory_usage(deep=True))

# 转换为分类
df_large['部门'] = df_large['部门'].astype('category')
df_large['评分'] = df_large['评分'].astype('category')

print("转换后内存使用:")
print(df_large.memory_usage(deep=True))

🔄 分类数据与 get_dummies

分类变量常用于生成虚拟变量(one-hot encoding)。

# 直接使用 get_dummies
dummies = pd.get_dummies(df['部门'])
print(dummies.head())

# 如果只想对某些分类列生成虚拟变量
df_with_dummies = pd.get_dummies(df, columns=['部门'], prefix='dept')
print(df_with_dummies.head())

🧪 综合示例:处理调查问卷数据

import pandas as pd

# 模拟用户满意度调查数据
survey = pd.DataFrame({
    'user_id': range(1, 11),
    '满意度': ['满意', '非常满意', '一般', '不满意', '满意', '一般', '非常满意', '满意', '不满意', '一般'],
    '频次': ['每天', '每周', '每月', '每天', '每天', '从不', '每周', '每月', '每天', '每周']
})

print("原始数据:")
print(survey)

# 1. 将满意度转为有序分类(不满意 < 一般 < 满意 < 非常满意)
满意程度 = ['不满意', '一般', '满意', '非常满意']
survey['满意度'] = pd.Categorical(survey['满意度'], categories=满意程度, ordered=True)

# 2. 将频次转为分类(无需顺序)
survey['频次'] = survey['频次'].astype('category')

# 3. 检查类型
print("\n数据类型:")
print(survey.dtypes)

# 4. 统计各满意度人数
print("\n满意度分布:")
print(survey['满意度'].value_counts())

# 5. 按满意度排序(按顺序)
print("\n按满意度排序:")
print(survey.sort_values('满意度')[['user_id', '满意度']])

# 6. 交叉表:满意度 vs 频次
cross = pd.crosstab(survey['满意度'], survey['频次'])
print("\n交叉表:")
print(cross)

# 7. 内存使用对比(此处数据小,仅示意)
print("\n内存占用:")
print(survey.memory_usage(deep=True))

⚠️ 注意事项

缺失值处理:分类类型中的缺失值仍用 NaN 表示,不会计入类别中。
类别不可变:默认情况下,不能直接给分类列赋值不在已有类别中的新值,除非先用 add_categories() 扩展类别。
性能陷阱:虽然分类通常能提升 groupby 等操作速度,但在某些操作(如 str 方法)中,分类类型需要先转为字符串,可能反而变慢。
类别顺序:有序分类的比较和排序遵循定义的顺序,而不是字典序。
最佳实践:
  • 当列的唯一值个数远小于行数时(如性别、省份等),强烈建议转为分类以节省内存。
  • 对于有序的离散变量(如评分等级),使用有序分类可以保持逻辑顺序。
  • 使用 .cat.codes 可以获取底层的整数编码,方便机器学习模型输入。
  • 在合并 DataFrame 时,注意两个分类列的类别集可能不同,可使用 union_categoricals() 合并。