Python-数据科学手册第二版-三-

119 阅读1小时+

Python 数据科学手册第二版(三)

原文:zh.annas-archive.org/md5/051facaf2908ae8198253e3a14b09ec1

译者:飞龙

协议:CC BY-NC-SA 4.0

第十四章:数据索引和选择

在第二部分中,我们详细讨论了访问、设置和修改 NumPy 数组中的值的方法和工具。这些包括索引(例如arr[2, 1])、切片(例如arr[:, 1:5])、掩码(例如arr[arr > 0])、花式索引(例如arr[0, [1, 5]])以及它们的组合(例如arr[:, [1, 5]])。在这里,我们将看一下类似的方法来访问和修改 Pandas SeriesDataFrame对象中的值。如果你使用过 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 代码的一个指导原则是“明确优于隐式”。lociloc的显式特性使它们在保持代码清晰和可读性方面非常有帮助;特别是在整数索引的情况下,始终一致地使用它们可以防止由于混合索引/切片约定而导致的微妙错误。

数据框选择

回想一下,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 再次使用了前面提到的lociloc索引器。使用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 的 SeriesDataFrame 对象上使用。让我们先定义一个简单的 SeriesDataFrame 来演示这一点:

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:索引对齐

对于两个 SeriesDataFrame 对象的二元操作,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,但允许可选地显式指定AB中可能缺失元素的填充值:

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 之间的操作

当对DataFrameSeries进行操作时,索引和列的对齐方式类似地保持,并且结果类似于二维数组和一维 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 有两种存储和操作空值的“模式”:

  • 默认模式是使用基于哨兵值的缺失数据方案,哨兵值为NaNNone,具体取决于数据类型。

  • 或者,您可以选择使用 Pandas 提供的可空数据类型(dtypes)(稍后在本章讨论),这将导致创建一个伴随的掩码数组来跟踪缺失的条目。然后,这些缺失的条目将被呈现给用户作为特殊的pd.NA值。

无论哪种情况,Pandas API 提供的数据操作和操作将以可预测的方式处理和传播这些缺失的条目。但是为了对为什么会做出这些选择有一些直觉,让我们快速探讨一下NoneNaNNA中固有的权衡。像往常一样,我们将从导入 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的算术运算,像summin这样的聚合通常会导致错误:

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

NaNNone都有它们的用途,而且 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无变化Nonenp.nan
integer转换为float64np.nan
boolean转换为objectNonenp.nan

请记住,在 Pandas 中,字符串数据始终以object类型存储。

Pandas 可空数据类型

在早期版本的 Pandas 中,NaNNone作为哨兵值是唯一可用的缺失数据表示。这引入的主要困难是隐式类型转换:例如,无法表示真正的整数数组带有缺失数据。

为了解决这个问题,Pandas 后来添加了可空数据类型,它们通过名称的大写区分于常规数据类型(例如,pd.Int32np.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 将 NoneNaNNA 视为基本可以互换,用于指示缺失或空值。为了促进这一约定,Pandas 提供了几种方法来检测、删除和替换 Pandas 数据结构中的空值。它们包括:

isnull

生成一个指示缺失值的布尔掩码

notnull

isnull 的反操作

dropna

返回数据的过滤版本

fillna

返回填充或插补了缺失值的数据副本

我们将以对这些程序的简要探索和演示来结束本章。

检测空值

Pandas 数据结构有两个有用的方法来检测空数据:isnullnotnull。任何一个都将返回数据的布尔掩码。例如:

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

正如在 第十四章 中提到的那样,布尔掩码可以直接用作 SeriesDataFrame 的索引:

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=1axis='columns' 将删除包含空值的所有列:

In [21]: df.dropna(axis='columns')
Out[21]:    2
         0  2
         1  5
         2  6

但是这样会丢掉一些好数据;您可能更感兴趣的是删除具有所有 NA 值或大多数 NA 值的行或列。这可以通过 howthresh 参数进行指定,这些参数允许对允许通过的空值数量进行精细控制。

默认值为 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 SeriesDataFrame 对象中的一维和二维数据。通常,超出这些维度存储更高维度的数据是有用的——也就是说,数据由超过一个或两个键索引。早期的 Pandas 版本提供了 PanelPanel4D 对象,可以视为二维 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中的二维数据一样,我们也可以用它来操作SeriesDataFrame中的三维或更高维度的数据。多重索引中的每个额外级别代表了数据的一个额外维度;利用这个特性使我们在能够表示的数据类型上有了更大的灵活性。具体来说,我们可能希望为每个州在每年的人口(例如 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 创建方法

构建一个多重索引的SeriesDataFrame最直接的方法是简单地将两个或更多索引数组列表传递给构造函数。例如:

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)],
                    )

