Pandas 第九章 分类数据

一、cat对象

1. cat对象的属性

在 pandas 中,category类型用于处理有限、固定取值的分类变量(如年级、性别、学历等),相比普通字符串类型,它能大幅节省内存、提升运算效率,还能自定义类别顺序。

(1)分类数据的创建

复制代码
import pandas as pd

# 读取数据,提取Grade列(年级),转换为category类型
df = pd.read_csv('../data/learn_pandas.csv', usecols = ['Grade', 'Name', 'Gender', 'Height', 'Weight'])
s = df.Grade.astype('category')
s.head()

运行结果:

复制代码
0     Freshman
1     Freshman
2       Senior
3    Sophomore
4    Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman', 'Junior', 'Senior', 'Sophomore']
  • dtype: category:标识这是分类类型
  • Categories:展示该列所有不重复的类别,以Index类型存储

(2)cat 对象的核心属性

分类类型的Series会自动生成.cat访问器(类似.str处理字符串),用于操作分类数据,核心属性如下:

属性 作用 示例
s.cat.categories 获取所有类别(Index 类型) Index(['Freshman', 'Junior', 'Senior', 'Sophomore'], dtype='object')
s.cat.ordered 判断类别是否有序(默认 False) False(无序分类,如性别;有序分类如成绩等级需手动指定)
s.cat.codes 类别对应的整数编码(按categories顺序编号,从 0 开始) 示例中Freshman=0Junior=1Senior=2Sophomore=3

2.类别的增改查删

categories本质是Index类型,不可直接修改 (如s.cat.categories[0] = '新类别'会报错),必须通过cat对象的专用方法操作。

(1) 查:查询类别

  • s.cat.categories:查看所有类别
  • s.cat.codes:查看每个元素的编码
  • s.cat.ordered:查看是否有序

(2)增:添加新类别

使用add_categories()方法,在现有类别后追加新类别,不会修改原数据

复制代码
# 新增"Graduate"(毕业生)类别
s = s.cat.add_categories('Graduate')
s.cat.categories

运行结果:

复制代码
Index(['Freshman', 'Junior', 'Senior', 'Sophomore', 'Graduate'], dtype='object')

(3)删:删除类别

(a)remove_categories():删除指定类别

删除后,原数据中属于该类别的值会被设为NaN(缺失值):

复制代码
# 删除"Freshman"(大一)类别
s = s.cat.remove_categories('Freshman')
s.cat.categories
s.head()

运行结果:

复制代码
# 类别列表
Index(['Junior', 'Senior', 'Sophomore', 'Graduate'], dtype='object')
# 原数据中Freshman变为NaN
0          NaN
1          NaN
2       Senior
3    Sophomore
4    Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Junior', 'Senior', 'Sophomore', 'Graduate']
(b)remove_unused_categories():删除未使用的类别

自动移除数据中没有出现过的类别,不影响原数据

复制代码
# 假设当前类别包含'Sophomore'和'PhD',但数据中无'PhD'
s = s.cat.remove_unused_categories()
s.cat.categories

运行结果:

复制代码
Index(['Sophomore'], dtype='object')  # 仅保留数据中存在的类别
(c)set_categories():直接重置类别列表

完全替换原有类别,原数据中不属于新类别的值会被设为 NaN

复制代码
# 重置类别为['Sophomore', 'PhD']
s = s.cat.set_categories(['Sophomore','PhD'])
s.cat.categories
s.head()

运行结果:

复制代码
# 新类别列表
Index(['Sophomore', 'PhD'], dtype='object')
# 原数据中不属于新类别的值变为NaN
0          NaN
1          NaN
2          NaN
3    Sophomore
4    Sophomore
Name: Grade, dtype: category
Categories (2, object): ['Sophomore', 'PhD']

(4) 改:重命名类别

使用rename_categories()方法,会同步修改原数据中的对应值,不会产生 NaN:

复制代码
# 将"Sophomore"重命名为"本科二年级学生"
s = s.cat.rename_categories({'Sophomore':'本科二年级学生'})
s.head()

运行结果:

