系统重温Pandas笔记:(九)分类数据

写在前面

本文内容源自Datawhale 组队学习教程,并结合了部分自己的笔记和感悟。对Datawhale感兴趣且想进一步了解:https://github.com/datawhalechina/joyful-pandas

一、cat对象

1. cat对象的属性

pandas中提供了category类型,使用户能够处理分类类型的变量,将一个普通序列转换成分类变量可以使用astype方法。
e.g.

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']

在一个分类类型的Series中定义了cat对象,它和上一章中介绍的str对象类似,定义了一些属性和方法来进行分类类别的操作。

s.cat

<pandas.core.arrays.categorical.CategoricalAccessor object at 0x000001E2909D2940>

对于一个具体的分类,有两个组成部分:

  • 类别的本身,它以Index类型存储
  • 是否有序

它们都可以通过cat的属性被访问:

s.cat.categories
Index(['Freshman', 'Junior', 'Senior', 'Sophomore'], dtype='object')

s.cat.ordered
False

另外,每一个序列的类别会被赋予唯一的整数编号,它们的编号取决于cat.categories中的顺序,该属性可以通过codes访问:

s.cat.codes.head()

0    0
1    0
2    2
3    3
4    3
dtype: int8

2. 类别的增加、删除和修改

通过cat对象的categories属性能够完成对类别的查询,但是类别不得直接修改:
在第三章中曾提到,索引 Index 类型是无法用 index_obj[0] = item 来修改的,而 categories 被存储在 Index 中,因此 pandascat 属性上定义了若干方法来达到相同的目的。

  • 类别的增加可以使用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')

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')

s.head()
0          NaN
1          NaN
2          NaN
3    Sophomore
4    Sophomore
Name: Grade, dtype: category
Categories (2, object): ['Sophomore', 'PhD']
  • 删除未出现在序列中的类别,可以使用remove_unused_categories来实现:
s = s.cat.remove_unused_categories() # 移除了未出现的博士生类别
s.cat.categories

Index(['Sophomore'], dtype='object')
  • 修改的操作可以通过rename_categories方法完成,同时需要注意的是,这个方法会对原序列的对应值也进行相应修改
s = s.cat.rename_categories({'Sophomore':'本科二年级学生'})
s.head()

0        NaN
1        NaN
2        NaN
3    本科二年级学生
4    本科二年级学生
Name: Grade, dtype: category
Categories (1, object): ['本科二年级学生']

二、有序分类

1. 序的建立

有序类别无序类别可以通过as_unorderedreorder_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进行具体的相对大小调整。

2. 排序和比较

在第二章中,曾提到了字符串和数值类型序列的排序,此时就要说明分类变量的排序只需把列的类型修改为category后,再赋予相应的大小关系,就能正常地使用sort_indexsort_values

df.Grade = df.Grade.astype('category')
df.Grade = df.Grade.cat.reorder_categories(['Freshman', 'Sophomore', 'Junior', 'Senior'],ordered=True)
df.sort_values('Grade').head() # 值排序

在这里插入图片描述
通过索引排序:

df.set_index('Grade').sort_index().head() # 索引排序

在这里插入图片描述
由于序的建立,因此就可以进行比较操作。分类变量的比较操作分为两类:

  • 第一种是==!=关系的比较,比较的对象可以是标量或者同长度的Series(或list
  • 第二种是>,>=,<,<=四类大小关系的比较,比较的对象和第一种类似,但是所有参与比较的元素必须属于原序列的categories,同时要和原序列具有相同的索引。
res1 = df.Grade == 'Sophomore'
res1.head()
0    False
1    False
2    False
3     True
4     True
Name: Grade, dtype: bool

res2 = df.Grade == ['PhD']*df.shape[0]
res2.head()
0    False
1    False
2    False
3    False
4    False
Name: Grade, dtype: bool

res3 = df.Grade <= 'Sophomore'
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) # 打乱后比较
res4.head()
0     True
1     True
2    False
3     True
4     True
Name: Grade, dtype: bool

三、区间类别

1. 利用cut和qcut进行区间构造

