import numpy as np
import pandas as pd
在pandas
中提供了category
类型,使用户能够处理分类类型的变量,将一个普通序列转换成分类变量可以使用astype
方法。
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
对于一个具体的分类,有两个组成部分,其一为类别的本身,它以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
通过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')
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
方法完成,同时需要注意的是,这个方法会对原序列的对应值也进行相应修改。例如,现在把Sophomore
改成中文的本科二年级学生
:
s = s.cat.rename_categories({'Sophomore':'本科二年级学生'})
s.head()
0 NaN
1 NaN
2 NaN
3 本科二年级学生
4 本科二年级学生
Name: Grade, dtype: category
Categories (1, object): ['本科二年级学生']
有序类别和无序类别可以通过as_unordered
和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
进行具体的相对大小调整。
在第二章中,曾提到了字符串和数值类型序列的排序,此时就要说明分类变量的排序:只需把列的类型修改为category
后,再赋予相应的大小关系,就能正常地使用sort_index
和sort_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() # 值排序
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 |
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
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 False
4 True
Name: Grade, dtype: bool
区间是一种特殊的类别,在实际数据分析中,区间序列往往是通过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])
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]]
另外两个常用参数为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']
res[1] # 该元素为返回的分割点
array([0.999, 1.5 , 2. ])
从用法上来说,qcut
和cut
几乎没有差别,只是把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]]
对于某一个具体的区间而言,其具备三个要素,即左端点、右端点和端点的开闭状态,其中开闭状态可以指定right, left, both, neither
中的一类:
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
的功能类似于cut
或qcut
函数,只不过后两个是通过计算得到的分割点,而前者是直接传入自定义的分割点:
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
中的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
中四个参数之间的恒等关系。
end
- start
= period
* frequency
除此之外,如果直接使用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]')
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
还有两个常用方法,包括contains
和overlaps
,分别指逐个判断每个区间是否包含某元素,以及是否和一个pd.Interval
对象有交集。
id_demo.contains(40)
array([ True, False, False, True, False])
id_demo.overlaps(pd.Interval(40,60))
array([ True, True, False, True, False])
在第五章中介绍了crosstab
函数,在默认参数下它能够对两个列的组合出现的频数进行统计汇总:
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
函数来完成上面的功能。
my_crosstab
函数 , 参数S1
代表index
, S2
代表columns
, dropna
默认为True
S2
是category
类型并且dropna
为True
, 则将S2
多余的category
删除S2
调用groupby
依据S1
分组后用apply
调用自定义方法 , 该方法统计了S1
的每个分组中S2
的各类别的count
数 , 将其变换为DataFrame
再转置即可index
将其删除 , count
计数如果为NaN
填充为0
, 转换为int64
, 重命名columns
层级名def my_crosstab(S1, S2, dropna=True):
if S2.dtype.name=='category' and dropna: S2 = S2.cat.remove_unused_categories()
return S2.groupby(S1).apply(lambda x:x.groupby(x).count().to_frame().T).droplevel(1).fillna(0).astype('int64').rename_axis(columns=S2.name)
my_crosstab(df.A, df.B, False)
B | cat | dog | sheep |
---|---|---|---|
A | |||
a | 2 | 0 | 0 |
b | 1 | 0 | 0 |
c | 0 | 1 | 0 |
现有一份关于钻石的数据集,其中carat, cut, clarity, price
分别表示克拉重量、切割质量、纯净度和价格,样例如下:
df = pd.read_csv('../data/diamonds.csv')
df.head(3)
carat | cut | clarity | price | |
---|---|---|---|---|
0 | 0.23 | Ideal | SI2 | 326 |
1 | 0.21 | Premium | SI1 | 326 |
2 | 0.23 | Good | VS1 | 327 |
1. 分别对df.cut
在object
类型和category
类型下使用nunique
函数,并比较它们的性能。
object
和category
类型的列 , 比较性能发现category
完胜obj = df.cut
cat = df.cut.astype('category')
%timeit -n 100 obj.nunique()
3.06 ms ± 76.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit -n 100 cat.nunique()
784 µs ± 15.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2. 钻石的切割质量可以分为五个等级,由次到好分别是Fair, Good, Very Good, Premium, Ideal
,纯净度有八个等级,由次到好分别是I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF
,请对切割质量按照由好到次的顺序排序,相同切割质量的钻石,按照纯净度进行由次到好的排序。
cut
和clarity
列转为category
类型 , 调用reorder_categories
重排序df.cut = df.cut.astype('category').cat.reorder_categories(['Fair','Good','Very Good','Premium','Ideal'], ordered=True)
df.clarity = df.clarity.astype('category').cat.reorder_categories(['I1', 'SI2', 'SI1', 'VS2', 'VS1', 'VVS2', 'VVS1', 'IF'], ordered=True)
cut
和clarity
先降序再升序排序df.sort_values(['cut','clarity'], ascending=[False,True]).head()
carat | cut | clarity | price | |
---|---|---|---|---|
315 | 0.96 | Ideal | I1 | 2801 |
535 | 0.96 | Ideal | I1 | 2826 |
551 | 0.97 | Ideal | I1 | 2830 |
653 | 1.01 | Ideal | I1 | 2844 |
718 | 0.97 | Ideal | I1 | 2856 |
3. 分别采用两种不同的方法,把cut, clarity
这两列按照由好到次的顺序,映射到从0
到n-1
的整数,其中n
表示类别的个数。
方法一
: 用category
下的rename_categories
重命名df.cut.cat.rename_categories(range(df.cut.nunique())[::-1]).head()
0 0
1 1
2 3
3 1
4 3
Name: cut, dtype: category
Categories (5, int64): [4 < 3 < 2 < 1 < 0]
方法二
: 用category
下的codes
替换df.cut = df.cut.nunique() - df.cut.cat.codes - 1
方法三
: 用replace
替换df.clarity.replace(df.clarity.cat.categories, range(df.clarity.nunique())[::-1], True)
df.head(3)
carat | cut | clarity | price | |
---|---|---|---|---|
0 | 0.23 | 0 | 6 | 326 |
1 | 0.21 | 1 | 5 | 326 |
2 | 0.23 | 3 | 3 | 327 |
4. 对每克拉的价格按照分别按照分位数q=[0.2, 0.4, 0.6, 0.8]
与[1000, 3500, 5500, 18000]
割点进行分箱得到五个类别Very Low, Low, Mid, High, Very High
,并把按这两种分箱方法得到的category
序列依次添加到原表中。
qcut
和cut
传入上述bins
, 添加labels
重命名df['avg_qcut'] = pd.qcut(df.price/df.carat, [0,.2,.4,.6,.8,1], ['Very Low','Low','Mid','High','Very High'])
df['avg_cut'] = pd.cut(df.price/df.carat, [-np.inf,1000,3500,5500,18000,np.inf], labels=['Very Low','Low','Mid','High','Very High'])
df.head(3)
carat | cut | clarity | price | avg_qcut | avg_cut | |
---|---|---|---|---|---|---|
0 | 0.23 | 0 | 6 | 326 | Very Low | Low |
1 | 0.21 | 1 | 5 | 326 | Very Low | Low |
2 | 0.23 | 3 | 3 | 327 | Very Low | Low |
5. 第4
问中按照整数分箱得到的序列中,是否出现了所有的类别?如果存在没有出现的类别请把该类别删除。
avg_cut
序列唯一值与categories
是否一致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')
remove_unused_categories
删除未出现的类别df.avg_cut.cat.remove_unused_categories().head(3)
0 Low
1 Low
2 Low
Name: avg_cut, dtype: category
Categories (3, object): ['Low' < 'Mid' < 'High']
6. 对第4
问中按照分位数分箱得到的序列,求每个样本对应所在区间的左右端点值和长度。
IntervalIndex
构造一个avg_qcut
区间interval_avg = pd.IntervalIndex(pd.qcut(df.price/df.carat, [0,.2,.4,.6,.8,1]))
left
, right
, length
即可interval_avg.left.to_frame(False)[0].head(3)
0 1051.162
1 1051.162
2 1051.162
Name: 0, dtype: float64
interval_avg.right.to_frame(False)[0].head(3)
0 2295.0
1 2295.0
2 2295.0
Name: 0, dtype: float64
interval_avg.length.to_frame(False)[0].head(3)
0 1243.838
1 1243.838
2 1243.838
Name: 0, dtype: float64