Python 数据科学手册第二版(三)
原文:
zh.annas-archive.org/md5/051facaf2908ae8198253e3a14b09ec1译者:飞龙
第十四章:数据索引和选择
在第二部分中,我们详细讨论了访问、设置和修改 NumPy 数组中的值的方法和工具。这些包括索引(例如arr[2, 1])、切片(例如arr[:, 1:5])、掩码(例如arr[arr > 0])、花式索引(例如arr[0, [1, 5]])以及它们的组合(例如arr[:, [1, 5]])。在这里,我们将看一下类似的方法来访问和修改 Pandas Series和DataFrame对象中的值。如果你使用过 NumPy 模式,Pandas 中的相应模式会感觉非常熟悉,尽管有一些需要注意的怪癖。
我们将从一维Series对象的简单情况开始,然后转向更复杂的二维DataFrame对象。
Series 中的数据选择
正如你在前一章中看到的,Series对象在许多方面都像一个一维 NumPy 数组,而在许多方面都像一个标准的 Python 字典。如果你记住这两个重叠的类比,将有助于你理解这些数组中的数据索引和选择模式。
Series作为字典
像字典一样,Series对象提供了从一组键到一组值的映射:
In [1]: import pandas as pd
data = pd.Series([0.25, 0.5, 0.75, 1.0],
index=['a', 'b', 'c', 'd'])
data
Out[1]: a 0.25
b 0.50
c 0.75
d 1.00
dtype: float64
In [2]: data['b']
Out[2]: 0.5
我们还可以使用类似字典的 Python 表达式和方法来查看键/索引和值:
In [3]: 'a' in data
Out[3]: True
In [4]: data.keys()
Out[4]: Index(['a', 'b', 'c', 'd'], dtype='object')
In [5]: list(data.items())
Out[5]: [('a', 0.25), ('b', 0.5), ('c', 0.75), ('d', 1.0)]
Series对象也可以用类似字典的语法进行修改。就像你可以通过分配给新键来扩展字典一样,你可以通过分配给新索引值来扩展Series:
In [6]: data['e'] = 1.25
data
Out[6]: a 0.25
b 0.50
c 0.75
d 1.00
e 1.25
dtype: float64
这种对象的易变性是一个方便的特性:在幕后,Pandas 正在做出关于内存布局和数据复制的决策,这可能需要进行,而用户通常不需要担心这些问题。
一维数组中的 Series
Series建立在这种类似字典的接口上,并通过与 NumPy 数组相同的基本机制提供了数组样式的项目选择——即切片、掩码和花式索引。以下是这些的示例:
In [7]: # slicing by explicit index
data['a':'c']
Out[7]: a 0.25
b 0.50
c 0.75
dtype: float64
In [8]: # slicing by implicit integer index
data[0:2]
Out[8]: a 0.25
b 0.50
dtype: float64
In [9]: # masking
data[(data > 0.3) & (data < 0.8)]
Out[9]: b 0.50
c 0.75
dtype: float64
In [10]: # fancy indexing
data[['a', 'e']]
Out[10]: a 0.25
e 1.25
dtype: float64
其中,切片可能是最容易混淆的来源。请注意,当使用显式索引进行切片(例如data['a':'c'])时,最终索引被包括在切片中,而当使用隐式索引进行切片(例如data[0:2])时,最终索引被排除在切片之外。
索引器:loc 和 iloc
如果你的Series有一个明确的整数索引,那么像data[1]这样的索引操作将使用明确的索引,而像data[1:3]这样的切片操作将使用隐式的 Python 风格索引:
In [11]: data = pd.Series(['a', 'b', 'c'], index=[1, 3, 5])
data
Out[11]: 1 a
3 b
5 c
dtype: object
In [12]: # explicit index when indexing
data[1]
Out[12]: 'a'
In [13]: # implicit index when slicing
data[1:3]
Out[13]: 3 b
5 c
dtype: object
由于整数索引可能会导致混淆,Pandas 提供了一些特殊的索引器属性,明确地暴露了某些索引方案。这些不是功能性方法,而是属性,它们向Series中的数据公开了特定的切片接口。
首先,loc属性允许始终引用显式索引的索引和切片:
In [14]: data.loc[1]
Out[14]: 'a'
In [15]: data.loc[1:3]
Out[15]: 1 a
3 b
dtype: object
iloc属性允许引用始终参考隐式 Python 样式索引的索引和切片:
In [16]: data.iloc[1]
Out[16]: 'b'
In [17]: data.iloc[1:3]
Out[17]: 3 b
5 c
dtype: object
Python 代码的一个指导原则是“明确优于隐式”。loc和iloc的显式特性使它们在保持代码清晰和可读性方面非常有帮助;特别是在整数索引的情况下,始终一致地使用它们可以防止由于混合索引/切片约定而导致的微妙错误。
数据框选择
回想一下,DataFrame在许多方面都像一个二维或结构化数组,而在其他方面则像一个共享相同索引的Series结构的字典。当我们探索在这种结构内进行数据选择时,这些类比可能会有所帮助。
DataFrame 作为字典
我们首先考虑的类比是将DataFrame视为一组相关Series对象的字典。让我们回到我们州的面积和人口的例子:
In [18]: area = pd.Series({'California': 423967, 'Texas': 695662,
'Florida': 170312, 'New York': 141297,
'Pennsylvania': 119280})
pop = pd.Series({'California': 39538223, 'Texas': 29145505,
'Florida': 21538187, 'New York': 20201249,
'Pennsylvania': 13002700})
data = pd.DataFrame({'area':area, 'pop':pop})
data
Out[18]: area pop
California 423967 39538223
Texas 695662 29145505
Florida 170312 21538187
New York 141297 20201249
Pennsylvania 119280 13002700
组成DataFrame列的单个Series可以通过列名的字典样式索引进行访问:
In [19]: data['area']
Out[19]: California 423967
Texas 695662
Florida 170312
New York 141297
Pennsylvania 119280
Name: area, dtype: int64
类似地,我们可以使用列名为字符串的属性样式访问:
In [20]: data.area
Out[20]: California 423967
Texas 695662
Florida 170312
New York 141297
Pennsylvania 119280
Name: area, dtype: int64
尽管这是一个有用的简写,但请记住,并非所有情况下都适用!例如,如果列名不是字符串,或者列名与DataFrame的方法冲突,这种属性样式访问就不可能。例如,DataFrame有一个pop方法,所以data.pop将指向这个方法而不是pop列:
In [21]: data.pop is data["pop"]
Out[21]: False
特别地,你应该避免尝试通过属性进行列赋值(即,使用data['pop'] = z而不是data.pop = z)。
像之前讨论过的Series对象一样,这种字典样式的语法也可以用来修改对象,比如在这种情况下添加一个新列:
In [22]: data['density'] = data['pop'] / data['area']
data
Out[22]: area pop density
California 423967 39538223 93.257784
Texas 695662 29145505 41.896072
Florida 170312 21538187 126.463121
New York 141297 20201249 142.970120
Pennsylvania 119280 13002700 109.009893
这展示了Series对象之间按元素进行算术运算的简单语法预览;我们将在第十五章进一步深入探讨这个问题。
DataFrame 作为二维数组
正如前面提到的,我们也可以将DataFrame视为增强的二维数组。我们可以使用values属性查看原始的底层数据数组:
In [23]: data.values
Out[23]: array([[4.23967000e+05, 3.95382230e+07, 9.32577842e+01],
[6.95662000e+05, 2.91455050e+07, 4.18960717e+01],
[1.70312000e+05, 2.15381870e+07, 1.26463121e+02],
[1.41297000e+05, 2.02012490e+07, 1.42970120e+02],
[1.19280000e+05, 1.30027000e+07, 1.09009893e+02]])
在这个画面中,许多熟悉的类似数组的操作可以在DataFrame本身上完成。例如,我们可以转置整个DataFrame来交换行和列:
In [24]: data.T
Out[24]: California Texas Florida New York Pennsylvania
area 4.239670e+05 6.956620e+05 1.703120e+05 1.412970e+05 1.192800e+05
pop 3.953822e+07 2.914550e+07 2.153819e+07 2.020125e+07 1.300270e+07
density 9.325778e+01 4.189607e+01 1.264631e+02 1.429701e+02 1.090099e+02
然而,当涉及到DataFrame对象的索引时,很明显,列的字典样式索引排除了我们简单将其视为 NumPy 数组的能力。特别是,将单个索引传递给数组会访问一行:
In [25]: data.values[0]
Out[25]: array([4.23967000e+05, 3.95382230e+07, 9.32577842e+01])
并且将一个单独的“索引”传递给DataFrame会访问一列:
In [26]: data['area']
Out[26]: California 423967
Texas 695662
Florida 170312
New York 141297
Pennsylvania 119280
Name: area, dtype: int64
因此,对于数组样式的索引,我们需要另一种约定。在这里,Pandas 再次使用了前面提到的loc和iloc索引器。使用iloc索引器,我们可以像使用简单的 NumPy 数组一样索引底层数组(使用隐式的 Python 风格索引),但结果中保持了DataFrame的索引和列标签:
In [27]: data.iloc[:3, :2]
Out[27]: area pop
California 423967 39538223
Texas 695662 29145505
Florida 170312 21538187
同样地,使用loc索引器,我们可以以类似于数组的样式索引底层数据,但使用显式的索引和列名:
In [28]: data.loc[:'Florida', :'pop']
Out[28]: area pop
California 423967 39538223
Texas 695662 29145505
Florida 170312 21538187
在这些索引器中,可以使用任何熟悉的类似于 NumPy 的数据访问模式。例如,在loc索引器中,我们可以按以下方式组合遮罩和花式索引:
In [29]: data.loc[data.density > 120, ['pop', 'density']]
Out[29]: pop density
Florida 21538187 126.463121
New York 20201249 142.970120
任何这些索引约定也可以用于设置或修改值;这是通过与您在使用 NumPy 工作时习惯的标准方式完成的:
In [30]: data.iloc[0, 2] = 90
data
Out[30]: area pop density
California 423967 39538223 90.000000
Texas 695662 29145505 41.896072
Florida 170312 21538187 126.463121
New York 141297 20201249 142.970120
Pennsylvania 119280 13002700 109.009893
要提升您在 Pandas 数据操作中的熟练程度,我建议您花一些时间使用一个简单的DataFrame,并探索这些不同索引方法允许的索引、切片、遮罩和花式索引类型。
额外的索引约定
还有一些额外的索引约定,可能与前面的讨论看似不符,但在实践中仍然很有用。首先,索引指的是列,而切片指的是行:
In [31]: data['Florida':'New York']
Out[31]: area pop density
Florida 170312 21538187 126.463121
New York 141297 20201249 142.970120
这种切片也可以通过数字而不是索引来引用行。
In [32]: data[1:3]
Out[32]: area pop density
Texas 695662 29145505 41.896072
Florida 170312 21538187 126.463121
类似地,直接的遮罩操作是按行而不是按列进行解释。
In [33]: data[data.density > 120]
Out[33]: area pop density
Florida 170312 21538187 126.463121
New York 141297 20201249 142.970120
这两种约定在语法上与 NumPy 数组上的约定类似,虽然它们可能不完全符合 Pandas 的约定模式,但由于它们的实际实用性,它们被包含了进来。
第十五章:在 Pandas 中操作数据
NumPy 的一个优点是它允许我们执行快速的逐元素操作,包括基本算术(加法、减法、乘法等)和更复杂的操作(三角函数、指数和对数函数等)。Pandas 从 NumPy 继承了许多这些功能,并且在第六章介绍的 ufuncs 对此至关重要。
然而,Pandas 还包括一些有用的技巧:对于像否定和三角函数这样的一元操作,这些 ufuncs 将在输出中 保留索引和列标签;对于像加法和乘法这样的二元操作,当将对象传递给 ufunc 时,Pandas 将自动 对齐索引。这意味着保持数据的上下文和组合来自不同来源的数据(这两个任务对于原始 NumPy 数组来说可能是错误的)在 Pandas 中基本上变得十分简单。我们还将看到在一维 Series 结构和二维 DataFrame 结构之间存在着明确定义的操作。
Ufuncs:索引保留
因为 Pandas 是设计用于与 NumPy 协作的,任何 NumPy 的 ufunc 都可以在 Pandas 的 Series 和 DataFrame 对象上使用。让我们先定义一个简单的 Series 和 DataFrame 来演示这一点:
In [1]: import pandas as pd
import numpy as np
In [2]: rng = np.random.default_rng(42)
ser = pd.Series(rng.integers(0, 10, 4))
ser
Out[2]: 0 0
1 7
2 6
3 4
dtype: int64
In [3]: df = pd.DataFrame(rng.integers(0, 10, (3, 4)),
columns=['A', 'B', 'C', 'D'])
df
Out[3]: A B C D
0 4 8 0 6
1 2 0 5 9
2 7 7 7 7
如果我们在这些对象中的任一对象上应用 NumPy 的 ufunc,结果将是另一个 Pandas 对象 并保留索引:
In [4]: np.exp(ser)
Out[4]: 0 1.000000
1 1096.633158
2 403.428793
3 54.598150
dtype: float64
对于更复杂的操作序列,情况也是如此:
In [5]: np.sin(df * np.pi / 4)
Out[5]: A B C D
0 1.224647e-16 -2.449294e-16 0.000000 -1.000000
1 1.000000e+00 0.000000e+00 -0.707107 0.707107
2 -7.071068e-01 -7.071068e-01 -0.707107 -0.707107
任何在第六章中讨论过的 ufunc 都可以以类似的方式使用。
Ufuncs:索引对齐
对于两个 Series 或 DataFrame 对象的二元操作,Pandas 将在执行操作的过程中对齐索引。这在处理不完整数据时非常方便,我们将在接下来的一些示例中看到。
Series 中的索引对齐
例如,假设我们正在结合两个不同的数据源,并希望仅找到按 面积 排名前三的美国州和按 人口 排名前三的美国州:
In [6]: area = pd.Series({'Alaska': 1723337, 'Texas': 695662,
'California': 423967}, name='area')
population = pd.Series({'California': 39538223, 'Texas': 29145505,
'Florida': 21538187}, name='population')
现在让我们来看看在进行人口密度计算时会发生什么:
In [7]: population / area
Out[7]: Alaska NaN
California 93.257784
Florida NaN
Texas 41.896072
dtype: float64
结果数组包含两个输入数组的索引的 并集,这可以直接从这些索引中确定:
In [8]: area.index.union(population.index)
Out[8]: Index(['Alaska', 'California', 'Florida', 'Texas'], dtype='object')
任何其中一个没有条目的项目都标记有 NaN,即“不是数字”,这是 Pandas 标记缺失数据的方式(详见第十六章对缺失数据的进一步讨论)。对于 Python 内置的任何算术表达式,都会实现这种索引匹配;任何缺失值都将被 NaN 标记:
In [9]: A = pd.Series([2, 4, 6], index=[0, 1, 2])
B = pd.Series([1, 3, 5], index=[1, 2, 3])
A + B
Out[9]: 0 NaN
1 5.0
2 9.0
3 NaN
dtype: float64
如果不希望使用NaN值,可以使用适当的对象方法修改填充值,而不是使用操作符。例如,调用A.add(B)等效于调用A + B,但允许可选地显式指定A或B中可能缺失元素的填充值:
In [10]: A.add(B, fill_value=0)
Out[10]: 0 2.0
1 5.0
2 9.0
3 5.0
dtype: float64
数据帧中的索引对齐
当对DataFrame对象进行操作时,同时在列和索引上进行类似的对齐:
In [11]: A = pd.DataFrame(rng.integers(0, 20, (2, 2)),
columns=['a', 'b'])
A
Out[11]: a b
0 10 2
1 16 9
In [12]: B = pd.DataFrame(rng.integers(0, 10, (3, 3)),
columns=['b', 'a', 'c'])
B
Out[12]: b a c
0 5 3 1
1 9 7 6
2 4 8 5
In [13]: A + B
Out[12]: a b c
0 13.0 7.0 NaN
1 23.0 18.0 NaN
2 NaN NaN NaN
请注意,无论这两个对象中的顺序如何,索引都正确对齐,并且结果中的索引是排序的。与Series一样,我们可以使用关联对象的算术方法,并传递任何希望用于替代缺失条目的fill_value。这里我们将用A中所有值的平均值填充:
In [14]: A.add(B, fill_value=A.values.mean())
Out[14]: a b c
0 13.00 7.00 10.25
1 23.00 18.00 15.25
2 17.25 13.25 14.25
表 15-1 列出了 Python 运算符及其相应的 Pandas 对象方法。
表 15-1。Python 运算符与 Pandas 方法的映射
| Python 运算符 | Pandas 方法 |
|---|---|
+ | add |
- | sub, subtract |
* | mul, multiply |
/ | truediv, div, divide |
// | floordiv |
% | mod |
** | pow |
Ufuncs:DataFrame 与 Series 之间的操作
当对DataFrame和Series进行操作时,索引和列的对齐方式类似地保持,并且结果类似于二维数组和一维 NumPy 数组之间的操作。考虑一种常见的操作,即查找二维数组与其一行之间的差异:
In [15]: A = rng.integers(10, size=(3, 4))
A
Out[15]: array([[4, 4, 2, 0],
[5, 8, 0, 8],
[8, 2, 6, 1]])
In [16]: A - A[0]
Out[16]: array([[ 0, 0, 0, 0],
[ 1, 4, -2, 8],
[ 4, -2, 4, 1]])
根据 NumPy 的广播规则(参见第八章),二维数组与其一行之间的减法操作是逐行应用的。
在 Pandas 中,默认情况下也是逐行操作的约定:
In [17]: df = pd.DataFrame(A, columns=['Q', 'R', 'S', 'T'])
df - df.iloc[0]
Out[17]: Q R S T
0 0 0 0 0
1 1 4 -2 8
2 4 -2 4 1
如果您希望以列为单位进行操作,可以使用前面提到的对象方法,并指定axis关键字:
In [18]: df.subtract(df['R'], axis=0)
Out[18]: Q R S T
0 0 0 -2 -4
1 -3 0 -8 0
2 6 0 4 -1
注意,像前面讨论过的操作一样,这些DataFrame/Series操作会自动对齐两个元素之间的索引:
In [19]: halfrow = df.iloc[0, ::2]
halfrow
Out[19]: Q 4
S 2
Name: 0, dtype: int64
In [20]: df - halfrow
Out[20]: Q R S T
0 0.0 NaN 0.0 NaN
1 1.0 NaN -2.0 NaN
2 4.0 NaN 4.0 NaN
这种索引和列的保留与对齐意味着在 Pandas 中对数据进行的操作将始终保持数据上下文,这可以防止在原始 NumPy 数组中处理异构和/或不对齐数据时可能出现的常见错误。
第十六章:处理缺失数据
许多教程中找到的数据与现实世界中的数据之间的区别在于,现实世界的数据很少是干净且同质的。特别是,许多有趣的数据集会有一些数据缺失。更复杂的是,不同的数据源可能以不同的方式指示缺失数据。
在本章中,我们将讨论一些有关缺失数据的一般考虑事项,看看 Pandas 如何选择表示它,并探索一些处理 Python 中缺失数据的内置 Pandas 工具。在本书中,我将通常将缺失数据总称为null、*NaN*或NA值。
缺失数据约定中的权衡
已开发出许多方法来跟踪表格或DataFrame中缺失数据的存在。通常,它们围绕两种策略之一展开:使用掩码全局指示缺失值,或选择指示缺失条目的哨兵值。
在掩码方法中,掩码可以是一个完全独立的布尔数组,也可以涉及在数据表示中占用一个比特来局部指示值的空状态。
在哨兵方法中,哨兵值可以是一些特定于数据的约定,比如用 -9999 表示缺失的整数值或一些罕见的比特模式,或者可以是一个更全局的约定,比如用NaN(不是数字)表示缺失的浮点值,这是 IEEE 浮点规范的一部分。
这两种方法都不是没有权衡的。使用单独的掩码数组需要分配额外的布尔数组,这在存储和计算上都增加了开销。哨兵值会减少可以表示的有效值范围,并且可能需要额外的(通常是非优化的)CPU 和 GPU 算术逻辑,因为常见的特殊值如NaN并不适用于所有数据类型。
就像大多数情况下没有普遍适用的最佳选择一样,不同的语言和系统使用不同的约定。例如,R 语言使用每种数据类型内的保留比特模式作为指示缺失数据的哨兵值,而 SciDB 系统使用附加到每个单元的额外字节来指示 NA 状态。
Pandas 中的缺失数据
Pandas 处理缺失值的方式受其对 NumPy 包的依赖限制,后者对非浮点数据类型没有内置的 NA 值概念。
也许 Pandas 本可以效仿 R 在指定每个单独数据类型的位模式以指示空值方面的领先地位,但是这种方法事实证明相当笨拙。虽然 R 只有 4 种主要数据类型,但是 NumPy 支持的数据类型远远超过这个数字:例如,虽然 R 只有一个整数类型,但是考虑到可用的位宽、符号性和编码的字节顺序,NumPy 支持 14 种基本整数类型。在所有可用的 NumPy 类型中保留特定的位模式将导致在各种类型的操作中特殊处理大量操作,很可能甚至需要新的 NumPy 软件包的分支。此外,对于较小的数据类型(如 8 位整数),牺牲一位用作掩码将显著减少其可以表示的值的范围。
由于这些限制和权衡,Pandas 有两种存储和操作空值的“模式”:
-
默认模式是使用基于哨兵值的缺失数据方案,哨兵值为
NaN或None,具体取决于数据类型。 -
或者,您可以选择使用 Pandas 提供的可空数据类型(dtypes)(稍后在本章讨论),这将导致创建一个伴随的掩码数组来跟踪缺失的条目。然后,这些缺失的条目将被呈现给用户作为特殊的
pd.NA值。
无论哪种情况,Pandas API 提供的数据操作和操作将以可预测的方式处理和传播这些缺失的条目。但是为了对为什么会做出这些选择有一些直觉,让我们快速探讨一下None、NaN和NA中固有的权衡。像往常一样,我们将从导入 NumPy 和 Pandas 开始:
In [1]: import numpy as np
import pandas as pd
None作为哨兵值
对于某些数据类型,Pandas 使用None作为哨兵值。None是一个 Python 对象,这意味着包含None的任何数组必须具有dtype=object,即它必须是 Python 对象的序列。
例如,观察将None传递给 NumPy 数组会发生什么:
In [2]: vals1 = np.array([1, None, 2, 3])
vals1
Out[2]: array([1, None, 2, 3], dtype=object)
这个dtype=object意味着 NumPy 可以推断出数组内容的最佳公共类型表示是 Python 对象。以这种方式使用None的缺点是数据的操作将在 Python 级别完成,其开销比通常对具有本地类型的数组所见的快速操作要大得多:
In [3]: %timeit np.arange(1E6, dtype=int).sum()
Out[3]: 2.73 ms ± 288 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [4]: %timeit np.arange(1E6, dtype=object).sum()
Out[4]: 92.1 ms ± 3.42 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
此外,因为 Python 不支持与None的算术运算,像sum或min这样的聚合通常会导致错误:
In [5]: vals1.sum()
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
因此,Pandas 在其数值数组中不使用None作为哨兵值。
NaN:缺失的数值数据
另一个缺失数据哨兵,NaN,是不同的;它是一种特殊的浮点值,在所有使用标准 IEEE 浮点表示的系统中都被识别:
In [6]: vals2 = np.array([1, np.nan, 3, 4])
vals2
Out[6]: array([ 1., nan, 3., 4.])
请注意,NumPy 为此数组选择了本地的浮点类型:这意味着与之前的对象数组不同,此数组支持快速操作并推入编译代码中。请记住,NaN有点像数据病毒——它会感染到任何它接触到的其他对象。
不管进行何种操作,带有NaN的算术运算的结果都将是另一个NaN:
In [7]: 1 + np.nan
Out[7]: nan
In [8]: 0 * np.nan
Out[8]: nan
这意味着对值的聚合是定义良好的(即,它们不会导致错误),但并不总是有用的:
In [9]: vals2.sum(), vals2.min(), vals2.max()
Out[9]: (nan, nan, nan)
也就是说,NumPy 确实提供了对NaN敏感的聚合函数版本,将忽略这些缺失值:
In [10]: np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)
Out[10]: (8.0, 1.0, 4.0)
NaN的主要缺点是它是特定的浮点值;对于整数、字符串或其他类型,都没有等价的NaN值。
Pandas 中的 NaN 和 None
NaN和None都有它们的用途,而且 Pandas 几乎可以在它们之间自由转换,视情况而定:
In [11]: pd.Series([1, np.nan, 2, None])
Out[11]: 0 1.0
1 NaN
2 2.0
3 NaN
dtype: float64
对于没有可用哨兵值的类型,当存在 NA 值时,Pandas 会自动进行类型转换。例如,如果我们将整数数组中的一个值设置为np.nan,它将自动提升为浮点类型以容纳 NA:
In [12]: x = pd.Series(range(2), dtype=int)
x
Out[12]: 0 0
1 1
dtype: int64
In [13]: x[0] = None
x
Out[13]: 0 NaN
1 1.0
dtype: float64
请注意,除了将整数数组转换为浮点数外,Pandas 还会自动将None转换为NaN值。
虽然这种类型的魔法操作可能与像 R 这样的特定领域语言中的 NA 值的更统一方法相比显得有些投机,但是 Pandas 的哨兵/转换方法在实践中运作得非常好,并且据我经验,很少引起问题。
表 16-1 列出了引入 NA 值时 Pandas 中的提升转换规则。
表 16-1. Pandas 按类型处理 NA 值
| 类型 | 存储 NA 时的转换 | NA 哨兵值 |
|---|---|---|
floating | 无变化 | np.nan |
object | 无变化 | None或np.nan |
integer | 转换为float64 | np.nan |
boolean | 转换为object | None或np.nan |
请记住,在 Pandas 中,字符串数据始终以object类型存储。
Pandas 可空数据类型
在早期版本的 Pandas 中,NaN和None作为哨兵值是唯一可用的缺失数据表示。这引入的主要困难是隐式类型转换:例如,无法表示真正的整数数组带有缺失数据。
为了解决这个问题,Pandas 后来添加了可空数据类型,它们通过名称的大写区分于常规数据类型(例如,pd.Int32与np.int32)。为了向后兼容,只有在明确请求时才会使用这些可空数据类型。
例如,这是一个带有缺失数据的整数Series,由包含所有三种可用缺失数据标记的列表创建:
In [14]: pd.Series([1, np.nan, 2, None, pd.NA], dtype='Int32')
Out[14]: 0 1
1 <NA>
2 2
3 <NA>
4 <NA>
dtype: Int32
这种表示可以在本章剩余的所有操作中与其他表示方法交替使用。
操作空值
正如我们所见,Pandas 将 None、NaN 和 NA 视为基本可以互换,用于指示缺失或空值。为了促进这一约定,Pandas 提供了几种方法来检测、删除和替换 Pandas 数据结构中的空值。它们包括:
isnull
生成一个指示缺失值的布尔掩码
notnull
isnull 的反操作
dropna
返回数据的过滤版本
fillna
返回填充或插补了缺失值的数据副本
我们将以对这些程序的简要探索和演示来结束本章。
检测空值
Pandas 数据结构有两个有用的方法来检测空数据:isnull 和 notnull。任何一个都将返回数据的布尔掩码。例如:
In [15]: data = pd.Series([1, np.nan, 'hello', None])
In [16]: data.isnull()
Out[16]: 0 False
1 True
2 False
3 True
dtype: bool
正如在 第十四章 中提到的那样,布尔掩码可以直接用作 Series 或 DataFrame 的索引:
In [17]: data[data.notnull()]
Out[17]: 0 1
2 hello
dtype: object
对于 DataFrame 对象,isnull() 和 notnull() 方法生成类似的布尔结果。
删除空值
除了这些掩码方法之外,还有方便的方法 dropna(用于删除 NA 值)和 fillna(用于填充 NA 值)。对于 Series,结果是直接的:
In [18]: data.dropna()
Out[18]: 0 1
2 hello
dtype: object
对于 DataFrame,有更多的选择。考虑以下 DataFrame:
In [19]: df = pd.DataFrame([[1, np.nan, 2],
[2, 3, 5],
[np.nan, 4, 6]])
df
Out[19]: 0 1 2
0 1.0 NaN 2
1 2.0 3.0 5
2 NaN 4.0 6
我们不能从 DataFrame 中删除单个值;我们只能删除整行或整列。根据应用程序的不同,您可能需要其中一个,因此 dropna 包含了一些 DataFrame 的选项。
默认情况下,dropna 将删除任何存在空值的行:
In [20]: df.dropna()
Out[20]: 0 1 2
1 2.0 3.0 5
或者,您可以沿不同的轴删除 NA 值。使用 axis=1 或 axis='columns' 将删除包含空值的所有列:
In [21]: df.dropna(axis='columns')
Out[21]: 2
0 2
1 5
2 6
但是这样会丢掉一些好数据;您可能更感兴趣的是删除具有所有 NA 值或大多数 NA 值的行或列。这可以通过 how 或 thresh 参数进行指定,这些参数允许对允许通过的空值数量进行精细控制。
默认值为 how='any',这样任何包含空值的行或列都将被删除。您还可以指定 how='all',这样只会删除包含所有空值的行/列:
In [22]: df[3] = np.nan
df
Out[22]: 0 1 2 3
0 1.0 NaN 2 NaN
1 2.0 3.0 5 NaN
2 NaN 4.0 6 NaN
In [23]: df.dropna(axis='columns', how='all')
Out[23]: 0 1 2
0 1.0 NaN 2
1 2.0 3.0 5
2 NaN 4.0 6
对于更精细的控制,thresh 参数允许您指定保留行/列的最小非空值数:
In [24]: df.dropna(axis='rows', thresh=3)
Out[24]: 0 1 2 3
1 2.0 3.0 5 NaN
在这里,第一行和最后一行已被删除,因为它们各自只包含两个非空值。
填充空值
有时候,您不想丢弃 NA 值,而是希望用有效值替换它们。这个值可以是一个单独的数字,比如零,或者可能是一些从好的值中插补或插值出来的值。您可以使用 isnull 方法作为掩码来原地进行这个操作,但是因为这是一个常见的操作,Pandas 提供了 fillna 方法,它返回一个替换了空值的数组的副本。
考虑以下 Series:
In [25]: data = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'),
dtype='Int32')
data
Out[25]: a 1
b <NA>
c 2
d <NA>
e 3
dtype: Int32
我们可以用一个单一的值(如零)填充 NA 条目:
In [26]: data.fillna(0)
Out[26]: a 1
b 0
c 2
d 0
e 3
dtype: Int32
我们可以指定向前填充以向前传播上一个值:
In [27]: # forward fill
data.fillna(method='ffill')
Out[27]: a 1
b 1
c 2
d 2
e 3
dtype: Int32
或者我们可以指定向后填充以向后传播下一个值:
In [28]: # back fill
data.fillna(method='bfill')
Out[28]: a 1
b 2
c 2
d 3
e 3
dtype: Int32
对于DataFrame,选项类似,但我们还可以指定填充应该沿着的axis:
In [29]: df
Out[29]: 0 1 2 3
0 1.0 NaN 2 NaN
1 2.0 3.0 5 NaN
2 NaN 4.0 6 NaN
In [30]: df.fillna(method='ffill', axis=1)
Out[30]: 0 1 2 3
0 1.0 1.0 2.0 2.0
1 2.0 3.0 5.0 5.0
2 NaN 4.0 6.0 6.0
如果在向前填充时前一个值不可用,NA 值将保留。
第十七章:分层索引
到目前为止,我们主要关注存储在 Pandas Series 和 DataFrame 对象中的一维和二维数据。通常,超出这些维度存储更高维度的数据是有用的——也就是说,数据由超过一个或两个键索引。早期的 Pandas 版本提供了 Panel 和 Panel4D 对象,可以视为二维 DataFrame 的三维或四维类比,但在实践中使用起来有些笨拙。处理更高维数据的更常见模式是利用分层索引(也称为多重索引),在单个索引中包含多个索引级别。通过这种方式,高维数据可以在熟悉的一维 Series 和二维 DataFrame 对象中紧凑地表示。(如果你对带有 Pandas 风格灵活索引的真正的 N 维数组感兴趣,可以查看优秀的Xarray 包。)
在本章中,我们将探讨直接创建 MultiIndex 对象;在多重索引数据中进行索引、切片和计算统计信息时的考虑;以及在简单索引和分层索引数据表示之间进行转换的有用程序。
我们从标准导入开始:
In [1]: import pandas as pd
import numpy as np
一个多重索引的系列
让我们首先考虑如何在一维 Series 中表示二维数据。为了具体起见,我们将考虑一个数据系列,其中每个点都有一个字符和数值键。
不好的方法
假设你想要跟踪两个不同年份的州数据。使用我们已经介绍过的 Pandas 工具,你可能会简单地使用 Python 元组作为键:
In [2]: index = [('California', 2010), ('California', 2020),
('New York', 2010), ('New York', 2020),
('Texas', 2010), ('Texas', 2020)]
populations = [37253956, 39538223,
19378102, 20201249,
25145561, 29145505]
pop = pd.Series(populations, index=index)
pop
Out[2]: (California, 2010) 37253956
(California, 2020) 39538223
(New York, 2010) 19378102
(New York, 2020) 20201249
(Texas, 2010) 25145561
(Texas, 2020) 29145505
dtype: int64
使用这种索引方案,你可以直接根据这个元组索引或切片系列:
In [3]: pop[('California', 2020):('Texas', 2010)]
Out[3]: (California, 2020) 39538223
(New York, 2010) 19378102
(New York, 2020) 20201249
(Texas, 2010) 25145561
dtype: int64
但便利性到此为止。例如,如果你需要选择所有 2010 年的值,你将需要做一些混乱的(可能是缓慢的)整理来实现它:
In [4]: pop[[i for i in pop.index if i[1] == 2010]]
Out[4]: (California, 2010) 37253956
(New York, 2010) 19378102
(Texas, 2010) 25145561
dtype: int64
这会产生期望的结果,但不如我们在 Pandas 中已经喜爱的切片语法那样清晰(或对于大型数据集来说不够高效)。
更好的方法:Pandas 多重索引
幸运的是,Pandas 提供了更好的方法。我们基于元组的索引本质上是一个简单的多重索引,而 Pandas 的 MultiIndex 类型给了我们希望拥有的操作类型。我们可以从元组创建一个多重索引,如下所示:
In [5]: index = pd.MultiIndex.from_tuples(index)
MultiIndex 表示多个索引级别——在这种情况下,州名和年份——以及每个数据点的多个标签,这些标签编码了这些级别。
如果我们使用这个 MultiIndex 重新索引我们的系列,我们将看到数据的分层表示:
In [6]: pop = pop.reindex(index)
pop
Out[6]: California 2010 37253956
2020 39538223
New York 2010 19378102
2020 20201249
Texas 2010 25145561
2020 29145505
dtype: int64
这里 Series 表示法的前两列显示了多个索引值,而第三列显示了数据。请注意,第一列中有些条目缺失:在这种多重索引表示中,任何空白条目表示与上一行相同的值。
现在,要访问所有第二个索引为 2020 的数据,我们可以使用 Pandas 切片表示法:
In [7]: pop[:, 2020]
Out[7]: California 39538223
New York 20201249
Texas 29145505
dtype: int64
结果是一个仅包含我们感兴趣键的单索引 Series。这种语法比我们最初使用的基于元组的多重索引解决方案更方便(并且操作效率更高!)。接下来我们将进一步讨论在具有分层索引数据上进行此类索引操作。
作为额外维度的 MultiIndex
您可能会注意到这里还有另一点:我们本可以使用带有索引和列标签的简单DataFrame存储相同的数据。实际上,Pandas 就是考虑到这种等价性而构建的。unstack方法将快速将一个多重索引的Series转换为传统索引的DataFrame:
In [8]: pop_df = pop.unstack()
pop_df
Out[8]: 2010 2020
California 37253956 39538223
New York 19378102 20201249
Texas 25145561 29145505
自然,stack方法提供了相反的操作:
In [9]: pop_df.stack()
Out[9]: California 2010 37253956
2020 39538223
New York 2010 19378102
2020 20201249
Texas 2010 25145561
2020 29145505
dtype: int64
看到这些,您可能会想知道为什么我们要费心处理分层索引。原因很简单:正如我们能够使用多重索引来操作一个维度的Series中的二维数据一样,我们也可以用它来操作Series或DataFrame中的三维或更高维度的数据。多重索引中的每个额外级别代表了数据的一个额外维度;利用这个特性使我们在能够表示的数据类型上有了更大的灵活性。具体来说,我们可能希望为每个州在每年的人口(例如 18 岁以下人口)添加另一列人口统计数据;使用MultiIndex,这就像在DataFrame中添加另一列数据那样简单:
In [10]: pop_df = pd.DataFrame({'total': pop,
'under18': [9284094, 8898092,
4318033, 4181528,
6879014, 7432474]})
pop_df
Out[10]: total under18
California 2010 37253956 9284094
2020 39538223 8898092
New York 2010 19378102 4318033
2020 20201249 4181528
Texas 2010 25145561 6879014
2020 29145505 7432474
此外,所有在第十五章讨论的 ufunc 和其他功能也适用于层次索引。在此我们计算按年龄小于 18 岁人口的比例,给定上述数据:
In [11]: f_u18 = pop_df['under18'] / pop_df['total']
f_u18.unstack()
Out[11]: 2010 2020
California 0.249211 0.225050
New York 0.222831 0.206994
Texas 0.273568 0.255013
这使我们能够轻松快速地操作和探索甚至是高维数据。
MultiIndex 创建方法
构建一个多重索引的Series或DataFrame最直接的方法是简单地将两个或更多索引数组列表传递给构造函数。例如:
In [12]: df = pd.DataFrame(np.random.rand(4, 2),
index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
columns=['data1', 'data2'])
df
Out[12]: data1 data2
a 1 0.748464 0.561409
2 0.379199 0.622461
b 1 0.701679 0.687932
2 0.436200 0.950664
创建MultiIndex的工作是在后台完成的。
类似地,如果您传递了适当的元组作为键的字典,Pandas 将自动识别并默认使用MultiIndex:
In [13]: data = {('California', 2010): 37253956,
('California', 2020): 39538223,
('New York', 2010): 19378102,
('New York', 2020): 20201249,
('Texas', 2010): 25145561,
('Texas', 2020): 29145505}
pd.Series(data)
Out[13]: California 2010 37253956
2020 39538223
New York 2010 19378102
2020 20201249
Texas 2010 25145561
2020 29145505
dtype: int64
尽管如此,有时明确创建MultiIndex也是有用的;我们将看看几种方法来完成这个操作。
显式 MultiIndex 构造器
为了更灵活地构建索引,您可以使用pd.MultiIndex类中提供的构造方法。例如,就像我们之前做的那样,您可以从给定每个级别索引值的简单数组列表构造一个MultiIndex:
In [14]: pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
Out[14]: MultiIndex([('a', 1),
('a', 2),
('b', 1),
('b', 2)],
)
或者可以通过提供每个点的多重索引值的元组列表来构建它:
In [15]: pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])
Out[15]: MultiIndex([('a', 1),
('a', 2),
('b', 1),
('b', 2)],
)
甚至可以通过单个索引的笛卡尔积构建它:
In [16]: pd.MultiIndex.from_product([['a', 'b'], [1, 2]])
Out[16]: MultiIndex([('a', 1),
('a', 2),
('b', 1),
('b', 2)],
)
同样地,可以直接使用其内部编码通过传递levels(包含每个级别可用索引值的列表的列表)和codes(引用这些标签的列表的列表)构造MultiIndex:
In [17]: pd.MultiIndex(levels=[['a', 'b'], [1, 2]],
codes=[[0, 0, 1, 1], [0, 1, 0, 1]])
Out[17]: MultiIndex([('a', 1),
('a', 2),
('b', 1),
('b', 2)],
)
在创建Series或DataFrame时,可以将任何这些对象作为index参数传递,或者将其传递给现有Series或DataFrame的reindex方法。
多重索引级别名称
有时候给MultiIndex的级别命名会很方便。可以通过在任何先前讨论过的MultiIndex构造函数中传递names参数来实现,或者在事后设置索引的names属性来完成:
In [18]: pop.index.names = ['state', 'year']
pop
Out[18]: state year
California 2010 37253956
2020 39538223
New York 2010 19378102
2020 20201249
Texas 2010 25145561
2020 29145505
dtype: int64
对于更复杂的数据集,这是一种跟踪各种索引值意义的有用方法。
列的多重索引
在DataFrame中,行和列是完全对称的,就像行可以具有多级索引一样,列也可以具有多级索引。考虑以下内容,这是一些(有些逼真的)医疗数据的模拟:
In [19]: # hierarchical indices and columns
index = pd.MultiIndex.from_product([[2013, 2014], [1, 2]],
names=['year', 'visit'])
columns = pd.MultiIndex.from_product([['Bob', 'Guido', 'Sue'],
['HR', 'Temp']],
names=['subject', 'type'])
# mock some data
data = np.round(np.random.randn(4, 6), 1)
data[:, ::2] *= 10
data += 37
# create the DataFrame
health_data = pd.DataFrame(data, index=index, columns=columns)
health_data
Out[19]: subject Bob Guido Sue
type HR Temp HR Temp HR Temp
year visit
2013 1 30.0 38.0 56.0 38.3 45.0 35.8
2 47.0 37.1 27.0 36.0 37.0 36.4
2014 1 51.0 35.9 24.0 36.7 32.0 36.2
2 49.0 36.3 48.0 39.2 31.0 35.7
这基本上是四维数据,维度包括主题、测量类型、年份和访问次数。有了这个设置,例如,我们可以通过人名索引顶级列,并获得一个只包含该人信息的完整DataFrame:
In [20]: health_data['Guido']
Out[20]: type HR Temp
year visit
2013 1 56.0 38.3
2 27.0 36.0
2014 1 24.0 36.7
2 48.0 39.2
对MultiIndex进行索引和切片
在MultiIndex上进行索引和切片设计得很直观,如果将索引视为增加的维度会有所帮助。我们首先看一下如何对多重索引的序列进行索引,然后再看如何对多重索引的数据框进行索引。
多重索引的序列
考虑我们之前看到的州人口的多重索引Series:
In [21]: pop
Out[21]: state year
California 2010 37253956
2020 39538223
New York 2010 19378102
2020 20201249
Texas 2010 25145561
2020 29145505
dtype: int64
我们可以通过使用多个项进行索引来访问单个元素:
In [22]: pop['California', 2010]
Out[22]: 37253956
MultiIndex还支持部分索引,即仅对索引中的一个级别进行索引。结果是另一个Series,保留较低级别的索引:
In [23]: pop['California']
Out[23]: year
2010 37253956
2020 39538223
dtype: int64
可以进行部分切片,只要MultiIndex是排序的(参见“排序和未排序索引”的讨论):
In [24]: poploc['california':'new york']
Out[24]: state year
california 2010 37253956
2020 39538223
new york 2010 19378102
2020 20201249
dtype: int64
对排序索引来说,可以通过在第一个索引中传递空切片来在较低级别执行部分索引:
In [25]: pop[:, 2010]
Out[25]: state
california 37253956
new york 19378102
texas 25145561
dtype: int64
其他类型的索引和选择(在第十四章中讨论)同样适用;例如,基于布尔掩码的选择:
In [26]: pop[pop > 22000000]
Out[26]: state year
California 2010 37253956
2020 39538223
Texas 2010 25145561
2020 29145505
dtype: int64
基于花式索引的选择也是有效的:
In [27]: pop[['California', 'Texas']]
Out[27]: state year
California 2010 37253956
2020 39538223
Texas 2010 25145561
2020 29145505
dtype: int64
多重索引的数据框
多重索引的DataFrame表现方式类似。考虑之前的医疗玩具DataFrame:
In [28]: health_data
Out[28]: subject Bob Guido Sue
type HR Temp HR Temp HR Temp
year visit
2013 1 30.0 38.0 56.0 38.3 45.0 35.8
2 47.0 37.1 27.0 36.0 37.0 36.4
2014 1 51.0 35.9 24.0 36.7 32.0 36.2
2 49.0 36.3 48.0 39.2 31.0 35.7
记住,在DataFrame中,列是主要的,用于多重索引的Series的语法适用于列。例如,我们可以通过简单的操作恢复 Guido 的心率数据:
In [29]: health_data['Guido', 'HR']
Out[29]: year visit
2013 1 56.0
2 27.0
2014 1 24.0
2 48.0
Name: (Guido, HR), dtype: float64
正如单索引情况一样,我们还可以使用在第十四章介绍的loc、iloc和ix索引器。例如:
In [30]: health_data.iloc[:2, :2]
Out[30]: subject Bob
type HR Temp
year visit
2013 1 30.0 38.0
2 47.0 37.1
这些索引器提供了底层二维数据的类似数组的视图,但每个loc或iloc中的单个索引可以传递多个索引的元组。例如:
In [31]: health_data.loc[:, ('Bob', 'HR')]
Out[31]: year visit
2013 1 30.0
2 47.0
2014 1 51.0
2 49.0
Name: (Bob, HR), dtype: float64
在这些索引元组内部工作切片并不特别方便;尝试在元组内创建切片将导致语法错误:
In [32]: health_data.loc[(:, 1), (:, 'HR')]
SyntaxError: invalid syntax (3311942670.py, line 1)
您可以通过使用 Python 的内置slice函数来明确构建所需的切片,但在这种情况下更好的方法是使用IndexSlice对象,Pandas 专门为此提供。例如:
In [33]: idx = pd.IndexSlice
health_data.loc[idx[:, 1], idx[:, 'HR']]
Out[33]: subject Bob Guido Sue
type HR HR HR
year visit
2013 1 30.0 56.0 45.0
2014 1 51.0 24.0 32.0
如您所见,在多重索引的Series和DataFrame中与数据交互的方式有很多,并且与本书中的许多工具一样,熟悉它们的最佳方法是尝试它们!
重新排列多重索引
处理多重索引数据的关键之一是知道如何有效地转换数据。有许多操作将保留数据集中的所有信息,但为各种计算目的重新排列数据。我们在stack和unstack方法中看到了一个简短的示例,但在控制数据在层次索引和列之间重新排列方面,还有许多其他方法,我们将在这里探讨它们。
排序和未排序的索引
我之前简要提到过一个警告,但我应该在这里更加强调。如果索引未排序,则许多MultiIndex切片操作将失败。让我们仔细看看。
我们将从创建一些简单的多重索引数据开始,其中索引未按词典顺序排序:
In [34]: index = pd.MultiIndex.from_product([['a', 'c', 'b'], [1, 2]])
data = pd.Series(np.random.rand(6), index=index)
data.index.names = ['char', 'int']
data
Out[34]: char int
a 1 0.280341
2 0.097290
c 1 0.206217
2 0.431771
b 1 0.100183
2 0.015851
dtype: float64
如果我们尝试对这个索引进行部分切片,将导致错误:
In [35]: try:
data['a':'b']
except KeyError as e:
print("KeyError", e)
KeyError 'Key length (1) was greater than MultiIndex lexsort depth (0)'
尽管从错误消息中不完全清楚,但这是由于MultiIndex未排序造成的。出于各种原因,部分切片和其他类似操作要求MultiIndex中的级别按排序(即词典)顺序排列。Pandas 提供了许多方便的例程来执行这种类型的排序,例如DataFrame的sort_index和sortlevel方法。我们在这里将使用最简单的sort_index:
In [36]: data = data.sort_index()
data
Out[36]: char int
a 1 0.280341
2 0.097290
b 1 0.100183
2 0.015851
c 1 0.206217
2 0.431771
dtype: float64
当索引以这种方式排序时,部分切片将按预期工作:
In [37]: data['a':'b']
Out[37]: char int
a 1 0.280341
2 0.097290
b 1 0.100183
2 0.015851
dtype: float64
堆叠和展开索引
正如我们之前简要看到的,可以将数据集从堆叠的多重索引转换为简单的二维表示,可选地指定要使用的级别:
In [38]: pop.unstack(level=0)
Out[38]: year 2010 2020
state
California 37253956 39538223
New York 19378102 20201249
Texas 25145561 29145505
In [39]: pop.unstack(level=1)
Out[39]: state year
California 2010 37253956
2020 39538223
New York 2010 19378102
2020 20201249
Texas 2010 25145561
2020 29145505
dtype: int64
unstack的相反操作是stack,在这里可以用它来恢复原始系列:
In [40]: pop.unstack().stack()
Out[40]: state year
California 2010 37253956
2020 39538223
New York 2010 19378102
2020 20201249
Texas 2010 25145561
2020 29145505
dtype: int64
索引设置和重置
重新排列分层数据的另一种方法是将索引标签转换为列;这可以通过reset_index方法来实现。对人口字典调用此方法将导致一个DataFrame,其中包含state和year列,这些列保存了以前在索引中的信息。为了清晰起见,我们可以选择指定数据的名称作为列的表示方式:
In [41]: pop_flat = pop.reset_index(name='population')
pop_flat
Out[41]: state year population
0 California 2010 37253956
1 California 2020 39538223
2 New York 2010 19378102
3 New York 2020 20201249
4 Texas 2010 25145561
5 Texas 2020 29145505
一个常见的模式是从列值构建一个MultiIndex。这可以通过DataFrame的set_index方法来实现,该方法返回一个多重索引的DataFrame:
In [42]: pop_flat.set_index(['state', 'year'])
Out[42]: population
state year
California 2010 37253956
2020 39538223
New York 2010 19378102
2020 20201249
Texas 2010 25145561
2020 29145505
在实践中,这种重新索引的方式是探索实际数据集时最有用的模式之一。
第十八章:组合数据集:concat 和 append
一些最有趣的数据研究来自于结合不同的数据源。这些操作可以涉及从两个不同数据集的非常简单的连接到更复杂的数据库风格的联接和合并,正确处理数据集之间的任何重叠。Series和DataFrame是专为这类操作而构建的,Pandas 包含使这种数据处理快速和简单的函数和方法。
在这里,我们将使用pd.concat函数查看Series和DataFrame的简单连接;稍后我们将深入探讨 Pandas 中实现的更复杂的内存合并和连接。
我们从标准导入开始:
In [1]: import pandas as pd
import numpy as np
为方便起见,我们将定义这个函数,它创建一个特定形式的DataFrame,在接下来的示例中将非常有用:
In [2]: def make_df(cols, ind):
"""Quickly make a DataFrame"""
data = {c: [str(c) + str(i) for i in ind]
for c in cols}
return pd.DataFrame(data, ind)
# example DataFrame
make_df('ABC', range(3))
Out[2]: A B C
0 A0 B0 C0
1 A1 B1 C1
2 A2 B2 C2
另外,我们将创建一个快速的类,允许我们将多个DataFrame并排显示。该代码利用了特殊的_repr_html_方法,IPython/Jupyter 用它来实现其丰富的对象显示:
In [3]: class display(object):
"""Display HTML representation of multiple objects"""
template = """<div style="float: left; padding: 10px;">
<p style='font-family:"Courier New", Courier, monospace'>{0}{1}
"""
def __init__(self, *args):
self.args = args
def _repr_html_(self):
return '\n'.join(self.template.format(a, eval(a)._repr_html_())
for a in self.args)
def __repr__(self):
return '\n\n'.join(a + '\n' + repr(eval(a))
for a in self.args)
随着我们在以下部分继续讨论,使用这个将会更加清晰。
回顾:NumPy 数组的连接
Series和DataFrame对象的连接行为与 NumPy 数组的连接类似,可以通过np.concatenate函数完成,如第五章中所讨论的那样。记住,您可以使用它将两个或多个数组的内容合并为单个数组:
In [4]: x = [1, 2, 3]
y = [4, 5, 6]
z = [7, 8, 9]
np.concatenate([x, y, z])
Out[4]: array([1, 2, 3, 4, 5, 6, 7, 8, 9])
第一个参数是要连接的数组的列表或元组。此外,在多维数组的情况下,它接受一个axis关键字,允许您指定沿其进行连接的轴:
In [5]: x = [[1, 2],
[3, 4]]
np.concatenate([x, x], axis=1)
Out[5]: array([[1, 2, 1, 2],
[3, 4, 3, 4]])
使用pd.concat进行简单连接
pd.concat函数提供了与np.concatenate类似的语法,但包含我们稍后将讨论的多个选项:
# Signature in Pandas v1.3.5
pd.concat(objs, axis=0, join='outer', ignore_index=False, keys=None,
levels=None, names=None, verify_integrity=False,
sort=False, copy=True)
pd.concat可用于简单连接Series或DataFrame对象,就像np.concatenate可用于数组的简单连接一样:
In [6]: ser1 = pd.Series(['A', 'B', 'C'], index=[1, 2, 3])
ser2 = pd.Series(['D', 'E', 'F'], index=[4, 5, 6])
pd.concat([ser1, ser2])
Out[6]: 1 A
2 B
3 C
4 D
5 E
6 F
dtype: object
它还可以用于连接更高维度的对象,如DataFrame:
In [7]: df1 = make_df('AB', [1, 2])
df2 = make_df('AB', [3, 4])
display('df1', 'df2', 'pd.concat([df1, df2])')
Out[7]: df1 df2 pd.concat([df1, df2])
A B A B A B
1 A1 B1 3 A3 B3 1 A1 B1
2 A2 B2 4 A4 B4 2 A2 B2
3 A3 B3
4 A4 B4
其默认行为是在DataFrame内按行连接(即axis=0)。与np.concatenate类似,pd.concat允许指定沿其进行连接的轴。考虑以下示例:
In [8]: df3 = make_df('AB', [0, 1])
df4 = make_df('CD', [0, 1])
display('df3', 'df4', "pd.concat([df3, df4], axis='columns')")
Out[8]: df3 df4 pd.concat([df3, df4], axis='columns')
A B C D A B C D
0 A0 B0 0 C0 D0 0 A0 B0 C0 D0
1 A1 B1 1 C1 D1 1 A1 B1 C1 D1
我们也可以等效地指定axis=1;这里我们使用了更直观的axis='columns'。
重复的索引
np.concatenate和pd.concat之间的一个重要区别是,Pandas 的连接保留索引,即使结果会有重复的索引!考虑以下简单示例:
In [9]: x = make_df('AB', [0, 1])
y = make_df('AB', [2, 3])
y.index = x.index # make indices match
display('x', 'y', 'pd.concat([x, y])')
Out[9]: x y pd.concat([x, y])
A B A B A B
0 A0 B0 0 A2 B2 0 A0 B0
1 A1 B1 1 A3 B3 1 A1 B1
0 A2 B2
1 A3 B3
注意结果中的重复索引。虽然这在DataFrame中是有效的,但结果通常不理想。pd.concat提供了几种处理方法。
将重复的索引视为错误处理
如果你想简单地验证pd.concat的结果中的索引是否重叠,可以包含verify_integrity标志。将其设置为True,如果存在重复索引,连接将引发异常。以下是一个示例,为了清晰起见,我们将捕获并打印错误消息:
In [10]: try:
pd.concat([x, y], verify_integrity=True)
except ValueError as e:
print("ValueError:", e)
ValueError: Indexes have overlapping values: Int64Index([0, 1], dtype='int64')
忽略索引
有时索引本身并不重要,你更希望它被简单地忽略。可以使用ignore_index标志指定此选项。将其设置为True,连接将为结果的DataFrame创建一个新的整数索引:
In [11]: display('x', 'y', 'pd.concat([x, y], ignore_index=True)')
Out[11]: x y pd.concat([x, y], ignore_index=True)
A B A B A B
0 A0 B0 0 A2 B2 0 A0 B0
1 A1 B1 1 A3 B3 1 A1 B1
2 A2 B2
3 A3 B3
添加 MultiIndex 键
另一个选项是使用keys选项指定数据源的标签;结果将是一个具有层次索引的系列,其中包含数据:
In [12]: display('x', 'y', "pd.concat([x, y], keys=['x', 'y'])")
Out[12]: x y pd.concat([x, y], keys=['x', 'y'])
A B A B A B
0 A0 B0 0 A2 B2 x 0 A0 B0
1 A1 B1 1 A3 B3 1 A1 B1
y 0 A2 B2
1 A3 B3
我们可以使用第十七章中讨论的工具将这个多重索引的 DataFrame 转换为我们感兴趣的表示形式。
使用连接进行连接
在我们刚刚查看的短示例中,我们主要是连接具有共享列名的 DataFrame。在实践中,来自不同来源的数据可能具有不同的列名集,pd.concat 在这种情况下提供了几个选项。考虑以下两个 DataFrame 的连接,它们具有一些(但不是全部!)共同的列:
In [13]: df5 = make_df('ABC', [1, 2])
df6 = make_df('BCD', [3, 4])
display('df5', 'df6', 'pd.concat([df5, df6])')
Out[13]: df5 df6 pd.concat([df5, df6])
A B C B C D A B C D
1 A1 B1 C1 3 B3 C3 D3 1 A1 B1 C1 NaN
2 A2 B2 C2 4 B4 C4 D4 2 A2 B2 C2 NaN
3 NaN B3 C3 D3
4 NaN B4 C4 D4
默认行为是用 NA 值填充无可用数据的条目。要更改这一点,可以调整concat函数的join参数。默认情况下,连接是输入列的并集(join='outer'),但我们可以使用join='inner'将其更改为列的交集:
In [14]: display('df5', 'df6',
"pd.concat([df5, df6], join='inner')")
Out[14]: df5 df6
A B C B C D
1 A1 B1 C1 3 B3 C3 D3
2 A2 B2 C2 4 B4 C4 D4
pd.concat([df5, df6], join='inner')
B C
1 B1 C1
2 B2 C2
3 B3 C3
4 B4 C4
另一个有用的模式是在连接之前使用reindex方法对要丢弃的列进行更精细的控制:
In [15]: pd.concat([df5, df6.reindex(df5.columns, axis=1)])
Out[15]: A B C
1 A1 B1 C1
2 A2 B2 C2
3 NaN B3 C3
4 NaN B4 C4
append 方法
因为直接数组连接是如此常见,Series 和 DataFrame 对象具有一个append方法,可以用更少的按键完成相同的操作。例如,可以使用 df1.append(df2) 替代 pd.concat([df1, df2]):
In [16]: display('df1', 'df2', 'df1.append(df2)')
Out[16]: df1 df2 df1.append(df2)
A B A B A B
1 A1 B1 3 A3 B3 1 A1 B1
2 A2 B2 4 A4 B4 2 A2 B2
3 A3 B3
4 A4 B4
请注意,与 Python 列表的 append 和 extend 方法不同,Pandas 中的 append 方法不会修改原始对象;相反,它会创建一个包含组合数据的新对象。它也不是一种非常有效的方法,因为它涉及到新索引的创建 以及 数据缓冲区。因此,如果你计划进行多个 append 操作,通常最好建立一个 DataFrame 对象的列表,并一次性将它们全部传递给 concat 函数。
在下一章中,我们将介绍一种更强大的方法来组合来自多个来源的数据:pd.merge 中实现的数据库风格的合并/连接。有关 concat、append 和相关功能的更多信息,请参阅Pandas 文档中的“Merge, Join, Concatenate and Compare”。
第十九章:合并数据集:merge 和 join
Pandas 提供的一个重要功能是其高性能、内存中的连接和合并操作,如果你曾经使用过数据库,可能对此有所了解。主要接口是pd.merge函数,我们将看到几个示例,说明其实际操作方式。
为方便起见,在通常的导入之后,我们再次定义从上一章中定义的display函数:
In [1]: import pandas as pd
import numpy as np
class display(object):
"""Display HTML representation of multiple objects"""
template = """<div style="float: left; padding: 10px;">
<p style='font-family:"Courier New", Courier, monospace'>{0}{1}
"""
def __init__(self, *args):
self.args = args
def _repr_html_(self):
return '\n'.join(self.template.format(a, eval(a)._repr_html_())
for a in self.args)
def __repr__(self):
return '\n\n'.join(a + '\n' + repr(eval(a))
for a in self.args)
关系代数
pd.merge中实现的行为是所谓的关系代数的一个子集,这是一组操作关系数据的正式规则,形成了大多数数据库中可用操作的概念基础。关系代数方法的优势在于它提出了几个基本操作,这些操作成为任何数据集上更复杂操作的基础。通过在数据库或其他程序中高效实现这些基本操作的词汇,可以执行广泛范围的相当复杂的组合操作。
Pandas 在pd.merge函数和Series和DataFrame对象的相关join方法中实现了几个这些基本构建块。正如你将看到的,这些功能让你能够有效地链接来自不同来源的数据。
合并的类别
pd.merge函数实现了几种类型的连接:一对一、多对一和多对多。通过对pd.merge接口进行相同的调用来访问这三种连接类型;所执行的连接类型取决于输入数据的形式。我们将从三种合并类型的简单示例开始,并稍后讨论详细的选项。
一对一连接
或许最简单的合并类型是一对一连接,这在许多方面类似于你在第十八章中看到的逐列串联。作为具体示例,请考虑以下两个包含公司几名员工信息的DataFrame对象:
In [2]: df1 = pd.DataFrame({'employee': ['Bob', 'Jake', 'Lisa', 'Sue'],
'group': ['Accounting', 'Engineering',
'Engineering', 'HR']})
df2 = pd.DataFrame({'employee': ['Lisa', 'Bob', 'Jake', 'Sue'],
'hire_date': [2004, 2008, 2012, 2014]})
display('df1', 'df2')
Out[2]: df1 df2
employee group employee hire_date
0 Bob Accounting 0 Lisa 2004
1 Jake Engineering 1 Bob 2008
2 Lisa Engineering 2 Jake 2012
3 Sue HR 3 Sue 2014
要将这些信息合并到一个DataFrame中,我们可以使用pd.merge函数:
In [3]: df3 = pd.merge(df1, df2)
df3
Out[3]: employee group hire_date
0 Bob Accounting 2008
1 Jake Engineering 2012
2 Lisa Engineering 2004
3 Sue HR 2014
pd.merge函数会识别每个DataFrame都有一个employee列,并自动使用该列作为键进行连接。合并的结果是一个新的DataFrame,它合并了两个输入的信息。请注意,每列中的条目顺序不一定保持一致:在这种情况下,df1和df2中的employee列顺序不同,pd.merge函数能够正确处理这一点。此外,请记住,一般情况下合并会丢弃索引,除非是通过索引进行合并(参见left_index和right_index关键字,稍后讨论)。
多对一连接
多对一连接是其中一个键列包含重复条目的连接。对于多对一情况,结果的DataFrame将适当地保留这些重复条目。考虑以下多对一连接的示例:
In [4]: df4 = pd.DataFrame({'group': ['Accounting', 'Engineering', 'HR'],
'supervisor': ['Carly', 'Guido', 'Steve']})
display('df3', 'df4', 'pd.merge(df3, df4)')
Out[4]: df3 df4
employee group hire_date group supervisor
0 Bob Accounting 2008 0 Accounting Carly
1 Jake Engineering 2012 1 Engineering Guido
2 Lisa Engineering 2004 2 HR Steve
3 Sue HR 2014
pd.merge(df3, df4)
employee group hire_date supervisor
0 Bob Accounting 2008 Carly
1 Jake Engineering 2012 Guido
2 Lisa Engineering 2004 Guido
3 Sue HR 2014 Steve
结果的DataFrame具有一个额外的列,其中“supervisor”信息重复出现在一个或多个位置,根据输入的要求。
多对多连接
多对多连接在概念上可能有点混乱,但仍然定义良好。如果左侧和右侧数组中的键列包含重复项,则结果是多对多合并。通过一个具体的例子可能更清楚。考虑以下例子,其中我们有一个显示特定组与一个或多个技能相关联的DataFrame。
通过执行多对多连接,我们可以恢复与任何个人相关联的技能:
In [5]: df5 = pd.DataFrame({'group': ['Accounting', 'Accounting',
'Engineering', 'Engineering', 'HR', 'HR'],
'skills': ['math', 'spreadsheets', 'software', 'math',
'spreadsheets', 'organization']})
display('df1', 'df5', "pd.merge(df1, df5)")
Out[5]: df1 df5
employee group group skills
0 Bob Accounting 0 Accounting math
1 Jake Engineering 1 Accounting spreadsheets
2 Lisa Engineering 2 Engineering software
3 Sue HR 3 Engineering math
4 HR spreadsheets
5 HR organization
pd.merge(df1, df5)
employee group skills
0 Bob Accounting math
1 Bob Accounting spreadsheets
2 Jake Engineering software
3 Jake Engineering math
4 Lisa Engineering software
5 Lisa Engineering math
6 Sue HR spreadsheets
7 Sue HR organization
这三种类型的连接可以与其他 Pandas 工具一起使用,实现广泛的功能。但实际上,数据集很少像我们这里使用的那样干净。在下一节中,我们将考虑由pd.merge提供的一些选项,这些选项使您能够调整连接操作的工作方式。
指定合并键
我们已经看到了pd.merge的默认行为:它查找两个输入之间一个或多个匹配的列名,并将其用作键。然而,通常列名不会那么匹配,pd.merge提供了多种选项来处理这种情况。
关键字 on
最简单的方法是使用on关键字明确指定键列的名称,该关键字接受列名或列名列表:
In [6]: display('df1', 'df2', "pd.merge(df1, df2, on='employee')")
Out[6]: df1 df2
employee group employee hire_date
0 Bob Accounting 0 Lisa 2004
1 Jake Engineering 1 Bob 2008
2 Lisa Engineering 2 Jake 2012
3 Sue HR 3 Sue 2014
pd.merge(df1, df2, on='employee')
employee group hire_date
0 Bob Accounting 2008
1 Jake Engineering 2012
2 Lisa Engineering 2004
3 Sue HR 2014
此选项仅在左侧和右侧的DataFrame都具有指定的列名时有效。
关键字 left_on 和 right_on
有时您可能希望合并两个具有不同列名的数据集;例如,我们可能有一个数据集,其中员工姓名标记为“name”而不是“employee”。在这种情况下,我们可以使用left_on和right_on关键字来指定这两个列名:
In [7]: df3 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'],
'salary': [70000, 80000, 120000, 90000]})
display('df1', 'df3', 'pd.merge(df1, df3, left_on="employee",
right_on="name")')
Out[7]: df1 df3
employee group name salary
0 Bob Accounting 0 Bob 70000
1 Jake Engineering 1 Jake 80000
2 Lisa Engineering 2 Lisa 120000
3 Sue HR 3 Sue 90000
pd.merge(df1, df3, left_on="employee", right_on="name")
employee group name salary
0 Bob Accounting Bob 70000
1 Jake Engineering Jake 80000
2 Lisa Engineering Lisa 120000
3 Sue HR Sue 90000
如果需要,可以使用DataFrame.drop()方法删除多余的列:
In [8]: pd.merge(df1, df3, left_on="employee", right_on="name").drop('name', axis=1)
Out[8]: employee group salary
0 Bob Accounting 70000
1 Jake Engineering 80000
2 Lisa Engineering 120000
3 Sue HR 90000
左索引和右索引关键字
有时,而不是在列上进行合并,您可能希望在索引上进行合并。例如,您的数据可能如下所示:
In [9]: df1a = df1.set_index('employee')
df2a = df2.set_index('employee')
display('df1a', 'df2a')
Out[9]: df1a df2a
group hire_date
employee employee
Bob Accounting Lisa 2004
Jake Engineering Bob 2008
Lisa Engineering Jake 2012
Sue HR Sue 2014
您可以通过在pd.merge()中指定left_index和/或right_index标志,将索引用作合并的键:
In [10]: display('df1a', 'df2a',
"pd.merge(df1a, df2a, left_index=True, right_index=True)")
Out[10]: df1a df2a
group hire_date
employee employee
Bob Accounting Lisa 2004
Jake Engineering Bob 2008
Lisa Engineering Jake 2012
Sue HR Sue 2014
pd.merge(df1a, df2a, left_index=True, right_index=True)
group hire_date
employee
Bob Accounting 2008
Jake Engineering 2012
Lisa Engineering 2004
Sue HR 2014
为方便起见,Pandas 包括DataFrame.join()方法,它执行基于索引的合并而无需额外的关键字:
In [11]: df1a.join(df2a)
Out[11]: group hire_date
employee
Bob Accounting 2008
Jake Engineering 2012
Lisa Engineering 2004
Sue HR 2014
如果您希望混合索引和列,可以将left_index与right_on或left_on与right_index结合使用,以获得所需的行为:
In [12]: display('df1a', 'df3', "pd.merge(df1a, df3, left_index=True,
right_on='name')")
Out[12]: df1a df3
group name salary
employee 0 Bob 70000
Bob Accounting 1 Jake 80000
Jake Engineering 2 Lisa 120000
Lisa Engineering 3 Sue 90000
Sue HR
pd.merge(df1a, df3, left_index=True, right_on='name')
group name salary
0 Accounting Bob 70000
1 Engineering Jake 80000
2 Engineering Lisa 120000
3 HR Sue 90000
所有这些选项也适用于多个索引和/或多个列;这种行为的界面非常直观。有关更多信息,请参阅Pandas 文档中的“Merge, Join, and Concatenate”部分。
指定连接的集合算术
在所有前面的示例中,我们忽略了在执行连接时的一个重要考虑因素:连接中使用的集合算术类型。当一个值出现在一个键列中而不出现在另一个键列中时,就会出现这种情况。考虑这个例子:
In [13]: df6 = pd.DataFrame({'name': ['Peter', 'Paul', 'Mary'],
'food': ['fish', 'beans', 'bread']},
columns=['name', 'food'])
df7 = pd.DataFrame({'name': ['Mary', 'Joseph'],
'drink': ['wine', 'beer']},
columns=['name', 'drink'])
display('df6', 'df7', 'pd.merge(df6, df7)')
Out[13]: df6 df7
name food name drink
0 Peter fish 0 Mary wine
1 Paul beans 1 Joseph beer
2 Mary bread
pd.merge(df6, df7)
name food drink
0 Mary bread wine
在这里,我们已经合并了两个仅具有一个共同“name”条目的数据集:Mary。默认情况下,结果包含输入集合的交集;这称为内连接。我们可以使用how关键字显式指定为"inner":
In [14]: pd.merge(df6, df7, how='inner')
Out[14]: name food drink
0 Mary bread wine
how关键字的其他选项包括'outer'、'left'和'right'。外连接返回输入列的并集并用 NA 填充缺失值:
In [15]: display('df6', 'df7', "pd.merge(df6, df7, how='outer')")
Out[15]: df6 df7
name food name drink
0 Peter fish 0 Mary wine
1 Paul beans 1 Joseph beer
2 Mary bread
pd.merge(df6, df7, how='outer')
name food drink
0 Peter fish NaN
1 Paul beans NaN
2 Mary bread wine
3 Joseph NaN beer
左连接和右连接分别返回左输入和右输入的连接。例如:
In [16]: display('df6', 'df7', "pd.merge(df6, df7, how='left')")
Out[16]: df6 df7
name food name drink
0 Peter fish 0 Mary wine
1 Paul beans 1 Joseph beer
2 Mary bread
pd.merge(df6, df7, how='left')
name food drink
0 Peter fish NaN
1 Paul beans NaN
2 Mary bread wine
现在输出行对应于左输入中的条目。使用how='right'的方式也类似工作。
所有这些选项都可以直接应用于之前的任何连接类型。
重叠的列名:后缀关键字
最后,您可能会遇到两个输入DataFrame具有冲突列名的情况。考虑这个例子:
In [17]: df8 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'],
'rank': [1, 2, 3, 4]})
df9 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'],
'rank': [3, 1, 4, 2]})
display('df8', 'df9', 'pd.merge(df8, df9, on="name")')
Out[17]: df8 df9
name rank name rank
0 Bob 1 0 Bob 3
1 Jake 2 1 Jake 1
2 Lisa 3 2 Lisa 4
3 Sue 4 3 Sue 2
pd.merge(df8, df9, on="name")
name rank_x rank_y
0 Bob 1 3
1 Jake 2 1
2 Lisa 3 4
3 Sue 4 2
因为输出将具有两个冲突的列名,merge函数会自动附加后缀_x和_y以使输出列唯一。如果这些默认值不合适,可以使用suffixes关键字指定自定义后缀:
In [18]: pd.merge(df8, df9, on="name", suffixes=["_L", "_R"])
Out[18]: name rank_L rank_R
0 Bob 1 3
1 Jake 2 1
2 Lisa 3 4
3 Sue 4 2
这些后缀适用于可能的任何连接模式,并且在多个重叠列的情况下也适用。
在第二十章中,我们将深入探讨关系代数。有关更多讨论,请参阅 Pandas 文档中的“Merge, Join, Concatenate and Compare”部分。
示例:美国各州数据
在合并数据来自不同来源时,合并和连接操作经常出现。在这里,我们将考虑一些关于美国各州及其人口数据的数据示例:
In [19]: # Following are commands to download the data
# repo = "https://raw.githubusercontent.com/jakevdp/data-USstates/master"
# !cd data && curl -O {repo}/state-population.csv
# !cd data && curl -O {repo}/state-areas.csv
# !cd data && curl -O {repo}/state-abbrevs.csv
让我们使用 Pandas 的read_csv函数查看这三个数据集:
In [20]: pop = pd.read_csv('data/state-population.csv')
areas = pd.read_csv('data/state-areas.csv')
abbrevs = pd.read_csv('data/state-abbrevs.csv')
display('pop.head()', 'areas.head()', 'abbrevs.head()')
Out[20]: pop.head()
state/region ages year population
0 AL under18 2012 1117489.0
1 AL total 2012 4817528.0
2 AL under18 2010 1130966.0
3 AL total 2010 4785570.0
4 AL under18 2011 1125763.0
areas.head()
state area (sq. mi)
0 Alabama 52423
1 Alaska 656425
2 Arizona 114006
3 Arkansas 53182
4 California 163707
abbrevs.head()
state abbreviation
0 Alabama AL
1 Alaska AK
2 Arizona AZ
3 Arkansas AR
4 California CA
根据这些信息,假设我们想要计算一个相对简单的结果:按照 2010 年人口密度对美国各州和领地进行排名。显然,我们在这里有数据来找到这个结果,但我们需要合并数据集来实现这一点。
我们将从一个多对一的合并开始,这将使我们在人口DataFrame中得到完整的州名。我们要基于pop的state/region列和abbrevs的abbreviation列进行合并。我们将使用how='outer'以确保由于标签不匹配而丢失数据:
In [21]: merged = pd.merge(pop, abbrevs, how='outer',
left_on='state/region', right_on='abbreviation')
merged = merged.drop('abbreviation', axis=1) # drop duplicate info
merged.head()
Out[21]: state/region ages year population state
0 AL under18 2012 1117489.0 Alabama
1 AL total 2012 4817528.0 Alabama
2 AL under18 2010 1130966.0 Alabama
3 AL total 2010 4785570.0 Alabama
4 AL under18 2011 1125763.0 Alabama
让我们再次仔细检查是否存在任何不匹配,可以通过查找具有空值的行来完成:
In [22]: merged.isnull().any()
Out[22]: state/region False
ages False
year False
population True
state True
dtype: bool
一些population值为 null;让我们找出它们是哪些!
In [23]: merged[merged['population'].isnull()].head()
Out[23]: state/region ages year population state
2448 PR under18 1990 NaN NaN
2449 PR total 1990 NaN NaN
2450 PR total 1991 NaN NaN
2451 PR under18 1991 NaN NaN
2452 PR total 1993 NaN NaN
所有空值人口数据似乎来自于 2000 年之前的波多黎各;这很可能是因为原始来源中没有这些数据。
更重要的是,我们看到一些新的state条目也为空,这意味着在abbrevs键中没有相应的条目!让我们找出哪些地区缺少这种匹配:
In [24]: merged.loc[merged['state'].isnull(), 'state/region'].unique()
Out[24]: array(['PR', 'USA'], dtype=object)
我们可以快速推断问题所在:我们的人口数据包括了波多黎各(PR)和整个美国(USA)的条目,而这些条目在州缩写键中并未出现。我们可以通过填写适当的条目来快速修复这些问题:
In [25]: merged.loc[merged['state/region'] == 'PR', 'state'] = 'Puerto Rico'
merged.loc[merged['state/region'] == 'USA', 'state'] = 'United States'
merged.isnull().any()
Out[25]: state/region False
ages False
year False
population True
state False
dtype: bool
state列中不再有空值:我们已经准备就绪!
现在我们可以使用类似的过程将结果与区域数据合并。检查我们的结果时,我们将希望在state列上进行连接:
In [26]: final = pd.merge(merged, areas, on='state', how='left')
final.head()
Out[26]: state/region ages year population state area (sq. mi)
0 AL under18 2012 1117489.0 Alabama 52423.0
1 AL total 2012 4817528.0 Alabama 52423.0
2 AL under18 2010 1130966.0 Alabama 52423.0
3 AL total 2010 4785570.0 Alabama 52423.0
4 AL under18 2011 1125763.0 Alabama 52423.0
再次,让我们检查是否存在空值以查看是否存在任何不匹配:
In [27]: final.isnull().any()
Out[27]: state/region False
ages False
year False
population True
state False
area (sq. mi) True
dtype: bool
area列中有空值;我们可以查看这里被忽略的地区是哪些:
In [28]: final['state'][final['area (sq. mi)'].isnull()].unique()
Out[28]: array(['United States'], dtype=object)
我们发现我们的areas DataFrame中并不包含整个美国的面积。我们可以插入适当的值(例如使用所有州面积的总和),但在这种情况下,我们将仅删除空值,因为整个美国的人口密度与我们当前的讨论无关:
In [29]: final.dropna(inplace=True)
final.head()
Out[29]: state/region ages year population state area (sq. mi)
0 AL under18 2012 1117489.0 Alabama 52423.0
1 AL total 2012 4817528.0 Alabama 52423.0
2 AL under18 2010 1130966.0 Alabama 52423.0
3 AL total 2010 4785570.0 Alabama 52423.0
4 AL under18 2011 1125763.0 Alabama 52423.0
现在我们已经拥有了所有需要的数据。为了回答我们感兴趣的问题,让我们首先选择与 2010 年对应的数据部分和总人口。我们将使用query函数来快速完成这一点(这需要安装 NumExpr 包,请参阅第二十四章):
In [30]: data2010 = final.query("year == 2010 & ages == 'total'")
data2010.head()
Out[30]: state/region ages year population state area (sq. mi)
3 AL total 2010 4785570.0 Alabama 52423.0
91 AK total 2010 713868.0 Alaska 656425.0
101 AZ total 2010 6408790.0 Arizona 114006.0
189 AR total 2010 2922280.0 Arkansas 53182.0
197 CA total 2010 37333601.0 California 163707.0
现在让我们计算人口密度并按顺序显示结果。我们将首先根据州重新索引我们的数据,然后计算结果:
In [31]: data2010.set_index('state', inplace=True)
density = data2010['population'] / data2010['area (sq. mi)']
In [32]: density.sort_values(ascending=False, inplace=True)
density.head()
Out[32]: state
District of Columbia 8898.897059
Puerto Rico 1058.665149
New Jersey 1009.253268
Rhode Island 681.339159
Connecticut 645.600649
dtype: float64
结果是美国各州,以及华盛顿特区和波多黎各,按照其 2010 年人口密度(每平方英里居民数)的排名。我们可以看到,数据集中迄今为止最密集的地区是华盛顿特区(即哥伦比亚特区);在各州中,密度最大的是新泽西州。
我们还可以检查列表的末尾:
In [33]: density.tail()
Out[33]: state
South Dakota 10.583512
North Dakota 9.537565
Montana 6.736171
Wyoming 5.768079
Alaska 1.087509
dtype: float64
我们看到迄今为止最稀疏的州是阿拉斯加,平均每平方英里略高于一名居民。
当尝试使用真实数据源回答问题时,这种数据合并是一项常见任务。希望这个例子给您提供了一些想法,展示了如何结合我们涵盖的工具来从数据中获取洞察!