区间是一种特殊的类别,在实际数据分析中,区间序列往往是通过cutqcut方法进行构造的,这两个函数能够把原序列的数值特征进行装箱,即用区间位置来代替原来的具体数值。

  • 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])
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]]

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可以表示无穷大):

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]]

另外两个常用参数为labelsretbins,分别代表了区间的名字是否返回分割点(默认不返回)

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']

res[1] # 该元素为返回的分割点
array([0.999, 1.5  , 2.   ])
  • qcut:从用法上来说,qcutcut几乎没有差别,只是把bins参数变成的q参数,qcut中的q是指quantile这里的q为整数n时,指按照n等分位数把数据分箱,还可以传入浮点列表指代相应的分位数分割点。
s = df.Weight
pd.qcut(s, q=3).head()
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]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 48.0] < (48.0, 55.0] < (55.0, 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]]

2. 一般区间的构造

对于某一个具体的区间而言,其具备三个要素,即左端点、右端点端点的开闭状态,其中开闭状态可以指定right, left, both, neither中的一类:

参数介绍:

  • left:有顺序的定值,间隔的左边界
  • right:有顺序的定值,间隔的右边界
  • closed:字符,可选 {‘right’, ‘left’, ‘both’, ‘neither’}, 默认为 ‘right’,区间是在左侧、右侧还是同时闭合,或者都不闭合

说明:

  • 参数 left 和 right 必须具有相同的类型,必须能够比较它们,并且它们必须满足 left <= right
  • 闭合区间(在数学中用方括号表示)包含其端点,即闭合区间[0,5]的特征是条件0 <= x <=5,这就是 close ='both’所代表的意思
  • 开区间(用括号表示的数学形式)不包含其端点,即区间(0,5)的条件是0<x<5,这就是 closed=‘nether’ 代表的意思
  • 间隔也可以半开或半闭,即[0,5)用0 <= x <5(closed =‘left’)描述,(0,5] 用0 <x <= 5( close =‘right’)
my_interval = pd.Interval(0, 1, 'right')
my_interval

Interval(0, 1, closed='right')

其属性包含了mid, length, right, left, closed,,分别表示中点、长度、右端点、左端点和开闭状态。

使用in可以判断元素是否属于区间

0.5 in my_interval

True

使用overlaps可以判断两个区间是否有交集

my_interval_2 = pd.Interval(0.5, 1.5, 'left')
my_interval.overlaps(my_interval_2)

True

一般而言,pd.IntervalIndex对象有四类方法生成,分别是from_breaks, from_arrays, from_tuples, interval_range,它们分别应用于不同的情况:

  • from_breaks的功能类似于cutqcut函数,只不过后两个是通过计算得到的风格点,而前者是直接传入自定义的分割点
pd.IntervalIndex.from_breaks([1,3,6,10], closed='both')

IntervalIndex([[1, 3], [3, 6], [6, 10]],
              closed='both',
              dtype='interval[int64]')
  • from_arrays分别传入左端点和右端点的列表,适用于有交集并且知道起点和终点的情况:
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:一个等差的区间序列由起点、终点、区间个数区间长度决定,其中三个量确定的情况下,剩下一个量就确定了,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]')
              
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中四个参数之间的恒等关系。
解:
freq = (end-start)/periods

除此之外,如果直接使用pd.IntervalIndex([...], closed=...),把Interval类型的列表组成传入其中转为区间索引,那么所有的区间会被强制转为指定的closed类型,因为pd.IntervalIndex只允许存放同一种开闭区间的Interval对象。

pd.IntervalIndex([my_interval, my_interval_2], closed='left')

IntervalIndex([[0.0, 1.0), [0.5, 1.5)],
              closed='left',
              dtype='interval[float64]')

3. 区间的属性与方法

IntervalIndex上也定义了一些有用的属性和方法。同时,如果想要具体利用cut或者qcut的结果进行分析,那么需要先将其转为该种索引类型:

id_interval = pd.IntervalIndex(pd.cut(s, 3))
id_interval[:3]

IntervalIndex([(33.945, 52.333], (52.333, 70.667], (70.667, 89.0]],
              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]')
              