复制代码
0          NaN
1          NaN
2          NaN
3    本科二年级学生
4    本科二年级学生
Name: Grade, dtype: category
Categories (1, object): ['本科二年级学生']
  • 支持字典映射(旧类别名:新类别名),也支持函数批量重命名
  • 仅修改类别名称,不改变类别顺序和编码
  • 禁止直接修改s.cat.categories:必须用专用方法,否则报错
  • set_categories会清空原数据:若新类别不包含旧类别,对应值会变为 NaN,需谨慎使用
  • 忽略ordered属性:无序分类的排序结果是按类别字典序,而非业务顺序
  • remove_unused_categories清理冗余类别:定期清理未使用的类别,保持数据整洁

二.有序分类

1.序的建立

(1) 核心方法

有序类别和无序类别可以通过两个核心方法互相转化:

方法 作用 关键要求
reorder_categories(new_categories, ordered=True) 将无序分类转为有序分类,并自定义顺序 1. new_categories 必须是原类别列表的重排 ,不能新增 / 缺失类别2. 必须显式指定 ordered=True(否则方法无效)
as_unordered() 将有序分类转回无序分类 无额外要求,直接调用即可
as_ordered() 将无序分类转为有序分类(默认按原类别字典序) 可配合 reorder_categories 调整顺序

(2)示例

复制代码
import pandas as pd

# 1. 读取数据,创建无序分类
df = pd.read_csv('../data/learn_pandas.csv', usecols = ['Grade', 'Name', 'Gender', 'Height', 'Weight'])
s = df.Grade.astype('category')

# 2. 自定义年级顺序(大一 < 大二 < 大三 < 大四),转为有序分类
s = s.cat.reorder_categories(
    ['Freshman', 'Sophomore', 'Junior', 'Senior'],
    ordered=True
)
print("有序分类:\n", s.head())
# 输出:Categories (4, object): ['Freshman' < 'Sophomore' < 'Junior' < 'Senior']

# 3. 转回无序分类
s_unordered = s.cat.as_unordered()
print("\n转回无序分类:\n", s_unordered.head())
# 输出:Categories (4, object): ['Freshman', 'Sophomore', 'Junior', 'Senior'](无<符号)

(3)补充技巧

如果不想在 reorder_categories 中指定 ordered=True,可以分两步操作:

复制代码
# 先转为有序,再调整顺序
s = df.Grade.astype('category').cat.as_ordered()
s = s.cat.reorder_categories(['Freshman', 'Sophomore', 'Junior', 'Senior'])

2.排序

普通字符串列的排序是字典序 ,而有序分类的排序会严格遵循自定义的类别顺序,完全符合业务逻辑。

(1)两种排序方式

(a)按值排序:sort_values()

直接对 DataFrame 按分类列排序,顺序为自定义的类别顺序:

复制代码
# 先将Grade列转为有序分类
df.Grade = df.Grade.astype('category')
df.Grade = df.Grade.cat.reorder_categories(
    ['Freshman', 'Sophomore', 'Junior', 'Senior'],
    ordered=True
)

# 按Grade值排序
df_sorted = df.sort_values('Grade')
print(df_sorted.head())

运行结果会按「大一 → 大二 → 大三 → 大四」的顺序排列,而非字典序。

(b)按索引排序:sort_index()

将分类列设为索引后,按索引排序,同样遵循自定义顺序:

复制代码
# 将Grade设为索引,再排序
df_index_sorted = df.set_index('Grade').sort_index()
print(df_index_sorted.head())

3.比较

有序分类建立顺序后,支持两类比较操作:

