[学习笔记]Python for Data Analysis, 3E-10.数据聚合和组操作

对数据集进行分类并将函数应用于每个组(无论是聚合还是转化)可能是数据分析工作流的关键组件。加载、合并和准备数据集后,你可能需要计算组统计信息或可能的数据透视表,以便进行报告或可视化。pandas提供了一个多功能的groupby界面,使你能够以自然的方式对数据集进行切片,切块和汇总。
关系数据库和SQL(代表’结构化查询语言’)流行的一个原因是数据可以很容易地被联接、过滤、转换和聚合。但是,查询语言(如SQL)对可以执行的组操作类型施加某种限制。正如你将看到的,凭借Python和pandas的表现力,我们可以通过将它们表示为自定义Python函数来执行相当复杂的组操作,这些函数可以操作每个组关联的数据。在本章中,你将学习如何:

  • 使用一个或多个键(以函数、数组或DataFrame列名的形式)将pandas对象拆分为多个部分
  • 计算组汇总统计数据,如计数、平均值、标准差或用户定义的函数
  • 应用组内变换或其他操作,如归一化、线性回归、秩或子集选择
  • 计算数据透视表和交叉表(列联表)
  • 执行分位数分析和其他统计组分析

注意:基于时间的时间序列数据聚合(groupby的一种特殊使用),在本书中被称为重采样,将在’第11章:时间序列’中单独处理。

与其他章一样,我们从导入NumPy和pandas开始:

import numpy as np
import pandas as pd

10.1如何考虑组操作