id_demo.left
Float64Index([33.945, 52.333, 70.667, 33.945, 70.667], dtype='float64')

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还有两个常用方法,包括containsoverlaps,分别指逐个判断每个区间是否包含某元素,以及是否和一个pd.Interval对象有交集


id_demo.contains(4)
array([False, False, False, False, False])

id_demo.overlaps(pd.Interval(40,60))
array([ True,  True, False,  True, False])

四、练习

Ex1:统计未出现的类别

在第五章中介绍了crosstab函数,在默认参数下它能够对两个列的组合出现的频数进行统计汇总:

df = pd.DataFrame({'A':['a','b','c','a'], 'B':['cat','cat','dog','cat']})
pd.crosstab(df.A, df.B)

在这里插入图片描述
但事实上有些列存储的是分类变量,列中并不一定包含所有的类别,此时如果想要对这些未出现的类别在crosstab结果中也进行汇总,则可以指定dropna参数为False

df.B = df.B.astype('category').cat.add_categories('sheep')
pd.crosstab(df.A, df.B, dropna=False)

在这里插入图片描述
请实现一个带有dropna参数的my_crosstab函数来完成上面的功能。
解:
以下是对本题参考答案的阅读理解:
先给df.B增加未出现的类型sheep:

df = pd.DataFrame({'A':['a','b','c','a'], 'B':['cat','cat','dog','cat']})
df.B = df.B.astype('category').cat.add_categories('sheep')
df.B

0    cat
1    cat
2    dog
3    cat
Name: B, dtype: category
Categories (3, object): ['cat', 'dog', 'sheep']

构造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

实现上述功能:

my_crosstab(df.A, df.B, dropna=False)

在这里插入图片描述

Ex2:钻石数据集

现有一份关于钻石的数据集,其中carat, cut, clarity, price分别表示克拉重量、切割质量、纯净度和价格,样例如下:

df = pd.read_csv('data/diamonds.csv') 
df.head(3)

在这里插入图片描述
1.分别对df.cutobject类型和category类型下使用nunique函数,并比较它们的性能。
2. 钻石的切割质量可以分为五个等级,由次到好分别是Fair, Good, Very Good, Premium, Ideal,纯净度有八个等级,由次到好分别是I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF,请对切割质量按照由好到次的顺序排序,相同切割质量的钻石,按照纯净度进行由次到好的排序。
3. 分别采用两种不同的方法,把cut, clarity这两列按照由好到次的顺序,映射到从0到n-1的整数,其中n表示类别的个数。
4. 对每克拉的价格按照分别按照分位数(q=[0.2, 0.4, 0.6, 0.8])与[1000, 3500, 5500, 18000]割点进行分箱得到五个类别Very Low, Low, Mid, High, Very High,并把按这两种分箱方法得到的category序列依次添加到原表中。
5. 第4问中按照整数分箱得到的序列中,是否出现了所有的类别?如果存在没有出现的类别请把该类别删除。
6. 对第4问中按照分位数分箱得到的序列,求每个样本对应所在区间的左右端点值和长度。

1.分别对df.cutobject类型和category类型下使用nunique函数,并比较它们的性能。
解:
object类型下使用nunique函数:

%timeit -n 30 df.cut.nunique()

2.11 ms ± 205 µs per loop (mean ± std. dev. of 7 runs, 30 loops each)

category类型下使用nunique函数:

%timeit -n 30 df.cut.astype('category').nunique()

3.26 ms ± 89.4 µs per loop (mean ± std. dev. of 7 runs, 30 loops each)

object类型下使用nunique函数性能更好。

2.钻石的切割质量可以分为五个等级,由次到好分别是Fair, Good, Very Good, Premium, Ideal,纯净度有八个等级,由次到好分别是I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF,请对切割质量按照由好到次的顺序排序,相同切割质量的钻石,按照纯净度进行由次到好的排序。
解:
首先使用reorder_categories方法对切割质量和纯净度分别进行序的建立:

df.cut = df.cut.astype('category').cat.reorder_categories(['Fair', 'Good', 'Very Good', 'Premium', 'Ideal'],ordered=True)
df.cut