(1)相等 / 不等比较(== / !=

  • 支持对象:标量同长度 Series列表

  • 特点:无论有序 / 无序分类都支持,仅判断值是否相等

  • 示例:

    1. 标量比较:筛选大二学生

    res1 = df.Grade == 'Sophomore'
    print(res1.head())

    输出:

    0 False

    1 False

    2 False

    3 True

    4 True

    Name: Grade, dtype: bool

    2. 列表比较:和全为'PhD'的列表比较

    res2 = df.Grade == ['PhD'] * df.shape[0]
    print(res2.head())

    输出全为False(无PhD类别)

(2) 大小比较(> / >= / < / <=

  • 有序分类支持,严格遵循自定义的类别顺序

  • 要求:参与比较的元素必须属于原序列的categories,且和原序列有相同索引

  • 示例:

    筛选大二及以下年级(Freshman、Sophomore)

    res3 = df.Grade <= 'Sophomore'
    print(res3.head())

    输出:

    0 True

    1 True

    2 False

    3 True

    4 True

    Name: Grade, dtype: bool

    错误示例:打乱索引后比较(索引不一致,会报错)

    res4 = df.Grade <= df.Grade.sample(frac=1).reset_index(drop=True)

有序分类的底层是整数编码 + 顺序定义

  • 类别顺序对应编码的大小(Freshman=0 < Sophomore=1 < Junior=2 < Senior=3
  • 排序、比较操作本质是对整数编码的操作,因此效率远高于字符串
错误操作 问题 正确做法
reorder_categories 新增 / 缺失类别 直接报错 必须传入原类别的完整重排列表
无序分类使用大小比较 报错TypeError 先通过reorder_categories转为有序分类
比较时索引不一致 报错ValueError 确保比较双方索引完全一致
直接修改s.cat.categories 报错 必须用reorder_categories等专用方法

三.区间类别

1.利用 cutqcut 进行区间构造

区间是特殊的分类类型,在数据分析中常用于将连续数值离散化(分箱),核心工具是pd.cut()pd.qcut()

(1)pd.cut():等距分箱

核心参数与用法

参数 作用 关键说明
bins 分箱规则 1. 传入整数n:按数据最大 / 最小值等距 分为n段2. 传入列表:自定义分箱分割点(支持np.infty表示无穷大)
right 区间开闭 默认True(左开右闭);设为False则为左闭右开
labels 区间别名 为每个区间自定义名称(替代默认的区间字符串)
retbins 是否返回分割点 默认False;设为True则返回分箱结果 + 分割点数组

关键细节:区间边界自动调整

  • 默认左开右闭时,为包含最小值,pandas 会在最小区间左端点减去 0.001*(max-min)
  • 左闭右开时,会在最大区间右端点加上 0.001*(max-min),确保包含最大值

示例

复制代码
import pandas as pd
import numpy as np

# 示例1:等距分箱
s = pd.Series([1, 2])
print("默认左开右闭分箱:\n", pd.cut(s, bins=2))
# 输出:
# 0    (0.999, 1.5]
# 1    (1.5, 2.0]
# dtype: category
# Categories (2, interval[float64]): [(0.999, 1.5] < (1.5, 2.0]]

print("\n左闭右开分箱:\n", pd.cut(s, bins=2, right=False))
# 输出:
# 0    [1.0, 1.5)
# 1    [1.5, 2.001)
# dtype: category
# Categories (2, interval[float64]): [[1.0, 1.5) < [1.5, 2.001)]

# 示例2:自定义分箱分割点
print("\n自定义分箱:\n", pd.cut(s, bins=[-np.infty, 1.2, 1.8, 2.2, np.infty]))

# 示例3:自定义别名+返回分割点
res = pd.cut(s, bins=2, labels=['small', 'big'], retbins=True)
print("\n自定义别名分箱:\n", res[0])
print("分箱分割点:\n", res[1])

(2)pd.qcut():分位数分箱

核心参数与用法

  • 核心参数q:替代cutbins,表示分位数
    • 传入整数n:按n 等分位数分箱(每个区间样本量近似相等)
    • 传入列表:自定义分位数分割点(如[0, 0.2, 0.8, 1]表示按 20%、80% 分位数分箱)
  • 其他参数(right/labels/retbins)与cut完全一致

cut的核心区别

特性 pd.cut() pd.qcut()
分箱逻辑 数值等距分箱 分位数等样本量分箱
适用场景 数据分布均匀,需固定区间宽度 数据分布不均匀,需保证每个区间样本量均衡
区间宽度 固定相等 不固定(由数据分布决定)

示例

复制代码
# 基于体重数据分箱
s = df.Weight  # 假设df为学生数据,Weight为体重列

# 示例1:3等分位数分箱
print("3等分位数分箱:\n", pd.qcut(s, q=3).head())

# 示例2:自定义分位数分箱
print("\n自定义分位数分箱:\n", pd.qcut(s, q=[0, 0.2, 0.8, 1]).head())

2.一般区间的构造

(1)pd.Interval:单个区间对象

用于表示单个区间,核心要素:左端点、右端点、开闭状态

核心属性与方法

属性 / 方法 作用
left/right 区间左 / 右端点
mid 区间中点
length 区间长度
closed 区间开闭状态(right/left/both/neither
in 运算符 判断元素是否属于区间
overlaps() 判断两个区间是否有交集

示例

复制代码
# 创建左开右闭区间(0,1]
my_interval = pd.Interval(0, 1, 'right')
print("区间对象:", my_interval)
print("0.5是否在区间内:", 0.5 in my_interval)  # True

# 创建左闭右开区间[0.5,1.5)
my_interval_2 = pd.Interval(0.5, 1.5, 'left')
print("两个区间是否相交:", my_interval.overlaps(my_interval_2))  # True

(2)pd.IntervalIndex:区间索引对象

用于存储一组区间,要求所有区间开闭状态一致,支持 4 种构造方法:

(a)from_breaks():按分割点构造(类似cut

直接传入自定义分割点,生成连续区间:

复制代码
# 按[1,3,6,10]分割,生成[1,3],[3,6],[6,10],左右闭区间
idx1 = pd.IntervalIndex.from_breaks([1,3,6,10], closed='both')
print(idx1)
(b)from_arrays():按左右端点列表构造

分别传入左端点、右端点列表,支持非连续 / 有交集的区间:

复制代码
idx2 = pd.IntervalIndex.from_arrays(
    left = [1,3,6,10],
    right = [5,4,9,11],
    closed = 'neither'
)
print(idx2)
(c)from_tuples():按元组列表构造

传入(左端点,右端点)元组列表,适合手动定义区间:

复制代码
idx3 = pd.IntervalIndex.from_tuples(
    [(1,5),(3,4),(6,9),(10,11)],
    closed='neither'
)
print(idx3)
(d)interval_range():构造等差区间序列

通过start/end/periods/freq构造等差区间(三量确定即可):

复制代码
# 从1到5,生成8个等距区间
print(pd.interval_range(start=1, end=5, periods=8))

# 到5结束,8个区间,每个长度0.5
print(pd.interval_range(end=5, periods=8, freq=0.5))
(e)补充:直接构造IntervalIndex

直接传入Interval对象列表,会强制统一开闭状态:

复制代码
# 两个不同开闭的区间,强制转为左闭右开
idx4 = pd.IntervalIndex([my_interval, my_interval_2], closed='left')
print(idx4)
  • 区间分类是特殊的有序分类,区间顺序由左端点大小决定
  • 底层存储为整数编码,支持排序、比较、分组等操作,效率远高于字符串区间
  • IntervalIndex是 pandas 的专用索引类型,可作为 DataFrame 的索引,支持区间查找、切片等操作
错误操作 问题 正确做法
cut分箱时遗漏边界值 最小值 / 最大值被排除 利用 pandas 自动边界调整,或手动指定right=False
qcut分箱时数据量不足 分位数重复导致报错 减少分箱数,或用duplicates='drop'处理
IntervalIndex中区间开闭不一致 强制转换导致区间逻辑错误 构造时统一closed参数
直接修改区间类别 报错(categories不可直接修改) cat对象的专用方法操作

3.区间的属性与方法

(1)从 cut/qcut 结果转换为 IntervalIndex

cut/qcut 的返回结果是 Categorical 类型,若要使用区间索引的专属属性 / 方法,需要先将其转换为 IntervalIndex

复制代码
import pandas as pd
import numpy as np

# 示例:对体重列分箱后转为IntervalIndex
s = df.Weight  # 假设df为学生数据,Weight为体重列
id_interval = pd.IntervalIndex(pd.cut(s, 3))  # 3等距分箱后转为区间索引

# 切片查看前3个区间
print(id_interval[:3])
# 输出:
# IntervalIndex([(33.945, 52.333], (52.333, 70.667], (70.667, 89.0]],
#               closed='right', name='Weight', dtype='interval[float64]')

(2)IntervalIndex 核心属性

IntervalIndex 继承了单个 Interval 的核心属性,批量获取所有区间的端点、中点、长度:

属性 作用 示例输出
left 所有区间的左端点 ,返回 Float64Index Float64Index([33.945, 52.333, 70.667, 33.945, 70.667], dtype='float64')
right 所有区间的右端点 ,返回 Float64Index Float64Index([52.333, 70.667, 89.0, 52.333, 89.0], dtype='float64')
mid 所有区间的中点 (左右端点均值),返回 Float64Index Float64Index([43.138999999999996, 61.5, 79.8335, 43.138999999999996, 79.8335], dtype='float64')
length 所有区间的长度 (右端点 - 左端点),返回 Float64Index Float64Index([18.387999999999998, 18.334000000000003, 18.333, 18.387999999999998, 18.333], dtype='float64')

示例

复制代码
# 选取前5个区间做演示
id_demo = id_interval[:5]
print("前5个区间:\n", id_demo)

# 批量获取属性
print("\n左端点:\n", id_demo.left)
print("右端点:\n", id_demo.right)
print("中点:\n", id_demo.mid)
print("区间长度:\n", id_demo.length)

(3)IntervalIndex 核心方法

(a)contains():批量判断元素是否在区间内

逐个判断每个区间是否包含指定元素,返回布尔数组:

复制代码
# 判断4是否在每个区间内
print(id_demo.contains(4))
# 输出:array([False, False, False, False, False])
# 原因:所有区间的左端点都远大于4,因此无包含关系
(b)overlaps():批量判断区间交集

逐个判断每个区间是否与指定的 pd.Interval 对象有交集,返回布尔数组:

复制代码
# 判断每个区间是否与(40,60)有交集
print(id_demo.overlaps(pd.Interval(40, 60)))
# 输出:array([ True,  True, False,  True, False])
# 解析:
# 1. (33.945, 52.333] 与 (40,60) 相交 → True
# 2. (52.333, 70.667] 与 (40,60) 相交 → True
# 3. (70.667, 89.0] 与 (40,60) 无交集 → False
# 4. (33.945, 52.333] 与 (40,60) 相交 → True
# 5. (70.667, 89.0] 与 (40,60) 无交集 → False
  • IntervalIndex 是 pandas 专门为区间数据设计的索引类型,底层存储区间的端点信息,支持向量化操作,效率远高于循环处理

  • 继承了 Index 的所有特性(如切片、筛选),同时新增了区间专属的属性和方法

  • Categorical 类型的区别:IntervalIndex 是索引类型,可直接作为 DataFrame 的索引,支持区间查找、切片等操作;Categorical 是列类型,用于分类数据存储

  • 直接对 cut/qcutCategorical 结果调用 IntervalIndex 方法:必须先转换为 IntervalIndex

  • 忽略区间开闭状态:contains()/overlaps() 会严格遵循区间的开闭规则

  • 非数值区间的属性计算:mid/length 仅支持数值型区间,时间区间需用 Timedelta 计算

相关推荐
MediaTea1 天前
Pandas :索引机制与数据访问
pandas
TRACER~851 天前
项目实战:pandas+pytest+allure+adb
adb·pandas·pytest
橘子编程2 天前
Django全栈开发终极指南
后端·python·django·npm·html·pandas·html5
MediaTea2 天前
Pandas:文件读写与数据接口
pandas
凌波粒3 天前
D2L学习笔记:安装、张量与数据处理
笔记·python·学习·pandas
沪漂阿龙4 天前
深入浅出 Pandas apply():从入门到向量化思维
人工智能·python·pandas
沪漂阿龙4 天前
深度解析Pandas数据组合:从concat到merge,打通你的数据处理任督二脉
python·数据分析·pandas
哈伦20195 天前
Python 生成随机数
python·机器学习·pandas
大数据魔法师6 天前
云南省天气数据可视化分析大屏的设计与实现(二)- 云南省各城市天气数据预处理
python·mysql·pandas