高阶pandas ¶
分类数据 ¶
import numpy as np
import pandas as pd
背景和目标 ¶
一个列经常会包含重复值,这些重复值是一个小型的不同值的集合。 unique
和value_counts
这样的函数允许我们从一个数组中提取不同值并分别计算这些不同值的频率:
values = pd.Series(['apple', 'orange', 'apple', 'apple'] * 2)
print(values)
# 0 apple
# 1 orange
# 2 apple
# 3 apple
# 4 apple
# 5 orange
# 6 apple
# 7 apple
# dtype: object
print(pd.unique(values))
# ['apple' 'orange']
print(pd.value_counts(values))
# apple 6
# orange 2
# dtype: int64
在数据入库的操作中,使用维度表是一种最佳实践,维度表包含了不同值,并将主要观测值存储为引用维度表的整数键:
values = pd.Series([0, 1, 0, 0] * 2)
dim = pd.Series(['apple', 'oragne'])
使用take
方法来恢复原来的字符串Series。(0对应到apple)。 这种按照整数展现的方式被称为分类或字典编码展现。不同值的数组可以被称为数据的类别、字典或层级。
print(dim.take(values))
# 0 apple
# 1 oragne
# 0 apple
# 0 apple
# 0 apple
# 1 oragne
# 0 apple
# 0 apple
# dtype: object
在做数据分析时,分类展示会产生显著的性能提升。可以在类别上进行转换同时不改变代码。
以下是一些相对低开销的转换示例:
- 重命名类别
- 在不改变已有的类别顺序的情况下添加一个新的类别
pandas中的Categorical类型 ¶
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)
},
columns=['basket_id', 'fruit', 'count', 'weight']
)
print(df)
# basket_id fruit count weight
# 0 0 apple 8 1.288867
# 1 1 orange 4 3.414430
# 2 2 apple 7 3.222160
# 3 3 apple 14 2.724804
# 4 4 apple 8 3.548828
# 5 5 orange 10 0.918739
# 6 6 apple 4 0.784816
# 7 7 apple 10 3.140607
df['fruit']
是一个Python字符串对象组成的数组。可以通过调用函数将它转换为Categorical
对象:
fruit_cat = df['fruit'].astype('category')
print(fruit_cat)
# 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']
fruit_cat
的值并不是NumPy数组,而是pandas.Categorical
的实例:
c = fruit_cat.values
print(type(c))
# <class 'pandas.core.arrays.categorical.Categorical'>
print(c)
# ['apple', 'orange', 'apple', 'apple', 'apple', 'orange', 'apple', 'apple']
# Categories (2, object): ['apple', 'orange']
Categorical
对象拥有categories
和codes
属性:
print(c.categories)
# Index(['apple', 'orange'], dtype='object')
print(c.codes)
# [0 1 0 0 0 1 0 0]
通过分配已转换的结果将DataFrame的一列转换为Categorical
对象:
print(df['fruit'])
# 0 apple
# 1 orange
# 2 apple
# 3 apple
# 4 apple
# 5 orange
# 6 apple
# 7 apple
# Name: fruit, dtype: object
df['fruit'] = df['fruit'].astype('category')
print(df['fruit'])
# 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']
也可以从其他Python序列类型直接生成pandas.Categorical
:
my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])
print(my_categories)
# ['foo', 'bar', 'baz', 'foo', 'bar']
# Categories (3, object): ['bar', 'baz', 'foo']
也可以使用from_codes
构造函数来转换其他数据源的分类编码数据:
categories = ['foo', 'bar', 'baz']
codes = [0, 1, 2, 0, 0, 1]
my_cats_2 = pd.Categorical.from_codes(codes, categories)
print(my_cats_2)
# ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
# Categories (3, object): ['foo', 'bar', 'baz']
这个未排序的分类实例可以使用as_ordered
进行排序:
print(my_cats_2.as_ordered())
# ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
# Categories (3, object): ['foo' < 'bar' < 'baz']
除非显式地指定,分类转换是不会指定类别的顺序的。因此categories
数组可能会与输入数据的顺序不同。 当使用from_codes
或其他任意构造函数时,可以为类别指定一个有意义的顺序:输出的[foo<bar<baz]
表明foo
的顺序在bar
之前,以此类推。
my_categories_ordered = pd.Categorical.from_codes(codes=codes, categories=categories, ordered=True)
print(my_categories_ordered)
# ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
# Categories (3, object): ['foo' < 'bar' < 'baz']
分类数据可以不是字符串,尽管举的例子都是字符串例子。一个分类数组可以包含任一不可变的值类型。
使用Categorical对象进行计算 ¶
在pandas中使用Categorical
与非编码版本相比(例如字符串数组)整体上是一致的。 pandas中的某些部分,比如groupby
函数,在与Categorical
对象协同工作时性能更好。 还有一些函数可以利用ordered标识。 下面考虑一些随机数字数据,并使用pandas.qcut
分箱函数。结果会返回pandas.Categorical
; 在前面章节使用过pandas.cut
,但当时没有分析分类是如何工作的细节。
np.random.seed(12345)
draws = np.random.randn(1000)
print(draws[:5])
# [-0.20470766 0.47894334 -0.51943872 -0.5557303 1.96578057]
计算上面数据的四分位分箱,并提取一些统计值:
bins = pd.qcut(draws, 4)
print(bins)
# [(-0.684, -0.0101], (-0.0101, 0.63], (-0.684, -0.0101], (-0.684, -0.0101], (0.63, 3.928], ..., (-0.0101, 0.63], (-0.684, -0.0101], (-2.9499999999999997, -0.684], (-0.0101, 0.63], (0.63, 3.928]]
# Length: 1000
# Categories (4, interval[float64, right]): [(-2.9499999999999997, -0.684] < (-0.684, -0.0101] < (-0.0101, 0.63] < (0.63, 3.928]]
通过在qcut
函数中使用labels
参数来四分位数名称:
bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
print(bins)
# ['Q2', 'Q3', 'Q2', 'Q2', 'Q4', ..., 'Q3', 'Q2', 'Q1', 'Q3', 'Q4']
# Length: 1000
# Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']
print(bins.codes[:10])
# [1 2 1 1 3 3 2 2 3 3]
被标记的bins
分类数据并不包含数据中箱体边界的相关信息,因此可以使用groupby
来提取一些汇总统计值:
bins = pd.Series(bins, name='quartile')
result = (pd.Series(draws).groupby(bins).agg(['count', 'min', 'max']).reset_index())
print(result)
# quartile count min max
# 0 Q1 250 -2.949343 -0.685484
# 1 Q2 250 -0.683066 -0.010115
# 2 Q3 250 -0.010032 0.628894
# 3 Q4 250 0.634238 3.927528
结果中的quartile
列保留了bins
中原始的分类信息,包括顺序:
print(result['quartile'])
# 0 Q1
# 1 Q2
# 2 Q3
# 3 Q4
# Name: quartile, dtype: category
# Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']
使用分类获得更高性能 ¶
如果对特定的数据集上做了大量的分析,将数据转换为分类数据可以产生大幅的性能提升。DateFrame中一列的分类版本通常也会明显使用更少内存。
下面的例子含有一千万元素的Series以及少量的不同类别:
N = 10000000
draws = pd.Series(np.random.randn(N))
labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))
现在将labels
转换为Categorical
对象:
categories = labels.astype('category')
print(labels.memory_usage()) # labels比categories使用了明显更多的内存
# 80000128
print(categories.memory_usage())
# 10000332
分类方法 ¶
Series包含的分类数据拥有一些特殊方法,这些方法类似于Series.str的特殊字符串方法。这些方法提供了快捷访问类别和代码的方式。
s = pd.Series(['a', 'b', 'c', 'd'] * 2)
cat_s = s.astype('category')
print(cat_s)
# 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']
特殊属性cat
提供了对分类方法的访问:
print(cat_s.cat.codes)
# 0 0
# 1 1
# 2 2
# 3 3
# 4 0
# 5 1
# 6 2
# 7 3
# dtype: int8
print(cat_s.cat.categories)
# Index(['a', 'b', 'c', 'd'], dtype='object')
假设数据的实际类别集合超出了数据中观察到的四个值,可以使用set_categories
方法来改变类别:
actual_categories = ['a', 'b', 'c', 'd', 'e']
cat_s2 = cat_s.cat.set_categories(actual_categories)
print(cat_s2)
# 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']
虽然看起来数据并未改变,但新类别将反映在使用它们的操作中。例如,value_counts
将遵循新的类别(如果存在):
print(cat_s.value_counts())
# a 2
# b 2
# c 2
# d 2
# dtype: int64
print(cat_s2.value_counts())
# a 2
# b 2
# c 2
# d 2
# e 0
# dtype: int64
大型数据集中,分类数据经常被用于节省内存和更高性能的便捷工具。 在过滤了一个大型DataFrame或Series之后,很多类别将不会出现在数据中。 可以使用remove_unused_categories
方法来去除未观察到的类别:
cat_s3 = cat_s[cat_s.isin(['a', 'b'])]
print(cat_s3)
# 0 a
# 1 b
# 4 a
# 5 b
# dtype: category
# Categories (4, object): ['a', 'b', 'c', 'd']
print(cat_s3.cat.remove_unused_categories())
# 0 a
# 1 b
# 4 a
# 5 b
# dtype: category
# Categories (2, object): ['a', 'b']
创建用于建模的虚拟变量 ¶
当使用统计数据或机器学习工具时,通常会将分类数据转换为虚拟变量,也称为one-hot编码。 这会产生一个DataFrame,每个不同的类别都是它的一列。这些列包含一个特定类别的出现次数,否则为0。
cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')
使用pandas.get_dummies
函数将一维的分类数据转换为一个包含虚拟变量的DataFrame:
print(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
高阶GroupBy应用 ¶
import numpy as np
import pandas as pd
分组转换和“展开”GroupBy ¶
在分组操作中可以使用apply方法实现转换操作。还有另一个内建方法transform,与apply方法类似但是可以对使用的函数加上更多的限制:
- transform可以产生一个标量值,并广播到各分组的尺寸数据中。
- transform可以产生一个与输入分组尺寸相同的对象。
- transform不可改变它的输入。
df = pd.DataFrame(
{
'key': ['a', 'b', 'c'] * 4,
'value': np.arange(12)
}
)
print(df)
# key value
# 0 a 0
# 1 b 1
# 2 c 2
# 3 a 3
# 4 b 4
# 5 c 5
# 6 a 6
# 7 b 7
# 8 c 8
# 9 a 9
# 10 b 10
# 11 c 11
按key
分组的均值:
g = df.groupby('key').value
print(g.mean())
# key
# a 4.5
# b 5.5
# c 6.5
# Name: value, dtype: float64
假设要产生一个Series,它的尺寸和df['value']
一样,但值都被按key
分组的均值替代。 可以向transfrom
传递匿名函数lambda x: x.mean()
:
result = g.transform(lambda x: x.mean())
print(result)
# 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
对于内建的聚合函数,可以像GroupBy
的agg
方法一样传递一个字符串别名:
result = g.transform('mean')
print(result)
# 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
的函数一起使用,但结果必须和输入有相同的大小。
例如,可以使用lambda
函数给每个组乘以2:
result = g.transform(lambda x: x * 2)
print(result)
# 0 0
# 1 2
# 2 4
# 3 6
# 4 8
# 5 10
# 6 12
# 7 14
# 8 16
# 9 18
# 10 20
# 11 22
# Name: value, dtype: int64
更复杂一些,可以按照每个组的降序计算排名:
result = g.transform(lambda x: x.rank(ascending=False))
print(result)
# 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
考虑一个由简单聚合构成的分组转换函数:
def normalize(x):
return (x - x.mean()) / x.std()
使用transform
或apply
可以获得等价的结果:
result = g.transform(normalize)
print(result)
# 0 -1.161895
# 1 -1.161895
# 2 -1.161895
# 3 -0.387298
# 4 -0.387298
# 5 -0.387298
# 6 0.387298
# 7 0.387298
# 8 0.387298
# 9 1.161895
# 10 1.161895
# 11 1.161895
# Name: value, dtype: float64
result = g.apply(normalize)
print(result)
# 0 -1.161895
# 1 -1.161895
# 2 -1.161895
# 3 -0.387298
# 4 -0.387298
# 5 -0.387298
# 6 0.387298
# 7 0.387298
# 8 0.387298
# 9 1.161895
# 10 1.161895
# 11 1.161895
# Name: value, dtype: float64
内建的聚合函数如mean
或sum
通常会比apply
函数更快。 这些函数在与transform
一起使用时也会存在一个"快速通过"。 这允许我们执行一个所谓的展开分组操作。 一个展开分组操作可能会包含多个分组聚合,矢量化操作的整体优势往往超过了这一点。
result = g.transform('mean')
print(result)
# 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
normalized = (df['value'] - g.transform('mean')) / g.transform('std')
print(normalized)
# 0 -1.161895
# 1 -1.161895
# 2 -1.161895
# 3 -0.387298
# 4 -0.387298
# 5 -0.387298
# 6 0.387298
# 7 0.387298
# 8 0.387298
# 9 1.161895
# 10 1.161895
# 11 1.161895
# Name: value, dtype: float64
分组的时间重新采样 ¶
对于时间序列数据,resample
方法在语义上是一种基于时间分段的分组操作。
N = 15
times = pd.date_range('2020-5-20 00:00', freq='1min', periods=N)
df = pd.DataFrame(
{
'time': times,
'value': np.arange(N)
}
)
print(df)
# time value
# 0 2020-05-20 00:00:00 0
# 1 2020-05-20 00:01:00 1
# 2 2020-05-20 00:02:00 2
# 3 2020-05-20 00:03:00 3
# 4 2020-05-20 00:04:00 4
# 5 2020-05-20 00:05:00 5
# 6 2020-05-20 00:06:00 6
# 7 2020-05-20 00:07:00 7
# 8 2020-05-20 00:08:00 8
# 9 2020-05-20 00:09:00 9
# 10 2020-05-20 00:10:00 10
# 11 2020-05-20 00:11:00 11
# 12 2020-05-20 00:12:00 12
# 13 2020-05-20 00:13:00 13
# 14 2020-05-20 00:14:00 14
这里,可以按time
进行索引,然后重新采样:
result = df.set_index('time').resample('5min').count()
print(result)
# value
# time
# 2020-05-20 00:00:00 5
# 2020-05-20 00:05:00 5
# 2020-05-20 00:10:00 5
假设DataFrame包含多个时间序列,并按一个附加的分组键列进行了标记:
df2 = pd.DataFrame(
{
'time': times.repeat(3),
'key': np.tile(['a', 'b', 'c'], N),
'value': np.arange((N * 3))
}
)
print(df2)
# time key value
# 0 2020-05-20 00:00:00 a 0
# 1 2020-05-20 00:00:00 b 1
# 2 2020-05-20 00:00:00 c 2
# 3 2020-05-20 00:01:00 a 3
# ......
# 43 2020-05-20 00:14:00 b 43
# 44 2020-05-20 00:14:00 c 44
使用pandas.TimeGrouper
对象,每个key
的值进行相同的重新采样: pd.TimeGrouper() was formally deprecated in pandas v0.21.0 in favor of pd.Grouper().
方法链技术 ¶
import numpy as np
import pandas as pd
from numpy import nan as NA
df = pd.DataFrame(
[[1., 2., 3.],
[1., NA, NA],
[NA, NA, NA],
[NA, 2., 3.]]
)
v = ['a', 'b', 'c', 'd']
print(df)
# 0 1 2
# 0 1.0 2.0 3.0
# 1 1.0 NaN NaN
# 2 NaN NaN NaN
# 3 NaN 2.0 3.0
非函数赋值的方式。
df2 = df.copy()
df2['k'] = v
print(df2)
# 0 1 2 k
# 0 1.0 2.0 3.0 a
# 1 1.0 NaN NaN b
# 2 NaN NaN NaN c
# 3 NaN 2.0 3.0 d
函数赋值的方式。 DataFrame.assign
方法是对df[k] = v
的赋值方式的一种功能替代。它返回的是一个按指定修改的新的DataFrame,而不是在原对象上进行修改。
df2 = df.assign(k=v)
print(df2)
# 0 1 2 k
# 0 1.0 2.0 3.0 a
# 1 1.0 NaN NaN b
# 2 NaN NaN NaN c
# 3 NaN 2.0 3.0 d
pipe方法 ¶
对数据连续操作形成方法链(多个方法连续调用对数据进行处理)。 Series.pipe
,DataFrame.pipe
意味着 x.pipe(f, *args, **kwargs)
和 f(x, *args, **kwargs)
效果相同。换句话说,该函数应用于整个数据。
以 DataFrame 为例:
- 语法:
DataFrame.pipe(func, *args, **kwargs)
- 参数:
- func:函数,应用于系列/数据帧的函数。args 和 kwargs 被传递到 func。或者是一个(callable,data_keyword)元组,其中 data_keyword 是一个字符串,表示需要Series/DataFrame 的 callable 关键字
- args:可迭代对象, 可选,函数的位置参数
- kwargs:mapping, 可选,传入 func 的关键字参数字典
- 返回:object:func 处理后的任意数据类型
DataFrame示例 ¶
df = pd.DataFrame(
[[1., 2., 3.],
[1., NA, NA],
[NA, NA, NA],
[NA, 2., 3.]]
)
被传递的类型是调用的实例。
df.pipe(type) # 传递的是type实例
# <class 'pandas.core.frame.DataFrame'>
df.pipe(len) # 传递的是len实例
# 4
def fun(df):
return df * 2
fun(df)
# 0 1 2
# 0 2.0 4.0 6.0
# 1 2.0 NaN NaN
# 2 NaN NaN NaN
# 3 NaN 4.0 6.0
df.pipe(fun) # 传递的是fun函数
# 0 1 2
# 0 2.0 4.0 6.0
# 1 2.0 NaN NaN
# 2 NaN NaN NaN
# 3 NaN 4.0 6.0
def fun2(x, df): # 数据是第二个参数
return df * 3
df.pipe((fun2, 'df'), 2) # 注意传值
# 0 1 2
# 0 3.0 6.0 9.0
# 1 3.0 NaN NaN
# 2 NaN NaN NaN
# 3 NaN 6.0 9.0
Series 示例 ¶
s = pd.Series([1, 2, 3, 4, 5])
s.pipe(type)
# <class 'pandas.core.series.Series'>
s.pipe(len)
# 5
def fun3(x, ss):
return ss * 3
s.pipe((fun3, 'ss'), 2)
# 0 3
# 1 6
# 2 9
# 3 12
# 4 15
# dtype: int64
GroupBy 示例 ¶
df = pd.DataFrame({'A': 'a b a b'.split(), 'B': [1, 2, 3, 4]})
print(df)
# A B
# 0 a 1
# 1 b 2
# 2 a 3
# 3 b 4
求每组最大值和最小值之间的差异。
df.groupby('A').pipe(lambda x: x.max() - x.min())
# B
# A
# a 2
# b 2
def mean1(groupby):
return groupby.mean()
df.groupby(['A']).pipe(mean1)
# B
# A
# a 2.0
# b 3.0