0            Ideal
1          Premium
2             Good
3          Premium
4             Good
           ...    
53935        Ideal
53936         Good
53937    Very Good
53938      Premium
53939        Ideal
Name: cut, Length: 53940, dtype: category
Categories (5, object): ['Fair' < 'Good' < 'Very Good' < 'Premium' < 'Ideal']
df.clarity = df.clarity.astype('category').cat.reorder_categories(['I1', 'SI2', 'SI1', 'VS2', 'VS1', 'VVS2', 'VVS1', 'IF'],ordered=True)
df.clarity

0        SI2
1        SI1
2        VS1
3        VS2
4        SI2
        ... 
53935    SI1
53936    SI1
53937    SI1
53938    SI2
53939    SI2
Name: clarity, Length: 53940, dtype: category
Categories (8, object): ['I1' < 'SI2' < 'SI1' < 'VS2' < 'VS1' < 'VVS2' < 'VVS1' < 'IF']

然后按照题目要求进行排序:

res = df.sort_values(['cut', 'clarity'], ascending=[False, True])
res

在这里插入图片描述
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])

方法一:利用cat.codes:

df.cut = df.cut.cat.codes 

方法二:使用replace映射:

clarity_cat = df.clarity.cat.categories
df.clarity = df.clarity.replace(dict(zip(clarity_cat, np.arange(len(clarity_cat)))))
df

在这里插入图片描述
4. 对每克拉的价格按照分别按照分位数(q=[0.2, 0.4, 0.6, 0.8])与[1000, 3500, 5500, 18000]割点进行分箱得到五个类别Very Low, Low, Mid, High, Very High,并把按这两种分箱方法得到的category序列依次添加到原表中。
解:
即使用cut和qcut两种方法:
使用qcut:

avg = df.price / df.carat
df['avg_qcut'] = pd.qcut(avg, q=[0, 0.2, 0.4, 0.6, 0.8, 1], labels=['Very Low', 'Low', 'Mid', 'High', 'Very High'])

使用cut:

df['avg_cut'] = pd.cut(avg, bins=[-np.infty, 1000, 3500, 5500, 18000, np.infty], labels=['Very Low', 'Low', 'Mid', 'High', 'Very High'])

得到新表:
在这里插入图片描述
5. 第4问中按照整数分箱得到的序列中,是否出现了所有的类别?如果存在没有出现的类别请把该类别删除。
解:
查看表中出现的类别:

df.avg_cut.unique()

['Low', 'Mid', 'High']
Categories (3, object): ['Low' < 'Mid' < 'High']

查看原本序列的类别:

df.avg_cut.cat.categories

Index(['Very Low', 'Low', 'Mid', 'High', 'Very High'], dtype='object')

'Very Low’和’Very High’在表中没有出现,所以需要使用remove_categories来删除:

df.avg_cut = df.avg_cut.cat.remove_categories(['Very Low', 'Very High'])
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']

6.对第4问中按照分位数分箱得到的序列,求每个样本对应所在区间的左右端点值和长度。
解:
左端点值:

interval_avg = pd.IntervalIndex(pd.qcut(avg, q=[0, 0.2, 0.4, 0.6, 0.8, 1]))
interval_avg.left.to_series().reset_index(drop=True)

0        1051.162
1        1051.162
2        1051.162
3        1051.162
4        1051.162
           ...   
53935    3073.293
53936    3073.293
53937    3073.293
53938    3073.293
53939    3073.293
Length: 53940, dtype: float64

右端点值:

interval_avg.right.to_series().reset_index(drop=True)

0        2295.000
1        2295.000
2        2295.000
3        2295.000
4        2295.000
           ...   
53935    4031.683
53936    4031.683
53937    4031.683
53938    4031.683
53939    4031.683
Length: 53940, dtype: float64

长度:

interval_avg.length.to_series().reset_index(drop=True)

0        1243.838
1        1243.838
2        1243.838
3        1243.838
4        1243.838
           ...   
53935     958.390
53936     958.390
53937     958.390
53938     958.390
53939     958.390
Length: 53940, dtype: float64
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值