前言
分类数据直白来说就是取值为有限的,或者说是固定数量的可能值,这个概念与C或Java中的enum枚举类型相似:
接下来让我们介绍分类数据的创建方法:
一、创建分类数据
1.新建Series时直接指定
s_blood = pd.Series(data=["A", "AB", np.nan, "AB", "O", "B"],dtype="category")
s_blood
0 A
1 AB
2 NaN
3 AB
4 O
5 B
dtype: category
Categories (4, object): ['A', 'AB', 'B', 'O']
2.改变Series的dtype
s = pd.Series(data=["A", "AB", np.nan, "AB", "O", "B"])
s.astype('category')
0 A
1 AB
2 NaN
3 AB
4 O
5 B
dtype: category
Categories (4, object): ['A', 'AB', 'B', 'O']
3.使用pd.Categorical方法
在pandas内置的Categorical方法可以直接将列表或Series数据转换为分类数据:
pd.Categorical(["A", "AB", np.nan, "AB", "O", "B"])
['A', 'AB', NaN, 'AB', 'O', 'B']
Categories (4, object): ['A', 'AB', 'B', 'O']
pd.Categorical(pd.Series(["A", "AB", np.nan, "AB", "O", "B"]))
['A', 'AB', NaN, 'AB', 'O', 'B']
Categories (4, object): ['A', 'AB', 'B', 'O']
这里注意采用这种方法建立的分类数据的返回格式与上面有所不同。
我们也可以利用categories参数手动指明分类数据中的类别:
pd.Categorical(["A", "AB", np.nan, "AB", "O", "B"], categories=["A", "B", "O"])
['A', NaN, NaN, NaN, 'O', 'B']
Categories (3, object): ['A', 'B', 'O']
可以看到AB型血从分类中被剔除掉了。
另外,使用cut和qcut方法返回的数据也属于分类数据,在本文的后半部分会进行说明。
二、cat对象
我们可以访问分类数据的cat属性来得到cat对象:
s_blood = pd.Series(data=["A", "AB", np.nan, "AB", "O", "B"],dtype="category")
s_blood.cat
<pandas.core.arrays.categorical.CategoricalAccessor object at 0x0000021A0974CA90>
分类数据也可以访问str属性:
s_blood.str
<pandas.core.strings.StringMethods at 0x21a0974ca00>
注意,如果访问非分类数据的cat属性,会报如下错误:
1.cat对象的属性
cat对象有categories和codes属性可供访问,分别代表所有分类按序排名和数据所属分类的位置(从0开始),若不存在则返回-1:
s_blood.cat.categories
Index(['A', 'AB', 'B', 'O'], dtype='object')
s_blood.cat.codes
0 0
1 1
2 -1
3 1
4 3
5 2
dtype: int8
cat对象的ordered可以返回该分类数据是否 有序 :
s_blood.cat.ordered
False
2.类别的增删改
我们可以通过cat对象的categories属性访问它包含的类别,也可以通过相应方法进行增删改:
1)类别的增加
cat对象内置了add_categories方法实现对分类数据中分类的增加:
s_blood.cat.add_categories('C')
0 A
1 AB
2 NaN
3 AB
4 O
5 B
dtype: category
Categories (5, object): ['A', 'AB', 'B', 'O', 'C']
s_blood.cat.add_categories(['C','D'])
0 A
1 AB
2 NaN
3 AB
4 O
5 B
dtype: category
Categories (6, object): ['A', 'AB', 'B', 'O', 'C', 'D']
我们可以传入单个字符串或者字符串数组增加类别,注意它并不会改变原来的分类数据:
s_blood
0 A
1 AB
2 NaN
3 AB
4 O
5 B
dtype: category
Categories (4, object): ['A', 'AB', 'B', 'O']
2)类别的删除
cat对象内置了remove_categories方法实现对分类数据中分类的删除:
被删除的分类的数据会被置为np.nan
删除多个分类:
删除不存在的分类:
s_blood.cat.remove_categories(['A','C'])
我们可以通过remove_unused_categories方法移除掉未使用的类别:
s_blood2 = pd.Categorical(["A", "AB", np.nan, "AB", "B"], categories=["A", "B", "O"])
s_blood2
['A', NaN, NaN, NaN, 'B']
Categories (3, object): ['A', 'B', 'O']
s_blood2.remove_unused_categories()
['A', NaN, NaN, NaN, 'B']
Categories (2, object): ['A', 'B']
最后也可以使用set_categories直接设置新的类别:
之前的旧类别均被置为np.nan
注:以上所有操作都不会对原数据产生改变。
3)类别的修改
cat对象内置了rename_categories方法实现对分类数据中的分类进行修改:
s_blood.cat.rename_categories({'AB':'A+B'})
0 A
1 A+B
2 NaN
3 A+B
4 O
5 B
dtype: category
Categories (4, object): ['A', 'A+B', 'B', 'O']
是不是与修改Series和DataFrame的行列索引值有些类似呢?
三、有序分类
1.序的建立和消除
通过cat对象内置的reorder_categories方法进行建序:
s_blood_ordered = s_blood.cat.reorder_categories(['A','B','O','AB'],ordered = True)
s_blood_ordered
0 A
1 AB
2 NaN
3 AB
4 O
5 B
dtype: category
Categories (4, object): ['A' < 'B' < 'O' < 'AB']
s_blood_ordered.cat.ordered
True
建序后访问ordered属性进行验证,发现cat对象已经有序。
通过cat对象内置的as_unordered方法进行乱序:
s_blood.equals(s_blood_ordered.cat.as_unordered())
True
发现已然归于无序。
2.排序和比较
对分类数据建序实际上是为指定排序顺序:
#通过列排序
s_blood_ordered.sort_values()
0 A
5 B
4 O
1 AB
3 AB
2 NaN
dtype: category
Categories (4, object): ['A' < 'B' < 'O' < 'AB']
#通过行索引排序
s_blood_ordered.to_frame().reset_index().set_index(0).sort_index()
这里注意两个点,第一个是黄框这种情况,类别存在重复;第二个是红框,np.nan默认排到最后。
我们可以利用分类类型对数据进行比较:
#由于numpy中的向量化特性,下面两句代码是等价的
s_blood == 'A'
s_blood == ['A']*s_blood.shape[0]
0 True
1 False
2 False
3 False
4 False
5 False
dtype: bool
s_blood != 'AB'
0 True
1 False
2 False
3 False
4 True
5 True
dtype: bool
注意用大于号和小于号进行比较时,须保证cat对象有序,否则:
正例:
s_blood_ordered <= 'B'
0 True
1 False
2 False
3 False
4 False
5 True
dtype: bool
s_blood_ordered > 'B'
0 False
1 True
2 False
3 True
4 True
5 False
dtype: bool
四、区间类别
1.通过cut创建区间
当参数bin为整数时,表示将分类数据类别数:
s = pd.Series([10,20])
pd.cut(s, bins=2)
0 (9.99, 15.0]
1 (15.0, 20.0]
dtype: category
Categories (2, interval[float64]): [(9.99, 15.0] < (15.0, 20.0]]
默认左开右闭,左侧要减去max-min的0.1%,在这里是0.01
可以将right参数设置为False从而显式地指定为左闭右开:
s = pd.Series([10,20])
pd.cut(s, bins=2, right=False)
0 [10.0, 15.0)
1 [15.0, 20.01)
dtype: category
Categories (2, interval[float64]): [[10.0, 15.0) < [15.0, 20.01)]
同样地右侧需要加上0.01
当参数bin为列表时表示划定的分类范围:
pd.cut(s, bins=[-np.infty,15,30,np.infty])
0 (-inf, 15.0]
1 (15.0, 30.0]
dtype: category
Categories (3, interval[float64]): [(-inf, 15.0] < (15.0, 30.0] < (30.0, inf]]
注意这里分类数据中行索引依然保持2个
cut方法中另外2个常用参数为labels和retbins:
res = pd.cut(s, bins=2, labels=['small', 'big'], retbins=True)
res[0]
0 small
1 big
dtype: category
Categories (2, object): ['small' < 'big']
res[1]
array([ 9.99, 15. , 20. ])
它们分别代表划分区间的名字和是否返回区间之间的分割点
2.通过qcut创建区间
qcut和cut的关系好比之前iloc和loc的关系,前者通过输入数值(百分比)进行后者功能上的实现:
pd.qcut(s,q=3)
0 (9.999, 13.333]
1 (16.667, 20.0]
dtype: category
Categories (3, interval[float64]): [(9.999, 13.333] < (13.333, 16.667] < (16.667, 20.0]]
pd.qcut(s,q=[0,0.2,0.6,1])
0 (9.999, 12.0]
1 (16.0, 20.0]
dtype: category
Categories (3, interval[float64]): [(9.999, 12.0] < (12.0, 16.0] < (16.0, 20.0]]
qcut没有right参数:
3.一般区间的构造
my_interval = pd.Interval(0, 1, 'right')
my_interval
0 in my_interval
False
1 in my_interval
True
‘right’代表右端点1是闭的:
my_interval_02 = pd.Interval(0.5, 1.5, 'left')
my_interval_02
Interval(0.5, 1.5, closed='left')
my_interval.overlaps(my_interval_02)
True
overlaps方法用来判断两个区间是否有交集
练一练
尾项与首项中间有periods个区间,每个区间长度为freq,end代表最右端点,start代表最左端点:
end - start = periods * freq
4.区间的属性和方法
id_interval = pd.IntervalIndex(pd.cut(s, 3))
id_interval
IntervalIndex([(9.99, 13.333], (16.667, 20.0]],
closed='right',
dtype='interval[float64]')
区间的属性有:
#左端点
id_interval.left
Float64Index([9.99, 16.667], dtype='float64')
#右端点
id_interval.right
Float64Index([13.333, 20.0], dtype='float64')
#两端点均值
id_interval.mid
Float64Index([11.6615, 18.3335], dtype='float64')
#区间长度
id_interval.length
Float64Index([3.343, 3.3329999999999984], dtype='float64')
#区间是否包含某元素
id_interval.contains(10)
array([ True, False])
练习
Ex1:统计未出现的类别
df = pd.DataFrame({'A':['a','b','c','a'], 'B':['cat','cat','dog','cat']})
df
pd.crosstab(df.A, df.B)
df.B = df.B.astype('category').cat.add_categories('sheep')
pd.crosstab(df.A, df.B, dropna=False)
请实现一个带有dropna参数的my_crosstab函数来完成上面的功能。
版本01
def my_crosstab(s1,s2,dropna=True):
#用于查询出现次数
def myfunc(x):
return df_count.loc[(x[0],x[1])]
df_new = pd.concat([s1,s2],axis=1)
#新增一列
df_new['count'] = pd.Series([0]*s1.size)
#分组统计a+b 作为查询表
df_count = df_new.groupby(['A','B']).count()
#改变count列
df_new['count'] = df_new.apply(myfunc,axis=1)
#取唯一值后,进行长宽表转换
res = df_new.drop_duplicates().pivot(index='A',columns='B',values='count')
#将np.nan替换为0并转换dtype类型
res = res.mask(res.isna(),0).astype('int')
if dropna!=True:
index = 2
length = res.shape[0]
for x in s2.cat.categories:
if x not in res.columns:
res.insert(index,x,[0]*length)
return res
my_crosstab(df.A,df.B,True)
my_crosstab(df.A,df.B,False)
版本02(讲解版)
这个版本对上一版本的代码进行了优化,并且给予分步讲解:
步骤1 合并Series并新增count列
步骤2 统计出现次数并更新count列
注,由于分组时存在np.nan类型的值,所以统计后的结果为浮点型。
步骤3 长表转宽表
由题意进行长表转宽表的操作
步骤4 若dropna为False,则新增列
完整代码:
def my_crosstab(s1,s2,dropna=True):
#第一步,合并Series并新增count列
df_new = pd.concat([s1,s2,pd.Series([0]*s1.size,name='count')],axis=1)
#第二步,统计出现次数并更新count列
df_count = df_new.groupby([s1.name,s2.name]).count()
df_new['count'] = df_new.drop_duplicates().apply(lambda x: df_count.loc[(x[0],x[1])],axis=1)
#第三步,长表转宽表
df_new = pd.pivot_table(df_new,index = s1.name,columns = s2.name,values = 'count',fill_value = 0)
#第四步,若dropna为False,则新增
if dropna != True:
for x in set(df_new.columns.categories) - set(df_new.columns):
df_new = pd.concat([df_new,pd.Series([0]*df_new.shape[0],index=df_new.index,name=x)],axis=1)
return df_new
my_crosstab(df.A,df.B,True)
my_crosstab(df.A,df.B)
my_crosstab(df.A,df.B,False)
Ex2:钻石数据集
现有一份关于钻石的数据集,其中carat, cut, clarity, price分别表示克拉重量、切割质量、纯净度和价格,样例如下:
df = pd.read_csv('./data/diamonds.csv')
df.head()
1.分别对df.cut在object类型和category类型下使用nunique函数,并比较它们的性能。
df.cut.dtype
dtype('O')
2.钻石的切割质量可以分为五个等级,由次到好分别是Fair, Good, Very Good, Premium, Ideal,纯净度有八个等级,由次到好分别是I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF,请对切割质量按照由好到次的顺序排序,相同切割质量的钻石,按照纯净度进行由次到好的排序。
df.cut = df.cut.astype('category')
df.cut.cat.reorder_categories(['Fair', 'Good', 'Very Good', 'Premium', 'Ideal'],ordered = True)
df.clarity = df.clarity.astype('category')
df.clarity.cat.reorder_categories(['I1', 'SI2', 'SI1', 'VS2', 'VS1', 'VVS2', 'VVS1', 'IF'],ordered = True)
df.sort_values(['cut','clarity'],ascending=[0,1])
【TODO】这题存在一些问题,等待改正
3.分别采用两种不同的方法,把cut, clarity这两列按照由好到次的顺序,映射到从0到n-1的整数,其中n表示类别的个数。
df.cut = df.cut.cat.reorder_categories(df.cut.cat.categories[::-1])
df.clarity = df.clarity.cat.reorder_categories(df.clarity.cat.categories[::-1])
df.cut = df.cut.cat.codes
df.clarity = df.clarity.cat.codes
df.cut
0 2
1 1
2 3
3 1
4 3
..
53935 2
53936 3
53937 0
53938 1
53939 2
Name: cut, Length: 53940, dtype: int8
4.对每克拉的价格按照分别按照分位数(q=[0.2, 0.4, 0.6, 0.8])与[1000, 3500, 5500, 18000]割点进行分箱得到五个类别Very Low, Low, Mid, High, Very High,并把按这两种分箱方法得到的category序列依次添加到原表中。
q = np.linspace(0,1,6)
point = [-np.infty, 1000, 3500, 5500, 18000, np.infty]
avg = df.price / df.carat
df['avg_cut'] = pd.cut(avg, bins=point, labels=['Very Low', 'Low', 'Mid', 'High', 'Very High'])
df['avg_qcut'] = pd.qcut(avg, q=q, labels=['Very Low', 'Low', 'Mid', 'High', 'Very High'])
df
5.第4问中按照整数分箱得到的序列中,是否出现了所有的类别?如果存在没有出现的类别请把该类别删除。
df.avg_cut = df.avg_cut.cat.remove_unused_categories()
df.avg_cut
0 Low
1 Low
2 Low
3 Low
4 Low
...
53935 Mid
53936 Mid
53937 Mid
53938 Low
53939 Mid
Name: avg_cut, Length: 53940, dtype: category
Categories (3, object): ['Low' < 'Mid' < 'High']
df.avg_qcut = df.avg_qcut.cat.remove_unused_categories()
df.avg_qcut
0 Very Low
1 Very Low
2 Very Low
3 Very Low
4 Very Low
...
53935 Mid
53936 Mid
53937 Mid
53938 Mid
53939 Mid
Name: avg_qcut, Length: 53940, dtype: category
Categories (5, object): ['Very Low' < 'Low' < 'Mid' < 'High' < 'Very High']
6.对第4问中按照分位数分箱得到的序列,求每个样本对应所在区间的左右端点值和长度。
interval_avg = pd.IntervalIndex(pd.qcut(avg, q=q))
list(interval_avg.left)
list(interval_avg.right)
list(interval_avg.length)
参考文献
1.C enum(枚举)
https://www.runoob.com/cprogramming/c-enum.html
2.Pandas系列(五)-分类数据处理
https://www.cnblogs.com/zhangyafei/p/10513729.html