Pandas 秘籍(三)
七、分组以进行汇总,过滤和转换
在本章中,我们将介绍以下主题:
- 定义聚合
- 使用函数对多个列执行分组和聚合
- 分组后删除多重索引
- 自定义聚合函数
- 使用
*args和**kwargs自定义聚合函数 - 检查
groupby对象 - 筛选少数人群居多的州
- 转换减肥赌注
- 计算每个州的 SAT 加权平均成绩
- 按连续变量分组
- 计算城市之间的航班总数
- 寻找最长的准时航班
介绍
数据分析过程中最基本的任务之一是在对每个组执行计算之前将数据分成独立的组。 该方法已经存在了相当长的时间,但是最近被称为拆分应用组合。 本章介绍了功能强大的groupby方法,该方法可让您以可想象的任何方式对数据进行分组,并在返回单个数据集之前将任何类型的函数独立地应用于每个组。
Hadley Wickham 创造了术语“拆分应用组合”,用于描述将数据分为独立的可管理块,将函数独立应用于这些块,然后将结果组合在一起的通用数据分析模式。 可以在他的论文中找到更多详细信息。
在开始使用秘籍之前,我们只需要了解一些术语。 所有基本的分组操作都有分组列,这些列中值的每个唯一组合代表数据的独立分组。 语法如下所示:
>>> df.groupby(['list', 'of', 'grouping', 'columns'])
>>> df.groupby('single_column') # when grouping by a single column
该操作的结果返回一个分组对象。 正是这个分组对象将成为驱动整个整章所有计算的引擎。 在通过对象创建此分组时,Pandas 实际上很少执行,仅验证了分组是可能的。 您必须在该分组对象上链接方法,以释放其潜能。
从技术上讲,该操作的结果将是DataFrameGroupBy或SeriesGroupBy,但为简单起见,在整章中将其称为分组对象。
定义聚合
groupby方法最常见的用途是执行聚合。 实际是什么聚合? 在我们的数据分析世界中,当许多输入的序列被汇总或组合为单个值输出时,就会发生汇总。 例如,对一列的所有值求和或求其最大值是应用于单个数据序列的常见聚合。 聚合仅获取许多值,然后将其转换为单个值。
除了介绍中定义的分组列外,大多数聚合还有两个其他组件,聚合列和聚合函数。 汇总列是其值将被汇总的列。 聚合函数定义聚集的方式。 主要的聚合函数包括sum,min,max,mean,count,variance和std等。
准备
在此秘籍中,我们检查航班数据集,并执行最简单的可能的汇总,仅涉及单个分组列,单个汇总列和单个汇总函数。 我们将找到每家航空公司的平均到达延误时间。 Pandas 具有相当多种不同的语法来产生聚合,本秘籍涵盖了它们。
操作步骤
- 读取飞行数据集,并定义分组列(
AIRLINE),聚合列(ARR_DELAY)和聚合函数(mean):
>>> flights = pd.read_csv('data/flights.csv')
>>> flights.head()
- 将分组列放在
groupby方法中,然后通过字典在agg方法中将聚集列及其聚合函数配对:
>>> flights.groupby('AIRLINE').agg({'ARR_DELAY':'mean'}).head()
- 或者,您可以将汇总列放在索引运算符中,然后将汇总函数作为字符串传递给
agg:
>>> flights.groupby('AIRLINE')['ARR_DELAY'].agg('mean').head()
AIRLINE
AA 5.542661
AS -0.833333
B6 8.692593
DL 0.339691
EV 7.034580
Name: ARR_DELAY, dtype: float64
- 上一步中使用的字符串名称是 Pandas 提供的一种便捷功能,可让您引用特定的聚合函数。 您可以将任何聚合函数直接传递给
agg方法,例如 NumPymean函数。 输出与上一步相同:
>>> flights.groupby('AIRLINE')['ARR_DELAY'].agg(np.mean).head()
- 在这种情况下,可以完全跳过
agg方法,而直接使用mean方法。 此输出也与步骤 3 相同:
>>> flights.groupby('AIRLINE')['ARR_DELAY'].mean().head()
工作原理
groupby方法的语法不像其他方法那么简单。 让我们通过将groupby方法的结果存储为自己的变量来拦截步骤 2 中的方法链
>>> grouped = flights.groupby('AIRLINE')
>>> type(grouped)
pandas.core.groupby.DataFrameGroupBy
首先使用其自己独特的属性和方法来生产一个全新的中间对象。 在此阶段没有任何计算。 Pandas 仅验证分组列。 该分组对象具有agg方法来执行聚合。 使用此方法的一种方法是向其传递一个字典,该字典将聚合列映射到聚合函数,如步骤 2 所示。
有几种不同的语法产生相似的结果,而步骤 3 显示了另一种方法。 与其标识字典中的聚合列,不如将其放在索引运算符中,就如同您从数据帧中将其选择为列一样。 然后,将函数字符串名称作为标量传递给agg方法。
您可以将任何汇总函数传递给agg方法。 为了简单起见,Pandas 允许您使用字符串名称,但是您也可以像在步骤 4 中一样明确地调用一个聚合函数。NumPy 提供了许多聚合值的函数。
步骤 5 显示了最后一种语法风格。 如本例所示,当仅应用单个聚合函数时,通常可以直接将其作为对分组对象本身的方法进行调用,而无需使用agg。 并非所有聚合函数都具有等效的方法,但是许多基本函数都有。 以下是几个聚合函数的列表,这些函数可以作为字符串传递给agg或作为方法直接链接到分组对象:
min max mean median sum count std var size describe nunique idxmin idxmax
更多
如果您不对agg使用聚合函数,则 pandas 会引发异常。 例如,让我们看看将平方根函数应用于每个组会发生什么:
>>> flights.groupby('AIRLINE')['ARR_DELAY'].agg(np.sqrt)
ValueError: function does not reduce
另见
使用函数对多个列执行分组和聚合
可以对多列进行分组和聚合。 语法仅与使用单个列进行分组和聚合时稍有不同。 与任何分组操作一样,它有助于识别三个组成部分:分组列,聚合列和聚合函数。
准备
在本秘籍中,我们通过回答以下查询来展示数据帧的groupby方法的灵活性:
- 查找每个工作日每个航空公司的已取消航班的数量
- 查找每个航空公司在工作日内已取消和改航航班的数量和百分比
- 对于每个始发地和目的地,查找航班总数,已取消航班的数量和百分比,以及通话时间的平均值和方差
操作步骤
- 读取航班数据集,并通过定义分组列(
AIRLINE, WEEKDAY),聚合列(CANCELLED)和聚合函数(sum)回答第一个查询:
>>> flights.groupby(['AIRLINE', 'WEEKDAY'])['CANCELLED'] \
.agg('sum').head(7)
AIRLINE WEEKDAY
AA 1 41
2 9
3 16
4 20
5 18
6 21
7 29
Name: CANCELLED, dtype: int64
- 通过使用每对分组和聚集列的列表来回答第二个查询。 另外,对聚合函数使用列表:
>>> flights.groupby(['AIRLINE', 'WEEKDAY']) \
['CANCELLED', 'DIVERTED'].agg(['sum', 'mean']).head(7)
- 使用
agg方法中的字典来回答第三个查询,以将特定的聚合列映射到特定的聚合函数:
>>> group_cols = ['ORG_AIR', 'DEST_AIR']
>>> agg_dict = {'CANCELLED':['sum', 'mean', 'size'],
'AIR_TIME':['mean', 'var']}
>>> flights.groupby(group_cols).agg(agg_dict).head()
工作原理
要像步骤 1 一样按多列分组,我们将字符串名称列表传递给groupby方法。AIRLINE和WEEKDAY的每个唯一组合均形成一个独立的组。 在每个组中,找到已取消航班的总数,然后将其作为序列返回。
步骤 2,再次按AIRLINE和WEEKDAY分组,但这一次汇总了两列。 它将两个聚合函数sum和mean中的每一个应用于每个列,从而每组返回四个列。
步骤 3 进一步进行,并使用字典将特定的聚合列映射到不同的聚合函数。 请注意,size聚合函数返回每个组的总行数。 这与count汇总函数不同,后者会返回每组非缺失值的数量。
更多
执行聚合时,会遇到几种主要的语法。 以下四个伪代码块总结了使用groupby方法执行聚合的主要方式:
- 将
agg与字典一起使用是最灵活的方法,它允许您为每一列指定聚合函数:
>>> df.groupby(['grouping', 'columns']) \
.agg({'agg_cols1':['list', 'of', 'functions'],
'agg_cols2':['other', 'functions']})
- 将
agg与聚合函数列表一起使用,会将每个函数应用于每个聚合列:
>>> df.groupby(['grouping', 'columns'])['aggregating', 'columns'] \
.agg([aggregating, functions])
- 直接使用紧随汇总列之后的方法而不是
agg,仅将该方法应用于每个汇总列。 这种方式不允许多种聚合函数:
>>> df.groupby(['grouping', 'columns'])['aggregating', 'columns'] \
.aggregating_method()
- 如果您未指定汇总列,则汇总方法将应用于所有非分组列:
>>> df.groupby(['grouping', 'columns']).aggregating_method()
在前面的四个代码块中,当按单个列进行分组或聚合时,可以用字符串代替任何列表。
分组后删除多重索引
不可避免地,当使用groupby时,您可能会在列或行或两者中都创建多重索引。 具有多重索引的数据帧更加难以导航,并且有时列名称也令人困惑。
准备
在本秘籍中,我们使用groupby方法执行聚合,以创建具有行和列多重索引的数据帧,然后对其进行处理,以使索引为单个级别,并且列名具有描述性。
操作步骤
- 读取航班数据集; 编写声明以查找飞行的总里程和平均里程; 以及每个航空公司在每个工作日的最大和最小到达延误:
>>> flights = pd.read_csv('data/flights.csv')
>>> airline_info = flights.groupby(['AIRLINE', 'WEEKDAY'])\
.agg({'DIST':['sum', 'mean'],
'ARR_DELAY':['min', 'max']}) \
.astype(int)
>>> airline_info.head(7)
- 行和列均由具有两个级别的多重索引标记。 让我们将其压缩到单个级别。 为了解决这些列,我们使用多重索引方法
get_level_values。 让我们显示每个级别的输出,然后将两个级别连接起来,然后再将其设置为新的列值:
>>> level0 = airline_info.columns.get_level_values(0)
Index(['DIST', 'DIST', 'ARR_DELAY', 'ARR_DELAY'], dtype='object')
>>> level1 = airline_info.columns.get_level_values(1)
Index(['sum', 'mean', 'min', 'max'], dtype='object')
>>> airline_info.columns = level0 + '_' + level1
>>> airline_info.head(7)
- 使用
reset_index将行标签返回到单个级别:
>>> airline_info.reset_index().head(7)
工作原理
当使用agg方法对多个列执行聚合时,pandas 将创建一个具有两个级别的索引对象。 聚合列变为顶层,聚合函数变为底层。 Pandas 显示的多重索引级别与单级别的列不同。 除了最里面的级别以外,屏幕上不会显示重复的索引值。 您可以检查第 1 步中的数据帧以进行验证。 例如,DIST列仅显示一次,但它引用了前两列。
最里面的多重索引级别是最接近数据的级别。 这将是最底部的列级别和最右边的索引级别。
步骤 2 通过首先使用多重索引方法get_level_values.检索每个级别的基础值来定义新列。此方法接受一个整数,该整数标识索引级别。 它们从顶部/左侧以零开始编号。 索引支持向量化操作,因此我们将两个级别与下划线分开。 我们将这些新值分配给columns属性。
在第 3 步中,我们将两个索引级别都设为reset_index作为列。 我们可以像在第 2 步中那样将级别连接在一起,但是将它们保留为单独的列更有意义。
更多
默认情况下,在分组操作结束时,pandas 将所有分组列放入索引中。 可以将groupby方法中的as_index参数设置为False,以避免此行为。 您可以在分组后将reset_index方法链接起来,以获得与步骤 3 中相同的效果。让我们看一下其中的一个示例,该示例通过查找每个航空公司从每个航班出发的平均距离来得出:
>>> flights.groupby(['AIRLINE'], as_index=False)['DIST'].agg('mean') \
.round(0)
看一下先前结果中航空公司的顺序。 默认情况下,pandas 对分组列进行排序。sort参数存在于groupby方法中,并且默认为True。 您可以将其设置为False,以使分组列的顺序与在数据集中遇到分组列的顺序相同。 通过不对数据进行排序,您还将获得较小的性能提升。
自定义聚合函数
Pandas 提供了许多最常见的聚合函数,供您与分组对象一起使用。 在某些时候,您将需要编写自己的自定义用户定义函数,而这些函数在 pandas 或 NumPy 中不存在。
准备
在此秘籍中,我们使用大学数据集来计算每个州的本科生人数的均值和标准差。 然后,我们使用此信息从每个状态的任何单一总体值的均值中找到最大标准差数。
操作步骤
- 读取大学数据集,并按州找到本科人口的均值和标准差:
>>> college = pd.read_csv('data/college.csv')
>>> college.groupby('STABBR')['UGDS'].agg(['mean', 'std']) \
.round(0).head()
- 这个输出不是我们想要的。 我们不是在寻找整个组的均值和标准差,而是寻找任何一个机构的均值的最大标准差数。 为了计算这一点,我们需要从每个机构的本科生人数中减去各州的本科生平均人数,然后除以标准差。 这使每个群体的本科生人数标准化。 然后,我们可以利用这些分数的绝对值的最大值来找到距离均值最远的那个。 Pandas 不提供能够执行此操作的函数。 相反,我们将需要创建一个自定义函数:
>>> def max_deviation(s):
std_score = (s - s.mean()) / s.std()
return std_score.abs().max()
- 定义函数后,将其直接传递给
agg方法以完成聚合:
>>> college.groupby('STABBR')['UGDS'].agg(max_deviation) \
.round(1).head()
STABBR
AK 2.6
AL 5.8
AR 6.3
AS NaN
AZ 9.9
Name: UGDS, dtype: float64
工作原理
不存在预定义的 Pandas 函数来计算偏离均值的最大标准差数。 我们被迫在步骤 2 中构造一个自定义函数。请注意,此自定义函数max_deviation接受单个参数s。 展望第 3 步,您会注意到函数名称位于agg方法内,而没有直接调用。 参数s没有明确传递给max_deviation的地方。 相反,Pandas 将UGDS列作为序列隐式传递给max_deviation。
每个组都会调用一次max_deviation函数。 由于s是序列,因此所有常规的序列方法均可用。 在称为标准化的过程中,从组中的每个值中减去该特定组的平均值,然后再除以标准差。
标准化是一种常见的统计过程,用于了解各个值与平均值之间的差异。 对于正态分布,数据的 99.7% 位于平均值的三个标准差之内。
由于我们对均值的绝对偏差感兴趣,因此我们从所有标准化得分中获取绝对值并返回最大值。agg方法必须从我们的自定义函数中返回单个标量值,否则将引发异常。 Pandas 默认使用样本标准差,该样本标准差对于只有单个值的任何组均未定义。 例如,州缩写AS(美属萨摩亚)返回了缺失值,因为它在数据集中只有一个机构。
更多
可以将我们的自定义函数应用于多个聚合列。 我们只需将更多列名称添加到索引运算符。max_deviation函数仅适用于数字列:
>>> college.groupby('STABBR')['UGDS', 'SATVRMID', 'SATMTMID'] \
.agg(max_deviation).round(1).head()
您还可以将自定义的聚合函数与预构建函数一起使用。 以下是按国家和宗教派别进行的分组:
>>> college.groupby(['STABBR', 'RELAFFIL']) \
['UGDS', 'SATVRMID', 'SATMTMID'] \
.agg([max_deviation, 'mean', 'std']).round(1).head()
请注意,pandas 使用函数名称作为返回列的名称。 您可以使用重命名方法直接更改列名称,也可以修改特殊功能属性__name__:
>>> max_deviation.__name__
'max_deviation'
>>> max_deviation.__name__ = 'Max Deviation'
>>> college.groupby(['STABBR', 'RELAFFIL']) \
['UGDS', 'SATVRMID', 'SATMTMID'] \
.agg([max_deviation, 'mean', 'std']).round(1).head()
使用*args和**kwargs自定义聚合函数
在编写自己的用户定义的自定义聚合函数时,pandas 隐式地将每个聚合列作为一个序列一次传递给它。 有时,您将需要向函数传递的参数不仅仅是序列本身。 为此,您需要了解 Python 将任意数量的参数传递给函数的能力。 在inspect模块的帮助下,让我们看一下分组对象的agg方法的签名:
>>> college = pd.read_csv('data/college.csv')
>>> grouped = college.groupby(['STABBR', 'RELAFFIL'])
>>> import inspect
>>> inspect.signature(grouped.agg)
<Signature (arg, *args, **kwargs)>
参数*args允许您将任意数量的非关键字参数传递给自定义的聚合函数。 同样,**kwargs允许您传递任意数量的关键字参数。
准备
在此秘籍中,我们为大学数据集构建了一个自定义函数,该函数可按州和宗教隶属关系找到本科生人口在两个值之间的学校所占的百分比。
操作步骤
- 定义一个函数,该函数返回大学人口在 1000 至 3,000 之间的学校的百分比:
>>> def pct_between_1_3k(s):
return s.between(1000, 3000).mean()
- 计算按州和宗教归属分类的百分比:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
.agg(pct_between_1_3k).head(9)
STABBR RELAFFIL
AK 0 0.142857
1 0.000000
AL 0 0.236111
1 0.333333
AR 0 0.279412
1 0.111111
AS 0 1.000000
AZ 0 0.096774
1 0.000000
Name: UGDS, dtype: float64
- 该函数可以正常工作,但不能给用户提供选择上下限的灵活性。 让我们创建一个新函数,该函数允许用户定义以下范围:
>>> def pct_between(s, low, high):
return s.between(low, high).mean()
- 将此上限和下限传递给
agg方法:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
.agg(pct_between, 1000, 10000).head(9)
STABBR RELAFFIL
AK 0 0.428571
1 0.000000
AL 0 0.458333
1 0.375000
AR 0 0.397059
1 0.166667
AS 0 1.000000
AZ 0 0.233871
1 0.111111
Name: UGDS, dtype: float64
工作原理
步骤 1 创建一个不接受任何额外参数的函数。 上下限必须硬编码到函数本身中,这不是很灵活。 步骤 2 显示了此聚合的结果。
我们在第 3 步中创建了一个更加灵活的函数,该函数允许用户动态定义上下限。 步骤 4 是*args和**kwargs的魔力发挥作用的地方。 在此特定示例中,我们将两个非关键字参数 1,000 和 10,000 传递给agg方法。 Pandas 分别将这两个参数传递给pct_between的low和high参数。
在步骤 4 中,有几种方法可以达到相同的结果。我们可以在以下命令中明确使用参数名称来产生相同的结果:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
.agg(pct_between, high=10000, low=1000).head(9)
关键字参数的顺序并不重要,只要它们位于函数名称之后即可。 更进一步,我们可以混合使用非关键字和关键字参数,只要关键字参数排在最后即可:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
.agg(pct_between, 1000, high=10000).head(9)
为了便于理解,最好按函数签名中定义的顺序包含所有参数名称。
从技术上讲,当调用agg时,所有非关键字参数都收集到名为args的元组中,而所有关键字参数都收集到名为kwargs的字典中。
更多
不幸的是,当同时使用多个聚合函数时,Pandas 没有直接使用这些附加参数的方法。 例如,如果您希望使用pct_between和mean函数进行汇总,则会出现以下异常:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
.agg(['mean', pct_between], low=100, high=1000)
TypeError: pct_between() missing 2 required positional arguments: 'low' and 'high'
Pandas 无法理解需要将额外的参数传递给pct_between。 为了将我们的自定义函数与其他内置函数甚至其他自定义函数一起使用,我们可以定义一种称为闭包的特殊类型的嵌套函数。 我们可以使用通用闭包来构建所有自定义函数:
>>> def make_agg_func(func, name, *args, **kwargs):
def wrapper(x):
return func(x, *args, **kwargs)
wrapper.__name__ = name
return wrapper
>>> my_agg1 = make_agg_func(pct_between, 'pct_1_3k', low=1000, high=3000)
>>> my_agg2 = make_agg_func(pct_between, 'pct_10_30k', 10000, 30000)
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
.agg(['mean', my_agg1, my_agg2]).head()
make_agg_func函数充当创建自定义聚合函数的工厂。 它接受您已经构建的自定义聚合函数(在这种情况下为pct_between),name参数以及任意数量的额外参数。 它返回一个已经设置了额外参数的函数。 例如,my_agg1是一个特定的定制聚合函数,可以找到大学人口在一千到三千之间的学校所占的百分比。 额外的参数(*args和**kwargs)为您的自定义函数(pct_between)指定了一组精确的参数 )。name参数非常重要,每次调用make_agg_func时必须唯一。 它将最终用于重命名聚合列。
闭包是一个在其中包含一个函数(一个嵌套函数),并返回此嵌套函数的函数。 此嵌套函数必须引用外部函数范围内的变量才能成为闭包。 在此示例中,make_agg_func是外部函数,并返回嵌套函数wrapper,该函数从外部函数访问变量func,args和kwargs。
另见
检查分组对象
在数据帧上使用groupby方法的直接结果将是一个分组对象。 通常,我们将继续对该对象进行操作以进行聚合或转换,而无需将其保存到变量中。 在中,检查此分组对象的主要目的是检查单个组。
准备
在本秘籍中,我们通过直接在其上调用方法以及遍历其每个组来检查分组对象本身。
操作步骤
- 首先,将大学数据集中的州和宗教隶属关序列进行分组,然后将结果保存到变量中并确认其类型:
>>> college = pd.read_csv('data/college.csv')
>>> grouped = college.groupby(['STABBR', 'RELAFFIL'])
>>> type(grouped)
pandas.core.groupby.DataFrameGroupBy
- 使用
dir函数发现其所有可用函数:
>>> print([attr for attr in dir(grouped) if not attr.startswith('_')])
['CITY', 'CURROPER', 'DISTANCEONLY', 'GRAD_DEBT_MDN_SUPP', 'HBCU', 'INSTNM', 'MD_EARN_WNE_P10', 'MENONLY', 'PCTFLOAN', 'PCTPELL', 'PPTUG_EF', 'RELAFFIL', 'SATMTMID', 'SATVRMID', 'STABBR', 'UG25ABV', 'UGDS', 'UGDS_2MOR', 'UGDS_AIAN', 'UGDS_ASIAN', 'UGDS_BLACK', 'UGDS_HISP', 'UGDS_NHPI', 'UGDS_NRA', 'UGDS_UNKN', 'UGDS_WHITE', 'WOMENONLY', 'agg', 'aggregate', 'all', 'any', 'apply', 'backfill', 'bfill', 'boxplot', 'corr', 'corrwith', 'count', 'cov', 'cumcount', 'cummax', 'cummin', 'cumprod', 'cumsum', 'describe', 'diff', 'dtypes', 'expanding', 'ffill', 'fillna', 'filter', 'first', 'get_group', 'groups', 'head', 'hist', 'idxmax', 'idxmin', 'indices', 'last', 'mad', 'max', 'mean', 'median', 'min', 'ndim', 'ngroup', 'ngroups', 'nth', 'nunique', 'ohlc', 'pad', 'pct_change', 'plot', 'prod', 'quantile', 'rank', 'resample', 'rolling', 'sem', 'shift', 'size', 'skew', 'std', 'sum', 'tail', 'take', 'transform', 'tshift', 'var']
- 查找具有
ngroups属性的组数:
>>> grouped.ngroups
112
- 要查找每个组的唯一标识标签,请查看
groups属性,该属性包含映射到该组的所有相应索引标签的每个唯一组的字典:
>>> groups = list(grouped.groups.keys())
>>> groups[:6]
[('AK', 0), ('AK', 1), ('AL', 0), ('AL', 1), ('AR', 0), ('AR', 1)]
- 通过将
get_group方法传递给一个确切的组标签的元组来检索单个组。 例如,要获得佛罗里达州的所有宗教附属学校,请执行以下操作:
>>> grouped.get_group(('FL', 1)).head()
- 您可能想看看每个单独的组。 这是可能的,因为分组对象是可迭代的:
>>> from IPython.display import display
>>> for name, group in grouped:
print(name)
display(group.head(3))
- 您还可以在分组对象上调用
head方法,以在单个数据帧中将每个组的第一行放在一起。
>>> grouped.head(2).head(6)
工作原理
步骤 1 正式创建了分组对象。 显示所有公共属性和方法以揭示所有可能的函数(如在步骤 2 中所做的那样)很有用。每个组由元组唯一标识,该元组包含分组列中值的唯一组合。 Pandas 允许您使用第 5 步中显示的get_group方法选择特定的组作为数据帧。
很少需要遍历整个组,通常,如果有必要,应避免这样做,因为这样做可能会很慢。 有时候,您别无选择。 当通过对象遍历分组时,将为您提供一个元组,其中包含组名和数据帧,而没有分组列。 在步骤 6 中,此元组在for循环中解包为变量name和group。
在遍历组时可以做的一件有趣的事情是直接在笔记本中显示每个组的几行。 为此,可以使用IPython.display模块中的打印函数或display函数。 使用print函数可得到纯文本格式的数据帧,而没有任何不错的 HTML 格式。 使用display函数将以其常规的易于阅读的格式生成数据帧。
更多
在步骤 2 的列表中没有探索几种有用的方法。例如nth方法,当给定一个整数列表时,该方法从每个组中选择那些特定的行。 例如,以下操作从每个组中选择第一行和最后一行:
>>> grouped.nth([1, -1]).head(8)
另见
筛选少数人群居多的州
在第 4 章,“选择数据子集”中,我们在过滤掉False行之前将每一行标记为True或False。 以类似的方式,可以在过滤掉False组之前将整个数据组标记为True或False。 为此,我们首先使用groupby方法形成组,然后应用filter方法。filter方法接受必须返回True或False来指示是否保留组的函数。
在调用groupby方法之后应用的filter方法,与第 2 章“基本数据帧操作”中的数据帧filter方法完全不同。
准备
在此秘籍中,我们使用大学数据集查找非白人大学生比白人多的所有州。 由于这是来自美国的数据集,因此白人占多数,因此,我们正在寻找少数居多的州。
操作步骤
- 读取大学数据集,按州分组,并显示分组总数。 这应该等于从
nunique序列方法检索的唯一状态数:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> grouped = college.groupby('STABBR')
>>> grouped.ngroups
59
>>> college['STABBR'].nunique() # verifying the same number
59
grouped变量具有filter方法,该方法接受一个自定义函数来确定是否保留组。 自定义函数将隐式传递给当前组的数据帧,并且需要返回一个布尔值。 我们定义一个函数来计算少数民族学生的总百分比,如果该百分比大于用户定义的阈值,则返回True:
>>> def check_minority(df, threshold):
minority_pct = 1 - df['UGDS_WHITE']
total_minority = (df['UGDS'] * minority_pct).sum()
total_ugds = df['UGDS'].sum()
total_minority_pct = total_minority / total_ugds
return total_minority_pct > threshold
- 使用
check_minority函数传递的filter方法和 50% 的阈值来查找具有少数多数的所有状态:
>>> college_filtered = grouped.filter(check_minority, threshold=.5)
>>> college_filtered.head()
- 仅查看输出可能并不表示实际发生了什么。数据帧以状态亚利桑那(
AZ)而不是阿拉斯加(AK)开头,因此我们可以从视觉上确认某些更改。 让我们将此过滤后的数据帧的shape与原始数据进行比较。 查看结果,大约 60% 的行已被过滤,仅剩下 20 个州占少数:
>>> college.shape
(7535, 26)
>>> college_filtered.shape
(3028, 26)
>>> college_filtered['STABBR'].nunique()
20
工作原理
此秘籍以州为单位查看所有机构的总人口。 目标是保留所有州中总体上占少数的所有行。 这要求我们按状态对数据进行分组,这是在步骤 1 中完成的。我们发现有 59 个独立的组。
filter分组方法将所有行保留在一个组中或将其过滤掉。 它不会更改列数。filter分组方法通过用户定义的函数(例如此秘籍中的check_minority)执行此关守。 要过滤的一个非常重要的方面是它将特定组的整个数据帧传递给用户定义的函数,并为每个组返回一个布尔值。
在check_minority函数内部,首先计算每个机构的非白人学生的百分比和总数,然后找到所有学生的总数。 最后,根据给定的阈值检查整个州的非白人学生百分比,这会产生布尔值。
最终结果是一个数据帧,其列与原始列相同,但过滤掉了不符合阈值的状态中的行。 由于过滤后的数据帧的标题可能与原始标题相同,因此您需要进行一些检查以确保操作成功完成。 我们通过检查行数和唯一状态数来验证这一点。
更多
我们的函数check_minority是灵活的,并接受参数以降低或提高少数群体阈值的百分比。 让我们检查几个其他阈值的唯一状态的形状和数量:
>>> college_filtered_20 = grouped.filter(check_minority, threshold=.2)
>>> college_filtered_20.shape
(7461, 26)
>>> college_filtered_20['STABBR'].nunique()
57
>>> college_filtered_70 = grouped.filter(check_minority, threshold=.7)
>>> college_filtered_70.shape
(957, 26)
>>> college_filtered_70['STABBR'].nunique()
10
另见
转换减肥赌注
增加减肥动机的一种方法是与他人打赌。 此秘籍中的方案将跟踪四个月内两个人的减肥情况,并确定获胜者。
准备
在此秘籍中,我们使用来自两个人的模拟数据来跟踪四个月内减肥的百分比。 在每个月底,将根据当月体重百分比最高的个人宣布获胜者。 要跟踪减肥,我们将数据按月和人分组,然后调用transform方法以查找从月初起每周每周的减肥百分比。
操作步骤
- 读取原始
weight_loss数据集,并检查两个人Amy和Bob的第一个月数据。 每月总共有四个称量:
>>> weight_loss = pd.read_csv('data/weight_loss.csv')
>>> weight_loss.query('Month == "Jan"')
- 要确定每个月的赢家,我们只需要比较每月第一周到最后一周的减肥效果即可。 但是,如果我们想每周更新一次,我们还可以计算从当前周到每月第一周的减肥。 让我们创建一个能够提供每周更新的函数:
>>> def find_perc_loss(s):
return (s - s.iloc[0]) / s.iloc[0]
- 让我们在一月份为
Bob测试此函数。
>>> bob_jan = weight_loss.query('Name=="Bob" and Month=="Jan"')
>>> find_perc_loss(bob_jan['Weight'])
0 0.000000
2 -0.010309
4 -0.027491
6 -0.027491
Name: Weight, dtype: float64
您应该忽略最后一个输出中的索引值。 0、2、4 和 6 只是引用数据帧的原始行标签,与星期无关。
- 第一周后,鲍勃减肥了 1% 。 他在第二周继续减肥,但在最后一周没有任何进展。 我们可以将此函数应用于人和周的每个单一组合,以获得相对于每月第一周的每周减肥。 为此,我们需要将数据按
Name和Month分组,然后使用transform方法应用此自定义函数:
>>> pcnt_loss = weight_loss.groupby(['Name', 'Month'])['Weight'] \
.transform(find_perc_loss)
>>> pcnt_loss.head(8)
0 0.000000
1 0.000000
2 -0.010309
3 -0.040609
4 -0.027491
5 -0.040609
6 -0.027491
7 -0.035533
Name: Weight, dtype: float64
transform方法必须返回与调用数据帧具有相同行数的对象。 让我们将此结果作为新列添加到原始数据帧中。 为了帮助缩短输出,我们将选择Bob的前两个月的数据:
>>> weight_loss['Perc Weight Loss'] = pcnt_loss.round(3)
>>> weight_loss.query('Name=="Bob" and Month in ["Jan", "Feb"]')
- 请注意,减肥百分比在新月后重新设置。 通过这个新的专栏,我们可以手动确定获胜者,但让我们看看是否可以找到一种自动执行此操作的方法。 由于唯一重要的一周是最后一周,所以我们选择第 4 周:
>>> week4 = weight_loss.query('Week == "Week 4"')
>>> week4
- 这缩小了周数,但仍然不会自动找出每个月的赢家。 让我们使用
pivot方法重塑此数据,以便Bob和Amy每月的减肥百分比并排:
>>> winner = week4.pivot(index='Month', columns='Name',
values='Perc Weight Loss')
>>> winner
- 此输出使每个月的获胜者更加清楚,但我们仍然可以走得更远。 NumPy 具有一个称为
where的向量化if-then-else函数,该函数可以将序列或布尔数组映射到其他值。 让我们为获奖者的名字创建一个列,并突出显示每个月的获奖百分比:
>>> winner['Winner'] = np.where(winner['Amy'] < winner['Bob'],
'Amy', 'Bob')
>>> winner.style.highlight_min(axis=1)
- 使用
value_counts方法以赢得的月份数返回最终分数:
>>> winner.Winner.value_counts()
Amy 3
Bob 1
Name: Winner, dtype: int64
工作原理
在整个秘籍中,query方法用于过滤数据,而不是布尔索引。 有关更多信息,请参阅第 5 章,“布尔索引”的“查询方法”秘籍,以提高布尔索引的可读性。
我们的目标是找到每个人每个月的减肥百分比。 一种完成此任务的方法是计算相对于每个月初的每周减肥。 此特定任务非常适合transform分组方法。transform方法接受一个函数作为其必需的参数。 该函数隐式地传递给每个非分组列(或仅使用在索引运算符中指定的列,如在此秘籍中使用Weight所做的那样)。 它必须返回与传递的组长度相同的值序列,否则将引发异常。 本质上,原始数据帧中的所有值都在转换。 没有聚集或过滤发生。
第 2 步创建一个函数,该函数从其所有值中减去传递的序列的第一个值,然后将该结果除以第一个值。 这将计算相对于第一个值的百分比损失(或收益)。 在第 3 步中,我们在一个月内对一个人测试了此函数。
在步骤 4 中,我们在人和周的每个组合上以相同的方式使用此函数。 从字面上看,我们正在将Weight列转换为当前一周的体重损失百分比。 为每个人输出第一个月的数据。 Pandas 将新数据作为序列返回。 该序列本身并没有什么用处,并且更有意义地作为新列附加到原始数据帧中。 我们在步骤 5 中完成此操作。
要确定获胜者,只需每月的第 4 周。 我们可以在这里停下来,手动确定获胜者,但 Pandas 提供了自动执行此功能的函数。 第 7 步中的pivot函数通过将一列的唯一值转换为新的列名称来重塑我们的数据集。index参数用于您不想旋转的列。 传递给values参数的列将平铺在index和columns参数中列的每个唯一组合上。
只有在index和columns参数中的列的每种唯一组合仅出现一次时,pivot方法才有效。 如果唯一的组合不止一个,则会引发异常。 在这种情况下,您可以使用pivot_table方法,该方法允许您将多个值聚合在一起。
枢纽化之后,我们利用高效且快速的 NumPy where函数,该函数的第一个参数是产生布尔序列的条件。True值映射到Amy,False值映射到Bob。我们突出显示每个月的获胜者,并使用value_counts方法统计最终得分。
更多
看一下第 7 步中的数据帧输出。您是否注意到月份是按字母顺序而不是按时间顺序排列的? 不幸的是,至少在这种情况下,Pandas 按字母顺序为我们排序了几个月。 我们可以通过将Month的数据类型更改为分类变量来解决此问题。 分类变量将每列的所有值映射为一个整数。 我们可以选择此映射为月份的正常时间顺序。 Pandas 在pivot方法期间使用此基础整数映射按时间顺序排列月份:
>>> week4a = week4.copy()
>>> month_chron = week4a['Month'].unique() # or use drop_duplicates
>>> month_chron
array(['Jan', 'Feb', 'Mar', 'Apr'], dtype=object)
>>> week4a['Month'] = pd.Categorical(week4a['Month'],
categories=month_chron,
ordered=True)
>>> week4a.pivot(index='Month', columns='Name',
values='Perc Weight Loss')
要转换Month列,请使用Categorical构造器。 将原始列作为序列传递,并将所有类别的唯一序列按所需顺序传递给categories参数。 由于Month列已经按时间顺序排列,因此我们可以简单地使用unique方法,该方法保留了获取所需数组的顺序。 通常,要按字母顺序以外的其他方式对对象数据类型的列进行排序,请将其转换为类别。
另见
计算每个州的 SAT 加权平均成绩
分组对象具有四个接受一个或多个函数以对每个组执行计算的方法。 这四种方法是agg,filter,transform和apply。 这些方法的前三个方法中的每个方法都有一个非常特定的输出,函数必须返回该输出。agg必须返回标量值,filter必须返回布尔值,transform必须返回与传递的组长度相同的序列。 但是,apply方法可能返回标量值,序列或什至任何形状的数据帧,因此使其非常灵活。 每个组也仅将其称为 ,这与对每个非分组列调用一次的transform和agg形成对比。apply方法能够同时对多个列进行操作时返回单个对象的能力,使得此秘籍中的计算成为可能。
准备
在此秘籍中,我们从大学数据集中计算每个州的数学和口头 SAT 分数的加权平均值。 我们根据每个学校的本科生人数对分数进行加权。
操作步骤
- 读取大学数据集,并在
UGDS,SATMTMID或SATVRMID列中删除所有缺少值的行。 这三列中的每一列都必须具有非缺失值:
>>> college = pd.read_csv('data/college.csv')
>>> subset = ['UGDS', 'SATMTMID', 'SATVRMID']
>>> college2 = college.dropna(subset=subset)
>>> college.shape
(7535, 27)
>>> college2.shape
(1184, 27)
- 绝大多数机构没有我们三个必填列的数据,但这仍然足够继续。 接下来,创建一个用户定义的函数以仅计算 SAT 数学分数的加权平均值:
>>> def weighted_math_average(df):
weighted_math = df['UGDS'] * df['SATMTMID']
return int(weighted_math.sum() / df['UGDS'].sum())
- 按状态分组,然后将此函数传递给
apply方法:
>>> college2.groupby('STABBR').apply(weighted_math_average).head()
STABBR
AK 503
AL 536
AR 529
AZ 569
CA 564
dtype: int64
- 我们成功为每个组返回了一个标量值。 让我们绕个小弯路,将相同的函数传递给
agg方法,看看结果如何:
>>> college2.groupby('STABBR').agg(weighted_math_average).head()
weighted_math_average函数将应用于数据帧中的每个非聚合列。 如果尝试将列限制为SATMTMID,则将出现错误,因为您将无法访问UGDS。 因此,完成对多列操作的最佳方法是使用apply:
>>> college2.groupby('STABBR')['SATMTMID'] \
.agg(weighted_math_average)
KeyError: 'UGDS'
apply的一个不错的功能是您可以通过返回一个序列来创建多个新列。 此返回的序列的索引将是新的列名。 让我们修改一下函数,以计算两个 SAT 分数的加权平均值和算术平均值,以及每个组中机构数量的计数。 我们以序列返回以下五个值:
>>> from collections import OrderedDict
>>> def weighted_average(df):
data = OrderedDict()
weight_m = df['UGDS'] * df['SATMTMID']
weight_v = df['UGDS'] * df['SATVRMID']
wm_avg = weight_m.sum() / df['UGDS'].sum()
wv_avg = weight_v.sum() / df['UGDS'].sum()
data['weighted_math_avg'] = wm_avg
data['weighted_verbal_avg'] = wv_avg
data['math_avg'] = df['SATMTMID'].mean()
data['verbal_avg'] = df['SATVRMID'].mean()
data['count'] = len(df)
return pd.Series(data, dtype='int')
>>> college2.groupby('STABBR').apply(weighted_average).head(10)
工作原理
为了正确完成此秘籍,我们需要首先过滤没有UGDS,SATMTMID和SATVRMID值缺失的机构。 默认情况下,dropna方法删除具有一个或多个缺失值的行。 我们必须使用subset参数来限制其查找缺少值的列。
在第 2 步中,我们定义一个仅计算SATMTMID列的加权平均值的函数。 加权平均值与算术平均值的不同之处在于,每个值都乘以一定的权重。 然后将这个数量相加并除以权重之和。 在这种情况下,我们的体重就是在校学生人数。
在第 3 步中,我们将此函数传递给apply方法。 我们的函数weighted_math_average传递了每个组所有原始列的数据帧。 它返回单个标量值,即SATMTMID的加权平均值。 此时,您可能认为可以使用agg方法进行此计算。 用agg直接替换apply不起作用,因为agg返回其每个聚合列的值。
实际上,可以通过预先计算UGDS和SATMTMID的乘法来间接使用agg。
步骤 6 确实显示了apply的多功能性。 我们构建了一个新函数,该函数计算两个 SAT 列的加权平均值和算术平均值以及每个组的行数。 为了使apply创建多个列,您必须返回一个序列。 索引值用作结果数据帧中的列名。 您可以使用此方法返回任意多个值。
请注意,OrderedDict类是从collections模块导入的,该模块是标准库的一部分。 该有序字典用于存储数据。 普通的 Python 字典不能用来存储数据,因为它不保留插入顺序。
构造器pd.Series确实具有一个索引参数,您可以使用它来指定顺序,但是使用OrderedDict会更干净。
更多
在此秘籍中,我们为每个组返回一行作为序列。 通过返回数据帧,可以为每个组返回任意数量的行和列。 除了查找算术和加权均值之外,我们还查找两个 SAT 列的几何和谐波均值,然后将结果作为数据帧返回,其中数据行是均值类型的名称,列是 SAT 类型。 为了减轻我们的负担,我们使用 NumPy 函数average来计算加权平均值,并使用 SciPy 函数gmean和hmean来计算几何和调和平均值:
>>> from scipy.stats import gmean, hmean
>>> def calculate_means(df):
df_means = pd.DataFrame(index=['Arithmetic', 'Weighted',
'Geometric', 'Harmonic'])
cols = ['SATMTMID', 'SATVRMID']
for col in cols:
arithmetic = df[col].mean()
weighted = np.average(df[col], weights=df['UGDS'])
geometric = gmean(df[col])
harmonic = hmean(df[col])
df_means[col] = [arithmetic, weighted,
geometric, harmonic]
df_means['count'] = len(df)
return df_means.astype(int)
>>> college2.groupby('STABBR').apply(calculate_means).head(12)
另见
按连续变量分组
在对 Pandas 进行分组时,通常使用具有离散重复值的列。 如果没有重复的值,则分组将毫无意义,因为每个组只有一行。 连续数字列通常具有很少的重复值,并且通常不用于形成组。 但是,如果我们可以将具有连续值的列转换为离散列,方法是将每个值放入一个桶中,四舍五入或使用其他映射,则将它们分组是有意义的。
准备
在此秘籍中,我们探索航班数据集以发现不同旅行距离的航空公司分布。 例如,这使我们能够找到在 500 到 1,000 英里之间飞行最多的航空公司。 为此,我们使用 Pandascut函数离散化每次飞行的距离。
操作步骤
- 读取航班数据集,并输出前五行:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights.head()
- 如果要查找一定距离范围内的航空公司分布,则需要将
DIST列的值放入离散的桶中。 让我们使用 pandascut函数将数据分为五个桶:
>>> bins = [-np.inf, 200, 500, 1000, 2000, np.inf]
>>> cuts = pd.cut(flights['DIST'], bins=bins)
>>> cuts.head()
0 (500.0, 1000.0]
1 (1000.0, 2000.0]
2 (500.0, 1000.0]
3 (1000.0, 2000.0]
4 (1000.0, 2000.0]
Name: DIST, dtype: category
Categories (5, interval[float64]): [(-inf, 200.0] < (200.0, 500.0] < (500.0, 1000.0] < (1000.0, 2000.0] < (2000.0, inf]]
- 创建有序的分类序列。 为了帮助您了解发生了什么,让我们计算每个类别的值:
>>> cuts.value_counts()
(500.0, 1000.0] 20659
(200.0, 500.0] 15874
(1000.0, 2000.0] 14186
(2000.0, inf] 4054
(-inf, 200.0] 3719
Name: DIST, dtype: int64
cuts序列现在可以用于形成组。 Pandas 允许您以任何希望的方式来分组。 将cuts序列传递到groupby方法,然后在AIRLINE列上调用value_counts方法以查找每个距离组的分布。 请注意,SkyWest(OO)组成了少于 200 英里的航班的 33%,但仅占 200 到 500 英里之间的航班的 16%:
>>> flights.groupby(cuts)['AIRLINE'].value_counts(normalize=True) \
.round(3).head(15)
DIST AIRLINE
(-inf, 200.0] OO 0.326
EV 0.289
MQ 0.211
DL 0.086
AA 0.052
UA 0.027
WN 0.009
(200.0, 500.0] WN 0.194
DL 0.189
OO 0.159
EV 0.156
MQ 0.100
AA 0.071
UA 0.062
VX 0.028
Name: AIRLINE, dtype: float64
工作原理
在步骤 2 中,cut函数将DIST列的每个值放入五个仓位之一。 箱由定义边缘的六个数字序列创建。 您总是需要比容器数多一个边缘。 您可以为bins参数传递一个整数,该整数将自动创建该数目的等宽槽。 NumPy 中提供了负无穷大对象和正无穷大对象,并确保将所有值放置在桶中。 如果您的值在箱边缘之外,则将使它们丢失并且不会放置在箱中。
cuts变量现在是五个有序类别的序列。 它具有所有常规的序列方法,在步骤 3 中,使用value_counts方法来了解其分布。
非常有趣的是,pandas 允许您将groupby方法传递给任何对象。 这意味着您可以从与当前数据帧完全无关的内容中形成组。 在这里,我们将cuts变量中的值分组。 对于每个分组,我们通过将normalize设置为True,以value_counts查找每个航空公司的航班百分比。
从这个结果可以得出一些有趣的见解。 从全部结果来看,SkyWest 是领先的航空公司,飞行距离不到 200 英里,但没有超过 2,000 英里的航班。 相比之下,美国航空公司在 200 英里以下的航班中排名第五,但到目前为止,在 1,000 到 2,000 英里之间的航班最多。
更多
当按cuts变量分组时,我们可以找到更多结果。 例如,我们可以为每个距离分组找到第 25、50 和 75% 的通话时间。 由于通话时间以分钟为单位,因此我们可以除以 60 得到小时:
>>> flights.groupby(cuts)['AIR_TIME'].quantile(q=[.25, .5, .75]) \
.div(60).round(2)
DIST
(-inf, 200.0] 0.25 0.43
0.50 0.50
0.75 0.57
(200.0, 500.0] 0.25 0.77
0.50 0.92
0.75 1.05
(500.0, 1000.0] 0.25 1.43
0.50 1.65
0.75 1.92
(1000.0, 2000.0] 0.25 2.50
0.50 2.93
0.75 3.40
(2000.0, inf] 0.25 4.30
0.50 4.70
0.75 5.03
Name: AIR_TIME, dtype: float64
当使用cut函数时,我们可以使用此信息来创建内容丰富的字符串标签。 这些标签代替了间隔符号。 我们还可以链接unstack方法,该方法将内部索引级别转换为列名称:
>>> labels=['Under an Hour', '1 Hour', '1-2 Hours',
'2-4 Hours', '4+ Hours']
>>> cuts2 = pd.cut(flights['DIST'], bins=bins, labels=labels)
>>> flights.groupby(cuts2)['AIRLINE'].value_counts(normalize=True) \
.round(3) \
.unstack() \
.style.highlight_max(axis=1)
另见
- Pandas
cut函数的官方文档 - 更多秘籍请参考第 8 章, “将数据整理为整齐的格式”
计算城市之间的航班总数
在航班数据集中,我们具有始发地和目的地机场的数据。 例如,计算从休斯敦出发并降落在亚特兰大的航班数量是微不足道的。 更困难的是计算两个城市之间的航班总数,而不管始发地或目的地是哪一个。
准备
在此秘籍中,我们计算两个城市之间的航班总数,而不管始发地或目的地是哪个。 为此,我们按字母顺序对始发和目的地机场进行排序,以使机场的每种组合始终以相同的顺序出现。 然后,我们可以使用这种新的列安排来形成组,然后进行计数。
操作步骤
- 读取航班数据集,并找到每个始发地与目的地机场之间的航班总数:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights_ct = flights.groupby(['ORG_AIR', 'DEST_AIR']).size()
>>> flights_ct.head()
ORG_AIR DEST_AIR
ATL ABE 31
ABQ 16
ABY 19
ACY 6
AEX 40
dtype: int64
- 选择在两个方向上的休斯顿(
IAH)和亚特兰大(ATL)之间的航班总数:
>>> flights_ct.loc[[('ATL', 'IAH'), ('IAH', 'ATL')]]
ORG_AIR DEST_AIR
ATL IAH 121
IAH ATL 148
dtype: int64
- 我们可以简单地将这两个数字相加得出城市之间的总航班,但是有一种更有效,更自动化的解决方案可以适用于所有航班。 让我们按照字母顺序对每一行的起点和终点城市进行独立排序:
>>> flights_sort = flights[['ORG_AIR', 'DEST_AIR']] \
.apply(sorted, axis=1)
>>> flights_sort.head()
- 现在,每行都已独立排序,列名不正确。 让我们将其重命名为更通用的名称,然后再次找到所有城市之间的航班总数:
>>> rename_dict = {'ORG_AIR':'AIR1', 'DEST_AIR':'AIR2'}
>>> flights_sort = flights_sort.rename(columns=rename_dict)
>>> flights_ct2 = flights_sort.groupby(['AIR1', 'AIR2']).size()
>>> flights_ct2.head()
AIR1 AIR2
ABE ATL 31
ORD 24
ABI DFW 74
ABQ ATL 16
DEN 46
dtype: int64
- 让我们选择亚特兰大和休斯顿之间的所有航班,并验证其是否与步骤 2 中的值之和匹配:
>>> flights_ct2.loc[('ATL', 'IAH')]
269
- 如果我们尝试选择休斯顿和亚特兰大的航班,则会出现错误:
>>> flights_ct2.loc[('IAH', 'ATL')]
IndexingError: Too many indexers
工作原理
在第 1 步中,我们按起点和目的地机场列形成分组,然后将size方法应用于分组对象,该对象仅返回每个组的总行数。 请注意,我们可以将字符串size传递给agg方法以获得相同的结果。 在步骤 2 中,选择了亚特兰大和休斯顿之间每个方向的航班总数。 序列flights_count具有两个级别的多重索引。 从多重索引中选择行的一种方法是将loc索引运算符传递给精确级别值的元组。 在这里,我们实际上选择了两个不同的行('ATL', 'HOU')和('HOU', 'ATL')。 我们使用元组列表来正确执行此操作。
步骤 3 是秘籍中最相关的步骤。 对于亚特兰大和休斯顿之间的所有航班,我们只希望有一个标签,到目前为止,我们有两个标签。 如果我们按字母顺序对出发地和目的地机场的每种组合进行排序,那么我们将为机场之间的航班使用一个标签。 为此,我们使用数据帧的apply方法。 这与分组的apply方法不同。 在步骤 3 中没有形成组。
必须向数据帧的apply方法传递一个函数。 在这种情况下,它是内置的sorted函数。 默认情况下,此函数作为序列应用于每个列。 我们可以使用axis=1(或axis='index')来改变计算方向。sorted函数将每行数据隐式地作为序列传递给它。 它返回已排序的机场代码的列表。 这是将第一行作为序列传递给排序函数的示例:
>>> sorted(flights.loc[0, ['ORG_AIR', 'DEST_AIR']])
['LAX', 'SLC']
apply方法以这种确切的方式使用sorted遍历所有行。 完成此操作后,将对每一行进行独立排序。 列名现在已无意义。 我们在下一步中对列名称进行重命名,然后执行与步骤 2 中相同的分组和汇总。这次,亚特兰大和休斯顿之间的所有航班都属于同一标签。
更多
您可能想知道为什么我们不能使用更简单的sort_values序列方法。 此方法不是独立进行排序,而是将行或列保留为单个记录,就像在进行数据分析时所期望的那样。 步骤 3 是非常昂贵的操作,需要几秒钟才能完成。 只有大约 60,000 行,因此该解决方案无法很好地扩展到更大的数据。
步骤 3 是非常昂贵的操作,需要几秒钟才能完成。 只有大约 60,000 行,因此该解决方案无法很好地扩展到更大的数据。 在所有 Pandas 中,用axis=1调用apply方法是性能最低的操作之一。 在内部,Pandas 在每行上循环,不会因 NumPy 提供任何速度提升。 如果可能,请避免将apply与axis=1一起使用。
使用 NumPy sort函数可以大大提高速度。 让我们继续使用此函数并分析其输出。 默认情况下,它将对每一行进行独立排序:
>>> data_sorted = np.sort(flights[['ORG_AIR', 'DEST_AIR']])
>>> data_sorted[:10]
array([['LAX', 'SLC'],
['DEN', 'IAD'],
['DFW', 'VPS'],
['DCA', 'DFW'],
['LAX', 'MCI'],
['IAH', 'SAN'],
['DFW', 'MSY'],
['PHX', 'SFO'],
['ORD', 'STL'],
['IAH', 'SJC']], dtype=object)
返回二维 NumPy 数组。 NumPy 并不容易进行分组操作,因此让我们使用数据帧构造器创建一个新的数据帧并检查它是否等于步骤 3 中的flights_sorted数据帧:
>>> flights_sort2 = pd.DataFrame(data_sorted, columns=['AIR1', 'AIR2'])
>>> fs_orig = flights_sort.rename(columns={'ORG_AIR':'AIR1',
'DEST_AIR':'AIR2'})
>>> flights_sort2.equals(fs_orig)
True
由于数据帧相同,因此您可以将第 3 步替换为先前的更快排序例程。 我们来计时一下每种不同的排序方法之间的区别:
>>> %%timeit
>>> flights_sort = flights[['ORG_AIR', 'DEST_AIR']] \
.apply(sorted, axis=1)
7.41 s ± 189 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> %%timeit
>>> data_sorted = np.sort(flights[['ORG_AIR', 'DEST_AIR']])
>>> flights_sort2 = pd.DataFrame(data_sorted,
columns=['AIR1', 'AIR2'])
10.6 ms ± 453 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
NumPy 解决方案的速度比对 Pandas 使用apply快 700 倍。
另见
寻找最长的准时航班
对于航空公司而言,最重要的指标之一是其准时飞行表现。 美国联邦航空管理局认为,航班在比原定抵达时间至少晚 15 分钟后才抵达。 Pandas 有直接的方法来计算每个航空公司的准时航班总数和百分比。 尽管这些基本摘要统计数据是一个重要的指标,但是还有其他一些重要的计算很有趣,例如,找出每个航空公司在其始发机场的连续准点飞行时间。
准备
在此秘籍中,我们找到了每个始发机场的每家航空公司的最长连续航班准点率。 这要求列中的每个值都必须知道紧随其后的值。 为了将条纹应用到每个组之前,我们巧妙地使用了diff和cumsum方法来发现条纹。
操作步骤
- 在开始实际的航班数据集之前,让我们练习计算带有少量样本序列的航班的条纹:
>>> s = pd.Series([0, 1, 1, 0, 1, 1, 1, 0])
>>> s
0 0
1 1
2 1
3 0
4 1
5 1
6 1
7 0
dtype: int64
- 我们对 1 的条纹的最终表示将是与原始序列相同长度的序列,每个条纹从 1 开始独立计数。 首先,我们使用
cumsum方法:
>>> s1 = s.cumsum()
>>> s1
0 0
1 1
2 2
3 2
4 3
5 4
6 5
7 5
dtype: int64
- 现在,我们已经积累了序列中的所有值。 让我们将此序列乘以原始序列:
>>> s.mul(s1)
0 0
1 1
2 2
3 0
4 3
5 4
6 5
7 0
dtype: int64
- 我们最初只有一个非零值。 这个结果非常接近我们的期望。 我们只需要重新开始每个连胜,而不是从累加的总和开始。 让我们链接
diff方法,该方法从当前值中减去前一个值:
>>> s.mul(s1).diff()
0 NaN
1 1.0
2 1.0
3 -2.0
4 3.0
5 1.0
6 1.0
7 -5.0
dtype: float64
- 负值表示条纹结束。 我们需要将负值向下传播到序列上,并使用它们减去步骤 2 中多余的累加。为此,我们将使用
where方法使所有非负值都丢失:
>>> s.mul(s1).diff().where(lambda x: x < 0)
0 NaN
1 NaN
2 NaN
3 -2.0
4 NaN
5 NaN
6 NaN
7 -5.0
dtype: float64
- 现在,我们可以使用
ffill方法向下传播这些值:
>>> s.mul(s1).diff().where(lambda x: x < 0).ffill()
0 NaN
1 NaN
2 NaN
3 -2.0
4 -2.0
5 -2.0
6 -2.0
7 -5.0
dtype: float64
- 最后,我们可以将此序列添加回
s1,以清除多余的累积量:
>>> s.mul(s1).diff().where(lambda x: x < 0).ffill() \
.add(s1, fill_value=0)
0 0.0
1 1.0
2 2.0
3 0.0
4 1.0
5 2.0
6 3.0
7 0.0
dtype: float64
- 现在我们有了一个连续工作的条纹查找器,我们可以找到每个航空公司和始发机场最长的条纹。 让我们读入航班数据集并创建一列以表示准时到达:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights['ON_TIME'] = flights['ARR_DELAY'].lt(15).astype(int)
>>> flights[['AIRLINE', 'ORG_AIR', 'ON_TIME']].head(10)
- 使用前七个步骤中的逻辑来定义一个函数,该函数返回给定序列的最大连胜数:
>>> def max_streak(s):
s1 = s.cumsum()
return s.mul(s1).diff().where(lambda x: x < 0) \
.ffill().add(s1, fill_value=0).max()
- 找出每个航空公司和始发机场的最大准点到达率,以及航班总数和准点到达率。 首先,对一年中的日期和预定的出发时间进行排序:
>>> flights.sort_values(['MONTH', 'DAY', 'SCHED_DEP']) \
.groupby(['AIRLINE', 'ORG_AIR'])['ON_TIME'] \
.agg(['mean', 'size', max_streak]).round(2).head()
工作原理
在 Pandas 中查找数据中的条纹并不是一项简单的操作,需要先行或后行的方法,例如diff或shift,或记住当前状态的方法,例如cumsum。 前七个步骤的最终结果是序列的长度与原始序列的长度相同,可以跟踪所有连续的序列。 在这些步骤中,我们使用mul和add方法代替它们的等效运算符(*)和(+)。 我认为,这样可以使计算从左到右的过程更加简洁。 您当然可以将它们替换为实际的运算符。
理想情况下,我们希望告诉 Pandas 在每个条纹开始时都应用cumsum方法,并在每个条纹结束后重新设置自身。 要将此信息传达给 Pandas,需要采取许多步骤。 第 2 步将整个序列中的所有结果累积起来。 其余步骤将慢慢清除所有多余的累积。 为了识别这种多余的累积,我们需要找到每个条纹的末尾并从下一个条纹的开始减去该值。
要找到每个条纹的结尾,请通过在步骤 3 中将s1乘以原始零序列和 1 来巧妙地使所有值不属于条纹零。 条纹。 很好,但是同样,我们需要消除多余的累积。 知道条纹结束的地方并不能使我们到达那里。
在第 4 步中,我们使用diff方法来查找此多余部分。diff方法获取当前值与位于距离其一定行数的任何值之间的差。 默认情况下,返回当前值与前一个值之间的差。
在步骤 4 中,只有负值才有意义。那些是连续结束后的值。 这些值需要向下传播,直到后续条纹结束。 为了消除(丢失)所有我们不关心的值,我们使用where方法,该方法采用与调用序列大小相同的条件序列。 默认情况下,所有True值保持不变,而False值丢失。where方法允许您通过将函数作为第一个参数来将调用序列用作条件的一部分。 使用一个匿名函数,该函数隐式传递给调用序列,并检查每个值是否小于零。 第 5 步的结果是一个序列,其中仅保留负值,其余更改为缺失值。
步骤 6 中的ffill方法将缺失值替换为在序列中前进/后退的最后一个非缺失值。 由于前三个值不跟随非缺失值,因此它们仍然丢失。 我们终于有了消除多余积蓄的序列。 我们将累加序列添加到步骤 6 的结果中,以使条纹全部从零开始。add方法允许我们用fill_value参数替换缺少的值。 这样就完成了在数据集中查找条纹的过程。 当执行这样的复杂逻辑时,最好使用一个小的数据集,在此您可以知道最终的输出是什么。 从第 8 步开始并在分组时建立这种寻路逻辑将是非常困难的任务。
在步骤 8 中,我们创建ON_TIME列。 值得注意的一项是,已取消的排期缺少ARR_DELAY的值,该值未通过布尔条件,因此ON_TIME列的值为零。 取消的航班与延迟的航班一样。
第 9 步将我们的逻辑从前七个步骤转变为一个函数,并链接max方法以返回最长的条纹。 由于我们的函数返回单个值,因此它正式是一个聚合函数,可以按照步骤 10 的操作传递给agg方法。为确保我们正在查看实际的连续航班,我们使用sort_values方法按日期和预定的出发时间进行排序。
更多
既然我们找到了准点到达时间最长的条纹,我们可以轻松地找到相反的地方-延迟到达的最长条纹。 以下函数为传递给它的每个组返回两行。 第一行是条纹的起点,最后一行是条纹的终点。 每行包含开始/结束条纹的月份和日期,以及条纹的总长度:
>>> def max_delay_streak(df):
df = df.reset_index(drop=True)
s = 1 - df['ON_TIME']
s1 = s.cumsum()
streak = s.mul(s1).diff().where(lambda x: x < 0) \
.ffill().add(s1, fill_value=0)
last_idx = streak.idxmax()
first_idx = last_idx - streak.max() + 1
df_return = df.loc[[first_idx, last_idx], ['MONTH', 'DAY']]
df_return['streak'] = streak.max()
df_return.index = ['first', 'last']
df_return.index.name='type'
return df_return
>>> flights.sort_values(['MONTH', 'DAY', 'SCHED_DEP']) \
.groupby(['AIRLINE', 'ORG_AIR']) \
.apply(max_delay_streak) \
.sort_values('streak', ascending=False).head(10)
当我们使用分组的apply方法时,每个组的数据帧都传递给max_delay_streak函数。 在此函数内部,删除了数据帧的索引并用RangeIndex代替,以便我们轻松找到条纹的第一行和最后一行。 反转ON_TIME列,然后使用相同的逻辑查找延迟飞行的条纹。 条纹的第一行和最后一行的索引存储为变量。 然后,这些索引用于选择条纹结束的月份和日期。 我们使用数据帧返回结果。 我们标记并命名索引以使最终结果更清晰。
我们的最终结果显示了最长的延迟条纹以及第一和最后一个日期。 让我们进行调查,看看是否可以找出导致这些延迟的原因。 天气恶劣是航班延误或取消的常见原因。 从第一行开始,美国航空(AA)从达拉斯沃思堡(DFW)机场开始拥有连续 38 班延误航班,从 2015 年 2 月 26 日至 2015 年 3 月 1 日。查看 2015 年 2 月 27 日的历史天气数据 ,降雪量为 2 英寸,这是当天的记录。 这是 DFW 的主要天气事件,并给整个城市造成了严重问题。 请注意,DFW 出现了第三次最长的连胜纪录,但这次是几天前,并且是另一家航空公司。
另见
- 请参阅第 1 章,“Pandas 基础”的“将运算符与序列一起使用”秘籍
- Pandas
ffill的官方文档
八、将数据重组为整齐的表格
在本章中,我们将介绍以下主题:
- 使用
stack将变量值整理为列名 - 使用
melt将变量值整理为列名 - 同时堆叠多组变量
- 反转堆叠数据
- 在
groupby聚合后解除堆叠 - 使用用
groupby聚合复制pivot_table - 重命名轴级别以方便重塑
- 将多个变量存储为列名时进行整理
- 将多个变量存储为列值时进行整理
- 在同一单元格中存储两个或多个值时进行整理
- 在列名和值中存储变量时进行整理
- 将多个观测单位存储在同一表中时进行整理
介绍
前几章中使用的所有数据集都没有做太多或做任何工作来更改其结构。 我们立即开始以原始形状处理数据集。 在开始更详细的分析之前,许多野外的数据集将需要大量的重组。 在某些情况下,整个项目可能只关心格式化数据,以便其他人可以轻松处理它。
有许多术语用于描述数据重组的过程,其中整齐的数据是数据科学家最常用的。 整洁的数据是 Hadley Wickham 创造的一个术语,用于描述使分析变得容易进行的数据形式。 本章将涵盖 Hadley 提出的许多想法以及如何用 Pandas 来实现它们。 要了解有关整理数据的更多信息,请阅读 Hadley 的论文。
什么是整洁的数据? Hadley 提出了三个简单的指导原则来确定数据集是否整洁:
- 每个变量组成一列
- 每个观测结果排成一行
- 每种观测单位组成一个表格
任何不符合这些准则的数据集都被认为是混乱的。 一旦开始将数据重组为整齐的格式,此定义将变得更有意义,但是现在,我们需要知道什么是变量,观测值和观测单位。
要获得关于变量实际含义的直觉,最好考虑一下变量名称和变量值之间的区别。 变量名称是标签,例如性别,种族,薪水和职位。 变量值是指每次观察都可能发生变化的事物,例如性别中的男性/女性或种族中的白色/黑色。 单个观测值是单个观测单位的所有变量值的集合。 为了帮助了解观察单位可能是什么,请考虑零售商店,该商店具有有关每个交易,员工,客户,物品和商店本身的数据。 这些中的每一个都可以视为观察单位,并且需要自己的表格。 将员工信息(例如,工作时间)与客户信息(例如,花费的金额)组合在同一张表中,将破坏这一整洁的原则。
解决杂乱数据的第一步是在存在杂乱数据时对其进行识别,并且存在无限可能。 Hadley 明确提到了五种最常见的混乱数据类型:
- 列名是值,不是变量名
- 多个变量存储在列名中
- 变量存储在行和列中
- 多种观测单位存储在同一表中
- 一个观测单位存储在多个表中
重要的是要了解,整理数据通常不涉及更改数据集的值,填写缺失的值或进行任何类型的分析。 整理数据涉及更改数据的形状或结构以符合整理原则。 整洁的数据类似于将所有工具都放在工具箱中,而不是随机散布在整个房屋中。 在工具箱中正确放置工具可以轻松完成所有其他任务。 数据格式正确后,进行进一步分析变得容易得多。
一旦发现混乱的数据,您将使用 Pandas 工具来重组数据,使数据整洁。 Pandas 提供给您的主要整洁工具是数据帧方法stack,melt,unstack和pivot。 较复杂的整理工作涉及撕裂文本,这需要str访问器。 其他辅助方法,例如rename,rename_axis,reset_index和set_index,将有助于对整洁的数据进行最终处理。
使用stack将变量值整理为列名
为了帮助理解整洁数据和混乱数据之间的差异,让我们看一下一个简单的表格,该表格可能是也可能不是整齐的:
>>> state_fruit = pd.read_csv('data/state_fruit.csv', index_col=0)
>>> state_fruit
该表似乎没有任何混乱,并且该信息很容易消耗。 但是,按照整洁的原则,它实际上并不是整洁的。 每个列名称实际上是变量的值。 实际上,数据帧中甚至都没有变量名。 将凌乱的数据集转换为整洁的数据的第一步之一就是识别所有变量。 在此特定数据集中,我们具有州和水果的变量。 在问题的背景下,还没有找到任何数字数据。 我们可以将此变量标记为权重或其他任何明智的名称。
准备
这个特定的混乱数据集包含变量值作为列名。 我们将需要将这些列名称转换为列值。 在本秘籍中,我们使用stack方法将数据帧重组为整齐的形式。
操作步骤
- 首先,请注意,状态名称位于数据帧的索引中。 这些状态正确地垂直放置,不需要重组。 问题是列名。
stack方法采用所有列名,并将其整形为垂直,作为单个索引级别:
>>> state_fruit.stack()
Texas Apple 12
Orange 10
Banana 40
Arizona Apple 9
Orange 7
Banana 12
Florida Apple 0
Orange 14
Banana 190
dtype: int64
- 注意,我们现在有了一个带有多重索引的序列。 现在索引中有两个级别。 原始索引已被推到左侧,以便为旧的列名腾出空间。 使用这一命令,我们现在基本上有了整洁的数据。 每个变量,状态,水果和重量都是垂直的。 让我们使用
reset_index方法将结果转换为数据帧:
>>> state_fruit_tidy = state_fruit.stack().reset_index()
>>> state_fruit_tidy
- 现在我们的结构是正确的,但是列名没有意义。 让我们用适当的标识符替换它们:
>>> state_fruit_tidy.columns = ['state', 'fruit', 'weight']
>>> state_fruit_tidy
- 可以直接使用鲜为人知的序列方法
rename_axis来设置索引级别的名称,而不是直接更改columns属性,然后再使用reset_index:
>>> state_fruit.stack()\
.rename_axis(['state', 'fruit'])
state fruit
Texas Apple 12
Orange 10
Banana 40
Arizona Apple 9
Orange 7
Banana 12
Florida Apple 0
Orange 14
Banana 190
dtype: int64
- 从这里,我们可以简单地将
reset_index方法与name参数链接起来,以重现步骤 3 的输出:
>>> state_fruit.stack()\
.rename_axis(['state', 'fruit'])\
.reset_index(name='weight')
工作原理
stack方法功能强大,需要花费一些时间才能完全理解和欣赏。 它接受所有列名并转置它们,因此它们成为新的最里面的索引级别。 请注意,每个旧列名称仍如何通过与每个状态配对来标记其原始值。3 x 3数据帧中有 9 个原始值,这些值被转换为具有相同数量值的单个序列。 原始的第一行数据成为结果序列中的前三个值。
在步骤 2 中重置索引后,pandas 将我们的数据帧的列默认设置为level_0,level_1和0。 这是因为调用此方法的序列具有两个未正式命名的索引级别。 Pandas 还从外部从零开始按整数引用索引。
步骤 3 显示了一种重命名列的简单直观的方法。 您可以通过将columns属性设置为等于列表来简单地为整个数据帧设置新列。
或者,可以通过链接rename_axis方法在一个步骤中设置列名称,该方法在将列表作为第一个参数传递时,将这些值用作索引级别名称。 重置索引时,Pandas 使用这些索引级别名称作为新的列名称。 此外,reset_index方法具有一个name参数,该参数对应于序列值的新列名称。
所有序列都有一个name属性,可以直接设置或使用rename方法设置。 当使用reset_index时,这个属性成为列名。
更多
使用stack的关键之一是将所有不希望转换的列都放在索引中。 最初使用索引中的状态读取此秘籍中的数据集。 让我们看一下如果不将状态读入索引,将会发生什么:
>>> state_fruit2 = pd.read_csv('data/state_fruit2.csv')
>>> state_fruit2
由于状态名称不在索引中,因此在此数据帧上使用stack可将所有值整形为一个长值序列:
>>> state_fruit2.stack()
0 State Texas
Apple 12
Orange 10
Banana 40
1 State Arizona
Apple 9
Orange 7
Banana 12
2 State Florida
Apple 0
Orange 14
Banana 190
dtype: object
这个命令将重塑所有列,这次包括状态,而这根本不是我们所需要的。 为了正确地重塑此数据,您需要首先使用set_index方法将所有未重塑的列放入索引中,然后使用stack。 下面的代码与步骤 1 产生相似的结果:
>>> state_fruit2.set_index('State').stack()
另见
使用melt将变量值整理为列名
像大多数大型 Python 库一样,Pandas 也有许多不同的方式来完成同一任务-区别通常是可读性和性能。 Pandas 包含一个名为melt的数据帧方法,该的工作原理与先前秘籍中介绍的stack方法相似,但灵活性更高。
在 Pandas 版本 0.20 之前,melt仅作为必须通过pd.melt访问的函数提供。 Pandas 仍然是一个不断发展的库,您需要期待每个新版本的变化。 Pandas 一直在推动将只能在数据帧上运行的所有函数移至方法上,例如它们对melt所做的一样。 这是使用melt的首选方法,也是本秘籍使用它的方式。 查看 Pandas 文档的“新增功能”部分,以了解所有更改的最新信息。
准备
在本秘籍中,我们使用melt方法来整理一个简单的数据帧,以变量值作为列名。
操作步骤
- 读取
state_fruit2数据集,并确定哪些列需要转换,哪些列不需要转换:
>>> state_fruit2 = pd.read_csv('data/state_fruit2.csv')
>>> state_fruit2
- 通过将适当的列传递给
id_vars和value_vars参数来使用melt方法:
>>> state_fruit2.melt(id_vars=['State'],
value_vars=['Apple', 'Orange', 'Banana'])
- 这一步为我们创建了整洁的数据。 默认情况下,
melt将转换后的前列名称称为变量,并将相应的值称为值。 方便地,melt有两个附加参数var_name和value_name,它们使您能够重命名这两列:
>>> state_fruit2.melt(id_vars=['State'],
value_vars=['Apple', 'Orange', 'Banana'],
var_name='Fruit',
value_name='Weight')
工作原理
melt方法功能强大,可以显着重塑您的数据帧。 它最多包含五个参数,其中两个参数对于理解如何正确重塑数据至关重要:
id_vars是您要保留为列且不重塑形状的列名列表value_vars是您想要重整为单个列的列名列表
id_vars或标识变量保留在同一列中,但对于传递给value_vars的每列重复一次。melt的一个关键方面是它忽略索引中的值,实际上,它默默地删除了您的索引并用默认的RangeIndex代替了它。 这意味着,如果您确实希望保留索引中的值,那么在使用melt之前,需要先重置索引。
将水平列名称转换为垂直列值的某些通用术语是“融化”,“解除堆叠”或“取消旋转”。
更多
melt方法的所有参数都是可选的,并且如果您希望所有值都位于单个列中,而它们的旧列标签位于另一个列中,则可以使用其默认值调用melt:
>>> state_fruit2.melt()
实际上,您可能有很多需要融合的变量,并且只想指定标识变量。 在这种情况下,以以下方式调用melt会产生与步骤 2 相同的结果。在融化单个列时,实际上甚至不需要列表,只需传递其字符串值即可:
>>> state_fruit2.melt(id_vars='State')
另见
同时堆叠多组变量
一些数据集包含多组变量作为列名,需要同时堆叠到自己的列中。 以movie数据集为例可以帮助阐明这一点。 首先,选择包含演员姓名及其对应的 Facebook 点赞的所有列:
>>> movie = pd.read_csv('data/movie.csv')
>>> actor = movie[['movie_title', 'actor_1_name',
'actor_2_name', 'actor_3_name',
'actor_1_facebook_likes',
'actor_2_facebook_likes',
'actor_3_facebook_likes']]
>>> actor.head()
如果我们将变量定义为电影的标题,演员名称和 Facebook 点赞数,那么我们将需要独立地堆叠两组列,而仅通过一次调用stack或melt。
准备
在本秘籍中,我们将通过同时堆叠演员名称及其与wide_to_long函数相对应的 Facebook 点赞来整理actor数据帧。
操作步骤
- 我们将使用通用的
wide_to_long函数将数据重整为整齐的形式。 要使用此函数,我们将需要更改要堆叠的列名,以使它们以数字结尾。 我们首先创建一个用户定义的函数来更改列名:
>>> def change_col_name(col_name):
col_name = col_name.replace('_name', '')
if 'facebook' in col_name:
fb_idx = col_name.find('facebook')
col_name = col_name[:5] + col_name[fb_idx - 1:] \
+ col_name[5:fb_idx-1]
return col_name
- 将此函数传递给方法以转换所有列名:
>>> actor2 = actor.rename(columns=change_col_name)
>>> actor2.head()
- 使用
wide_to_long函数可同时堆叠actor和actor_facebook_likes列集:
>>> stubs = ['actor', 'actor_facebook_likes']
>>> actor2_tidy = pd.wide_to_long(actor2,
stubnames=stubs,
i=['movie_title'],
j='actor_num',
sep='_')
>>> actor2_tidy.head()
工作原理
wide_to_long函数以相当特定的方式工作。 它的主要参数是stubnames,它是一个字符串列表。 每个字符串代表一个列分组。 以该字符串开头的所有列都将被堆叠到一个列中。 在此秘籍中,有两列列:actor和actor_facebook_likes。 默认情况下,这些列的每个组都需要以数字结尾。 此数字随后将用于标记整形数据。 每个列组都有一个下划线字符,将stubname与结尾数字分开。 为此,必须使用sep参数。
原始列名称与wide_to_long工作所需的模式不匹配。 可以通过使用列表精确指定列名称来手动更改列名称。 这很快就会成为很多类型的输入,因此,我们定义了一个函数,该函数自动将我们的列转换为有效的格式。change_col_name函数从参与者列中删除_name,并重新排列facebook列,以便现在它们都以数字结尾。
要实际完成列重命名,我们在步骤 2 中使用rename方法。它接受许多不同类型的参数,其中之一是函数。 将其传递给函数时,每个列名都会一次隐式传递给它。
现在,我们已经正确地创建了两个独立的列组,即以actor和actor_facebook_likes开头的列,它们将被堆叠。 除此之外,wide_to_long还需要一个唯一列,即参数i,用作不会堆叠的标识变量。 还需要参数j,该参数仅重命名从原始列名的末尾去除的标识数字。 默认情况下,prefix参数包含搜索一个或多个数字的正则表达式,\d+。\d是与数字 0-9 匹配的特殊令牌。 加号+使表达式与这些数字中的一个或多个匹配。
要成为str方法的强大用户,您将需要熟悉正则表达式,这是与某些文本中的特定模式匹配的字符序列。 它们由具有特殊含义的“元字符”和“字面值”字符组成。 要使自己对正则表达式有用,请查看 Regular-Expressions.info 中的简短教程。
更多
当所有变量分组具有相同的数字结尾(如此秘籍中的数字)时,函数wide_to_long起作用。 当您的变量没有相同的结尾或不是以数字结尾时,您仍然可以使用wide_to_long同时进行列堆叠。 例如,让我们看一下以下数据集:
>>> df = pd.read_csv('data/stackme.csv')
>>> df
假设我们希望将a1和b1列以及d和e列堆叠在一起。 另外,我们想使用a1和b1作为行的标签。 要完成此任务,我们需要重命名列,以便它们以所需的标签结尾:
>>> df2 = df.rename(columns = {'a1':'group1_a1', 'b2':'group1_b2',
'd':'group2_a1', 'e':'group2_b2'})
>>> df2
然后,我们需要修改后缀参数,该参数通常默认为选择数字的正则表达式。 在这里,我们只是简单地告诉它找到任意数量的字符:
>>> pd.wide_to_long(df2,
stubnames=['group1', 'group2'],
i=['State', 'Country', 'Test'],
j='Label',
suffix='.+',
sep='_')
另见
反转堆叠数据
数据帧具有两种相似的方法stack和melt,用于将水平列名称转换为垂直列值。数据帧分别具有分别通过unstack和pivot方法直接反转这两个操作的能力。stack/unstack是更简单的方法,仅允许控制列/行索引,而melt/pivot提供更大的灵活性来选择要重塑的列。
准备
在此秘籍中,我们将stack/melt一个数据集,并立即将unstack/pivot的操作转换回其原始形式。
操作步骤
- 读取
college数据集,以机构名称作为索引,并且仅包含大学生种族栏目:
>>> usecol_func = lambda x: 'UGDS_' in x or x == 'INSTNM'
>>> college = pd.read_csv('data/college.csv',
index_col='INSTNM',
usecols=usecol_func)
>>> college.head()
- 使用
stack方法将每个水平列名称转换为垂直索引级别:
>>> college_stacked = college.stack()
>>> college_stacked.head(18)
INSTNM
Alabama A & M University UGDS_WHITE 0.0333
UGDS_BLACK 0.9353
UGDS_HISP 0.0055
UGDS_ASIAN 0.0019
UGDS_AIAN 0.0024
UGDS_NHPI 0.0019
UGDS_2MOR 0.0000
UGDS_NRA 0.0059
UGDS_UNKN 0.0138
University of Alabama at Birmingham UGDS_WHITE 0.5922
UGDS_BLACK 0.2600
UGDS_HISP 0.0283
UGDS_ASIAN 0.0518
UGDS_AIAN 0.0022
UGDS_NHPI 0.0007
UGDS_2MOR 0.0368
UGDS_NRA 0.0179
UGDS_UNKN 0.0100
dtype: float64
- 使用
unstack序列方法将堆叠的数据转换回原始格式:
>>> college_stacked.unstack()
- 可以先执行
melt,然后执行pivot,然后执行类似的操作序列。 首先,读入数据而不将机构名称放在索引中:
>>> college2 = pd.read_csv('data/college.csv',
usecols=usecol_func)
>>> college2.head()
- 使用
melt方法将所有竞速列转置为单列:
>>> college_melted = college2.melt(id_vars='INSTNM',
var_name='Race',
value_name='Percentage')
>>> college_melted.head()
- 使用
pivot方法来反转之前的结果:
>>> melted_inv = college_melted.pivot(index='INSTNM',
columns='Race',
values='Percentage')
>>> melted_inv.head()
- 请注意,机构名称现在已转移到索引中,而不是按其原始顺序排列。 列名称不是按其原始顺序。 要从第 4 步中完全复制起始数据帧,请使用
.loc索引运算符同时选择行和列,然后重置索引:
>>> college2_replication = melted_inv.loc[college2['INSTNM'],
college2.columns[1:]]\
.reset_index()
>>> college2.equals(college2_replication)
True
工作原理
在步骤 1 中,有多种方法可以完成相同的任务。在这里,我们展示read_csv函数的多功能性。usecols参数接受我们要导入的列的列表或动态确定它们的函数。 我们使用匿名函数来检查列名是否包含UGDS_或等于INSTNM。 该函数以字符串的形式传递给每个列名,并且必须返回一个布尔值。 通过这种方式可以节省大量的内存。
步骤 2 中的stack方法将所有列名称放入最里面的索引级别,并返回一个序列。 在步骤 3 中,unstack方法通过获取最里面的索引级别中的所有值将它们转换为列名来反转此操作。
步骤 3 的结果与步骤 1 不太完全相同。 整行都缺少值,默认情况下,stack方法在步骤 2 中将其删除。 为了保留这些丢失的值并创建精确的副本,请在stack方法中使用dropna=False。
步骤 4 读取与步骤 1 相同的数据集,但没有将机构名称放入索引中,因为melt方法无法访问它。 步骤 5 使用melt方法转置所有Race列。 它通过将value_vars参数保留为其默认值None来执行此操作。 如果未指定,则id_vars参数中不存在的所有列都将转置。
步骤 6 用pivot方法反转了步骤 5 的操作,该方法接受三个参数。 每个参数都将一列作为字符串。index参数引用的列保持垂直并成为新索引。columns参数引用的列的值成为列名。values参数引用的值将平铺以对应于其先前索引和列标签的交集。
要使用pivot进行精确复制,我们需要按照与原始顺序完全相同的顺序对行和列进行排序。 由于机构名称在索引中,因此我们使用.loc索引运算符作为通过其原始索引对数据帧进行排序的方式。
更多
为了帮助进一步理解stack/unstack,让我们将它们用于转置college数据帧。
在这种情况下,我们使用矩阵转置的精确数学定义,其中新行是原始数据矩阵的旧列。
如果您看一下步骤 2 的输出,您会注意到有两个索引级别。 默认情况下,unstack方法使用最里面的索引级别作为新的列值。 索引级别从外部从零开始编号。 Pandas 默认将unstack方法的level参数设置为-1,这是指最里面的索引。 我们可以使用level=0代替unstack最外面的列:
>>> college.stack().unstack(0)
实际上,有一种非常简单的方法可以通过使用transpose方法或T属性来转置不需要stack或unstack的数据帧:
>>> college.T
>>> college.transpose()
另见
在groupby聚合后解除堆叠
按单个列对数据进行分组并在单个列上执行聚合将返回简单易用的结果,并且易于使用。 当按多个列进行分组时,可能不会以使消耗变得容易的方式来构造结果聚合。 由于默认情况下groupby操作将唯一的分组列放在索引中,因此unstack方法对于重新排列数据非常有用,以便以对解释更有用的方式显示数据。
准备
在此秘籍中,我们使用employee数据集执行聚合,并按多列分组。 然后,我们使用unstack方法将结果重塑为一种格式,以便于比较不同组。
操作步骤
- 读取员工数据集,并按种族找到平均工资:
>>> employee = pd.read_csv('data/employee.csv')
>>> employee.groupby('RACE')['BASE_SALARY'].mean().astype(int)
RACE
American Indian or Alaskan Native 60272
Asian/Pacific Islander 61660
Black or African American 50137
Hispanic/Latino 52345
Others 51278
White 64419
Name: BASE_SALARY, dtype: int64
- 这是一个非常简单的
groupby操作,可产生易于阅读且无需重塑的序列。 现在让我们按性别查找所有种族的平均工资:
>>> agg = employee.groupby(['RACE', 'GENDER'])['BASE_SALARY'] \
.mean().astype(int)
>>> agg
RACE GENDER
American Indian or Alaskan Native Female 60238
Male 60305
Asian/Pacific Islander Female 63226
Male 61033
Black or African American Female 48915
Male 51082
Hispanic/Latino Female 46503
Male 54782
Others Female 63785
Male 38771
White Female 66793
Male 63940
Name: BASE_SALARY, dtype: int64
- 这种聚合更加复杂,可以进行重塑以简化不同的比较。 例如,如果每个种族并排而不是像现在这样垂直,则比较男性和女性的工资会更容易。 让我们解开性别索引级别:
>>> agg.unstack('GENDER')
- 同样,我们可以
unstack竞赛索引级别:
>>> agg.unstack('RACE')
工作原理
第 1 步使用单个分组列(RACE),单个聚合列(BASE_SALARY)和单个聚合函数(mean)进行最简单的聚合。 此结果易于使用,不需要任何其他处理即可求值。 第 2 步通过将种族和性别分组在一起,稍微增加了复杂性。 生成的多重索引序列在一个维中包含所有值,这使得比较更加困难。 为了使信息更易于使用,我们使用unstack方法将一个(或多个)级别中的值转换为列。
默认情况下,unstack使用最里面的索引级别作为新列。 您可以使用level参数指定要取消堆叠的确切级别,该参数接受级别名称作为字符串或级别整数位置。 最好在整数位置上使用级别名称,以避免产生歧义。 第 3 步和第 4 步将每个级别拆栈,这将导致数据帧具有单级索引。 现在,按性别比较每个种族的薪水要容易得多。
更多
如果有多个分组和聚合列,则直接结果将是数据帧而不是序列。 例如,让我们计算除平均值以外的更多聚合,如步骤 2 所示:
>>> agg2 = employee.groupby(['RACE', 'GENDER'])['BASE_SALARY'] \
.agg(['mean', 'max', 'min']).astype(int)
>>> agg2
堆叠GENDER列将产生多重索引列。 从这里开始,您可以继续使用unstack和stack方法交换行和列级别,直到获得所需的数据结构为止:
>>> agg2.unstack('GENDER')
另见
- 请参阅第 7 章,“分组和多列聚合”的“进行聚集,过滤和转换的分组函数”秘籍
使用groupby聚合复制pivot_table
乍一看,pivot_table方法似乎提供了一种独特的数据分析方法。 但是,在进行少量按摩之后,可以使用groupby聚合完全复制其功能。 知道这种等效性可以帮助缩小 Pandas 功能的范围。
准备
在此秘籍中,我们使用flights数据集创建数据透视表,然后使用groupby操作重新创建它。
操作步骤
- 读取航班数据集,并使用
pivot_table方法查找每个航空公司每个始发机场已取消航班的总数:
>>> flights = pd.read_csv('data/flights.csv')
>>> fp = flights.pivot_table(index='AIRLINE',
columns='ORG_AIR',
values='CANCELLED',
aggfunc='sum',
fill_value=0).round(2)
>>> fp.head()
groupby聚合无法直接复制此表。 诀窍是首先对index和columns参数中的所有列进行分组:
>>> fg = flights.groupby(['AIRLINE', 'ORG_AIR'])['CANCELLED'].sum()
>>> fg.head()
AIRLINE ORG_AIR
AA ATL 3
DEN 4
DFW 86
IAH 3
LAS 3
Name: CANCELLED, dtype: int64
- 使用
unstack方法将ORG_AIR索引级别转换为列名称:
>>> fg_unstack = fg.unstack('ORG_AIR', fill_value=0)
>>> fp.equals(fg_unstack)
True
工作原理
pivot_table方法非常通用且灵活,但是执行与groupby聚合相当相似的操作,其中步骤 1 显示了一个简单示例。index参数采用一列(或多列),该列将不会被透视,并且其唯一值将放置在索引中。columns参数采用一列(或多列),该列将被透视,并且其唯一值将作为列名称。values参数采用将汇总的一列(或多列)。
还存在一个aggfunc参数,该参数带有一个或多个聚合函数,这些函数确定values参数中的列如何聚合。 它默认为均值,在此示例中,我们将其更改为计算总和。 此外,AIRLINE和ORG_AIR的某些唯一组合不存在。 这些缺失的组合将默认为结果数据帧中的缺失值。 在这里,我们使用fill_value参数将其更改为零。
步骤 2 使用index和columns参数中的所有列作为分组列开始复制过程。 这是使此秘籍生效的关键。 数据透视表只是分组列的所有唯一组合的交集。 步骤 3 通过使用unstack方法将最里面的索引级别转换为列名来完成复制。 就像pivot_table一样,并非AIRLINE和ORG_AIR的所有组合都存在。 我们再次使用fill_value参数将这些缺失的交集强制为零。
更多
可以使用groupby聚合复制更复杂的数据透视表。 例如,从pivot_table中获得以下结果:
>>> flights.pivot_table(index=['AIRLINE', 'MONTH'],
columns=['ORG_AIR', 'CANCELLED'],
values=['DEP_DELAY', 'DIST'],
aggfunc=[np.sum, np.mean],
fill_value=0)
要使用groupby聚合复制此代码,只需遵循秘籍中的相同模式,并将index和columns参数中的所有列放入groupby方法中,然后将unstack列中:
>>> flights.groupby(['AIRLINE', 'MONTH', 'ORG_AIR', 'CANCELLED']) \
['DEP_DELAY', 'DIST'] \
.agg(['mean', 'sum']) \
.unstack(['ORG_AIR', 'CANCELLED'], fill_value=0) \
.swaplevel(0, 1, axis='columns')
有一些区别。 当像agg分组方法那样作为列表传递时,pivot_table方法不接受聚合函数作为字符串。 相反,您必须使用 NumPy 函数。 列级别的顺序也有所不同,其中pivot_table将聚合函数置于values参数中列之前的级别。 这与swaplevel方法相等,在这种情况下,该方法将切换前两个级别的顺序。
截至撰写本书时,将多个列堆叠在一起时存在一个错误,即忽略fill_value参数。 要解决此错误,请将.fillna(0)链接到代码末尾。
重命名轴级别以方便重塑
当每个轴(索引/列)级别具有名称时,使用stack/unstack方法进行重塑要容易得多。 Pandas 允许用户按整数位置或名称引用每个轴级别。 由于整数位置是隐式的而不是显式的,因此应尽可能考虑使用级别名称。 此建议来自“Python 之禅”,这是 Python 的指导原则的简短列表,一个是“显式优于隐式”。
准备
当用多列进行分组或聚合时,所得的 Pandas 对象将在一个或两个轴上具有多个级别。 在本秘籍中,我们将命名每个轴的每个级别,然后使用stack/unstack方法将数据显着重塑为所需的形式。
操作步骤
- 阅读大学数据集,并按机构和宗教信仰找到一些关于大学人口和 SAT 数学成绩的基本摘要统计数据:
>>> college = pd.read_csv('data/college.csv')
>>> cg = college.groupby(['STABBR', 'RELAFFIL']) \
['UGDS', 'SATMTMID'] \
.agg(['size', 'min', 'max']).head(6)
- 请注意,两个索引级别都有名称,并且都是旧的列名称。 另一方面,列级别没有名称。 使用
rename_axis方法为它们提供级别名称:
>>> cg = cg.rename_axis(['AGG_COLS', 'AGG_FUNCS'], axis='columns')
>>> cg
- 现在每个轴级别都有一个名称,重塑变得轻而易举。 使用
stack方法将AGG_FUNCS列移至索引级别:
>>> cg.stack('AGG_FUNCS').head()
- 默认情况下,堆叠会将新的列级别放置在最里面的位置。 使用
swaplevel方法切换电平的位置:
>>> cg.stack('AGG_FUNCS').swaplevel('AGG_FUNCS', 'STABBR',
axis='index').head()
- 通过使用
sort_index方法对级别进行排序,我们可以继续使用轴级别名称:
>>> cg.stack('AGG_FUNCS') \
.swaplevel('AGG_FUNCS', 'STABBR', axis='index') \
.sort_index(level='RELAFFIL', axis='index') \
.sort_index(level='AGG_COLS', axis='columns').head(6)
- 为了完全重塑数据,您可能需要堆叠一些列,同时堆叠其他列。 在单个命令中将两个方法链接在一起:
>>> cg.stack('AGG_FUNCS').unstack(['RELAFFIL', 'STABBR'])
- 一次堆叠所有列以返回序列:
>>> cg.stack(['AGG_FUNCS', 'AGG_COLS']).head(12)
STABBR RELAFFIL AGG_FUNCS AGG_COLS
AK 0 count UGDS 7.0
SATMTMID 0.0
min UGDS 109.0
max UGDS 12865.0
1 count UGDS 3.0
SATMTMID 1.0
min UGDS 27.0
SATMTMID 503.0
max UGDS 275.0
SATMTMID 503.0
AL 0 count UGDS 71.0
SATMTMID 13.0
dtype: float64
工作原理
groupby聚合的结果通常会产生具有多个轴级别的数据帧或序列。 步骤 1 中groupby操作的结果数据帧每个轴具有多个级别。 列级别未命名,这将要求我们仅按其整数位置引用它们。 为了大大简化我们引用列级别的能力,我们使用rename_axis方法对其进行了重命名。
rename_axis方法有点奇怪,因为它可以根据传递给它的第一个参数的类型来修改级别名称和级别值。 向其传递一个列表(如果只有一个级别,则为标量)会更改级别的名称。 向其传递字典或函数会更改级别的值。 在第 2 步中,我们向rename_axis方法传递一个列表,并返回一个具有所有轴级别命名的数据帧。
一旦所有轴级别都有名称,我们就可以轻松明确地控制数据的结构。 步骤 3 将AGG_FUNCS列堆叠到最里面的索引级别。 步骤 4 中的swaplevel方法接受要交换的级别的名称或位置作为前两个参数。sort_index方法被调用两次,并对每个级别的实际值进行排序。 请注意,列级别的值是列名SATMTMID和UGDS。
通过步骤 6 进行堆叠和拆栈,我们可以得到截然不同的输出。也可以将每个单独的列级别堆叠到索引中以产生一个序列。
更多
如果您希望完全丢弃电平值,可以将它们设置为None。 当需要减少数据帧的可视输出中的混乱情况,或者很明显列级别代表什么并且不进行进一步处理时,可以采取这种措施:
>>> cg.rename_axis([None, None], axis='index') \
.rename_axis([None, None], axis='columns')
将多个变量存储为列名时进行整理
每当列名称本身包含多个不同的变量时,就会出现一种特殊的混乱数据。 当年龄和性别连接在一起时,便会出现这种情况的常见示例。 要整理这样的数据集,我们必须使用 pandas str访问器来操作列,该访问器包含用于字符串处理的其他方法。
准备
在本秘籍中,我们将首先确定所有变量,其中一些变量将被连接在一起作为列名。 然后,我们对数据进行整形并解析文本以提取正确的变量值。
操作步骤
- 读取男士的
weightlifting数据集,并标识变量:
>>> weightlifting = pd.read_csv('data/weightlifting_men.csv')
>>> weightlifting
- 变量是体重类别,性别/年龄类别和合格总数。 年龄和性别变量已合并为一个单元格。 在将它们分开之前,让我们使用
melt方法将age和sex列名称转置为单个垂直列:
>>> wl_melt = weightlifting.melt(id_vars='Weight Category',
var_name='sex_age',
value_name='Qual Total')
>>> wl_melt.head()
- 选择
sex_age列,然后使用str访问器中可用的split方法将该列分为两个不同的列:
>>> sex_age = wl_melt['sex_age'].str.split(expand=True)
>>> sex_age.head()
- 此操作返回了一个完全独立的数据帧,具有无意义的列名。 让我们重命名列,以便我们可以显式访问它们:
>>> sex_age.columns = ['Sex', 'Age Group']
>>> sex_age.head()
- 在
str访问器之后直接使用索引运算符从Sex列中选择第一个字符:
>>> sex_age['Sex'] = sex_age['Sex'].str[0]
>>> sex_age.head()
- 使用
pd.concat函数将此数据帧与wl_melt连接在一起,以生成整洁的数据集:
>>> wl_cat_total = wl_melt[['Weight Category', 'Qual Total']]
>>> wl_tidy = pd.concat([sex_age, wl_cat_total], axis='columns')
>>> wl_tidy.head()
- 可以使用以下方法创建相同的结果:
>>> cols = ['Weight Category', 'Qual Total']
>>> sex_age[cols] = wl_melt[cols]
工作原理
weightlifting数据集与许多数据集一样,具有原始格式的易于消化的信息,但是从技术上讲,它很混乱,因为除一个列名之外,所有其他列都包含性别和年龄信息。 一旦确定了变量,就可以开始整理数据集。 只要列名称包含变量,就需要使用melt(或stack)方法。Weight Category变量已经在正确的位置,因此我们通过将其传递给id_vars参数来将其保留为标识变量。 请注意,我们不需要明确地命名要与value_vars融合的所有列。 默认情况下,id_vars中不存在的所有列都会融化。
sex_age列需要解析,并分为两个变量。 为此,我们转向str访问器提供的额外函数,该函数仅适用于序列(单个数据帧的列)。 在这种情况下,split方法是较常见的方法之一,因为它可以将字符串的不同部分分成各自的列。 默认情况下,它在空白处分割,但是您也可以使用pat参数指定字符串或正则表达式。 当expand参数设置为True时,将为每个独立的分割字符段形成一个新列。 当False时,返回单个列,其中包含所有段的列表。
在第 4 步中重命名列之后,我们需要再次使用str访问器。 有趣的是,索引运算符可用于选择或分割字符串段。 在这里,我们选择第一个字符,这是性别变量。 我们可以更进一步,将年龄分为最小年龄和最大年龄两个单独的列,但是通常以这种方式指代整个年龄组,因此我们将其保持不变。
步骤 6 显示了将所有数据连接在一起的两种不同方法之一。concat函数接受数据帧的集合,并将它们垂直(axis='index')或水平(axis='columns')连接。 由于两个数据帧的索引相同,因此可以像第 7 步中那样将一个数据帧的值分配给另一列中的新列。
更多
从步骤 2 开始,完成此秘籍的另一种方法是直接从sex_age列中分配新列,而无需使用split方法。assign方法可用于动态添加以下新列:
>>> age_group = wl_melt.sex_age.str.extract('(\d{2}[-+](?:\d{2})?)',
expand=False)
>>> sex = wl_melt.sex_age.str[0]
>>> new_cols = {'Sex':sex,
'Age Group': age_group}
>>> wl_tidy2 = wl_melt.assign(**new_cols) \
.drop('sex_age',axis='columns')
>>> wl_tidy2.sort_index(axis=1).equals(wl_tidy.sort_index(axis=1))
True
以与步骤 5 完全相同的方式找到Sex列。由于我们没有使用split,因此必须以不同的方式提取Age Group列。extract方法使用复杂的正则表达式来提取字符串的非常特定的部分。 为了正确使用extract,您的图案必须包含捕获组。 通过将圆括号括在图案的一部分周围来形成捕获组。 在此示例中,整个表达式是一个大捕获组。 它以\d{2}开头,它精确地搜索两位数,然后是字面的正负号,或者是可选的后两位。 尽管表达式的最后部分(?:\d{2})?被括号括起来,但是?:表示它实际上不是捕获组。 从技术上讲,它是一个非捕获组,用于同时表示两个数字(可选)。 不再需要sex_age列,将其删除。 最后,将两个整洁的数据帧相互比较,发现它们是等效的。
另见
- 有关非捕获组的更多信息,请参见网站 Regular-Expressions.info
将多个变量存储为列值时进行整理
整洁的数据集每个变量必须有一个单独的列。 有时,多个变量名放在一列中,而其对应的值放在另一列中。 这种杂乱数据的一般格式如下:
在此示例中,前三行和后三行表示两个不同的观察值,每个观察值应为行。 需要对数据进行透视,使其最终如下所示:
准备
在此秘籍中,我们确定包含结构错误的变量的列,并将其旋转以创建整洁的数据。
操作步骤
- 读取餐厅
inspections数据集,然后将Date列数据类型转换为datetime64:
>>> inspections = pd.read_csv('data/restaurant_inspections.csv',
parse_dates=['Date'])
>>> inspections.head()
- 该数据集具有两个变量
Name和Date,它们分别正确地包含在单个列中。Info列本身具有五个不同的变量:Borough,Cuisine,Description,Grade和Score。 让我们尝试使用pivot方法使Name和Date列保持垂直,从Info列中的所有值中创建新列,并使用Value列作为它们的交集:
>>> inspections.pivot(index=['Name', 'Date'],
columns='Info', values='Value')
NotImplementedError: > 1 ndim Categorical are not supported at this time
- 不幸的是,Pandas 开发人员尚未为我们实现此功能。 将来,这行代码很有可能会起作用。 幸运的是,在大多数情况下,Pandas 有多种完成同一任务的方法。 让我们将
Name,Date和Info放入索引中:
>>> inspections.set_index(['Name','Date', 'Info']).head(10)
- 使用
unstack方法来旋转Info列中的所有值:
>>> inspections.set_index(['Name','Date', 'Info']) \
.unstack('Info').head()
- 使用
reset_index方法将索引级别分为几列:
>>> insp_tidy = inspections.set_index(['Name','Date', 'Info']) \
.unstack('Info') \
.reset_index(col_level=-1)
>>> insp_tidy.head()
- 数据集很整齐,但是有一些烦人的剩余 Pandas 残骸需要清除。 让我们使用多重索引方法
droplevel删除顶部的列级别,然后将索引级别重命名为None:
>>> insp_tidy.columns = insp_tidy.columns.droplevel(0) \
.rename(None)
>>> insp_tidy.head()
- 通过使用
squeeze方法将该列数据帧转换为序列,可以避免在步骤 4 中创建多重索引列。 以下代码产生与上一步相同的结果:
>>> inspections.set_index(['Name','Date', 'Info']) \
.squeeze() \
.unstack('Info') \
.reset_index() \
.rename_axis(None, axis='columns')
工作原理
在第 1 步中,我们注意到在Info列中垂直放置了五个变量,在Value列中有相应的值。 因为我们需要将这五个变量中的每一个作为水平列名进行透视,所以pivot方法似乎可以工作。 不幸的是,当有多个非枢轴列时,Pandas 开发人员尚未实现这种特殊情况。 我们被迫使用另一种方法。
unstack方法还枢转垂直数据,但仅适用于索引中的数据。 第 3 步通过使用set_index方法移动将和不会旋转到索引中的两个列来开始此过程。 这些列进入索引后,即可像在步骤 3 中一样操作unstack。
请注意,当我们拆开数据帧时,pandas 会保留原始的列名(在这里,它只是一个列Value),并创建一个以旧列名为上层的多重索引。 数据集现在基本上是整齐的,但是我们继续使用reset_index方法将无枢轴的列设置为普通列。 因为我们有多重索引列,所以我们可以使用col_level参数选择新列名称所属的级别。 默认情况下,名称会插入到最高级别(级别 0)。 我们使用-1表示最底层。
毕竟,我们还有一些多余的数据帧名称和索引需要丢弃。 不幸的是,没有可以删除级别的数据帧方法,因此我们必须进入索引并使用其droplevel方法。 在这里,我们用单级列覆盖了旧的多重索引列。 这些列仍具有无用的名称属性Info,该属性已重命名为None。
通过将步骤 3 中的结果数据帧强制为序列,可以避免清理多重索引列。squeeze方法仅适用于单列数据帧,并将其转换为序列。
更多
实际上,可以使用pivot_table方法,该方法对允许多少个非透视列没有限制。pivot_table方法与pivot不同,它对与index和columns参数中的列之间的交点相对应的所有值执行汇总。 由于此交点中可能存在多个值,因此pivot_table要求用户向其传递一个汇总函数,以便输出单个值。 我们使用first汇总函数,该函数采用组中的第一个值。 在此特定示例中,每个交叉点都只有一个值,因此没有任何要累加的值。 默认的聚合函数是均值,在这里会产生错误,因为某些值是字符串:
>>> inspections.pivot_table(index=['Name', 'Date'],
columns='Info',
values='Value',
aggfunc='first') \
.reset_index() \
.rename_axis(None, axis='columns')
另见
在同一单元格中存储两个或多个值时进行整理
表格数据本质上是二维的,因此,可以在单个单元格中显示的信息量有限。 解决方法是,您偶尔会看到在同一单元格中存储了多个值的数据集。 整洁的数据可为每个单元格精确地提供一个值。 为了纠正这些情况,通常需要使用str序列访问器中的方法将字符串数据解析为多列。
准备
在本秘籍中,我们检查一个数据集,该数据集的每个列中都有一个包含多个不同变量的列。 我们使用str访问器将这些字符串解析为单独的列以整理数据。
操作步骤
- 读取
texas_cities数据集,并标识变量:
>>> cities = pd.read_csv('data/texas_cities.csv')
>>> cities
City列看起来不错,并且仅包含一个值。 另一方面,Geolocation列包含四个变量:latitude,latitude direction,longitude和longitude direction。 让我们将Geolocation列分为四个单独的列:
>>> geolocations = cities.Geolocation.str.split(pat='. ',
expand=True)
>>> geolocations.columns = ['latitude', 'latitude direction',
'longitude', 'longitude direction']
>>> geolocations
- 因为
Geolocation的原始数据类型是对象,所以所有新列也是对象。 让我们将latitude和longitude更改为浮点数:
>>> geolocations = geolocations.astype({'latitude':'float',
'longitude':'float'})
>>> geolocations.dtypes
latitude float64
latitude direction object
longitude float64
longitude direction object
dtype: object
- 将这些新列与原始的
City列连接在一起:
>>> cities_tidy = pd.concat([cities['City'], geolocations],
axis='columns')
>>> cities_tidy
工作原理
读取数据后,我们决定数据集中有多少个变量。 在这里,我们选择将Geolocation列分为四个变量,但是我们可以只选择两个作为纬度和经度,并使用负号来区分西/东和南/北。
有几种方法可以使用str访问器中的方法来解析Geolocation列。 最简单的方法是使用split方法。 我们为它传递一个由任何字符(句点)和空格定义的简单正则表达式。 当空格跟随任何字符时,将进行分割,并形成一个新列。 该模式的首次出现在纬度的尽头。 空格紧跟度数字符,并形成分割。 分割字符将被丢弃,而不保留在结果列中。 下一个分割与逗号和空格匹配,紧跟在纬度方向之后。
总共进行了三个拆分,得到了四列。 步骤 2 的第二行为其提供了有意义的名称。 即使所得的latitude和longitude列似乎是浮点数,也并非如此。 它们最初是从对象列进行解析的,因此仍然是对象数据类型。 步骤 3 使用字典将列名称映射到其新类型。
您可以使用函数to_numeric尝试将每一列转换为整数或浮点数,而不是使用字典,如果字典有很多列名,则需要大量输入。 要在每列上迭代应用此函数,请对以下内容使用apply方法:
>>> geolocations.apply(pd.to_numeric, errors='ignore')
步骤 4 将城市连接到此新数据帧的前面,以完成整理数据的过程。
更多
split方法在此示例中使用简单的正则表达式非常有效。 对于其他示例,某些列可能会要求您根据几种不同的模式创建拆分。 要搜索多个正则表达式,请使用竖线字符|。 例如,如果我们只想分割度数符号和逗号,并在其后跟一个空格,则可以执行以下操作:
>>> cities.Geolocation.str.split(pat='° |, ', expand=True)
这将从步骤 2 返回相同的数据帧。可以使用管道字符将任意数量的其他拆分模式附加到前面的字符串模式。
extract方法是另一种出色的方法,它允许您提取每个单元格中的特定组。 这些捕获组必须用括号括起来。 结果中不存在任何括号外匹配的内容。 下一行产生与步骤 2 相同的输出:
>>> cities.Geolocation.str.extract('([0-9.]+). (N|S), ([0-9.]+). (E|W)',
expand=True)
此正则表达式具有四个捕获组。 第一组和第三组至少搜索一个或多个带小数的连续数字。 第二和第四组搜索单个字符(方向)。 第一个和第三个捕获组由任何字符分隔,后跟一个空格。 第二个捕获组用逗号分隔,然后用空格隔开。
在列名和值中存储变量时进行整理
每当变量在列名称中水平存储并且在列值垂直向下存储时,就会出现一种特别难以诊断的混乱数据形式。 通常,您会遇到这种类型的数据集,而不是在数据库中,而是从其他人已经生成的汇总报告中遇到。
准备
在此秘籍中,变量在垂直和水平方向都可以识别,并通过melt和pivot_table方法重新整理为整齐的数据。
操作步骤
- 读取
sensors数据集并标识变量:
>>> sensors = pd.read_csv('data/sensors.csv')
>>> sensors
- 正确放置在垂直列中的唯一变量是
Group。Property列似乎具有三个唯一变量Pressure,Temperature和Flow。2012至2016列的其余部分本身都是一个变量,我们可以明智地将其命名为Year。 用单个数据帧方法不可能重组这种混乱的数据。 让我们从melt方法开始,将年份分为自己的专栏:
>>> sensors.melt(id_vars=['Group', 'Property'], var_name='Year') \
.head(6)
- 这解决了我们的问题之一。 让我们使用
pivot_table方法将Property列转换为新的列名称:
>>> sensors.melt(id_vars=['Group', 'Property'], var_name='Year') \
.pivot_table(index=['Group', 'Year'],
columns='Property', values='value') \
.reset_index() \
.rename_axis(None, axis='columns')
工作原理
一旦在步骤 1 中确定了变量,就可以开始重组。 Pandas 没有同时旋转,列的方法,因此我们必须一次完成这一任务。 我们通过将Property列传递给melt方法中的id_vars参数来保持年份垂直。
现在,结果中还有混乱的数据部分。如前面的秘籍“将多个变量存储为列值时进行整理”秘籍所述,当在index参数中使用多个列时,我们必须使用pivot_table来旋转数据帧。 旋转后,Group和Year变量卡在索引中。 我们将它们以列的形式推出。pivot_table方法将columns参数中使用的列名称保留为列索引的名称。 重置索引后,该名称变得毫无意义,我们使用rename_axis将其删除。
更多
每当涉及melt,pivot_table或pivot的解决方案时,您都可以确定存在使用stack和unstack的替代方法。 诀窍是首先将当前未旋转到索引中的列移动:
>>> sensors.set_index(['Group', 'Property']) \
.stack() \
.unstack('Property') \
.rename_axis(['Group', 'Year'], axis='index') \
.rename_axis(None, axis='columns') \
.reset_index()
将多个观测单位存储在同一表中时进行整理
当每个表包含来自单个观察单位的信息时,通常更容易维护数据。 另一方面,当所有数据都在单个表中时,更容易发现见解;对于机器学习,所有数据都必须在单个表中。 整洁的数据的重点不是直接进行分析。 相反,它正在对数据进行结构化处理,以便更轻松地进行分析,并且在一个表中有多个观察单位时,可能需要将其分成各自的表。
准备
在本秘籍中,我们使用movie数据集来识别三个观察单位(电影,演员和导演),并分别为每个观察单位创建表格。 制定此秘籍的关键之一是了解演员和导演的 Facebook 点赞与电影无关。 每个演员和导演都映射到一个表示他们的 Facebook 点赞数的单一值。 由于这种独立性,我们可以将电影,导演和演员的数据分离到各自的表中。 数据库人员将此过程标准化,这可以提高数据完整性并减少冗余。
操作步骤
- 读入更改后的
movie数据集,并输出前五行:
>>> movie = pd.read_csv('data/movie_altered.csv')
>>> movie.head()
- 该数据集包含有关电影本身,导演和演员的信息。 这三个实体可以视为观测单位。 在开始之前,让我们使用
insert方法创建一列来唯一标识每个电影:
>>> movie.insert(0, 'id', np.arange(len(movie)))
>>> movie.head()
- 让我们尝试使用
wide_to_long函数整理此数据集,以将所有演员放在一列中,并将其对应的 Facebook 点赞放在另一列中,并为导演做同样的事情,即使每部电影只有一个 :
>>> stubnames = ['director', 'director_fb_likes',
'actor', 'actor_fb_likes']
>>> movie_long = pd.wide_to_long(movie,
stubnames=stubnames,
i='id',
j='num',
sep='_').reset_index()
>>> movie_long['num'] = movie_long['num'].astype(int)
>>> movie_long.head(9)
- 现在可以将数据集拆分为多个较小的表:
>>> movie_table = movie_long[['id', 'year', 'duration', 'rating']]
>>> director_table = movie_long[['id', 'num',
'director', 'director_fb_likes']]
>>> actor_table = movie_long[['id', 'num',
'actor', 'actor_fb_likes']]
- 这些表仍然存在几个问题。
movie表将每个电影重复三遍,导演表的每个 ID 都有两行缺失,而一些电影的某些演员有缺失值。 让我们来照顾这些问题:
>>> movie_entity = movie_entity.drop_duplicates() \
.reset_index(drop=True)
>>> director_entity = director_entity.dropna() \
.reset_index(drop=True)
>>> actor_table = actor_table.dropna() \
.reset_index(drop=True)
- 现在,我们已将观测单位分为各自的表,让我们将原始数据集的内存与这三个表进行比较:
>>> movie.memory_usage(deep=True).sum()
2318234
>>> movie_table.memory_usage(deep=True).sum() + \
director_table.memory_usage(deep=True).sum() + \
actor_table.memory_usage(deep=True).sum()
2627306
- 实际上,我们的新整理数据会占用更多的内存。 这是可以预期的,因为原始列中的所有数据都被简单地散布到新表中。 新表还每个都有索引,并且其中两个表都有一个额外的
num列,这些列占了额外的内存。 但是,我们可以利用以下事实:Facebook 点赞数与电影无关,这意味着每个演员和导演在所有电影中都有一个 Facebook 点赞数。 在执行此操作之前,我们需要创建另一个表,将每个电影映射到每个演员/导演。 首先,创建特定于演员和导演表的id列,以唯一标识每个演员/导演:
>>> director_cat = pd.Categorical(director_table['director'])
>>> director_table.insert(1, 'director_id', director_cat.codes)
>>> actor_cat = pd.Categorical(actor_table['actor'])
>>> actor_table.insert(1, 'actor_id', actor_cat.codes)
- 我们可以使用这些表形成中间表和唯一的
actor/director表。 我们首先使用director表执行此操作:
>>> director_associative = director_table[['id', 'director_id',
'num']]
>>> dcols = ['director_id', 'director', 'director_fb_likes']
>>> director_unique = director_table[dcols].drop_duplicates() \
.reset_index(drop=True)
- 让我们对
actor表做同样的事情:
>>> actor_associative = actor_table[['id', 'actor_id', 'num']]
>>> acols = ['actor_id', 'actor', 'actor_fb_likes']
>>> actor_unique = actor_table[acols].drop_duplicates() \
.reset_index(drop=True)
- 让我们找出我们的新表消耗了多少内存:
>>> movie_table.memory_usage(deep=True).sum() + \
director_associative.memory_usage(deep=True).sum() + \
director_unique.memory_usage(deep=True).sum() + \
actor_associative.memory_usage(deep=True).sum() + \
actor_unique.memory_usage(deep=True).sum()
1833402
- 现在我们已经标准化了表,我们可以构建一个实体关系图,显示所有表(实体),列和关系。 此图是使用易于使用的 ERDPlus 创建的:
工作原理
导入数据并识别这三个实体后,我们必须为每个观察创建一个唯一的标识符,以便在将电影,演员和导演分成不同的表格后,可以将它们链接在一起。 在第 2 步中,我们只需将 ID 列设置为从零开始的行号。 在第 3 步中,我们使用wide_to_long函数同时melt,actor和director列。 它使用列的整数后缀垂直对齐数据,并将此整数后缀放置在索引中。 参数j用于控制其名称。 重复stubnames列表中不在列中的值以与已熔化的列对齐。
在第 4 步中,我们创建三个新表,并在每个表中保留id列。 我们还保留num列以标识确切的director/actor列。 步骤 5 通过删除重复项和缺失值来压缩每个表。
在第 5 步之后,这三个观测单位在各自的表中,但它们仍然包含与原始相同的数据量(还有更多),如步骤 6 所示。要返回memory_usage方法从object数据类型列中获得正确的字节数,必须将deep参数设置为True。
每个演员/导演在其各自的表中仅需要一个条目。 我们不能简单地列出演员姓名和 Facebook 点赞的表格,因为无法将演员链接回原始电影。 电影和演员之间的关系称为多对多关系。 每个电影与多个演员相关联,每个演员可以出现在多个电影中。 为了解决此关系,创建了一个中间表或关联表,该表包含电影和演员的唯一标识符(主键)。
要创建关联表,我们必须唯一地标识每个演员/导演。 一种技巧是使用pd.Categorical从每个演员/导演姓名中创建一个分类数据类型。 分类数据类型具有从每个值到整数的内部映射。 在codes属性中可以找到该整数,该属性用作唯一 ID。 要设置关联表的创建,我们将此唯一 ID 添加到actor/director表中。
步骤 8 和步骤 9 通过选择两个唯一标识符来创建关联表。 现在,我们可以将actor和director表简化为唯一的名称和 Facebook 点赞的名称。 这种新的表安排使用的内存比原始表少 20% 。 正式的关系数据库具有实体关系图以可视化表格。 在第 10 步中,我们使用简单的 ERDPlus 工具进行可视化,这大大简化了对表之间关系的理解。
更多
通过将所有表重新结合在一起,可以重新创建原始的movie表。 首先,将关联表连接到actor/director表。 然后旋转num列,并向后添加列前缀:
>>> actors = actor_associative.merge(actor_unique, on='actor_id') \
.drop('actor_id', 1) \
.pivot_table(index='id',
columns='num',
aggfunc='first')
>>> actors.columns = actors.columns.get_level_values(0) + '_' + \
actors.columns.get_level_values(1).astype(str)
>>> directors = director_associative.merge(director_unique,
on='director_id') \
.drop('director_id', 1) \
.pivot_table(index='id',
columns='num',
aggfunc='first')
>>> directors.columns = directors.columns.get_level_values(0) + '_' + \
directors.columns.get_level_values(1) \
.astype(str)
这些表现在可以与movie_table结合在一起:
>>> movie2 = movie_table.merge(directors.reset_index(),
on='id', how='left') \
.merge(actors.reset_index(),
on='id', how='left')
>>> movie.equals(movie2[movie.columns])
True