Hadley Wickham,R语言的许多流行软件包的作者,他创造了术语’拆分-应用-组合’来描述组操作。在该过程的第一阶段,pandas对象中包含的数据(无论是Series、DataFrame还是其他对象),将根据你提供的一个或多个键拆分为多个组。拆分在对象的特定轴上执行。例如,一个DataFrame可以按照它的行(axis=‘index’)或者它的列(axis=‘columns’)分组。一旦此操作完成后,将对每个组应用一个函数,从而生成一个新值。最后,所有这些函数应用程序的结果都合并到一个结果对象中。结果对象的形式通常取决于对数据执行的操作。有关简单组聚类的模型,请参见图10.1。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tCxru7w1-1669683324104)(https://secure2.wostatic.cn/static/iVHdqYTthjGDMk3rPNKdBr/image.png?auth_key=1669683275-2mUupGETLT16vtvHrWX8xM-0-1267c47cb11537040e9ff5aa080fa75b)]

每个分组键可以采用多种形式,并且这些键不必全部为同一类型:

  • 与要分组的轴长度相同的值列表或数组
  • 指示DataFrame中列名的值
  • 字典或Series,提供被分组轴上的值与组名称之间的对应关系
  • 要在轴索引或索引中的各个标签上调用的函数

请注意,后三种方式是生成用于拆分对象的值数组的快捷方式。如果这一切看起来都是抽象的,请不要担心。在本章中,我将给出所有这些方法的许多示例。首先,下面是一个作为DataFrame的小表格数据集:

df = pd.DataFrame({'key1': ['a', 'a', None, 'b', 'b', 'a', None],
                   'key2': pd.Series([1, 2, 1, 2, 1, None, 1], dtype='Int64'),
                   'data1': np.random.standard_normal(7),
                   'data2': np.random.standard_normal(7)})
# 假设你要使用来自key1的标签计算data1列中的均值,则有多种方法可以做到这一点。
# 一种是通过访问data1并在key1的列(series)上调用groupby
grouped = df['data1'].groupby(df['key1']) # df['data1']按照df['key1']的值进行分组,返回一个特殊的'GroupBy'对象。
# 除了一些关于组键df['key1']的中间数据外,'GroupBy'对象实际上还没计算出任何东西。但这个对象具有将某些操作应用于每个组所需的所有信息
# 例如,计算组的均值,意味着我们可以调用GroupBy的mean方法:
grouped.mean() # 组内计算均值

稍后在’数据聚合’小节中,将详细介绍调用.mean()时发生了什么。此处的重点是,数据(series)已完成聚合:先按照组键拆分数据,再生成一个新series,该series由key1列中的唯一值索引。结果索引的名称为’key1’是因为DtaFrame的df[‘key1’]列的名为’key1’。

# 也可以用多个数组作为列表传递给groupby函数
means = df['data1'].groupby([df['key1'], df['key2']]).mean() # 使用两个键对数据分组,则生成的Series(因为对象是series)有分层索引,索引由观察到的唯一键对组成
means.unstack() # 将means展开,最高层的行索引转化为列索引,返回DataFrame

# groupby中的分组键可以是长度正确的任何数组(或数组组成的列表)
states = np.array(['OH', 'CA', 'CA', 'OH', 'OH', 'CA', 'OH'])
years = [2005, 2005, 2006, 2005, 2006, 2005, 2006]
df['data1'].groupby([states, years]).mean()

# 通常,分组信息与要处理的数据位于同一DataFrame中。在这种情况下,你可以将列名(无论是字符串、数字还是其他Python对象)作为组键传递:
df.groupby('key1').mean() # 按照'key1'列分组,并计算df其他列在该分组下的均值(缺失值不计入)
df.groupby('key2').mean() # 这种方法会报FutureWarning:未来仅以数值('key2'列全为数值)为组键,且调用对象为默认值(这里是df)的DataFrameGroupBY.mean()方法将被弃用
# 这种情况下,可以将调用对象具体化(df[['data1', 'data2']]),如使用df[['data1', 'data2']].groupby(df['key2']).mean()获得等效结果而不报Warning
df.groupby(["key1", "key2"]).mean() # 非仅以数组为数值为组键的DataFrameGroupBY.mean()方法则不报Warning

你可能注意到,在第二种情况下,df.groupby(‘key2’).mean(),结果中没有’key1’列。因为df[‘key1’]不是数值数据,它被称为令人讨厌的列,会自动从结果中排除。默认情况下,所有数字列都是聚合的,尽管可以向下筛选为子集,你很快就能看到。

无论使用groupby的目的是什么,一个普遍有用的GroupBy方法是size,它返回一个包含组大小的Series(无论调用对象df是Series还是DataFrame,返回一定是series):

df.groupby(['key1', 'key2']).size() # 返回Seires

# 默认情况下,组键中的任何缺失值都将从结果中排除。可以通过传递dropna=False到groupby来禁用此行为
df.groupby('key1', dropna=False).size() # 返回的Series中,NaN也会被认为是合法值,作为组键的元素出现
df.groupby(['key1', 'key2'], dropna=False).size()

类似的组函数是count,它计算每个组中对应列非空值的数量

df.groupby('key2').count() # 返回DataFrame(因为调用对象是DataFrame)

迭代组

groupby函数返回的对象支持迭代,生成一系列包含键值以及对应数字组成的二元组。

for name, group in df.groupby('key1'):
    print(name)
    print(group)

# 对于多个键的情况,元组中的第一个元素将是键值组成的元组
for (k1, k2), group in df.groupby(['key1', 'key2']):
    print((k1, k2))
    print(group)

# 数据片段作为字典的单行进行计算
pieces = {name: group for name, group in df.groupby('key1')}
pieces['b']

默认情况下,groupby在axis=‘index’(跨行分组、组内显示键值所在行的数据)上分组,但事实上可以在其他任何轴上分组。

# 例如,我们可以对df的列分组,根据它们以'key'或者'data'开始
grouped = df.groupby({'key1': 'key', 'key2': 'key', 'data1': 'data', 'data2': 'data'}, axis='columns')
# 传递字典,提供被分组轴上的值与组名称的对应关系,这里生成两个组,一个组名(键值)为'key',一个组名为'data';跨列分组,组内显示组名(键值)所在列的数据
for group_key, group_values in grouped:
    print(group_key)
    print(group_values)

选择列或列的子集

DataFrame的列名或者列名的数组分组得到GroupBy对象,对这样创建的GroupBy对象进行索引,具有用于聚合的列子集的效果。

df.groupby('key1')['data1'] # 等价于df['data1'].groupby(df['key1'])
df.groupby('key1')[['data2']] # 等价于df[['data2']].groupby(df['key1'])

特别是对大型数据集,可能只聚合几列。例如,在前面的数据集中,要仅计算data2列的均值并将结果作为DataFrame,可以编写:

# 传递的是列表或者数组,则返回DataFrame
df.groupby(['key1', 'key2'])[['data2']].mean()

# 传递的是仅单个列名作为标量,则返回Series
df.groupby(['key1', 'key2'])['data2'].mean()

使用字典和Series进行分组

分组信息可能以数组以外的形式存在。让我们考虑另一个DataFrame示例:

people = pd.DataFrame(np.random.standard_normal((5, 5)),
                      columns=['a', 'b', 'c', 'd', 'e'],
                      index=['Joe', 'Steve', 'Wanda', 'Jill', 'Trey'])
people.iloc[2:3, [1, 2]] = np.nan # people的第2行的第1列和第2列为np.nan
mapping = {'a': 'red', 'b': 'red', 'c': 'blue', 'd': 'blue', 'e': 'red', 'f': 'orange'} # 构建列和组键的对应关系
# 可以从这个字典构造一个数组传递给groupby,也可以直接传递这个字典(包含键'f'是为列突出显示包含未使用的分组键也是可以的)
by_column = people.groupby(mapping, axis='columns') # 按照组键分组,组内显示键值所在列的数据
by_column.sum() # 跨列求和

# 传递Series也可以
map_series = pd.Series(mapping)
people.groupby(map_series, axis='columns').count()

使用函数分组

与字典和Series相比,使用Python函数是定义组映射的更通用方法。作为组键传递的任何函数将为每个索引值调用一次(如果使用axis=‘columns’,则对每个列索引调用一次),返回值将用作祖名。

people.groupby(len).sum() # 按照行索引在len函数处理后的值分组,组内跨行求和

将函数与数组、字典或Series混合不是问题,因为所有内容都在内部转换为数组:

key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len, key_list]).min() # 按照行索引在len函数处理后的值、keylist给定的值进行分组,组内跨行求最小值

