在日常生活及工作中,类别型数据大量存在,对他们采用恰当的分析方法,是数据分析工作中重要的关键点之一。
在类别型数据中,有时候,不同类别之间没有好坏之分,比如性别中的男和女。但在实际生活中,还存在另一种有序类别,他们有好坏顺序之分,比如成绩分级中的优良中差。
针对这样的类别型数据,不管是无序的或是有序的,我们都一起来探讨适合他们各自的处理方法。
知识结构
数据离散化及分箱操作
在实际的数据处理过程中,有时需要我们将连续的数据值进行划分,放到不同的区间中,减少过多的数据值给分析工作带来的麻烦。
将连续型数据打散放入到不同的区间,就类似将货物进行分箱存放,在数据分析过程中,常被称为“分箱操作”。比如我们常做的将不同年龄的人归为婴幼儿、儿童、少年、青年、中年、老年。
在pandas中,使用cut()方法进行分箱操作,比如以下代码样例,数据中记录了几个学生的姓名和他们各自的成绩,我们对成绩进行分箱:
import pandas as pd
#创建数据
df = pd.DataFrame({'Name':['George','Andrea','micheal','maggie','Ravi','Xien','Jalpa','Tyieren'],
'Score':[63,48,56,75,32,77,85,22]})
df
Name | Score | |
---|---|---|
0 | George | 63 |
1 | Andrea | 48 |
2 | micheal | 56 |
3 | maggie | 75 |
4 | Ravi | 32 |
5 | Xien | 77 |
6 | Jalpa | 85 |
7 | Tyieren | 22 |
#对得分进行分箱操作,bins为整数,定义分为几个类别
pd.cut(df['Score'],bins=3)
0 (43.0, 64.0]
1 (43.0, 64.0]
2 (43.0, 64.0]
3 (64.0, 85.0]
4 (21.937, 43.0]
5 (64.0, 85.0]
6 (64.0, 85.0]
7 (21.937, 43.0]
Name: Score, dtype: category
Categories (3, interval[float64]): [(21.937, 43.0] < (43.0, 64.0] < (64.0, 85.0]]
向cut()中传入了两个参数,第一个是需要进行分箱操作的数据列,第二个参数bins表示我们希望将数据分配到几个区间之中。
cut()的操作将这列数据一共划分成了三个区间:(21.937, 43]、(43, 64]和(64, 85]。这个区间是系统根据提供的参数bins将数据区间进行平均划分得到的。请留意所有区间都是半开半闭形态,不包含区间开始的数字而包含区间的结束数字。
从cut()计算的结果列表中可以看到,第0、1、2个元素的结果是(43, 64],它表示原来的数据中,前三个数字(63、48、56)属于这个区间。接下来的数字75属于区间(64, 85],等等。你可以实际的观察一下结果的计算是否正确。
根据实际的应用场景,对于分箱边界的平均划分常常不能满足我们的需求。因此,cut()方法中,pandas让bins参数值可以用另一种方式定义——使用列表形式列出划分边界,如以下的代码:
import pandas as pd
# 创建数据
df = pd.DataFrame({'Name':['George','Andrea','micheal','maggie','Ravi','Xien','Jalpa','Tyieren'],
'Score':[63,48,56,75,32,77,85,22]})
#对得分进行分箱操作,bins为边界列表
pd.cut(df['Score'],bins=[0,60,80,90,100])
0 (60, 80]
1 (0, 60]
2 (0, 60]
3 (60, 80]
4 (0, 60]
5 (60, 80]
6 (80, 90]
7 (0, 60]
Name: Score, dtype: category
Categories (4, interval[int64]): [(0, 60] < (60, 80] < (80, 90] < (90, 100]]
这段代码中,传入cut()方法的bins参数被定义成了[0, 60, 80, 90, 100],于是分箱的区间就会被定义为(0, 60]、(60, 80]、(80, 90]和(90, 100]。从结果输出的最后一列也能很明确的看到这点。
分箱操作的结果中,会把每个数据所归属的结果区间直接显示出来。但我们可能希望用更直观的文本来表示分箱的结果。
比如成绩案例中,我们希望将前面划分的这几个区间从高分到低分分别叫做优、良、中、差。可以使用cut()方法的labels参数达到这一目的:
import pandas as pd
# 创建数据
df = pd.DataFrame({'Name':['George','Andrea','micheal','maggie','Ravi','Xien','Jalpa','Tyieren'],
'Score':[63,48,56,75,32,77,85,22]})
#对得分进行分箱操作,bins为边界列表,并且使用label定义区间的名字
df['Level'] = pd.cut(df['Score'],bins=[0,60,80,90,100],labels=['差','中','良','优'])
df
Name | Score | Level | |
---|---|---|---|
0 | George | 63 | 中 |
1 | Andrea | 48 | 差 |
2 | micheal | 56 | 差 |
3 | maggie | 75 | 中 |
4 | Ravi | 32 | 差 |
5 | Xien | 77 | 中 |
6 | Jalpa | 85 | 良 |
7 | Tyieren | 22 | 差 |
代码中,在进行了cut()操作之后,将分箱的结果与原有的df数据进行了拼接,成为了df中新的Level列。从结果中能更直观的看出不同学员成绩的好坏。
请注意在指定cut()方法的labels参数值时,labels定义的列表中的类别名顺序,是与bins定义的划分区间按顺序一一对应的。
分箱操作,其实就相当于将连续的数字类型数据(比如分数)映射成了类别型数据(比如这里的成绩等级)。分箱操作之后的结果,时常会再进行groupby操作。比如我们对分好等级之后的成绩再进行分组并计算每组中的平均值:
import pandas as pd
# 创建数据
df = pd.DataFrame({'Name':['George','Andrea','micheal','maggie','Ravi','Xien','Jalpa','Tyieren'],
'Score':[63,48,56,75,32,77,85,22]})
# 对得分进行分箱操作,bins为边界列表,并且使用labels定义区间的名字
df['Level'] = pd.cut(df['Score'], bins=[0, 60, 80, 90, 100], labels=['差', '中', '良', '优'])
#获取每个等级的平均成绩
df.groupby('Level').mean()
Score | |
---|---|
Level | |
差 | 39.500000 |
中 | 71.666667 |
良 | 85.000000 |
优 | NaN |
这就能得到不同等级的平均成绩分别是多少。
在这个计算每个等级平均分的案例中,由于没有成绩落入优级别,在进行分组以及求均值计算之后,对应结果单元中得到的是NaN。
类别型数据类型
在前面的小节中,我们使用一个学生成绩的案例直接体验了pandas处理类别型数据的方法。现在我们来仔细了解pandas中的类别型数据类型。
跟整型(int)、字符串(str)等类型类似,类别型数据类型在pandas中也是一种数据类型。它使用category表示。
就像整型可以进行加减乘除操作一样,category也包含自己特殊的操作方式。
在之前我们通过pandas的cut()方法操作数据之后,会在结果输出中看到dtype: category这样的字样,这就表示结果数组中的元素,是类别数据类型的。
主动构造类别数据类型的变量,有以下几种主要的方法:
dtype
在创建一个Series变量的时候,使用dtype=‘category’,可以指定元素为类别类型。
import pandas as pd
s=pd.Series(['a','b','c','a'],dtype='category')
s
0 a
1 b
2 c
3 a
dtype: category
Categories (3, object): [a, b, c]
从代码输出中可以看出,这个Series的元素类型已经是类别型了。
astype()
另一种方式,调用Series的astype(),也可以将其中的元素转换为类别型。
import pandas as pd
s=pd.Series(['a','b','c','a'])
s2=s.astype('category')
print('s的值:')
print(s)
print('\ns2的值:')
print(s2)
s的值:
0 a
1 b
2 c
3 a
dtype: object
s2的值:
0 a
1 b
2 c
3 a
dtype: category
Categories (3, object): [a, b, c]
astype()方法其实就是pandas中的类型转换方法,参数中传入类型的名字,就可以将数组中的元素类型转换成这个参数指定的类型。
我们在代码中打印了Series变量s和s2的值。从输出内容上看,他们好像是一样的,不同只在于结果中的dtype。
只是内容上看起来一样,并不能说明s与s2是一样的。看以下代码展示类别型元素与字符串类型元素的差别:
import pandas as pd
s=pd.Series(['a','b','c','a'])
s2=s.astype('category')
s.max()
'c'
s2.max()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-11-f92cd5067311> in <module>
----> 1 s2.max()
c:\users\administrator\appdata\local\programs\python\python37\lib\site-packages\pandas\core\generic.py in stat_func(self, axis, skipna, level, numeric_only, **kwargs)
11616 return self._agg_by_level(name, axis=axis, level=level, skipna=skipna)
11617 return self._reduce(
> 11618 f, name, axis=axis, skipna=skipna, numeric_only=numeric_only
11619 )
11620
c:\users\administrator\appdata\local\programs\python\python37\lib\site-packages\pandas\core\series.py in _reduce(self, op, name, axis, skipna, numeric_only, filter_type, **kwds)
4069 # TODO deprecate numeric_only argument for Categorical and use
4070 # skipna as well, see GH25303
-> 4071 return delegate._reduce(name, numeric_only=numeric_only, **kwds)
4072 elif isinstance(delegate, ExtensionArray):
4073 # dispatch to ExtensionArray interface
c:\users\administrator\appdata\local\programs\python\python37\lib\site-packages\pandas\core\arrays\categorical.py in _reduce(self, name, axis, **kwargs)
2259 msg = "Categorical cannot perform the operation {op}"
2260 raise TypeError(msg.format(op=name))
-> 2261 return func(**kwargs)
2262
2263 def min(self, numeric_only=None, **kwargs):
c:\users\administrator\appdata\local\programs\python\python37\lib\site-packages\pandas\core\arrays\categorical.py in max(self, numeric_only, **kwargs)
2302 max : the maximum of this `Categorical`
2303 """
-> 2304 self.check_for_ordered("max")
2305 if numeric_only:
2306 good = self._codes != -1
c:\users\administrator\appdata\local\programs\python\python37\lib\site-packages\pandas\core\arrays\categorical.py in check_for_ordered(self, op)
1584 "Categorical is not ordered for operation {op}\n"
1585 "you can use .as_ordered() to change the "
-> 1586 "Categorical to an ordered one\n".format(op=op)
1587 )
1588
TypeError: Categorical is not ordered for operation max
you can use .as_ordered() to change the Categorical to an ordered one
我们在代码中分别调用了元素是字符串类型的Series变量s以及元素是类别型的Series变量s2的max()方法。
从结果中你会发现,s.max()会输出结果’c’,因为字符串是可以比较大小的,在所有的元素中,'c’的值最大。
而s2.max()执行之后会发生错误。这就是我们之前提到的,类别型数据中,各类别在默认情况下没有大小之分,不能比较,因此就没有最大值。
对于有序型类别,pandas则提供了以下直接创建对象的方式,满足要求:
pandas.Categorical()
通过pandas.Categorical()构建一个对象,并在构建这个对象时,传入参数ordered=True,然后以这个Categorical作为数据模板创建Series,即可得到有序型的类别数据。看以下代码示例:
import pandas as pd
#pd.Categorical(),指定类别以及有序
s = pd.Series(pd.Categorical(['差','中','良','优'],ordered=True))
s
0 差
1 中
2 良
3 优
dtype: category
Categories (4, object): [中 < 优 < 差 < 良]
s.max()
'良'
在代码中,将有序型类别数据s进行输出,可以看到其中对优、良、中、差给出了明确的大小关系描述:[中 < 优 < 差 < 良]。
我们在第二段代码中打印了s.max(),可以从输出结果中看到最大值’良’。
但这里还有个问题,对于优、良、中、差的排序,并不是按照中文意思上的优最大、差最小进行的。
你可以尝试执行print(‘中’<‘优’<‘差’<‘良’),会得到结果True。这说明这里的类别顺序用的就是字符串的排序结果。
如果希望按照自己定义的顺序来定义类别,在构造Categorical对象的时候,还需要提供一个参数:cat