【Task09】Pandas之分类数据

前言

分类数据直白来说就是取值为有限的,或者说是固定数量的可能值,这个概念与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

3.pandas删除行删除列,增加行增加列

https://www.cnblogs.com/guxh/p/9420610.html

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值