按索引层分组

分层索引数据集的最后一个方便之处在于能够使用轴索引的其中一个层进行聚合。

columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'], [1, 3, 5, 1, 3], names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.standard_normal((4, 5)), columns=columns) # 创建含分层列索引的DataFrame
# 要按照层分组,使用level关键字传递层的编号或名称:
hier_df.groupby(level='cty', axis='columns').count()

10.2数据聚合

聚合是指从数组生成标量值的任何数据转换。前面的示例使用了几个,包括mean、count、min和sum。许多常见的聚合都优化了实现。

下表是优化了的groupby方法。

[表]优化了的groupby方法

你可以使用自己设计的聚合,并另外调用在被分组对象上定义的任何方法。例如,series的nsmallest方法会从数据中选择最小的请求数量的值。尽管nsmallest没有为GroupBy对象显式实现,但我们仍可以在非优化实现的情况下使用它。在内部,GroupBy将series切成薄片,对每个部分调用piece.nsmallest(n),然后将这些结果组合到结果对象中:

grouped = df.groupby('key1')
grouped['data1'].nsamllest(2)

要使用你自己的聚合函数,请将任何聚合数组的函数传递给aggregate方法或它的缩写agg方法:

def peak_to_peak(arr): # 创建数组映射到标量的函数作为后续的聚合函数
    return arr.max() - arr.min()

grouped.agg(peak_to_peak) # 将聚合函数传递到GroupBy的agg方法

你可能会注意到,严格来说,某些方法,如describe,也有效,即使它们不是聚合函数

grouped.describe()
# 将在'应用:一般拆分-应用-组合'中详细解释发生的事情

注意:自定义聚合函数通常比上面表中的优化过的函数慢得多。这是因为在构造中间组数据块时存在一些额外的开销(函数调用、数据重新排列).

按列和多函数应用

让我们回到上一章中使用的小费数据集。使用pandas.read_csv加载后,添加一个小费百分比列:

tips = pd.read_csv('examples/tips.csv')
tips['tip_pct'] = tips['tip'] / tips['total_bill'] # 添加小费百分比列

如你所见,聚合一个Series或者一个DataFrame的所有列是使用aggregate(或agg)和所需函数,或者调用如mean或者std方法。但是,你可能希望使用不同的函数进行聚合(具体取决于列),或者一次聚合多个函数。幸运的是,这是可以做到的,下面通过一些例子来说明。首先,按照day和smoker聚合tips:

