查看原文
其他

【翻译】《利用Python进行数据分析·第2版》第12章(下) pandas高级应用

SeanCheney Python爱好者社区 2019-04-07

作者:SeanCheney   Python爱好者社区专栏作者

简书专栏:https://www.jianshu.com/u/130f76596b02


前文传送门:

【翻译】《利用Python进行数据分析·第2版》第1章 准备工作

【翻译】《利用Python进行数据分析·第2版》第2章(上)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(中)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(下)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第3章(上)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(中)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(下)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第4章(上)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第4章(中)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第4章(下)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第5章(上)pandas入门

【翻译】《利用Python进行数据分析·第2版》第5章(中)pandas入门

【翻译】《利用Python进行数据分析·第2版》第5章(下)pandas入门

【翻译】《利用Python进行数据分析·第2版》第6章(上) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第6章(中) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第6章(下) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第7章(上)数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第7章(中) 数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第7章(下) 数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第8章(上) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第8章(中) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第8章(下) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第9章(上) 绘图和可视化

【翻译】《利用Python进行数据分析·第2版》第9章(中) 绘图和可视化

  【翻译】《利用Python进行数据分析·第2版》第9章(下) 绘图和可视化

  【翻译】《利用Python进行数据分析·第2版》第10章(上) 数据聚合与分组运算

  【翻译】《利用Python进行数据分析·第2版》第10章(中) 数据聚合与分组运算

  【翻译】《利用Python进行数据分析·第2版》第10章(下) 数据聚合与分组运算

  【翻译】《利用Python进行数据分析·第2版》第11章(上) 时间序列

  【翻译】《利用Python进行数据分析·第2版》第11章(中) 时间序列

  【翻译】《利用Python进行数据分析·第2版》第11章(中二) 时间序列

  【翻译】《利用Python进行数据分析·第2版》第11章(下) 时间序列

  【翻译】《利用Python进行数据分析·第2版》第12章(上) pandas高级应用

  【翻译】《利用Python进行数据分析·第2版》第12章(中) pandas高级应用


12.2 GroupBy高级应用

尽管我们在第10章已经深度学习了Series和DataFrame的Groupby方法,还有一些方法也是很有用的。

分组转换和“解封”GroupBy

在第10章,我们在分组操作中学习了apply方法,进行转换。还有另一个transform方法,它与apply很像,但是对使用的函数有一定限制:

  • 它可以产生向分组形状广播标量值

  • 它可以产生一个和输入组形状相同的对象

  • 它不能修改输入

来看一个简单的例子:

In [75]: df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4,   ....:                    'value': np.arange(12.)}) In [76]: df Out[76]:   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

按键进行分组:

In [77]: g = df.groupby('key').value In [78]: g.mean() Out[78]: key a    4.5 b    5.5 c    6.5 Name: value, dtype: float64

假设我们想产生一个和df['value']形状相同的Series,但值替换为按键分组的平均值。我们可以传递函数lambda x: x.mean()进行转换:

In [79]: g.transform(lambda x: x.mean()) Out[79]: 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方法:

In [80]: g.transform('mean') Out[80]: 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:

In [81]: g.transform(lambda x: x * 2) Out[81]: 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

再举一个复杂的例子,我们可以计算每个分组的降序排名:

In [82]: g.transform(lambda x: x.rank(ascending=False)) Out[82]: 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可以获得等价的结果:

In [84]: g.transform(normalize) Out[84]: 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 In [85]: g.apply(normalize) Out[85]: 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快。这允许我们进行一个所谓的解封(unwrapped)分组操作:

In [86]: g.transform('mean') Out[86]: 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 In [87]: normalized = (df['value'] - g.transform('mean')) / g.transform('std') In [88]: normalized Out[88]: 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方法从语义上是一个基于内在时间的分组操作。下面是一个示例表:

In [89]: N = 15 In [90]: times = pd.date_range('2017-05-20 00:00', freq='1min', periods=N) In [91]: df = pd.DataFrame({'time': times,   ....:                    'value': np.arange(N)}) In [92]: df Out[92]:                  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

这里,我们可以用time作为索引,然后重采样:

In [93]: df.set_index('time').resample('5min').count() Out[93]:                     value time                       2017-05-20 00:00:00      5 2017-05-20 00:05:00      5 2017-05-20 00:10:00      5

假设DataFrame包含多个时间序列,用一个额外的分组键的列进行标记:

