1. 分类数据
本节学习 pandas 的 Categorical 类型,学习在使用 pandas 进行某些操作时如何获取更好的性能和内存使用,另外还有一些在统计和机器学习中使用分类数据的工具
1.1 背景和目标
一个列经常会包含重复值,这些重复值时一个小型的不同值的集合,unique 和 value_counts 函数允许我们从一个数组中提取不同值并分别计算这些不同值的频率 为了提高性能,我们使用分类的方法(或者称为字典编码)来呈现数据,也就是说利用编码的方法,将数据以整数的方式呈现,相应的,存在一个类别(或者说字典),保存了数值所代表的含义 如下例子,分类数据和类别均用 Series 表示,并且在类别上使用 take 方法,传入分类数据,即可还原原始数据
import numpy as np
import pandas as pd
def sep ( ) :
print ( '*' * 20 )
values = pd. Series( [ 'apple' , 'orange' , 'apple' , 'apple' ] * 2 )
print ( values)
sep( )
values = pd. Series( [ 0 , 1 , 0 , 0 ] * 2 )
print ( values)
sep( )
dim = pd. Series( [ 'apple' , 'orange' ] )
print ( dim)
sep( )
print ( dim. take( values) . reset_index( drop= True ) )
0 apple
1 orange
2 apple
3 apple
4 apple
5 orange
6 apple
7 apple
dtype: object
********************
0 0
1 1
2 0
3 0
4 0
5 1
6 0
7 0
dtype: int64
********************
0 apple
1 orange
dtype: object
********************
0 apple
1 orange
2 apple
3 apple
4 apple
5 orange
6 apple
7 apple
dtype: object
1.2 pandas 中的 Categorical 类型
fruits = [ 'apple' , 'orange' , 'apple' , 'apple' ] * 2
N = len ( fruits)
df = pd. DataFrame( { 'fruit' : fruits,
'basket_id' : np. arange( N) ,
'count' : np. random. randint( 3 , 15 , size= N) ,
'weight' : np. random. uniform( 0 , 4 , size= N) } )
fruit_cat = df[ 'fruit' ] . astype( 'category' )
print ( fruit_cat)
sep( )
c = fruit_cat. values
print ( c)
sep( )
print ( type ( c) )
sep( )
print ( c. categories)
sep( )
print ( c. codes)
0 apple
1 orange
2 apple
3 apple
4 apple
5 orange
6 apple
7 apple
Name: fruit, dtype: category
Categories (2, object): [apple, orange]
********************
[apple, orange, apple, apple, apple, orange, apple, apple]
Categories (2, object): [apple, orange]
********************
<class 'pandas.core.arrays.categorical.Categorical'>
********************
Index(['apple', 'orange'], dtype='object')
********************
[0 1 0 0 0 1 0 0]
从其他 Python 序列类型直接生成 pd.Categorical 对象
my_categories = pd. Categorical( [ 'foo' , 'bar' , 'baz' , 'foo' , 'bar' ] )
print ( my_categories)
[foo, bar, baz, foo, bar]
Categories (3, object): [bar, baz, foo]
在已经获得分类编码数据的情况下使用 pd.Categorical.from_codes 方法
categories = [ 'foo' , 'baz' , 'bar' ]
codes = [ 0 , 1 , 2 , 0 , 0 , 1 ]
my_cats_2 = pd. Categorical. from_codes( codes, categories)
print ( my_cats_2)
[foo, baz, bar, foo, foo, baz]
Categories (3, object): [foo, baz, bar]
默认情况下,分类转换没有指定类别的顺序,因此 categories 数组可能和输入的顺序不同,在使用 from_codes 或其他任意构造函数时,通过传递 ordered=True 来严格指定顺序 一个未排序的分类实例可以使用 as_ordered() 方法进行排序
ordered_cat = pd. Categorical. from_codes( codes, categories, ordered= True )
print ( ordered_cat)
sep( )
print ( my_cats_2. as_ordered( ) )
[foo, baz, bar, foo, foo, baz]
Categories (3, object): [foo < baz < bar]
********************
[foo, baz, bar, foo, foo, baz]
Categories (3, object): [foo < baz < bar]
1.3 使用 Categorical 对象进行计算
在 pandas 中使用 Categorical 与普通的非编码版本相比,整体上是一致的,不过 pandas 中的某些函数,例如 groupby,在与 Categorical 对象协同工作时性能更好,还有一些函数可以利用 ordered 标识 如下例子,计算 1000 个随机数字的四分位分箱,并添加四分位数名称,然后再使用 groupby 提取一些汇总统计值
draws = np. random. randn( 1000 )
print ( draws[ : 5 ] )
sep( )
bins = pd. qcut( draws, 4 )
print ( bins)
sep( )
bins = pd. qcut( draws, 4 , labels= [ 'Q1' , 'Q2' , 'Q3' , 'Q4' ] )
print ( bins)
sep( )
print ( bins. codes[ : 10 ] )
sep( )
bins = pd. Series( bins, name= 'quartile' )
result = ( pd. Series( draws) . groupby( bins) . agg( [ 'count' , 'min' , 'max' ] ) . reset_index( ) )
result
[-2.24878235 0.47012543 -0.333057 1.04582468 -0.94772562]
********************
[(-2.876, -0.682], (-0.029, 0.675], (-0.682, -0.029], (0.675, 3.349], (-2.876, -0.682], ..., (-0.029, 0.675], (-0.682, -0.029], (-0.029, 0.675], (-0.029, 0.675], (0.675, 3.349]]
Length: 1000
Categories (4, interval[float64]): [(-2.876, -0.682] < (-0.682, -0.029] < (-0.029, 0.675] < (0.675, 3.349]]
********************
[Q1, Q3, Q2, Q4, Q1, ..., Q3, Q2, Q3, Q3, Q4]
Length: 1000
Categories (4, object): [Q1 < Q2 < Q3 < Q4]
********************
[0 2 1 3 0 3 2 0 0 0]
********************
quartile count min max 0 Q1 250 -2.875225 -0.684647 1 Q2 250 -0.681031 -0.029240 2 Q3 250 -0.028850 0.674355 3 Q4 250 0.675663 3.348693
对于大量的数据集,使用分类能够获得更好的性能 如下示例数据集包含 1000 万条数据,明显 categories 使用了更少的内存
N = 10000000
labels = pd. Series( [ 'foo' , 'bar' , 'baz' , 'qux' ] * ( N // 4 ) )
categories = labels. astype( 'category' )
print ( labels. memory_usage( ) )
print ( categories. memory_usage( ) )
% time _ = labels. astype( 'category' )
80000080
10000272
Wall time: 294 ms
1.4 分类方法
Series 包含的分类数据拥有一些特殊的方法,这些方法提供了快捷访问类别和代码的方式 如特殊属性 cat 提供了对分类方法的访问
s = pd. Series( [ 'a' , 'b' , 'c' , 'd' ] * 2 )
cat_s = s. astype( 'category' )
print ( cat_s)
sep( )
print ( cat_s. cat. codes)
sep( )
print ( cat_s. cat. categories)
0 a
1 b
2 c
3 d
4 a
5 b
6 c
7 d
dtype: category
Categories (4, object): [a, b, c, d]
********************
0 0
1 1
2 2
3 3
4 0
5 1
6 2
7 3
dtype: int8
********************
Index(['a', 'b', 'c', 'd'], dtype='object')
cat.set_categories() 方法可以改变类别
actual_categories = [ 'a' , 'b' , 'c' , 'd' , 'e' ]
cat_s2 = cat_s. cat. set_categories( actual_categories)
print ( cat_s2)
sep( )
print ( cat_s2. value_counts( ) )
0 a
1 b
2 c
3 d
4 a
5 b
6 c
7 d
dtype: category
Categories (5, object): [a, b, c, d, e]
********************
d 2
c 2
b 2
a 2
e 0
dtype: int64
cat.remove_unused_categories 方法可以去除未观察到的类别
cat_s3 = cat_s[ cat_s. isin( [ 'a' , 'b' ] ) ]
print ( cat_s3)
sep( )
print ( cat_s3. cat. remove_unused_categories( ) )
0 a
1 b
4 a
5 b
dtype: category
Categories (4, object): [a, b, c, d]
********************
0 a
1 b
4 a
5 b
dtype: category
Categories (2, object): [a, b]
方法 描述 add_categories 将新的类别(未使用过的)添加到已有类别的尾部 as_ordered 对类别排序 as_unordered 使类别无序 remove_categories 去除类别,将被移除的值置为 null remove_unused_categories 去除所有没有出现在数据中的类别 rename_categories 使用新的类别名称替代现有的类别,不会改变类别的数量 reorder_categories 与 rename_categories 类似,但结果是经过排序的类别 set_categories 用指定的一组新类别替换现有类别,可以添加或删除类别
在使用统计数据或机器学习工具时,通常会将分类数据转换为虚拟变量,也称为 one-hot 编码 pandas.get_dummies() 函数将一维的分类数据转换为一个包含虚拟变量的 DataFrame,每个不同的类别都是它的一列,这些列包含特定类别的出现次数,否则为 0
pd. get_dummies( cat_s)
a b c d 0 1 0 0 0 1 0 1 0 0 2 0 0 1 0 3 0 0 0 1 4 1 0 0 0 5 0 1 0 0 6 0 0 1 0 7 0 0 0 1
2. 高阶 GroupBy 应用
2.1 分组转换和 “展开” GroupBy
groupby 除了使用 apply 方法执行转换操作外,还有一个内建方法 transform,它与 apply 类似但是对可以使用的函数种类有更多的限制
transform 可以产生一个标量值,并广播到各分组的尺寸数据中 transform 可以产生一个与输入分组尺寸相同的对象 transform 不可改变它的输入
df = pd. DataFrame( { 'key' : [ 'a' , 'b' , 'c' ] * 4 ,
'value' : np. arange( 12 . ) } )
df
key value 0 a 0.0 1 b 1.0 2 c 2.0 3 a 3.0 4 b 4.0 5 c 5.0 6 a 6.0 7 b 7.0 8 c 8.0 9 a 9.0 10 b 10.0 11 c 11.0
g = df. groupby( 'key' ) . value
print ( g. mean( ) )
key
a 4.5
b 5.5
c 6.5
Name: value, dtype: float64
使用 transform 方法,并传入匿名函数或字符串别名(仅针对内建的聚合函数),可以得到一个尺寸和 df[‘value’] 一样,但值都被按‘key’分组的均值替代的 Series 也就是前面说的 transform 内的函数如果返回的是标量值,则会广播到各分组中
print ( g. transform( lambda x: x. mean( ) ) )
sep( )
print ( g. transform( 'mean' ) )
0 4.5
1 5.5
2 6.5
3 4.5
4 5.5
5 6.5
6 4.5
7 5.5
8 6.5
9 4.5
10 5.5
11 6.5
Name: value, dtype: float64
********************
0 4.5
1 5.5
2 6.5
3 4.5
4 5.5
5 6.5
6 4.5
7 5.5
8 6.5
9 4.5
10 5.5
11 6.5
Name: value, dtype: float64
与 apply 类似,transform 可以与返回 series 的函数一起使用,但结果必须和输入有相同的大小
print ( g. transform( lambda x: x * 2 ) )
sep( )
print ( g. transform( lambda x: x. rank( ascending= False ) ) )
0 0.0
1 2.0
2 4.0
3 6.0
4 8.0
5 10.0
6 12.0
7 14.0
8 16.0
9 18.0
10 20.0
11 22.0
Name: value, dtype: float64
********************
0 4.0
1 4.0
2 4.0
3 3.0
4 3.0
5 3.0
6 2.0
7 2.0
8 2.0
9 1.0
10 1.0
11 1.0
Name: value, dtype: float64
2.2 分组的时间重新采样
对于时间序列数据,resample 方法在语义上是一种基于时间分段的分组操作,如下示例,对 df 进行 5min 重采样
N = 15
times = pd. date_range( '2017-05-20 00:00' , freq= '1min' , periods= N)
df = pd. DataFrame( { 'time' : times,
'value' : np. arange( N) } )
print ( df)
sep( )
print ( df. set_index( 'time' ) . resample( '5min' ) . count( ) )
time value
0 2017-05-20 00:00:00 0
1 2017-05-20 00:01:00 1
2 2017-05-20 00:02:00 2
3 2017-05-20 00:03:00 3
4 2017-05-20 00:04:00 4
5 2017-05-20 00:05:00 5
6 2017-05-20 00:06:00 6
7 2017-05-20 00:07:00 7
8 2017-05-20 00:08:00 8
9 2017-05-20 00:09:00 9
10 2017-05-20 00:10:00 10
11 2017-05-20 00:11:00 11
12 2017-05-20 00:12:00 12
13 2017-05-20 00:13:00 13
14 2017-05-20 00:14:00 14
********************
value
time
2017-05-20 00:00:00 5
2017-05-20 00:05:00 5
2017-05-20 00:10:00 5
对于一个包含多个时间序列,并且每个时间序列都使用一个附加的分组键进行标记的 DataFrame,要为每个时间序列进行相同的 5min 重采样,可以使用 pandas.TimeGrouper 对象,该对象可以被用作分组键 使用 pandas.TimeGrouper 的一个限制是时间必须是 Series 或 DataFrame 的索引 官方提示 TimeGrouper 未来会被取消掉,使用 pd.Grouper(freq=…)替代
df2 = pd. DataFrame( { 'time' : times. repeat( 3 ) ,
'key' : np. tile( [ 'a' , 'b' , 'c' ] , N) ,
'value' : np. arange( N * 3 . ) } )
print ( df2[ : 10 ] )
sep( )
time_key = pd. TimeGrouper( '5min' )
resampled = ( df2. set_index( 'time' ) . groupby( [ 'key' , time_key] ) ) . sum ( )
print ( resampled)
sep( )
print ( resampled. reset_index( ) )
time key value
0 2017-05-20 00:00:00 a 0.0
1 2017-05-20 00:00:00 b 1.0
2 2017-05-20 00:00:00 c 2.0
3 2017-05-20 00:01:00 a 3.0
4 2017-05-20 00:01:00 b 4.0
5 2017-05-20 00:01:00 c 5.0
6 2017-05-20 00:02:00 a 6.0
7 2017-05-20 00:02:00 b 7.0
8 2017-05-20 00:02:00 c 8.0
9 2017-05-20 00:03:00 a 9.0
********************
value
key time
a 2017-05-20 00:00:00 30.0
2017-05-20 00:05:00 105.0
2017-05-20 00:10:00 180.0
b 2017-05-20 00:00:00 35.0
2017-05-20 00:05:00 110.0
2017-05-20 00:10:00 185.0
c 2017-05-20 00:00:00 40.0
2017-05-20 00:05:00 115.0
2017-05-20 00:10:00 190.0
********************
key time value
0 a 2017-05-20 00:00:00 30.0
1 a 2017-05-20 00:05:00 105.0
2 a 2017-05-20 00:10:00 180.0
3 b 2017-05-20 00:00:00 35.0
4 b 2017-05-20 00:05:00 110.0
5 b 2017-05-20 00:10:00 185.0
6 c 2017-05-20 00:00:00 40.0
7 c 2017-05-20 00:05:00 115.0
8 c 2017-05-20 00:10:00 190.0
d:\python 3.7\lib\site-packages\ipykernel_launcher.py:6: FutureWarning: pd.TimeGrouper is deprecated and will be removed; Please use pd.Grouper(freq=...)