grouped = tips.groupby(['day', 'smoker'])
# 如[表]:优化了的groupby方法中所示的描述性统计函数,你可以将函数名称作为字符串传递给agg方法
grouped_pct = grouped['tip_pct']
grouped_pct.agg('mean')

# 如果改为传递函数或者函数名称的列表,则会返回一个DataFrame,其中包含从函数中获得的列名(不包含原列名'tip_pct')
grouped_pct.agg(['mean', 'std', peak_to_peak]) # 传递聚合函数列表给agg方法,以独立评估数据组

你不需要接受GroupBy为列提供的名称;注意到,lambda(匿名)函数有名称,这使它难以识别(你可以通过查看函数的__name__属性来查看函数的名称)。此时,你可以传递(name, function)元组,则元组的第一个元组将用于DataFrame的列名:

grouped_pct.agg([('average', 'mean'), ('stdev', np.std)])

使用DataFrame,你有更多的选择,因为你可以指定要应用于所有列或每列不同函数的函数列表。

# 首先,假设我们希望计算为tip_pct和total_bill列计算相同的三个统计量
functions = ['count', 'mean', 'max']
result = grouped[['tip_pct', 'total_bill']].agg(functions) # 生成的DataFrame具有分层列,就像分别聚合每列并使用列名作为keys参数,通过concat将结果粘合起来一样
result['tip_pct']
# 也可以传递具有自定义名称的元组列表
ftuples = [('Average', 'mean'), ('Variance', np.var)]
grouped[['tip_pct', 'total_bill']].agg(ftuples)

# 现在,假设你要将不同的函数应用于一个或多个列。为此,需要传递一个字典到agg,其中包含列名到'到目前为止列出的任何规范函数'的映射
grouped.agg({'tip': np.max, 'size': 'sum'}) # 分别用np.max聚合'tip'列,sum聚合'size‘列。
grouped.agg({'tip_pct': ['min', 'max', 'mean', 'std'], 'size': 'sum'})

返回不带行索引的聚合数据

到目前为止的所有示例中,聚合数据都返回一个索引,该索引可能是分层的,由唯一的组键组成。由于这并不总是需要的,所以你可以通过传递as_index=False到groupby来禁用它:

tips.groupby['day', 'smoker'], as_index=False).mean() # 调用as_index=False之后,则索引变成RangeIndex。
# 当然,通过对结果调用reset_index也可以获得这个结果。但使用ax_index=False可以避免一些不必要的计算

10.3应用:一般拆分-应用-组合

最通用的GroupBy方法是apply,这是本节的主题。apply将正在操作的对象拆分为多个部分,在每个部分上调用传递的函数,然后尝试连接这些部分。
返回之前的小费素聚集,假设你要按照组选择tip_pct最大的五个值。首先,编写一个函数,用于选择特定列中值最大的若干行:

def top(df, n=5, column='tip_pct'):
    return df.sort_values(column, ascending=False)[:n] # 按照某列(默认为'tip_pct'列)对DataFrame降序排列,并取前n行

# 现在,我们按照smoker分组,并使用此函数调用apply方法
tips.groupby('smoker').apply(top)

这里发生了什么?首先,根据smoker的值,tips DataFrame拆分为多个组,然后在每个组上调用top函数,并使用pandas.concat函数将每个函数调用的结果粘合在一起,用组名标记各个部分。因此,结果具有一个具有内部层的分层索引,该索引层包含来自于原始DataFrame的索引值。

# 如果你想传递一个携带其他参数或者关键字的函数到apply方法中,则可以将这些值传递到函数后面
tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')

除了这些基本的使用机制之外,充分利用apply方法可能需要一些创造力。传递的函数内部发生的取决于你;它必须返回pandas对象或者一个标量。本章其余部分将主要由实例组成,向你展示如何使用groupby解决各种问题。

result = tips.groupby('smoker')['tip_pct'].describe()
result.unstack('smoker') # 将行索引中的'smoker'层移到列索引的最内层,返回series

# 在GroupBy内部,当你调用类似describe函数,实际上就是以下方法的便捷写法:
def f(group):
    return group.describe()

grouped.apply(f)

取消组键

