import numpy as np
import pandas as pd
9.1 cat对象
9.1.1 cat对象的属性
pandas提供了category类型,使用户能够处理分类类型的变量,将一个普通序列转换成分类变量,可以使用astype方法
df = pd.read_csv('learn_pandas.csv',usecols = ['Grade', 'Name', 'Gender', 'Height', 'Weight'])
df.head()
Grade | Name | Gender | Height | Weight | |
---|---|---|---|---|---|
0 | Freshman | Gaopeng Yang | Female | 158.9 | 46.0 |
1 | Freshman | Changqiang You | Male | 166.5 | 70.0 |
2 | Senior | Mei Sun | Male | 188.9 | 89.0 |
3 | Sophomore | Xiaojuan Sun | Female | NaN | 41.0 |
4 | Sophomore | Gaojuan You | Male | 174.0 | 74.0 |
# 分类结果Grade一共有4类
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']
在一个分类类型的Series中定义了cat对象,和上一章介绍的str对象类似,定义了一些属性和方法来进行分类类别的操作
s.cat
对于一个具体的分类,有两个组成部分:一个是类别的本身categories,以Index类型存储;二为是否有序ordered,它们都可以通过cat属性被访问
s.cat.categories
Index(['Freshman', 'Junior', 'Senior', 'Sophomore'], dtype='object')
s.cat.ordered
False
每一个序列的类别会被赋予唯一的整数编号codes,它们的编号取决于cat.categories中的顺序,该属性可以通过codes访问
0:Freshman, 1:Junior, 2:Senior, 3:Sophomore
s.cat.codes.head()
0 0
1 0
2 2
3 3
4 3
dtype: int8
9.1.2 类别的增加、删除和修改
通过cat对象的categories属性能够完成对类别的查询
类别不得直接修改:
在第三章中曾提到,索引 Index 类型是无法用 index_obj[0] = item 来修改的,而 categories 被存储在 Index 中,因此 pandas 在 cat 属性上定义了若干方法来达到相同的目的。
# 对于类别的增加可以用add_categories:
s = s.cat.add_categories('Graduate')
s.cat.categories
Index(['Freshman', 'Junior', 'Senior', 'Sophomore', 'Graduate'], dtype='object')
删除某一个类别可以用remove_categories, 所有原来序列中的该类会被设置为缺失,如删除大一的类别:
s = s.cat.remove_categories('Freshman')
s.cat.categories
Index(['Junior', 'Senior', 'Sophomore', 'Graduate'], dtype='object')
# 被删除的序列置为NaN
s.head()
0 NaN
1 NaN
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Junior', 'Senior', 'Sophomore', 'Graduate']
可以使用set_categories直接设置序列的新类别,原来的类别中如果存在元素不属于新类别,那么会被设置为缺失
s = s.cat.set_categories(['Sophomore', 'PhD'])
s.cat.categories
Index(['Sophomore', 'PhD'], dtype='object')
# 原先类别的Senior等都被设置为NaN
s.head(7)
0 NaN
1 NaN
2 NaN
3 Sophomore
4 Sophomore
5 NaN
6 NaN
Name: Grade, dtype: category
Categories (2, object): ['Sophomore', 'PhD']
# 如果想要删除未出现在序列中的类别,可以使用remove_unused_categories来实现
s = s.cat.remove_unused_categories()
# 移除了未出现的PhD类别
s.cat.categories
Index(['Sophomore'], dtype='object')
修改操作可以通过rename_categories方法完成,这个方法会对原序列的对应值也进行相应修改,例如把Sophomore改为中文的 本科二年级学生:
s = s.cat.rename_categories({'Sophomore':'本科二年级学生'})
s.head()
0 NaN
1 NaN
2 NaN
3 本科二年级学生
4 本科二年级学生
Name: Grade, dtype: category
Categories (1, object): ['本科二年级学生']
9.2 有序分类
9.2.1 序的建立
有序类别和无序类别可以通过as_unordered和reorder_categories 互相转化
reorder_categories传入的参数必须是由当前序列的无需类别构成的列表,不能够增加新的类别,也不能缺少原来的类别,并且必须指定参数ordered=True,否则方法无效
如,对年级高低进行相对大小的类别划分,然后再恢复无序状态:
s = df.Grade.astype('category')
# 对年级高低进行从小到大的排序
s = s.cat.reorder_categories(['Freshman','Sophomore', 'Junior', 'Senior'], ordered=True)
s.head()
0 Freshman
1 Freshman
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman' < 'Sophomore' < 'Junior' < 'Senior']
# 再恢复无序状态
s.cat.as_unordered().head()
0 Freshman
1 Freshman
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman', 'Sophomore', 'Junior', 'Senior']
类别不得直接修改
如果不想指定 ordered=True 参数,那么可以先用 s.cat.as_ordered() 转化为有序类别,再利用reorder_categories 进行具体的相对大小调整。9.2.2 排序和比较
分类变量的排序,只需把列的类型修改为category后,再赋予相应大小关系,就能正常地使用sort_index和sort_values。如对年级进行排序:
df.Grade = df.Grade.astype('category')
df.Grade = df.Grade.cat.reorder_categories(['Freshman','Sophomore', 'Junior', 'Senior'], ordered=True)
# 根据Grade从小到大排序
# 对列的值进行排序,根据顺序规则排序
df.sort_values('Grade').head()
Grade | Name | Gender | Height | Weight | |
---|---|---|---|---|---|
0 | Freshman | Gaopeng Yang | Female | 158.9 | 46.0 |
105 | Freshman | Qiang Shi | Female | 164.5 | 52.0 |
96 | Freshman | Changmei Feng | Female | 163.8 | 56.0 |
88 | Freshman | Xiaopeng Han | Female | 164.1 | 53.0 |
81 | Freshman | Yanli Zhang | Female | 165.1 | 52.0 |
# 先设Grade为索引,然后对Grade索引进行排序(所以相比于上一句没有另外的索引)
df.set_index('Grade').sort_index().head()
Name | Gender | Height | Weight | |
---|---|---|---|---|
Grade | ||||
Freshman | Gaopeng Yang | Female | 158.9 | 46.0 |
Freshman | Qiang Shi | Female | 164.5 | 52.0 |
Freshman | Changmei Feng | Female | 163.8 | 56.0 |
Freshman | Xiaopeng Han | Female | 164.1 | 53.0 |
Freshman | Yanli Zhang | Female | 165.1 | 52.0 |
由于序的建立,可以进行比较操作。分类变量的比较操作分为两类,第一种是 == 或!=的比较,比较对象可以是标量或者同长度的Series(或list)。第二种是>, >=,<, <=四类大小关系的比较,比较的对象和第一种类似,但所有参与比较的元素必须属于原序列的categories,同时要和原序列具有相同的索引
# 比较对象是标量
res1 = df.Grade == 'Sophomore'
res1.head()
0 False
1 False
2 False
3 True
4 True
Name: Grade, dtype: bool
# 比较对象是同长度的Series
res2 = df.Grade == ['PhD']*df.shape[0]
res2.head()
0 False
1 False
2 False
3 False
4 False
Name: Grade, dtype: bool
# Sophomore大二,大一或大二的返回True
res3 = df.Grade <= 'Sophomore'
res3.head()
0 True
1 True
2 False
3 True
4 True
Name: Grade, dtype: bool
# 打乱之后进行比较
# sample随机抽样,frac为从总体中抽取10%的样本
# 所以每次抽取判断的结果不一样
res4 = df.Grade <= df.Grade.sample(frac=1).reset_index(drop=True)
res4.head(10)
0 True
1 True
2 False
3 False
4 True
5 True
6 True
7 False
8 True
9 True
Name: Grade, dtype: bool
9.3 区间类别
9.3.1 利用cut和qcut进行区间构造
区间是一种特殊的类别,区间序列往往是通过cut和qcut方法进行构造的,这两个函数能够把原序列的数值特征进行装箱,即用区间位置来代替原来的具体数值
首先介绍 cut 的常见用法:
其中,最重要的参数是 bin ,如果传入整数 n ,则代表把整个传入数组的按照最大和最小值等间距地分为 n段。由于区间默认是左开右闭,需要进行调整把最小值包含进去,在 pandas 中的解决方案是在值最小的区间左端点再减去 0.001*(max-min) ,因此如果对序列 [1,2] 划分为 2 个箱子时,第一个箱子的范围 (0.999,1.5],第二个箱子的范围是 (1.5,2] 。如果需要指定左闭右开时,需要把 right 参数设置为 False ,相应的区间调整方法是在值最大的区间右端点再加上 0.001*(max-min) 。
s = pd.Series([1,2])
# 左开右闭,左端点减掉0.001*(2-1)
# 会自动将区间进行排序
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]]
# 左闭右开, 右端点加上0.001*(2-1)
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)]
# bins的另一个常见用法是指定区间分割点的列表(使用np.infty表示无穷大)
# 因为指定在1.2旁边的端点是负无穷,所以不会返回1而是返回负无穷
# 同理1.8右侧的端点是2.2,所以返回[1.8, 2.2]
pd.cut(s, bins=[-np.infty, 1.2, 1.8, 2.2, np.infty])
0 (-inf, 1.2]
1 (1.8, 2.2]
dtype: category
Categories (4, interval[float64]): [(-inf, 1.2] < (1.2, 1.8] < (1.8, 2.2] < (2.2, inf]]
另外两个常用参数为labels和retbins,分别代表了区间的名字和是否返回分割点(默认不返回)
s = pd.Series([1,2])
res = pd.cut(s, bins=2, labels = ['small','big'], retbins=True)
res[0]
0 small
1 big
dtype: category
Categories (2, object): ['small' < 'big']
# 默认retbins=False,不返回分割点
# 原本res是一个tuple,第0个值为区间结果
res = pd.cut(s, bins=2, labels = ['small','big'], retbins=False)
res[0]
'small'
# 该元素为返回的分割点
res[1]
'big'
从用法上来说,qcut和cut几乎没有差别,只是把bins参数变成q参数,qcut中的q是指quantile。这里的q为整数n时,指按照n等分位数把数据分箱,还可以传入浮点列表指代相应的分位数分割点。
s = df.Weight
# 按照3分位数把数据分箱:(33.999, 48.0] < (48.0, 55.0] < (55.0, 89.0]
pd.qcut(s, q=3).head(7)
0 (33.999, 48.0]
1 (55.0, 89.0]
2 (55.0, 89.0]
3 (33.999, 48.0]
4 (55.0, 89.0]
5 (48.0, 55.0]
6 (48.0, 55.0]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 48.0] < (48.0, 55.0] < (55.0, 89.0]]
# 用0和1指定浮点数分割的最小和最大界限,相当于在1中0.2和0.8为分界
# 实际用 (33.999, 44.0] < (44.0, 69.4] < (69.4, 89.0]
pd.qcut(s, q=[0,0.2, 0.8,1]).head()
0 (44.0, 69.4]
1 (69.4, 89.0]
2 (69.4, 89.0]
3 (33.999, 44.0]
4 (69.4, 89.0]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 44.0] < (44.0, 69.4] < (69.4, 89.0]]
9.3.2 一般区间的构造
对于某一个具体的区间而言,其具备三个要素:左端点、右端点和端点的开闭状态,其中开闭状态可以指定right,left,both,neither中的一类:
# 指定的状态为闭
my_interval = pd.Interval(0,1,'right')
my_interval
Interval(0, 1, closed='right')
my_interval = pd.Interval(0,1,'neither')
my_interval
Interval(0, 1, closed='neither')
其属性包含了mid, length, right, left, closed, 分别表示中点、长度、右端点、左端点和开闭状态
使用in可以判断元素是否属于区间
0.99 in my_interval
True
# 1处于开状态,所以是False
1 in my_interval
False
使用overlaps可以判断两个区间是否有交集:
my_interval_2 = pd.Interval(0.8, 1.5, 'left')
# (0,1) 和[0.8,1.5)有交集
my_interval.overlaps(my_interval_2)
True
pd.IntervalIndex对象有四类方法生成,分别是from_breaks, from_arrays, from_tuples, interval_range,它们分别应用于不同的情况:
from_breaks的功能类似于cut或qcut函数,from_breaks, from_arrays直接传入自定义的分割点;from_tuples, interval_range是通过计算得到的分割点
# 传入自定义分割点,返回由分割点构成的列表
pd.IntervalIndex.from_breaks([1,3,6,10], closed='both')
IntervalIndex([[1, 3], [3, 6], [6, 10]],
closed='both',
dtype='interval[int64]')
from_array是分别传入左端点和右端点的列表,适用于有交集并且知道起点和终点的情况:
pd.IntervalIndex.from_arrays(left=[1,3,6,10],
right=[5,4,9,11],
closed='neither')
IntervalIndex([(1, 5), (3, 4), (6, 9), (10, 11)],
closed='neither',
dtype='interval[int64]')
from_tuples传入的是起点和终点元组构成的列表:
# 一对已知起点和终点的元组
pd.IntervalIndex.from_tuples([(1,5), (3,4), (6,9),(10,11)], closed='neither')
IntervalIndex([(1, 5), (3, 4), (6, 9), (10, 11)],
closed='neither',
dtype='interval[int64]')
一个等差的区间序列由起点、终点、区间个数和区间长度决定,其中三个两确定的情况下,剩下一个量就确定了, interval_range中的start,end,periods,freq参数就对应了这四个量, 从而就能构造出相应的区间:
pd.interval_range(start=1, end=5, periods=8)
IntervalIndex([(1.0, 1.5], (1.5, 2.0], (2.0, 2.5], (2.5, 3.0], (3.0, 3.5], (3.5, 4.0], (4.0, 4.5], (4.5, 5.0]],
closed='right',
dtype='interval[float64]')
# 两种参数给的不一样,说明当没有start或者end时,需要periods和freq来补充说明如何分区间
pd.interval_range(end=5, periods=8, freq=0.5)
IntervalIndex([(1.0, 1.5], (1.5, 2.0], (2.0, 2.5], (2.5, 3.0], (3.0, 3.5], (3.5, 4.0], (4.0, 4.5], (4.5, 5.0]],
closed='right',
dtype='interval[float64]')
练一练
无论是 interval_range 还是下一章时间序列中的 date_range 都是给定了等差序列中四要素中的三个,从而确定整个序列。请回顾等差数列中的首项、末项、项数和公差的联系,写出 interval_range中四个参数之间的恒等关系。end-start = periods * freq除此之外,如果直接使用pd.IntervalIndex([], closed=…), 把Interval类型的列表组成传入其中转为区间索引,那么所有的区间会被强制转为指定的closed类型,因为pd.IntervalIndex 只允许存放同一种开闭区间的Interval对象
pd.IntervalIndex([my_interval, my_interval_2], closed='left')
IntervalIndex([[0.0, 1.0), [0.8, 1.5)],
closed='left',
dtype='interval[float64]')
9.3.3 区间的属性与方法
IntervalIndex上也定义了一些有用的属性和方法, 如果想要具体利用cut或者qcut的结果进行分析,那么需要先将其转为该种索引类型:
# 按照3分位数把数据分箱:(33.999, 48.0] < (48.0, 55.0] < (55.0, 89.0]
id_interval = pd.IntervalIndex(pd.cut(s,3))
id_interval
IntervalIndex([(33.945, 52.333], (52.333, 70.667], (70.667, 89.0], (33.945, 52.333], (70.667, 89.0] ... (33.945, 52.333], (33.945, 52.333], (33.945, 52.333], (70.667, 89.0], (33.945, 52.333]],
closed='right',
name='Weight',
dtype='interval[float64]')
与单个Interval类型相似,IntervalIndex有若干常用属性:left, right, mid, length,分别表示左右端点、 两端点均值和区间长度
id_demo = id_interval[:5] # 选出前5个展示
id_demo
IntervalIndex([(33.945, 52.333], (52.333, 70.667], (70.667, 89.0], (33.945, 52.333], (70.667, 89.0]],
closed='right',
name='Weight',
dtype='interval[float64]')
# 前5个的左端点
id_demo.left
Float64Index([33.945, 52.333, 70.667, 33.945, 70.667], dtype='float64')
# 前5个的右端点
id_demo.right
Float64Index([52.333, 70.667, 89.0, 52.333, 89.0], dtype='float64')
# 前五个的区间均值
id_demo.mid
Float64Index([43.138999999999996, 61.5, 79.8335, 43.138999999999996, 79.8335], dtype='float64')
# 区间长度
id_demo.length
Float64Index([18.387999999999998, 18.334000000000003, 18.333,
18.387999999999998, 18.333],
dtype='float64')
IntervalIndex还有两个常用方法,包括contains和overlaps, 分别指逐个判断每个区间是否包含某元素,以及是否和一个pd.Interval对象有交集
# 正如函数其名,判断每个区间是否包含该元素
id_demo.contains(18)
array([False, False, False, False, False])
# 判断是否和(40,60)有交集
id_demo.overlaps(pd.Interval(40,60))
array([ True, True, False, True, False])
9.4
9.4.1 EX1:统计未出现的列表,在默认参数下能够对两个列的组合出现的频数进行统计汇总:
df = pd.DataFrame({'A': ['a','b', 'c','a'],
'B': ['cat', 'cat', 'dog','cat']})
pd.crosstab(df.A, df.B)
B | cat | dog |
---|---|---|
A | ||
a | 2 | 0 |
b | 1 | 0 |
c | 0 | 1 |
事实上,有些列存储的是分类变量, 列中并不一定包含所有的类别,如果想要对这些未出现的类别在crosstab结果中也进行汇总,则可以指定dropna参数为False
df.B = df.B.astype('category').cat.add_categories('sheep')
pd.crosstab(df.A, df.B, dropna=False)
B | cat | dog | sheep |
---|---|---|---|
A | |||
a | 2 | 0 | 0 |
b | 1 | 0 | 0 |
c | 0 | 1 | 0 |
请实现一个带有dropna参数的my_crosstab函数来完成上面的功能
def my_crosstab(s1, s2, dropna=True):
idx1 = (s1.cat.categories if s1.dtype.name == 'category' and not dropna else s1.unique())
idx2 = (s2.cat.categories if s2.dtype.name == 'category' and not dropna else s2.unique())
res = pd.DataFrame(np.zeros((idx1.shape[0], idx2.shape[0])), index=idx1, columns=idx2)
for i, j in zip(s1, s2):
res.at[i, j] += 1
res = res.rename_axis(index=s1.name, columns=s2.name).astype('int')
return res
df = pd.DataFrame({'A': ['a','b', 'c','a'],
'B': ['cat', 'cat', 'dog','cat']})
df.B = df.B.astype('category').cat.add_categories('sheep')
my_crosstab(df.A, df.B)
B | cat | dog |
---|---|---|
A | ||
a | 2 | 0 |
b | 1 | 0 |
c | 0 | 1 |
my_crosstab(df.A, df.B, dropna=False)
B | cat | dog | sheep |
---|---|---|---|
A | |||
a | 2 | 0 | 0 |
b | 1 | 0 | 0 |
c | 0 | 1 | 0 |