Python 数值计算(五)
原文:
annas-archive.org/md5/9e81efca12eeaa9a42c1e05702b8c0c0译者:飞龙
第十三章:聚合、过滤和转换的分组
数据分析中的一项最基本任务是将数据拆分成独立的组,然后对每个组执行计算。这种方法已经存在很长时间了,但最近被称为split-apply-combine。本章将介绍强大的 groupby 方法,它允许你以任何想象得到的方式对数据进行分组,并独立地对每个组应用任何类型的函数,然后返回一个单一的数据集。
Hadley Wickham 创造了split-apply-combine这个术语,用来描述常见的数据分析模式:将数据拆分成独立且可管理的块,独立地对这些块应用函数,然后将结果重新组合。更多细节可以在他的论文中找到(bit.ly/2isFuL9)。
在我们开始具体的操作之前,我们需要了解一些基本的术语。所有基本的 groupby 操作都有分组列,这些列中每种独特的值组合代表数据的一个独立分组。语法如下:
>>> df.groupby(['list', 'of', 'grouping', 'columns'])
>>> df.groupby('single_column') # when grouping by a single column
这个操作的结果返回一个 groupby 对象。正是这个 groupby 对象将作为驱动整个章节所有计算的引擎。实际上,Pandas 在创建这个 groupby 对象时几乎不做任何事情,只是验证分组是否可能。你需要在这个 groupby 对象上链式调用方法,以释放它的强大功能。
从技术上讲,操作的结果将是 DataFrameGroupBy 或 SeriesGroupBy,但为了简便起见,本章将统一称之为 groupby 对象。
本章将涵盖以下主题:
-
定义聚合
-
使用多个列和函数进行分组和聚合
-
分组后移除 MultiIndex
-
自定义聚合函数
-
使用
*args和**kwargs自定义聚合函数 -
检查
groupby对象 -
过滤拥有少数族裔多数的州
-
通过减肥赌注进行转换
-
使用 apply 计算每个州的加权平均 SAT 分数
-
按连续变量分组
-
计算城市之间的航班总数
-
查找最长的准时航班连续记录
定义聚合
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方法,例如 NumPy 的mean函数。输出结果与前一步相同:
>>> 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 仅验证分组列。这个 groupby 对象有一个agg方法用于执行聚合操作。使用这种方法的一种方式是将一个字典传递给它,将聚合列与聚合函数进行映射,正如步骤 2 所示。
有几种不同的语法可以实现相似的结果,步骤 3 展示了一种替代方法。无需在字典中指定聚合列,可以像从 DataFrame 中选择列一样将其放入索引操作符内。然后,将函数的字符串名称作为标量传递给agg方法。
你可以将任何聚合函数传递给agg方法。Pandas 为了简化操作,允许使用字符串名称,但你也可以像步骤 4 中那样显式地调用聚合函数。NumPy 提供了许多聚合函数。
第 5 步展示了最后一种语法风格。当你只应用单个聚合函数时,通常可以直接作为方法调用到 groupby 对象本身,而无需使用agg。并非所有聚合函数都有相应的方法,但许多基本的聚合函数是有的。以下是一些可以作为字符串传递给agg或直接作为方法链调用到 groupby 对象的聚合函数:
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
另见
- Pandas 官方文档关于聚合的说明 (
bit.ly/2iuf1Nc)
使用多个列和函数进行分组和聚合
可以使用多个列进行分组和聚合。语法与使用单个列进行分组和聚合仅有轻微不同。像任何分组操作一样,识别三个组成部分会有所帮助:分组列、聚合列和聚合函数。
准备就绪
在这个示例中,我们通过回答以下查询展示了groupby DataFrame 方法的灵活性:
-
查找每家航空公司在每个工作日取消的航班数量
-
查找每家航空公司在每个工作日取消和转机航班的数量和百分比
-
对于每个出发地和目的地,查找航班的总数、取消航班的数量和百分比,以及空中时间的平均值和方差
如何操作...
- 读取航班数据集,并通过定义分组列(
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的每种唯一组合都形成一个独立的组。在这些组内,找到取消航班的总和并作为一个 Series 返回。
第 2 步,再次按AIRLINE和WEEKDAY分组,但这次对两列进行聚合。它将sum和mean两个聚合函数应用于每一列,从而每个组返回四列结果。
第三步更进一步,使用字典将特定的聚合列映射到不同的聚合函数。注意,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时,你很可能会在列或行,甚至两者中创建多重索引。具有多重索引的 DataFrame 更难以操作,偶尔还会有令人困惑的列名。
准备工作
在这个例子中,我们使用groupby方法进行聚合,创建一个具有行和列多重索引的 DataFrame,然后对其进行操作,使得索引变为单层,并且列名具有描述性。
如何操作...
- 读取航班数据集;写一个语句,找出每个航空公司在每个工作日的飞行总里程和平均里程,以及最大和最小的到达延误:
>>> 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 会以不同于单级列的方式显示 MultiIndex 级别。除了 最内层 的级别外,重复的索引值不会显示在屏幕上。你可以查看步骤 1 中的 DataFrame 来验证这一点。例如,DIST 列只会显示一次,但它指代的是前两列的内容。
最内层的 MultiIndex 级别是最接近数据的级别。这通常是最底层的列级别和最右侧的索引级别。
步骤 2 通过首先使用 MultiIndex 方法 get_level_values 检索每个级别的基础值来定义新列。该方法接受一个整数,表示索引级别。索引级别从上/左开始编号,编号从零开始。索引支持向量化操作,因此我们将两个级别连接起来,并用下划线分隔。然后将这些新值分配给 columns 属性。
在步骤 3 中,我们使用 reset_index 将两个索引级别转为列。我们本可以像步骤 2 中那样将这些级别连接在一起,但将它们保留为单独的列更有意义。
还有更多...
默认情况下,在 groupby 操作结束时,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 提供了许多最常用的聚合函数,供你在 groupby 对象上使用。在某些情况下,你可能需要编写自己定制的用户定义函数,这些函数在 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列作为 Series 传递给max_deviation。
max_deviation函数会对每个组调用一次。由于s是一个 Series,因此所有常规的 Series 方法都可以使用。它会从该组中每个值减去该组的均值,然后除以标准差,这一过程称为标准化。
标准化是一个常见的统计程序,用于了解个体值与均值之间的差异有多大。对于正态分布,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 使用函数的名称作为返回列的名称。你可以通过rename方法直接更改列名,或者修改特殊函数属性__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 会隐式地将每个聚合列逐一作为 Series 传递给它。偶尔,你需要向函数传递的不仅仅是 Series 本身的参数。为此,你需要了解 Python 可以向函数传递任意数量的参数。让我们借助inspect模块来看一下groupby对象的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允许你将任意数量的关键字参数传递给函数。
准备好了吗
在这个示例中,我们为大学数据集构建了一个自定义函数,计算按州和宗教信仰分类的学校百分比,这些学校的本科生人数在两个值之间。
如何做...
- 定义一个函数,返回本科生人数在 1,000 到 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发挥作用的地方。在这个例子中,我们向agg方法传递了两个非关键字参数,1,000 和 10,000,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变量。
另见
-
来自官方 Python 文档的任意参数列表(
bit.ly/2vumbTE) -
Python 闭包的教程(
bit.ly/2xFdYga)
检查 groupby 对象
使用groupby方法对 DataFrame 进行操作的即时结果将是一个 groupby 对象。通常,我们继续对该对象进行操作,进行聚合或转换,而不会将其保存到变量中。检查这个 groupby 对象的主要目的是检查单个分组。
准备工作
在这个示例中,我们通过直接调用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()
- 你可能想查看每个单独的组。因为 groupby 对象是可迭代的,所以这是可能的:
>>> from IPython.display import display
>>> for name, group in grouped:
print(name)
display(group.head(3))
- 你也可以在 groupby 对象上调用 head 方法,将每个组的前几行放在一个单独的 DataFrame 中。
>>> grouped.head(2).head(6)
它是如何工作的...
第一步正式创建我们的 groupby 对象。显示所有公共属性和方法很有用,这样可以揭示所有可能的功能,如第二步所示。每个组通过包含分组列中值的唯一组合的元组来唯一标识。Pandas 允许你使用第五步中展示的get_group方法选择特定的组作为 DataFrame。
你通常不需要遍历你的组,通常如果不是必要的话应该避免这样做,因为这样可能非常慢。偶尔,你可能别无选择。当遍历 groupby 对象时,你会得到一个元组,其中包含组名称和不包含分组列的 DataFrame。这个元组在第六步的 for 循环中被解包到name和group变量中。
在遍历组时,你可以做的有趣的事情之一是直接在笔记本中显示每个组的几行数据。为此,你可以使用print函数或来自IPython.display模块的display函数。使用print函数时,结果是没有任何漂亮 HTML 格式的纯文本 DataFrame。而使用display函数则会以正常且易于阅读的格式显示 DataFrame。
还有更多...
第 2 步中的列表中有几个有用的方法没有被探索。以nth方法为例,当给定一个整数列表时,它会从每个组中选择这些特定的行。例如,以下操作选择每个组的第一行和最后一行:
>>> grouped.nth([1, -1]).head(8)
另见
display函数的官方文档来自 IPython (bit.ly/2iAIogC)
筛选具有少数派多数的州
在第十章,选择数据子集,我们在过滤掉False行之前,将每一行标记为True或False。以类似的方式,也可以在过滤掉False组之前,将整个数据组标记为True或False。为此,我们首先使用groupby方法对数据进行分组,然后应用filter方法。filter方法接受一个必须返回True或False的函数,用来指示是否保留某个组。
在调用groupby方法后应用的这个filter方法与数据框(DataFrame)的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函数和 50%阈值的filter方法来查找所有有少数族裔多数的州:
>>> college_filtered = grouped.filter(check_minority, threshold=.5)
>>> college_filtered.head()
- 仅仅查看输出结果可能无法反映实际发生的情况。数据框(DataFrame)从亚利桑那州(Arizona,简称 AZ)开始,而不是阿拉斯加州(Alaska,简称 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函数内部,首先计算每个机构的少数族裔学生百分比和非白人学生的总数,然后计算所有学生的总数。最后,检查整个州的非白人学生百分比是否超过给定的阈值,结果为布尔值。
最终结果是一个与原始数据框(DataFrame)具有相同列数的数据框,但其中的行已过滤掉不符合阈值的州。由于过滤后的数据框头部可能与原始数据框相同,因此需要进行检查,以确保操作成功完成。我们通过检查行数和独特州的数量来验证这一点。
还有更多...
我们的函数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
另见
- Pandas 官方文档关于过滤(
bit.ly/2xGUoA7)
通过体重减轻比赛进行转变
增加减肥动力的一种方法是和别人打赌。在这个配方中,我们将追踪两个人在四个月期间的体重减轻情况,并确定谁是最终的赢家。
准备开始
在这个配方中,我们使用了两个人的模拟数据来追踪他们在四个月内的体重减轻百分比。在每个月结束时,根据该月体重减轻百分比最多的人来宣布赢家。为了追踪体重减轻情况,我们按月和人物对数据进行分组,然后调用transform方法来找出每周体重减轻的百分比,从而得出每月开始时的变化情况。
如何做...
- 读取原始的体重减轻数据集,并检查
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 在 1 月期间使用这个函数的结果。
>>> 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 仅仅是原始数据框的行标签,与周次无关。
- 在第一周后,Bob 减轻了 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方法必须返回与调用的 DataFrame 具有相同数量行的对象。让我们将这个结果作为新列附加到原始 DataFrame 中。为了缩短输出,我们将选择 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 有一个矢量化的 if-then-else 函数,叫做
where,它可以将布尔值的 Series 或数组映射到其他值。让我们创建一个列,记录获胜者的名字,并突出显示每个月的获胜百分比:
>>> 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方法来过滤数据,而不是使用布尔索引。有关更多信息,请参考第十一章中的通过 query 方法提高布尔索引的可读性一节,布尔索引。
我们的目标是找出每个人每个月的百分比体重减轻。一种实现此任务的方法是计算每周的体重减轻相对于每个月开始时的情况。这个任务非常适合使用transform的 groupby 方法。transform方法接受一个函数作为其唯一的必需参数。这个函数会隐式传递每个非分组列(或者只传递在索引操作符中指定的列,如本节中对Weight的处理)。它必须返回与传入分组相同长度的值序列,否则会引发异常。从本质上讲,原始 DataFrame 中的所有值都会被转换。没有进行聚合或筛选操作。
步骤 2 创建了一个函数,它从传入的 Series 的所有值中减去第一个值,然后将结果除以第一个值。这计算了相对于第一个值的百分比损失(或增益)。在步骤 3 中,我们在一个人和一个月的数据上测试了这个函数。
在步骤 4 中,我们以相同的方式在每个人和每周的所有组合上使用这个函数。从字面意义上讲,我们正在将 Weight 列转换为当前周的体重减轻百分比。每个人的第一个月数据会被输出。Pandas 将新数据作为一个 Series 返回。这个 Series 本身并不是特别有用,最好是作为一个新列追加到原始的 DataFrame 中。我们在步骤 5 中完成这个操作。
要确定赢家,只需要每个月的第 4 周的数据。我们可以在这里停下来手动确定赢家,但 pandas 为我们提供了自动化的功能。步骤 7 中的 pivot 函数通过将一个列的唯一值转换为新的列名来重新塑形数据集。index 参数用于指定不需要透视的列。传递给 values 参数的列将在 index 和 columns 参数的每个唯一组合上铺开。
pivot 方法仅在 index 和 columns 参数中的每个唯一组合只出现一次时才有效。如果有多个唯一组合,将会抛出异常。在这种情况下,你可以使用 pivot_table 方法,它允许你聚合多个值。
在透视之后,我们利用高效且快速的 NumPy where 函数,其第一个参数是一个条件,返回一个布尔值的 Series。True 值会映射到Amy,False 值会映射到Bob。我们标出每个月的赢家,并用 value_counts 方法统计最终得分。
还有更多……
看一下步骤 7 输出的 DataFrame。你是否注意到月份的顺序是按字母顺序排列的,而不是按时间顺序?遗憾的是,至少在这种情况下,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构造函数。将原始列作为 Series 传递给它,并将所需顺序中的所有类别的唯一序列传递给categories参数。由于Month列已经按时间顺序排列,我们可以直接使用unique方法,该方法保留顺序,从而获取所需的数组。一般来说,要按字母顺序以外的方式对对象数据类型的列进行排序,可以将其转换为分类数据类型。
另见
-
Pandas 官方文档中的
groupby转换(bit.ly/2vBkpA7) -
NumPy 官方文档中的
where函数(bit.ly/2weT21l)
使用 apply 计算按州加权的 SAT 数学成绩平均值
groupby对象有四个方法可以接受一个函数(或多个函数),对每个组进行计算。这四个方法分别是agg、filter、transform和apply。这些方法中的前三个都有非常具体的输出要求,函数必须返回特定的值。agg必须返回一个标量值,filter必须返回一个布尔值,而transform必须返回一个与传入组长度相同的 Series。然而,apply方法可以返回标量值、Series,甚至是任意形状的 DataFrame,因此非常灵活。它每次只调用一次每个组,这与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函数应用于 DataFrame 中的每个非聚合列。如果你尝试将列限制为仅SATMTMID,你会遇到错误,因为你无法访问UGDS。因此,完成对多个列进行操作的最佳方法是使用apply:
>>> college2.groupby('STABBR')['SATMTMID'] \
.agg(weighted_math_average)
KeyError: 'UGDS'
apply的一个好功能是,你可以通过返回一个 Series 来创建多个新列。这个返回的 Series 的索引将成为新列的名称。让我们修改我们的函数,以计算两个 SAT 分数的加权平均值和算术平均值,并统计每个组中院校的数量。我们将这五个值以 Series 的形式返回:
>>> 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会接收每个组的所有原始列的 DataFrame,并返回一个标量值,即SATMTMID的加权平均值。此时,你可能会认为可以使用agg方法来进行此计算。直接用agg替换apply是行不通的,因为agg会为每个聚合列返回一个值。
实际上,可以通过先计算UGDS和SATMTMID的乘积,间接使用agg方法。
第 6 步真正展示了apply的多功能性。我们构建了一个新函数,计算两个 SAT 列的加权平均值、算术平均值以及每个组的行数。为了让apply创建多个列,你必须返回一个 Series。索引值将作为结果 DataFrame 中的列名。你可以用这种方法返回任意数量的值。
注意,OrderedDict类是从collections模块导入的,这个模块是标准库的一部分。这个有序字典用来存储数据。普通的 Python 字典不能用来存储这些数据,因为它不能保持插入顺序。
构造器pd.Series确实有一个index参数,你可以用它来指定顺序,但使用OrderedDict会更简洁。
还有更多……
在这个示例中,我们返回了每个组的单行数据作为一个 Series。通过返回一个 DataFrame,可以为每个组返回任意数量的行和列。除了计算算术和加权平均数之外,我们还要计算两个 SAT 列的几何平均数和调和平均数,并将结果返回为一个 DataFrame,其中行是平均数的类型名称,列是 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 官方文档的
applygroupby 方法(bit.ly/2wmG9ki) -
Python 官方文档的
OrderedDict类(bit.ly/2xwtUCa) -
SciPy 官方文档的统计模块(
bit.ly/2wHtQ4L)
按连续变量分组
在 pandas 中进行分组时,通常使用具有离散重复值的列。如果没有重复值,那么分组就没有意义,因为每个组只有一行。连续的数字列通常重复值较少,通常不会用来分组。然而,如果我们能通过将每个值放入一个区间、四舍五入或使用其他映射,将连续值列转换为离散列,那么使用它们进行分组是有意义的。
准备工作
在这个示例中,我们探索了航班数据集,以发现不同旅行距离下航空公司的分布。例如,这使我们能够找到在 500 到 1000 英里之间飞行次数最多的航空公司。为此,我们使用 pandas 的cut函数来离散化每个航班的距离。
如何操作...
- 读取航班数据集,并输出前五行:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights.head()
- 如果我们想要找到在不同距离范围内的航空公司分布,我们需要将
DIST列的值放入离散的区间中。让我们使用 pandas 的cut函数将数据划分为五个区间:
>>> 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]]
- 创建了一个有序分类 Series。为了帮助理解发生了什么,让我们统计一下每个类别的值:
>>> 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
- 现在可以使用
cutsSeries 来形成组。Pandas 允许您以任何方式形成组。将cutsSeries 传递给groupby方法,然后调用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变量现在是一个包含五个有序类别的 Series。它拥有所有常规 Series 方法,并且在步骤 3 中,使用value_counts方法来了解其分布情况。
非常有趣的是,pandas 允许你将任何对象传递给groupby方法。这意味着你可以从与当前 DataFrame 完全无关的东西中创建分组。在这里,我们根据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函数(bit.ly/2whcUkJ) -
请参阅第十四章,将数据重构为整洁的形式,了解更多使用 unstack 的技巧
统计两座城市之间的航班总数
在航班数据集中,我们有关于起点和目的地机场的数据。例如,统计从休斯顿出发并降落在亚特兰大的航班数量是微不足道的。更困难的是统计两座城市之间的航班总数,而不考虑哪座城市是起点或目的地。
准备工作
在这个食谱中,我们统计了两座城市之间的航班总数,而不考虑哪一个是起点或目的地。为此,我们按字母顺序对起点和目的地机场进行排序,使得每一对机场的组合总是按照相同的顺序出现。然后,我们可以使用这种新的列排列方式来形成分组并进行计数。
如何实现...
- 读取航班数据集,并找到每个起始和目的地机场之间的总航班数:
>>> 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
工作原理...
在第一步中,我们通过起始和目的地机场列形成分组,然后将 size 方法应用于 groupby 对象,它简单地返回每个组的总行数。请注意,我们可以将字符串 size 传递给 agg 方法以达到相同的结果。在第二步中,选择了亚特兰大和休斯顿之间每个方向的总航班数。Series flights_count 具有两个级别的 MultiIndex。从 MultiIndex 中选择行的一种方法是向 loc 索引运算符传递一个确切级别值的元组。在这里,我们实际上选择了两行,('ATL', 'HOU') 和 ('HOU', 'ATL')。我们使用一个元组列表来正确执行此操作。
第 3 步是这个步骤中最相关的步骤。我们希望对亚特兰大和休斯顿之间的所有航班只有一个标签,到目前为止我们有两个标签。如果我们按字母顺序对每个起始和目的地机场的组合进行排序,那么我们将有一个单一的标签用于机场之间的航班。为此,我们使用 DataFrame apply 方法。这与 groupby apply 方法不同。在第 3 步中不形成组。
DataFrame apply 方法必须传递一个函数。在这种情况下,它是内置的 sorted 函数。默认情况下,此函数将应用于每一列作为一个 Series。我们可以通过使用 axis=1(或 axis='index')来改变计算的方向。sorted 函数将每一行数据隐式地作为一个 Series 传递给它。它返回一个排序后的机场代码列表。这里是将第一行作为一个 Series 传递给 sorted 函数的示例:
>>> sorted(flights.loc[0, ['ORG_AIR', 'DEST_AIR']])
['LAX', 'SLC']
apply 方法以这种确切的方式使用 sorted 迭代所有行。完成此操作后,每行都会被独立排序。列名现在毫无意义。我们在下一步中重命名列名,然后执行与第 2 步相同的分组和聚合操作。这次,所有亚特兰大和休斯顿之间的航班都归为同一标签。
还有更多...
你可能会想知道为什么我们不能使用更简单的sort_values系列方法。这个方法不能独立排序,而是保留每一行或每一列作为一个完整的记录,正如我们在进行数据分析时所期望的那样。步骤 3 是一个非常耗时的操作,完成需要几秒钟。虽然只有大约 60,000 行数据,但这个解决方案不适合处理更大的数据集。调用
步骤 3 是一个非常耗时的操作,完成需要几秒钟。虽然只有大约 60,000 行数据,但这个解决方案不适合处理更大的数据集。调用apply方法并使用axis=1是所有 pandas 操作中性能最差的之一。在内部,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 不容易进行分组操作,因此我们可以使用 DataFrame 构造函数来创建一个新的 DataFrame,并检查它是否等于步骤 3 中的flights_sorted DataFrame:
>>> 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
由于 DataFrame 是相同的,你可以用之前更快的排序方法替代步骤 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 倍。
另见
- NumPy 官方文档关于
sort函数的说明(bit.ly/2vtRt0M)
查找准时航班的最长连续段
航空公司最重要的指标之一就是它们的准时航班表现。联邦航空管理局(FAA)将航班视为延误航班,如果它比计划到达时间晚了至少 15 分钟。Pandas 提供了直接的方法来计算每个航空公司准时航班的总数和百分比。虽然这些基本的总结统计数据是一个重要的指标,但也有一些其他的非平凡计算很有意思,比如查找每个航空公司在每个起始机场的连续准时航班长度。
准备开始
在这个方案中,我们找到每个航空公司在每个起始机场的最长连续准时航班段。这要求每个列中的值能够察觉到紧跟其后的值。我们巧妙地使用了diff和cumsum方法来找到连续段,在将这种方法应用于每个组之前。
如何做...
- 在我们开始处理实际的航班数据集之前,先练习使用一个小样本 Series 来计数连续的 1:
>>> 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 的表示将是一个与原始数据长度相同的 Series,每一段连续 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
- 我们现在已经累积了所有沿着序列向下的 1。让我们将这个序列与原始序列相乘:
>>> s.mul(s1)
0 0
1 1
2 2
3 0
4 3
5 4
6 5
7 0
dtype: int64
- 现在我们只有在原始数据中为 1 的地方出现非零值。这个结果与我们期望的非常接近。我们只需要让每个 streak 从 1 重新开始,而不是从累积和的结果开始。让我们连接
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
- 负值表示 streak 的结束。我们需要将负值向下传播,并用它们来从步骤 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
- 现在我们有了一个可以工作的连续 streak 查找器,我们可以找到每个航空公司和起始机场的最长 streak。让我们读取航班数据集,并创建一列来表示准时到达:
>>> 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)
- 使用我们前七步中的逻辑,定义一个函数来返回给定序列中的最大连续 1:
>>> def max_streak(s):
s1 = s.cumsum()
return s.mul(s1).diff().where(lambda x: x < 0) \
.ffill().add(s1, fill_value=0).max()
- 找到每个航空公司和起始机场的最大准时到达 streak,以及航班总数和准时到达的百分比。首先,按照年份中的日期和计划的起飞时间排序:
>>> flights.sort_values(['MONTH', 'DAY', 'SCHED_DEP']) \
.groupby(['AIRLINE', 'ORG_AIR'])['ON_TIME'] \
.agg(['mean', 'size', max_streak]).round(2).head()
它是如何工作的...
在 pandas 中找到 streak 并不是一个简单的操作,需要使用一些前瞻或回溯的方法,比如diff或shift,或者那些能记住当前状态的方法,比如cumsum。前七步的最终结果是一个与原始序列长度相同的序列,记录了所有连续的 1。在这些步骤中,我们使用了mul和add方法,而不是其运算符等价物(*)和(+)。我认为,这样做可以让计算的过程从左到右更加简洁。你当然可以用实际的运算符来替换它们。
理想情况下,我们希望告诉 pandas 在每个 streak 开始时应用cumsum方法,并在每个 streak 结束后重置它。这需要许多步骤来传达给 pandas。步骤 2 将序列中的所有 1 累积在一起。接下来的步骤则逐渐去除多余的累积。为了识别这些多余的累积,我们需要找到每个 streak 的结束位置,并从下一个 streak 的开始位置减去这个值。
为了找到每个 streak 的结束,我们巧妙地通过将s1与第 3 步中的零和一的原始序列相乘,来将所有不属于 streak 的值变为零。跟随第一个零的非零值标志着一个 streak 的结束。这个方法不错,但我们还需要消除多余的累积。知道 streak 的结束位置并不能完全解决问题。
在第 4 步中,我们使用diff方法来找出这些多余的值。diff方法计算当前值与距离它一定行数的任何值之间的差异。默认情况下,它返回当前值与紧接着的前一个值之间的差异。
只有负值在第 4 步中才是有意义的。这些值位于连续序列的末尾。需要将这些值向下传播,直到下一个连续序列的结束。为了消除(使其缺失)我们不关心的所有值,我们使用where方法,它接受一个与调用的 Series 大小相同的条件 Series。默认情况下,所有True值保持不变,而False值则变为缺失。where方法允许你通过将一个函数作为其第一个参数来使用调用的 Series 作为条件的一部分。这里使用了一个匿名函数,它隐式地接受调用的 Series 并检查每个值是否小于零。第 5 步的结果是一个 Series,其中只有负值被保留,其余的都变成缺失值。
第 6 步中的ffill方法将缺失值替换为向前(或向下)传播的最后一个非缺失值。由于前面三个值没有跟随任何非缺失值,因此它们保持为缺失值。最终,我们得到了一个移除多余累积的 Series。我们将这个累积 Series 与第 6 步的结果相加,从而得到所有从零开始的连续序列。add方法允许我们使用fill_value参数替换缺失值。这个过程完成了在数据集中查找连续的 1 值序列。当做复杂逻辑处理时,最好使用一个小数据集,这样你可以预知最终结果。如果从第 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的 groupby 方法时,每个组的 DataFrame 会被传递给max_delay_streak函数。在这个函数内部,DataFrame 的索引会被删除,并用RangeIndex替换,这样我们可以轻松找到连续记录的第一行和最后一行。接着,ON_TIME列会被反转,然后使用相同的逻辑来查找延误航班的连续记录。连续记录的第一行和最后一行的索引会被存储为变量。这些索引随后用于选择连续记录结束的月份和日期。我们使用 DataFrame 来返回结果,并为索引添加标签和名称,以便让最终结果更清晰。
我们的最终结果显示了最长的延迟连续记录,并伴随有起始日期和结束日期。让我们来调查一下,看看是否能找出这些延迟发生的原因。恶劣天气是航班延误或取消的常见原因。查看第一行,来自美国航空公司(AA)的航班从 2015 年 2 月 26 日开始,从达拉斯沃斯堡机场(DFW)起飞,连续 38 个航班发生延误,直到 3 月 1 日。根据 2015 年 2 月 27 日的历史天气数据,当天降雪达到两英寸,这也是当天的降雪记录(bit.ly/2iLGsCg)。这场大规模的天气事件对 DFW 造成了严重影响,并给整个城市带来了巨大的麻烦(bit.ly/2wmsHPj)。请注意,DFW 再次出现,成为第三长的延误记录,但这次发生的时间稍早一些,并且是由另一家航空公司造成的。
另见
- Pandas 官方文档的
ffill(bit.ly/2gn5zGU)
第十四章:将数据重构为整洁形式
前几章中使用的所有数据集都没有经过太多或任何结构变化的处理。我们直接开始在原始形态下处理这些数据集。许多实际中的数据集在开始更详细分析之前需要进行大量的重构。在某些情况下,整个项目可能仅仅是为了将数据格式化成某种方式,以便其他人能够轻松处理。
在本章中,我们将讨论以下主题:
-
使用
stack将变量值整洁化为列名 -
使用
melt将变量值整洁化为列名 -
同时堆叠多个变量组
-
反转堆叠数据
-
在
groupby聚合后进行反向堆叠 -
使用
groupby聚合实现pivot_table的功能 -
为了方便重构而重命名轴级
-
当多个变量作为列名存储时的整洁化
-
当多个变量存储为列值时的整洁化
-
当两个或多个值存储在同一单元格中时的整洁化
-
当变量存储在列名和列值中时的整洁化
-
当多个观察单元存储在同一张表格中时进行整洁化
用于描述数据重构过程的术语有很多,其中整洁数据是数据科学家最常用的术语。整洁数据是 Hadley Wickham 创造的术语,用来描述一种便于分析的数据形式。本章将讨论 Hadley 提出的许多想法以及如何通过 pandas 实现这些想法。要深入了解整洁数据,请阅读 Hadley 的论文(vita.had.co.nz/papers/tidy-data.pdf)。
什么是整洁数据?Hadley 提出了三个简单的指导原则来决定一个数据集是否整洁:
-
每个变量形成一列
-
每个观察形成一行
-
每种类型的观察单元形成一个表格
任何不符合这些指南的数据集都被认为是杂乱的。这个定义在我们开始将数据重构为整洁形式后会更有意义,但现在我们需要知道什么是变量、观察和观察单元。
为了更好地理解变量究竟是什么,考虑一下变量名与变量值的区别是非常有帮助的。变量名是标签,如性别、种族、薪资和职位;变量值则是那些在每次观察中可能变化的内容,如性别的“男/女”或种族的“白人/黑人”。一个单独的观察就是一个观察单元所有变量值的集合。为了帮助理解观察单元的概念,假设有一家零售店,它有每个交易、员工、顾客、商品和店铺本身的数据。每个都可以被认为是一个观察单元,需要自己独立的表格。将员工信息(如工作时长)与顾客信息(如消费金额)放在同一张表格中会破坏整洁化原则。
解决杂乱数据的第一步是识别它的存在,而杂乱数据的种类有无穷多种。Hadley 明确提到五种最常见的杂乱数据类型:
-
列名是值,而不是变量名
-
多个变量存储在列名中
-
变量同时存储在行和列中
-
不同类型的观察单元存储在同一个表格中
-
单一观察单元存储在多个表格中
需要理解的是,整理数据通常不涉及更改数据集的值、填补缺失值或进行任何类型的分析。整理数据涉及改变数据的形状或结构,以符合整洁原则。整洁数据类似于将所有工具放在工具箱里,而不是散乱地放在房子各处。将工具正确地放入工具箱,可以让所有其他任务更容易完成。一旦数据以正确的形式存在,进行进一步分析就变得更加容易。
一旦你发现了杂乱数据,就可以使用 pandas 工具来重构数据,使其变得整洁。pandas 提供的主要整洁工具包括 DataFrame 方法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
这个表格似乎没有什么杂乱,信息也很容易读取。然而,根据整洁原则,它实际上并不整洁。每个列名实际上是一个变量的值。事实上,数据框中甚至没有出现任何变量名。将杂乱数据集转换为整洁数据的第一步是识别所有变量。在这个数据集中,我们有state和fruit两个变量。还有数字数据,它在问题的上下文中并没有被明确识别。我们可以将这个变量标记为weight或任何其他合理的名称。
准备工作
这个特别的杂乱数据集将变量值作为列名存储。我们需要将这些列名转换为列值。在这个示例中,我们使用stack方法将 DataFrame 重构为整洁形式。
如何做到这一点...
- 首先,注意到州名在 DataFrame 的索引中。这些州名正确地垂直排列,不需要重新结构化。问题出在列名上。
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
- 注意,我们现在有一个带有 MultiIndex 的 Series。索引现在有两个级别。原始的索引被推到左侧,为旧的列名腾出空间。通过这一条命令,我们现在基本上得到了整洁的数据。每个变量、状态、水果和重量都是垂直排列的。我们可以使用
reset_index方法将结果转换为 DataFrame:
>>> state_fruit_tidy = state_fruit.stack().reset_index()
>>> state_fruit_tidy
- 现在我们的结构已经正确,但列名没有意义。让我们用适当的标识符替换它们:
>>> state_fruit_tidy.columns = ['state', 'fruit', 'weight']
>>> state_fruit_tidy
- 除了直接改变
columns属性,还可以使用不太为人所知的 Series 方法rename_axis在使用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参数链式调用,以再现第三步的输出:
>>> state_fruit.stack()\
.rename_axis(['state', 'fruit'])\
.reset_index(name='weight')
它是如何工作的...
stack方法非常强大,需要一定时间才能完全理解和掌握。它将所有列名转置,使它们成为新的最内层索引级别。注意,每个旧的列名仍然通过与每个状态配对来标记其原始值。在一个 3 x 3 的 DataFrame 中有九个原始值,它们被转化成一个包含相同数量值的单列 Series。原来的第一行数据变成了结果 Series 中的前三个值。
在第二步重置索引后,pandas 会默认将我们的 DataFrame 列命名为level_0、level_1和0。这是因为调用此方法的 Series 有两个未命名的索引级别。Pandas 还会将索引从外部按从零开始的整数进行引用。
第三步展示了一种简单直观的方式来重命名列。你可以通过将columns属性设置为一个列表,直接为整个 DataFrame 设置新的列名。
另外,可以通过链式调用rename_axis方法一步设置列名。当传递一个列表作为第一个参数时,Pandas 使用这些值作为索引级别名称。Pandas 在重置索引时,会将这些索引级别名称作为新的列名。此外,reset_index方法有一个name参数,对应 Series 值的新列名。
所有 Series 都有一个name属性,可以直接设置或通过rename方法设置。正是这个属性在使用reset_index时成为列名。
还有更多...
使用stack的一个关键点是将所有不希望转换的列放在索引中。本例中的数据集最初是将状态作为索引读取的。我们来看一下如果没有将状态读入索引会发生什么:
>>> state_fruit2 = pd.read_csv('data/state_fruit2.csv')
>>> state_fruit2
由于状态名称不在索引中,在这个 DataFrame 上使用stack会将所有值重塑为一个长 Series:
>>> 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()
参见
-
Pandas 官方文档关于重塑和透视表 (
bit.ly/2xbnNms) -
Pandas 官方文档关于
stack方法 (bit.ly/2vWZhH1)
使用 melt 方法整理变量值作为列名
像大多数大型 Python 库一样,pandas 有许多完成相同任务的不同方法--区别通常在于可读性和性能。Pandas 包含一个名为melt的 DataFrame 方法,其工作方式与前面示例中描述的stack方法类似,但提供了更多灵活性。
在 pandas 版本 0.20 之前,melt仅作为一个函数提供,需要通过pd.melt访问。Pandas 仍然是一个不断发展的库,您需要预期每个新版本都会带来变化。Pandas 一直在推动将所有仅对 DataFrame 操作的函数移动到方法中,就像他们对melt所做的那样。这是使用melt的首选方式,也是本示例使用的方式。查看 pandas 文档中的新内容部分,以了解所有更改 (bit.ly/2xzXIhG)。
准备工作
在这个示例中,我们使用melt方法来整理一个包含变量值作为列名的简单 DataFrame。
如何实现...
- 读取
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将转换前的列名称为variable,相应的值称为value。方便地,melt还有两个额外的参数,var_name和value_name,允许您重新命名这两列:
>>> state_fruit2.melt(id_vars=['State'],
value_vars=['Apple', 'Orange', 'Banana'],
var_name='Fruit',
value_name='Weight')
工作原理...
melt方法功能强大,可以显著重塑您的 DataFrame。它最多接受五个参数,其中两个对于理解如何正确重塑数据至关重要:
-
id_vars是您想要保留为列而不重塑的列名列表 -
value_vars是您想要重塑为单列的列名列表
id_vars,或者称为标识变量,将保留在同一列中,但对于传递给 value_vars 的每一列都会重复。一项关键的 melt 特性是它会忽略索引中的值,实际上它会默默地丢弃你的索引,并用默认的 RangeIndex 代替。这意味着,如果你的索引中有你希望保留的值,你需要先重置索引,然后再使用 melt。
将水平列名转换为垂直列值的过程通常称为 melt、stacking 或 unpivoting。
还有更多...
melt 方法的所有参数都是可选的,如果你希望所有的值都在一列中,而它们原来的列标签在另一列中,你可以只使用 melt 的默认值来调用它:
>>> state_fruit2.melt()
更实际的情况是,你可能有许多需要“融化”的变量,并且只想指定标识变量。在这种情况下,以以下方式调用 melt 将得到与步骤 2 相同的结果。实际上,当只融化单列时,你甚至不需要列表,可以直接传递它的字符串值:
>>> state_fruit2.melt(id_vars='State')
另见
-
Pandas 官方文档中的
melt方法(bit.ly/2vcuZNJ) -
Pandas 开发者讨论将
melt和其他类似函数转换为方法的内容(bit.ly/2iqIQhI)
同时堆叠多个变量组
一些数据集包含多个作为列名的变量组,这些变量需要同时堆叠到它们自己的列中。以下是 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
- 将此函数传递给
rename方法来转换所有列名:
>>> actor2 = actor.rename(columns=change_col_name)
>>> actor2.head()
- 使用
wide_to_long函数同时堆叠演员和 Facebook 列集:
>>> 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 列,使它们都以数字结尾。
为了实际完成列重命名,我们在第二步中使用了rename方法。它接受多种不同类型的参数,其中之一是一个函数。当将其传递给一个函数时,每个列名会逐一隐式地传递给该函数。
我们现在已经正确创建了两组独立的列,这些列分别以actor和actor_facebook_likes开头,将被堆叠**。除此之外,wide_to_long需要一个独特的列,参数i,作为标识变量,该变量不会被堆叠。另一个必需的参数是j,它仅仅是将原始列名末尾的标识数字重命名。默认情况下,前缀参数包含正则表达式**,\d+,它用于搜索一个或多个数字。\d是一个特殊的标记,表示匹配数字 0-9。加号**+**使表达式匹配一个或多个数字。
要成为str方法的强大用户,您需要熟悉正则表达式,正则表达式是一系列字符,用于匹配文本中的特定模式。它们由元字符组成,这些字符具有特殊含义,以及字面字符。为了让自己更好地使用正则表达式,您可以查看Regular-Expressions.info的这个简短教程(bit.ly/2wiWPbz)。
还有更多...
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='_')
参见
- Pandas 官方文档关于
wide_to_long(bit.ly/2xb8NVP)
反转堆叠数据
DataFrame 有两个相似的方法,stack 和 melt,可以将水平的列名转换为垂直的列值。DataFrame 还可以通过 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
- 使用
unstackSeries 方法将堆叠的数据恢复为其原始形式:
>>> 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 中的原始 DataFrame,可以使用
.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方法将所有列名放入最内层的索引级别,并返回一个 Series。在第 3 步中,unstack方法通过将最内层索引级别中的所有值转换为列名,反转了这一操作。
第 3 步的结果并不是第 1 步的完全复制。存在整行缺失值,默认情况下,stack方法会在第 2 步时丢弃这些值。要保留这些缺失值并实现精确复制,可以在stack方法中使用dropna=False。
第 4 步读取与第 1 步相同的数据集,但由于melt方法无法访问,机构名称没有被放入索引中。第 5 步使用melt方法转置所有的Race列。通过将value_vars参数保持为默认值None来实现这一点。未指定时,所有不在id_vars参数中的列都会被转置。
第 6 步使用pivot方法反转第 5 步的操作,pivot方法接受三个参数。每个参数都作为字符串引用单独的列。index参数引用的列保持垂直,并成为新的索引。columns参数引用的列的值变为新的列名。values参数引用的列的值会在其原始索引和列标签交叉处进行排列。
为了用pivot实现精确复制,我们需要按照原始数据集中的顺序对行和列进行排序。由于机构名称在索引中,我们使用.loc索引运算符作为排序 DataFrame 的方式,以便按照原始索引顺序排列。
还有更多内容...
为了更好地理解stack/unstack,我们将它们用于转置college数据集。
在这个上下文中,我们使用的是矩阵转置的精确定义,其中新的行是原始数据矩阵中的旧列。
如果查看第 2 步的输出,你会注意到有两个索引级别。默认情况下,unstack方法使用最内层的索引级别作为新的列值。索引级别从外到内编号,从 0 开始。Pandas 将unstack方法的level参数默认为-1,表示最内层索引。我们可以使用level=0来unstack最外层的列:
>>> college.stack().unstack(0)
其实,有一种非常简单的方法可以转置 DataFrame,无需使用stack或unstack,只需使用transpose方法或T属性,如下所示:
>>> college.T
>>> college.transpose()
另见
-
请参阅第十章中的同时选择 DataFrame 行和列部分,选择数据子集。
-
Pandas 官方文档中的
unstack(bit.ly/2xIyFvr)和pivot(bit.ly/2f3qAWP)方法
在groupby聚合后进行 unstack 操作
按单个列对数据进行分组并对单列进行聚合,返回的结果简单直观,易于使用。当按多个列分组时,聚合结果可能不会以易于理解的方式结构化。由于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操作,返回一个易于读取且无需重塑的 Series。现在让我们通过性别计算所有种族的平均薪资:
>>> 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
- 这个聚合更复杂,可以重新调整形状,使不同的比较变得更容易。例如,如果男性与女性在每个种族中的薪资并排显示,而不是像现在这样垂直显示,那么比较将会更加容易。让我们对性别索引级别进行 unstack 操作:
>>> agg.unstack('GENDER')
- 同样,我们也可以对种族索引级别进行
unstack操作:
>>> agg.unstack('RACE')
它是如何工作的...
第一步是最简单的聚合操作,只有一个分组列(RACE)、一个聚合列(BASE_SALARY)和一个聚合函数(mean)。这个结果易于消费,不需要进一步处理。第二步稍微增加了复杂性,按种族和性别一起分组。结果是一个 MultiIndex Series,所有值都在一个维度中,这使得比较变得更困难。为了使信息更容易消费,我们使用unstack方法将某一(或多个)级别的值转换为列。
默认情况下,unstack使用最内层的索引级别作为新的列。你可以通过level参数指定你想要进行 unstack 的确切级别,level参数接受级别名称(作为字符串)或级别的整数位置。为了避免歧义,最好使用级别名称而非整数位置。步骤 3 和步骤 4 对每个级别执行 unstack 操作,结果是一个具有单级索引的 DataFrame。现在,通过性别比较每个种族的薪资就容易得多。
还有更多内容...
如果有多个分组和聚合列,那么立即得到的将是一个 DataFrame,而不是 Series。例如,让我们计算多个聚合,而不仅仅是第一步中的均值:
>>> agg2 = employee.groupby(['RACE', 'GENDER'])['BASE_SALARY'] \
.agg(['mean', 'max', 'min']).astype(int)
>>> agg2
对Gender列进行 unstack 操作将导致 MultiIndex 列。从这里开始,你可以使用unstack和stack方法交换行列层级,直到达到你想要的数据结构:
>>> agg2.unstack('GENDER')
另见
- 参考使用多个列进行分组和聚合的食谱以及第十三章中的函数,用于聚合、过滤和转换的分组
使用 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聚合非常相似,第一步展示了一个简单的例子。index参数接受一个(或多个)不会被透视的列,并将这些列的唯一值放置在索引中。columns参数接受一个(或多个)将被透视的列,并将这些列的唯一值转换为列名。values参数接受一个(或多个)将被聚合的列。
还有一个aggfunc参数,它接受一个聚合函数(或多个函数),决定如何对values参数中的列进行聚合。默认情况下为均值,在这个例子中,我们将其更改为计算总和。此外,某些AIRLINE和ORG_AIR的唯一组合并不存在。这些缺失的组合将在结果 DataFrame 中默认显示为缺失值。在这里,我们使用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')
有一些区别。pivot_table 方法在作为列表传递时,不像 agg 的 groupby 方法那样接受作为字符串的聚合函数。相反,您必须使用 NumPy 函数。列级别的顺序也有所不同,pivot_table 将聚合函数放在 values 参数中的列之前的一个级别。这可以通过 swaplevel 方法来统一,在此实例中,它交换了前两个级别的顺序。
截至本书撰写时,当堆叠多个列时,存在一个 bug。fill_value 参数会被忽略(bit.ly/2jCPnWZ)。为了解决这个 bug,可以在代码末尾链接 .fillna(0)。
重命名轴级别以便于重新塑形
当每个轴(索引/列)级别都有名称时,使用 stack/unstack 方法进行重新塑形要容易得多。Pandas 允许用户通过整数位置或名称引用每个轴级别。由于整数位置是隐式的而非显式的,因此建议尽可能使用级别名称。这个建议来自于 The Zen of Python(bit.ly/2xE83uC),它是 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'])
- 一次性堆叠所有列以返回一个 Series:
>>> 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 聚合的结果通常会产生具有多个轴级别的 DataFrame 或 Series。在步骤 1 中的 groupby 操作产生的 DataFrame 对每个轴都有多个级别。列级别没有命名,这意味着我们只能通过其整数位置来引用它们。为了大大简化引用列级别的操作,我们使用 rename_axis 方法对其进行重命名。
rename_axis 方法有些奇怪,因为它可以根据传入的第一个参数的类型修改级别名称和级别值。传入一个列表(如果只有一个级别,则传入标量)会改变级别的名称。传入字典或函数会改变级别的值。在步骤 2 中,我们传递给 rename_axis 方法一个列表,并返回一个所有轴级别都有名称的 DataFrame。
一旦所有轴级别都有了名称,我们就可以轻松明确地控制数据结构。步骤 3 将 AGG_FUNCS 列堆叠到最内层的索引级别。步骤 4 中的 swaplevel 方法接受您希望交换的级别的名称或位置作为前两个参数。sort_index 方法被调用两次,对每个级别的实际值进行排序。注意,列级别的值是列名 SATMTMID 和 UGDS。
通过堆叠和取消堆叠(如步骤 6 中所做的),我们可以得到截然不同的输出。还可以将每个列级别堆叠到索引中,从而生成一个 Series。
还有更多内容...
如果您希望完全删除级别值,可以将其设置为 None。这种做法适用于需要减少 DataFrame 可视化输出中的杂乱,或当列级别显然表示的内容已足够清晰,且不再进行其他处理时:
>>> 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方法将年龄和性别列名转置为一个单一的垂直列:
>>> 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()
- 这个操作返回了一个完全独立的 DataFrame,列名没有意义。我们需要重命名这些列,以便能够明确地访问它们:
>>> sex_age.columns = ['Sex', 'Age Group']
>>> sex_age.head()
- 直接在
str访问器后使用索引操作符,从Sex列中选择第一个字符:
>>> sex_age['Sex'] = sex_age['Sex'].str[0]
>>> sex_age.head()
- 使用
pd.concat函数将此 DataFrame 与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参数,将其作为标识变量保留。注意,我们不需要显式列出所有被“融化”的列,默认情况下,所有未出现在id_vars中的列都会被融化。
sex_age 列需要解析,并分割为两个变量。为此,我们利用了str访问器提供的额外功能,这仅适用于 Series(单个数据框列)。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}-+?)',
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
Sex 列的查找方式与第 5 步完全相同。因为我们没有使用 split,所以必须以不同的方式提取 Age Group 列。extract 方法使用复杂的正则表达式提取字符串的特定部分。要正确使用 extract,您的模式必须包含捕获组。捕获组通过在模式的一部分周围加括号形成。在这个例子中,整个表达式是一个大的捕获组。它以 \d{2} 开始,搜索精确两位数字,接着是一个字面上的加号或减号,后面可以跟着两位数字。虽然表达式的最后部分 (?:\d{2})? 周围有括号,但 ?: 表示它实际上不是捕获组。这在技术上是一个非捕获组,用于表达两位数字一起作为可选项。sex_age 列现在不再需要并被丢弃。最后,这两个整洁的数据框相互比较,并且发现它们是等价的。
另请参阅
- 请参阅 Regular-Expressions.info 网站以了解更多关于非捕获组的信息 (
bit.ly/2f60KSd)
当多个变量存储为列值时进行整理
整洁的数据集必须为每个变量设置单独的列。偶尔,多个变量名被放置在一个列中,并且它们对应的值放在另一个列中。这种混乱数据的通用格式如下:
在这个例子中,前后三行表示两组不同的观测数据,它们本应分别作为单独的行。需要对数据进行透视,使其变成如下所示:
准备工作
在这个案例中,我们找出包含结构不正确的变量的列,并将其进行透视以创建整洁的数据。
如何实现...
- 读取餐厅
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 残留物需要清理。我们使用 MultiIndex 方法
droplevel来删除顶部的列级别,然后将索引级别重命名为None:
>>> insp_tidy.columns = insp_tidy.columns.droplevel(0) \
.rename(None)
>>> insp_tidy.head()
- 第 4 步中创建的列 MultiIndex 本可以通过将该单列 DataFrame 转换为 Series 并使用
squeeze方法避免。以下代码产生与上一步相同的结果:
>>> 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方法将需要透视和不需要透视的列都移动到索引中,开始了这一过程。将这些列放入索引后,unstack就可以像第 3 步那样使用了。
请注意,在我们对 DataFrame 进行 unstack 操作时,pandas 会保留原来的列名(这里只有一列,Value),并用旧的列名作为上层级创建一个 MultiIndex。现在数据集基本上已经是整洁的,但我们继续使用reset_index方法,将非透视列变成常规列。因为我们有 MultiIndex 列,我们可以使用col_level参数来选择新列名属于哪个层级。默认情况下,名称会被插入到最上层(层级 0)。我们使用-1来指示最底层。
在完成这些操作后,我们有一些多余的 DataFrame 名称和索引需要被丢弃。不幸的是,没有 DataFrame 方法可以删除层级,所以我们必须深入到索引中,使用其droplevel方法。在这里,我们用单级列覆盖了旧的 MultiIndex 列。这些列仍然有一个无用的名称属性Info,我们将其重命名为None。
通过将第 3 步的结果强制转换为 Series,可以避免清理 MultiIndex 列。squeeze方法只适用于单列 DataFrame,并将其转换为 Series。
还有更多内容...
实际上可以使用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')
另请参见
- Pandas 官方文档中的
droplevel方法(bit.ly/2yo5BXf)和squeeze方法(bit.ly/2yo5TgN)
当两个或更多的值存储在同一个单元格中时的整理
表格数据天生是二维的,因此每个单元格中可以展示的信息是有限的。为了解决这个问题,您有时会看到数据集中一个单元格中存储了多个值。整洁的数据要求每个单元格恰好包含一个值。要解决这些情况,通常需要使用str系列访问器中的方法将字符串数据解析成多个列。
准备工作...
在这个示例中,我们查看了一个数据集,其中有一列包含每个单元格中多个不同的变量。我们使用str访问器将这些字符串解析成单独的列,以整理数据。
如何操作...
- 读取德克萨斯州的
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 步将城市名称附加到这个新 DataFrame 的前面,以完成整洁数据的制作过程。
还有更多...
在这个例子中,split方法与简单的正则表达式配合得非常好。对于其他例子,某些列可能需要你创建多个不同模式的拆分。要搜索多个正则表达式,可以使用管道字符|。例如,如果我们只想拆分度符号和逗号,并且每个符号后面都有一个空格,我们将这样做:
>>> cities.Geolocation.str.split(pat='° |, ', expand=True)
这将返回与第 2 步相同的 DataFrame。任何数量的额外拆分模式可以通过管道字符追加到前面的字符串模式中。
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。这种凌乱的数据无法通过单一的 DataFrame 方法重构。让我们先使用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')
它是如何工作的...
一旦我们在第一步中识别出了变量,我们就可以开始进行重构。Pandas 没有一个方法可以同时旋转多个列,因此我们必须一步步地处理。我们通过将Property列传递给melt方法中的id_vars参数来保持其竖直排列,从而纠正年份数据。
结果现在正是前面“当多个变量作为列值存储时进行整洁化”章节中的混乱数据模式。如该章节中的“还有更多...”部分所述,当index参数中使用多个列时,我们必须使用pivot_table来旋转 DataFrame。在旋转之后,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', 'title', '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_table = movie_table.drop_duplicates() \
.reset_index(drop=True)
>>> director_table = director_table.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 工具创建的(
erdplus.com):
它是如何工作的...
在导入数据并识别出三个实体之后,我们必须为每个观察值创建一个唯一标识符,这样一来,当它们被分隔到不同的表格时,我们就能将电影、演员和导演关联在一起。在步骤 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%的内存。正式的关系数据库有实体关系图(ERD)来可视化表格。在步骤 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
另请参见
-
关于数据库规范化的更多信息(
bit.ly/2w8wahQ)、关联表(bit.ly/2yqE4oh)以及主键和外键(bit.ly/2xgIvEb) -
请参考本章中的同时堆叠多个变量组配方,了解有关
wide_to_long函数的更多信息。