在前面的示例中,你可以看到生成的对象具有由组键形成的分层索引,以及原始对象的每个部分的索引。可以通过传递group_keys=False到groupby方法中,则不显示组键:

tips.groupby('smoker', group_keys=False).apply(top)

分位数和桶分析

你可能还记得’第八章:数据整理:连接,合并,和重塑’,pandas有一些工具,特别是pandas.cut和pandas.qcut,用于将数据切入桶中,通过你选择的箱子或者样本分位数。将这些函数与groupby结合可以方便地对数据集执行桶或者分位数分析。

# 考虑一个简单随机数据集和一个用pandas.cut函数获得的等长的存储桶分类
frame = pd.DataFrame({'data1': np.random.standard_normal(1000), 'data2': np.random.standard_normal(1000)})
quartiles = pd.cut(frame['data1'], 4) # 返回Categorical对象,元素是各值归属的桶
# 由pd.cut函数返回的Categorical对象可以直接传递给groupby函数。因此,我们可以计算四分位数组统计信息
def get_stats(group):
    return pd.DataFrame(
        {'min': group.min(), 'max': group.max(),
        'count': group.count(), 'mean': group.mean()}
    )

grouped = frame.groupby(quartiles)
grouped.apply(get_stats)

# 请记住,相同的结果可以更简单地计算出来
grouped.agg(['min', 'max', 'count', 'mean'])

这些是等长的桶;要根据样本分位数计算大小相等的桶,则需要使用pandas.qcut函数。

# 传递4作为桶数,用于计算样本分位数,传递labels=False来仅仅获得分位数索引而不是区间
quartiles_samp = pd.qcut(frame['data1'], 4, labels=False)
grouped = frame.groupby(quartiles_samp)
grouped.apply(get_stats)

示例:使用组特定的值填充缺失值

清理缺失值时,在某些情况下,你将使用dropna删除数据观测值,但在其他情况下,你可能希望使用固定值或从数据派生的某些值来填充null(NA)值。fillna是使用的正确的工具;例如,在这里我们用平均值填充null值:

s = pd.Series(np.random.standard_normal(6))
s[::2] = np.nan
s.fillna(s.mean()) # 用均值填充缺失值

假设你需要填充值因组而异。执行此操作的一种方法是对数据进行分组,并用apply函数在每个数据块上调用fillna函数。以下是美国各州划分为东部和西部地区的一些示例数据:

states = ["Ohio", "New York", "Vermont", "Florida", "Oregon", "Nevada", "California", "Idaho"]
group_key = ["East", "East", "East", "East", "West", "West", "West", "West"]
data = pd.Series(np.random.standard_normal(8), index=states) # 构建series
data[['Vermont', 'Nevada', 'Idaho']] = np.nan # 设置一些缺失值
data.groupby(group_key).size()
data.groupby(group_key).count()
data.groupby(group_key).mean()

# 使用组均值填充缺失值
def fill_mean(group):
    return group.fillna(group.mean())

data.groupby(group_key, group_keys=False).apply(fill_mean)

在另一种情况下,代码中可能具有预定义的填充值,这些值因组而异。因为组在内部设置了一个name属性,我们可以使用它:

fill_values = {'East': 0.5, 'West': -1}
def fill_func(group):
    return group.fillna(fill_values[group.name]) # 根据组的name属性,通过字典获得填充值

data.groupby(group_key, group_keys=False).apply(fill_func)

示例:随机抽样和排列

假设你要从大型数据集中抽取一个随机样本(重新放回或者不重新放回),用于蒙特卡罗模拟或者其他一些应用程序。有许多方法可以执行’抽样’,这里,我们使用用于series的sample方法。

# 构建英式扑克牌
suits = ['H', 'S', 'C', 'D'] # 红心,黑桃,梅花,方块
card_val = (list(range(1, 11)) + [10] * 3) * 4
base_names = ['A'] + list(range(2, 11)) + ['J', 'K', 'Q']
cards = []
for suit in suits:
    cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val, index=cards) # 构建了长为52的Series,其索引包含卡牌名称(简单起见,规定'A'的值为1)

# 抽五张牌
def draw(deck, n=5):
    return deck.sample(n)

draw(deck) # 返回series