In [94]: df2 = pd.DataFrame({'time': times.repeat(3),   ....:                     'key': np.tile(['a', 'b', 'c'], N),   ....:                     'value': np.arange(N * 3.)}) In [95]: df2[:7] Out[95]:  key                time  value 0   a 2017-05-20 00:00:00    0.0 1   b 2017-05-20 00:00:00    1.0 2   c 2017-05-20 00:00:00    2.0 3   a 2017-05-20 00:01:00    3.0 4   b 2017-05-20 00:01:00    4.0 5   c 2017-05-20 00:01:00    5.0 6   a 2017-05-20 00:02:00    6.0

要对每个key值进行相同的重采样,我们引入pandas.TimeGrouper对象:

In [96]: time_key = pd.TimeGrouper('5min')

我们然后设定时间索引,用key和time_key分组,然后聚合:

In [97]: resampled = (df2.set_index('time')   ....:              .groupby(['key', time_key])   ....:              .sum()) In [98]: resampled Out[98]:                         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 In [99]: resampled.reset_index() Out[99]: 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

使用TimeGrouper的限制是时间必须是Series或DataFrame的索引。

12.3 链式编程技术

当对数据集进行一系列变换时,你可能发现创建的多个临时变量其实并没有在分析中用到。看下面的例子:

df = load_data() df2 = df[df['col2'] < 0] df2['col1_demeaned'] = df2['col1'] - df2['col1'].mean() result = df2.groupby('key').col1_demeaned.std()

虽然这里没有使用真实的数据,这个例子却指出了一些新方法。首先,DataFrame.assign方法是一个df[k] = v形式的函数式的列分配方法。它不是就地修改对象,而是返回新的修改过的DataFrame。因此,下面的语句是等价的:

# Usual non-functional way df2 = df.copy() df2['k'] = v # Functional assign way df2 = df.assign(k=v)

就地分配可能会比assign快,但是assign可以方便地进行链式编程:

result = (df2.assign(col1_demeaned=df2.col1 - df2.col2.mean())          .groupby('key')          .col1_demeaned.std())

我使用外括号,这样便于添加换行符。

使用链式编程时要注意,你可能会需要涉及临时对象。在前面的例子中,我们不能使用load_data的结果,直到它被赋值给临时变量df。为了这么做,assign和许多其它pandas函数可以接收类似函数的参数,即可调用对象(callable)。为了展示可调用对象,看一个前面例子的片段:

df = load_data() df2 = df[df['col2'] < 0]

它可以重写为:

df = (load_data()      [lambda x: x['col2'] < 0])

这里,load_data的结果没有赋值给某个变量,因此传递到[ ]的函数在这一步被绑定到了对象。

我们可以把整个过程写为一个单链表达式:

result = (load_data()          [lambda x: x.col2 < 0]          .assign(col1_demeaned=lambda x: x.col1 - x.col1.mean())          .groupby('key')          .col1_demeaned.std())

是否将代码写成这种形式只是习惯而已,将它分开成若干步可以提高可读性。

管道方法

你可以用Python内置的pandas函数和方法,用带有可调用对象的链式编程做许多工作。但是,有时你需要使用自己的函数,或是第三方库的函数。这时就要用到管道方法。

看下面的函数调用:

a = f(df, arg1=v1) b = g(a, v2, arg3=v3) c = h(b, arg4=v4)

当使用接收、返回Series或DataFrame对象的函数式,你可以调用pipe将其重写:

result = (df.pipe(f, arg1=v1)          .pipe(g, v2, arg3=v3)          .pipe(h, arg4=v4))

f(df)和df.pipe(f)是等价的,但是pipe使得链式声明更容易。

pipe的另一个有用的地方是提炼操作为可复用的函数。看一个从列减去分组方法的例子:

g = df.groupby(['key1', 'key2']) df['col1'] = df['col1'] - g.transform('mean')

假设你想转换多列,并修改分组的键。另外,你想用链式编程做这个转换。下面就是一个方法:

def group_demean(df, by, cols):    result = df.copy()    g = df.groupby(by)    for c in cols:        result[c] = df[c] - g[c].transform('mean')    return result

然后可以写为:

result = (df[df.col1 < 0]          .pipe(group_demean, ['key1', 'key2'], ['col1']))

12.4 总结

和其它许多开源项目一样,pandas仍然在不断的变化和进步中。和本书中其它地方一样,这里的重点是放在接下来几年不会发生什么改变且稳定的功能。

为了深入学习pandas的知识,我建议你学习官方文档,并阅读开发团队发布的更新文档。我们还邀请你加入pandas的开发工作:修改bug、创建新功能、完善文档。

赞赏作者

好课推荐,名额有限,下图扫码即可免费学习

Python爱好者社区历史文章大合集

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门视频课程!!!

崔老师爬虫实战案例免费学习视频。

丘老师数据科学入门指导免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。

丘老师Python网络爬虫实战免费学习视频。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存