在创建SeriesDataFrame时,可以将任何这些对象作为index参数传递,或者将其传递给现有SeriesDataFramereindex方法。

多重索引级别名称

有时候给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

正如单索引情况一样,我们还可以使用在第十四章介绍的locilocix索引器。例如:

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

这些索引器提供了底层二维数据的类似数组的视图,但每个lociloc中的单个索引可以传递多个索引的元组。例如:

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

如您所见,在多重索引的SeriesDataFrame中与数据交互的方式有很多,并且与本书中的许多工具一样,熟悉它们的最佳方法是尝试它们!

重新排列多重索引

处理多重索引数据的关键之一是知道如何有效地转换数据。有许多操作将保留数据集中的所有信息,但为各种计算目的重新排列数据。我们在stackunstack方法中看到了一个简短的示例,但在控制数据在层次索引和列之间重新排列方面,还有许多其他方法,我们将在这里探讨它们。

排序和未排序的索引

我之前简要提到过一个警告,但我应该在这里更加强调。如果索引未排序,则许多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 提供了许多方便的例程来执行这种类型的排序,例如DataFramesort_indexsortlevel方法。我们在这里将使用最简单的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,其中包含stateyear列,这些列保存了以前在索引中的信息。为了清晰起见,我们可以选择指定数据的名称作为列的表示方式:

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。这可以通过DataFrameset_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

一些最有趣的数据研究来自于结合不同的数据源。这些操作可以涉及从两个不同数据集的非常简单的连接到更复杂的数据库风格的联接和合并,正确处理数据集之间的任何重叠。SeriesDataFrame是专为这类操作而构建的,Pandas 包含使这种数据处理快速和简单的函数和方法。

在这里,我们将使用pd.concat函数查看SeriesDataFrame的简单连接;稍后我们将深入探讨 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 数组的连接

SeriesDataFrame对象的连接行为与 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可用于简单连接SeriesDataFrame对象,就像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.concatenatepd.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 方法

因为直接数组连接是如此常见,SeriesDataFrame 对象具有一个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 列表的 appendextend 方法不同,Pandas 中的 append 方法不会修改原始对象;相反,它会创建一个包含组合数据的新对象。它也不是一种非常有效的方法,因为它涉及到新索引的创建 以及 数据缓冲区。因此,如果你计划进行多个 append 操作,通常最好建立一个 DataFrame 对象的列表,并一次性将它们全部传递给 concat 函数。

在下一章中,我们将介绍一种更强大的方法来组合来自多个来源的数据:pd.merge 中实现的数据库风格的合并/连接。有关 concatappend 和相关功能的更多信息,请参阅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函数和SeriesDataFrame对象的相关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,它合并了两个输入的信息。请注意,每列中的条目顺序不一定保持一致:在这种情况下,df1df2中的employee列顺序不同,pd.merge函数能够正确处理这一点。此外,请记住,一般情况下合并会丢弃索引,除非是通过索引进行合并(参见left_indexright_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_onright_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_indexright_onleft_onright_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中得到完整的州名。我们要基于popstate/region列和abbrevsabbreviation列进行合并。我们将使用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

我们看到迄今为止最稀疏的州是阿拉斯加,平均每平方英里略高于一名居民。

当尝试使用真实数据源回答问题时,这种数据合并是一项常见任务。希望这个例子给您提供了一些想法,展示了如何结合我们涵盖的工具来从数据中获取洞察!