# 假设你想要从每套花色中随机抽取两张牌。因为花色时每张牌名的最后一个字符,所以可以根据这个进行分组并使用apply:
def get_suit(card):
    return card[-1] # 获得卡牌的最后一个字符

deck.groupby(get_suit).apply(draw, n=2) # 按最后一个字符分组,分组后,组内应用draw函数,并设置参数n=2(抽两张牌)
# 可以传递group_keys=False来删除外部的花色索引,只留下选定的卡牌:
deck.groupby(get_suit, group_keys=False).apply(draw, n=2)

示例:组加权平均值和相关性

在groupby的拆分-应用-组合范例下,可以在DataFrame或两个Series中的列之间操作称为可能,例如组加权平均值。例如,以包含组键、值和一些权重的数据集为例:

df = pd.DataFrame({'category': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b'],
                   'data': np.random.standard_normal(8),
                   'weights': np.random.uniform(size=8)})
# 计算'category'分组的加权平均值(weighted average)
grouped = df.groupby('category')

def get_wavg(group):
    return np.average(group['data'], weights=group['weights']) # 对一个组,计算它对应的加权平均值

grouped.apply(get_wavg)

再举一个例子,考虑一个最初从雅虎财经获得的金融数据集,其中包含几只股票的收盘价和标准普尔500指数(SPX符号):

close_px = pd.read_csv('examples/stock_px.csv', parse_dates=True, index_col=0)
close_px.info() # DataFrame的info方法时获取DataFrame内容概述的便捷方法

# 一个感兴趣的任务可能是计算一个DataFrame,它包含SPX的每日回报的年相关性(根据百分比变化计算)组成。
# 作为执行此操作的一种方法,我们首先创建一个函数用于计算每列与'SPX'列的成对相关性:
def spx_corr(group):
    return group.corrwith(group['SPX'])
# 接下来,我们用pct_change计算close_px上的百分比改变(pct_change会计算当前元素和上一个元素之间的百分比变化,即(当前元素-上一个元素)/上一个元素)
rets = close_px.pct_change().dropna()
# 最后,我们按年份对这些百分比变化进行分组,组键可以从每个行datetime标签的year属性中提取:
def get_year(x):
    return x.year

by_year = rets.groupby(get_year)
by_year.apply(spx_corr)

# 也可以计算列间相关性,这里我们计算'Apple'和'Microsoft'的年度相关性
def corr_aapl_msft(group):
    return group['AAPL'].corr(group['MSFT'])

by_year.apply(corr_aapl_msft)

示例:分组线性回归

与上一个示例相同主题中,只要函数返回pandas对象或者标量,就可以用groupby执行更复杂的组间统计分析。例如,可以定义下面的regress函数(使用statsmodels经济学库),该函数对每个数据块执行普通最小二乘法(OLS)回归:

import statsmodels.api as sm
def regress(data, yvar=None, xvars=None): # 构建计算每个组的最小二乘回归的函数
    Y = data[yvar]
    X = data[xvars]
    X['intercept'] = 1
    result = sm.OLS(Y, X).fit()
    return result.params

# 为了运行AAPL在SPX回报上的年度线性回归:
by_year.apply(regress, yvar='AAPL', xvar=['SPX'])

10.4组变换和’解封’GroupBys

在’应用:一般拆分-应用-组合’小节中,我们研究了用于执行转换的分组操作中的apply方法。还有另一种内置的方法称为transform,它和apply类似,但对你能使用的函数类型施加了更多约束:

  • 它可以生成要广播到组形状的标量值
  • 它可以生成与输入组有相同形状的对象
  • 它绝不能更改输入

让我们考虑一个简单的例子进行说明:

df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4, 'value': np.arange(12.)})
g = df.groupby('key')['value'] # 按'key'的值分组,并选择'value'列
g.mean() # 组内求'value'的均值

# 假设我们要生成一个形状和df['value']相同的series,但其值由'key'分组的平均值代替。我们可以将计算组内平均值的函数传递给transform
def get_mean(group):
    return group.mean()

g.transform(get_mean)

# 对于内置聚合函数,我们可以像GroupBy的agg方法一样传递字符串别名:
g.transform('mean')

# 类似apply方法,transform方法适用于返回Series的函数,但结果的大小必须与输入大小相同
# 例如,我们可以使用函数将每个组乘以2
def times_two(group):
    return group * 2

g.transform(times_two)

# 更复杂的例子:降序计算每个组的排名:
def get_ranks(group):
    return group.rank(ascending=False)

g.transform(get_ranks)

# 考虑一个由简单聚合函数组合的组转换函数
def normailize(x):
    return (x - x.mean()) / x.std()
# 在这种情况下,我们可以通过transform方法和apply方法获得等效的结果
g.transform(normalize)
g.apply(normalize)

# 内置聚合函数如'mean'或'sum'通常比同样的apply函数要快。当和transform一起使用时,还有快速路径。这允许我们执行所谓的'解封'组操作:
g.transform('mean')
normailized = (df['value'] - g.transform('mean')) / g.transform('std')
# 在这里,我们在多个GroupBy操作的输出之间执行算术操作,而不是编写函数传递给groupby(...).apply。这就是'解封'的意思。
# 虽然解封的组操作可能涉及多个组聚合,但矢量化操作的总体优势通常更大。

10.5数据透视表和交叉表(列联表)

数据透视表是电子表格程序和其他数据分析软件中常见的数据汇总工具。它按一个或多个键聚合数据表,将数据排列在一个矩形中,其中一些组键沿行,一些组键沿列排列。Python中带有pandas的数据透视表可以通过本章描述的groupby并结合使用分层索引的重塑操作实现。DataFrame也有一个pivot_table函数,还有一个顶层pandas.pivot_table函数。除了提供方便的groupby接口外,pivot_table还可以添加部分总计,也称为margins。

# 回到小费数据集,假设你想要计算在行上按'day'和'smoker'排列的组均值表(mean是默认pivot_table聚合类型):
tips.pivot_table(index=['day', 'smoker'])
# 也可以使用tips.groupby(['day', 'smoker']).mean()获得

# 现在,假定我们仅仅想要tip_pct和size的平均值,并且另外按照time分组。将smoker传入columns参数,将time和day传入index参数
tips.pivot_table(index=['time', 'day'], columns='smoker', values = ['tip_pct', 'size'])

# 可以通过传递margins=True来扩产此表以包括部分总计。
# 这具有添加'All'行和'All'列标签的效果,相应的值是单个层中所有数据的组统计信息
tips.pivot_table(index=['time', 'day'], columns='smoker', values=['tip_pct', 'size'], margins=True)
# 这里,All值是不考虑吸烟者和非吸烟者(All列)的均值或行上两个水平的分组中的任何一个的平均值(All行)

# 要使用mean以外的聚合函数,需要传递aggfunc关键字参数。
# 例如,'count'或len会提供组大小的交叉表(计数或频数)('count'会排除null值,len不会)
tips.pivot_table(index=['time', 'smoker'], columns='day', values='tip_pct', aggfunc=len, margins=True)

# 如果某些组合适合未空(或者NA),你可以传递fill_value来填充缺失值:
tips.pivot_table(index=['time', 'size', 'smoker'], columns='day', values='tip_pct', fill_value=0)

下表是关于pivot_table函数的参数。

[表]pivot_table函数的参数

交叉表:crosstab

交叉表(或简写为crosstab)是计算组频率的数据透视表的一种特殊情况。

# 示例
from io import StringIO
data = """Sample  Nationality  Handedness
1   USA  Right-handed
2   Japan    Left-handed
3   USA  Right-handed
4   Japan    Right-handed
5   Japan    Left-handed
6   Japan    Right-handed
7   USA  Right-handed
8   USA  Left-handed
9   Japan    Right-handed
10  USA  Right-handed"""
data = pd.read_table(StringIO(data), sep='\s+') # 创建一个StringIO对象,将data寄存在缓冲区,通过read_table转化为DataFrame
# 希望按照国籍和手性总结这些数据。可以使用pivot_table来执行此操作,但pandas.crosstab函数可以更方便:
pd.crosstab(data['Nationality'], data['Handedness'], margins=True)
# 传递给crosstab前两个参数可以是数组或者series或者数组列表,如小费数据:
pd.crosstab([tips['time'], tips['day']], tips['smoker'], margins=True)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值