四、NumPy 基础知识:数组和向量化计算

NumPy,即 Numerical Python,是 Python 中最重要的数值计算基础包之一。许多提供科学功能的计算包使用 NumPy 的数组对象作为数据交换的标准接口之一。我涵盖的关于 NumPy 的许多知识也适用于 pandas。

以下是您将在 NumPy 中找到的一些内容:

  • ndarray,一种高效的多维数组,提供快速的基于数组的算术运算和灵活的广播功能

  • 用于在整个数据数组上快速操作的数学函数,而无需编写循环

  • 用于读取/写入数组数据到磁盘和处理内存映射文件的工具

  • 线性代数、随机数生成和傅里叶变换功能

  • 用于将 NumPy 与用 C、C++或 FORTRAN 编写的库连接的 C API

由于 NumPy 提供了全面且有文档的 C API,因此将数据传递给用低级语言编写的外部库,以及让外部库将数据作为 NumPy 数组返回给 Python 是很简单的。这个特性使 Python 成为封装传统 C、C++或 FORTRAN 代码库并为其提供动态和可访问接口的首选语言。

虽然 NumPy 本身并不提供建模或科学功能,但了解 NumPy 数组和面向数组的计算将帮助您更有效地使用具有数组计算语义的工具,如 pandas。由于 NumPy 是一个庞大的主题,我将在以后更深入地涵盖许多高级 NumPy 功能,比如广播(参见附录 A:高级 NumPy)。这些高级功能中的许多并不需要遵循本书的其余部分,但在您深入研究 Python 科学计算时可能会有所帮助。

对于大多数数据分析应用程序,我将关注的主要功能领域是:

  • 用于数据整理和清洗、子集和过滤、转换以及任何其他类型计算的快速基于数组的操作

  • 常见的数组算法,如排序、唯一值和集合操作

  • 高效的描述统计和聚合/汇总数据

  • 数据对齐和关系数据操作,用于合并和连接异构数据集

  • 将条件逻辑表达为数组表达式,而不是使用if-elif-else分支循环

  • 分组数据操作(聚合、转换和函数应用)

虽然 NumPy 为一般数值数据处理提供了计算基础,但许多读者将希望使用 pandas 作为大多数统计或分析的基础,尤其是在表格数据上。此外,pandas 还提供了一些更具领域特定功能,如时间序列操作,这在 NumPy 中不存在。

注意

Python 中的面向数组计算可以追溯到 1995 年,当时 Jim Hugunin 创建了 Numeric 库。在接下来的 10 年里,许多科学编程社区开始在 Python 中进行数组编程,但在 2000 年代初,库生态系统变得分散。2005 年,Travis Oliphant 能够从当时的 Numeric 和 Numarray 项目中打造出 NumPy 项目,将社区团结在一个单一的数组计算框架周围。

NumPy 在 Python 中进行数值计算如此重要的原因之一是因为它专为大型数据数组的效率而设计。这有几个原因:*

  • NumPy 在内部以连续的内存块存储数据,独立于其他内置 Python 对象。NumPy 的用 C 语言编写的算法库可以在这个内存上操作,而无需进行任何类型检查或其他开销。NumPy 数组也比内置 Python 序列使用更少的内存。

  • NumPy 操作在整个数组上执行复杂计算,无需 Python for循环,对于大型序列来说,这可能会很慢。NumPy 比常规 Python 代码更快,因为它的基于 C 的算法避免了常规解释 Python 代码的开销。

为了让您了解性能差异,考虑一个包含一百万个整数的 NumPy 数组,以及等效的 Python 列表:

In [7]: import numpy as np

In [8]: my_arr = np.arange(1_000_000)

In [9]: my_list = list(range(1_000_000))

现在让我们将每个序列乘以 2:

In [10]: %timeit my_arr2 = my_arr * 2
309 us +- 7.48 us per loop (mean +- std. dev. of 7 runs, 1000 loops each)

In [11]: %timeit my_list2 = [x * 2 for x in my_list]
46.4 ms +- 526 us per loop (mean +- std. dev. of 7 runs, 10 loops each)

基于 NumPy 的算法通常比纯 Python 对应算法快 10 到 100 倍(或更多),并且使用的内存明显更少。

4.1 NumPy ndarray:多维数组对象

NumPy 的一个关键特性是其 N 维数组对象,或者 ndarray,它是 Python 中大型数据集的快速、灵活的容器。数组使您能够使用类似标量元素之间等效操作的语法在整个数据块上执行数学运算。

为了让您了解 NumPy 如何使用类似标量值的语法在内置 Python 对象上进行批量计算,我首先导入 NumPy 并创建一个小数组:

In [12]: import numpy as np

In [13]: data = np.array([[1.5, -0.1, 3], [0, -3, 6.5]])

In [14]: data
Out[14]: 
array([[ 1.5, -0.1,  3. ],
 [ 0. , -3. ,  6.5]])

然后我用data编写数学运算:

In [15]: data * 10
Out[15]: 
array([[ 15.,  -1.,  30.],
 [  0., -30.,  65.]])

In [16]: data + data
Out[16]: 
array([[ 3. , -0.2,  6. ],
 [ 0. , -6. , 13. ]])

在第一个示例中,所有元素都乘以了 10。在第二个示例中,数组中每个“单元格”中的相应值已经相加。

注意

在本章和整本书中,我使用标准的 NumPy 约定,始终使用import numpy as np。您可以在代码中使用from numpy import *来避免编写np.,但我建议不要养成这种习惯。numpy命名空间很大,包含许多函数,它们的名称与内置 Python 函数(如minmax)冲突。遵循这些标准约定几乎总是一个好主意。

ndarray 是一个用于同质数据的通用多维容器;也就是说,所有元素必须是相同类型。每个数组都有一个shape,一个指示每个维度大小的元组,以及一个dtype,描述数组的数据类型的对象:

In [17]: data.shape
Out[17]: (2, 3)

In [18]: data.dtype
Out[18]: dtype('float64')

本章将介绍使用 NumPy 数组的基础知识,这应该足以跟随本书的其余部分。虽然对于许多数据分析应用程序来说,深入了解 NumPy 并不是必需的,但精通面向数组的编程和思维是成为科学 Python 大师的关键步骤。

注意

在书中文本中,每当您看到“array”,“NumPy array”或“ndarray”时,在大多数情况下它们都指的是 ndarray 对象。

创建 ndarrays

创建数组的最简单方法是使用array函数。它接受任何类似序列的对象(包括其他数组)并生成包含传递数据的新 NumPy 数组。例如,列表是一个很好的转换候选:

In [19]: data1 = [6, 7.5, 8, 0, 1]

In [20]: arr1 = np.array(data1)

In [21]: arr1
Out[21]: array([6. , 7.5, 8. , 0. , 1. ])

嵌套序列,比如等长列表的列表,将被转换为多维数组:

In [22]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]

In [23]: arr2 = np.array(data2)

In [24]: arr2
Out[24]: 
array([[1, 2, 3, 4],
 [5, 6, 7, 8]])

由于data2是一个列表的列表,NumPy 数组arr2具有两个维度,形状从数据中推断出。我们可以通过检查ndimshape属性来确认这一点:

In [25]: arr2.ndim
Out[25]: 2

In [26]: arr2.shape
Out[26]: (2, 4)

除非明确指定(在 ndarrays 的数据类型中讨论),numpy.array会尝试推断创建的数组的良好数据类型。数据类型存储在特殊的dtype元数据对象中;例如,在前两个示例中我们有:

In [27]: arr1.dtype
Out[27]: dtype('float64')

In [28]: arr2.dtype
Out[28]: dtype('int64')

除了numpy.array之外,还有许多其他用于创建新数组的函数。例如,numpy.zerosnumpy.ones分别创建长度或形状为 0 或 1 的数组。numpy.empty创建一个数组,而不将其值初始化为任何特定值。要使用这些方法创建更高维度的数组,请传递一个形状的元组:

In [29]: np.zeros(10)
Out[29]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

In [30]: np.zeros((3, 6))
Out[30]: 
array([[0., 0., 0., 0., 0., 0.],
 [0., 0., 0., 0., 0., 0.],
 [0., 0., 0., 0., 0., 0.]])

In [31]: np.empty((2, 3, 2))
Out[31]: 
array([[[0., 0.],
 [0., 0.],
 [0., 0.]],
 [[0., 0.],
 [0., 0.],
 [0., 0.]]])

注意

不能假设numpy.empty会返回一个全为零的数组。该函数返回未初始化的内存,因此可能包含非零的“垃圾”值。只有在打算用数据填充新数组时才应使用此函数。

numpy.arange是内置 Python range函数的数组版本:

In [32]: np.arange(15)
Out[32]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

请参见表 4.1 中的一些标准数组创建函数的简要列表。由于 NumPy 专注于数值计算,如果未指定数据类型,数据类型在许多情况下将是float64(浮点数)。

表 4.1:一些重要的 NumPy 数组创建函数

| eye, identity | 创建一个 N×N 的方阵单位矩阵(对角线上为 1,其他地方为 0) |

ndarrays 的数据类型

数据类型dtype是一个特殊对象,包含 ndarray 需要将内存块解释为特定类型数据的信息(或元数据,关于数据的数据):

In [33]: arr1 = np.array([1, 2, 3], dtype=np.float64)

In [34]: arr2 = np.array([1, 2, 3], dtype=np.int32)

In [35]: arr1.dtype
Out[35]: dtype('float64')

In [36]: arr2.dtype
Out[36]: dtype('int32')

数据类型是 NumPy 灵活性的来源,用于与来自其他系统的数据进行交互。在大多数情况下,它们直接映射到底层磁盘或内存表示,这使得可以将数据的二进制流读写到磁盘,并连接到用低级语言(如 C 或 FORTRAN)编写的代码。数值数据类型的命名方式相同:类型名称,如floatint,后跟表示每个元素的位数的数字。标准的双精度浮点值(Python 中float对象底层使用的)占用 8 字节或 64 位。因此,在 NumPy 中,此类型称为float64。请参见表 4.2 以获取 NumPy 支持的数据类型的完整列表。

注意

不要担心记住 NumPy 数据类型,特别是如果您是新用户。通常只需要关心您正在处理的数据的一般类型,无论是浮点数、复数、整数、布尔值、字符串还是一般的 Python 对象。当您需要更多控制数据在内存和磁盘上的存储方式,特别是对于大型数据集时,知道您可以控制存储类型是很好的。

表 4.2:NumPy 数据类型

注意

有符号无符号整数类型,许多读者可能不熟悉这个术语。有符号整数可以表示正整数和负整数,而无符号整数只能表示非零整数。例如,int8(有符号 8 位整数)可以表示从-128 到 127(包括)的整数,而uint8(无符号 8 位整数)可以表示 0 到 255。

您可以使用 ndarray 的astype方法显式地将数组从一种数据类型转换为另一种数据类型:

In [37]: arr = np.array([1, 2, 3, 4, 5])

In [38]: arr.dtype
Out[38]: dtype('int64')

In [39]: float_arr = arr.astype(np.float64)

In [40]: float_arr
Out[40]: array([1., 2., 3., 4., 5.])

In [41]: float_arr.dtype
Out[41]: dtype('float64')

在这个例子中,整数被转换为浮点数。如果我将一些浮点数转换为整数数据类型,小数部分将被截断:

In [42]: arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])

In [43]: arr
Out[43]: array([ 3.7, -1.2, -2.6,  0.5, 12.9, 10.1])

In [44]: arr.astype(np.int32)
Out[44]: array([ 3, -1, -2,  0, 12, 10], dtype=int32)

如果您有一个表示数字的字符串数组,可以使用astype将它们转换为数值形式:

In [45]: numeric_strings = np.array(["1.25", "-9.6", "42"], dtype=np.string_)

In [46]: numeric_strings.astype(float)
Out[46]: array([ 1.25, -9.6 , 42.  ])

注意

在使用numpy.string_类型时要小心,因为 NumPy 中的字符串数据是固定大小的,可能会在没有警告的情况下截断输入。pandas 对非数值数据具有更直观的开箱即用行为。

如果由于某种原因(例如无法将字符串转换为float64)而转换失败,将引发ValueError。以前,我有点懒,写了float而不是np.float64;NumPy 将 Python 类型别名为其自己的等效数据类型。

您还可以使用另一个数组的dtype属性:

In [47]: int_array = np.arange(10)

In [48]: calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)

In [49]: int_array.astype(calibers.dtype)
Out[49]: array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])

有简写类型代码字符串,您也可以使用它们来引用dtype

In [50]: zeros_uint32 = np.zeros(8, dtype="u4")

In [51]: zeros_uint32
Out[51]: array([0, 0, 0, 0, 0, 0, 0, 0], dtype=uint32)

注意

调用astype 总是会创建一个新数组(数据的副本),即使新数据类型与旧数据类型相同。

NumPy 数组的算术运算

数组很重要,因为它们使您能够在不编写任何for循环的情况下对数据执行批量操作。NumPy 用户称之为向量化。任何等大小数组之间的算术运算都会逐元素应用该操作:

In [52]: arr = np.array([[1., 2., 3.], [4., 5., 6.]])

In [53]: arr
Out[53]: 
array([[1., 2., 3.],
 [4., 5., 6.]])

In [54]: arr * arr
Out[54]: 
array([[ 1.,  4.,  9.],
 [16., 25., 36.]])

In [55]: arr - arr
Out[55]: 
array([[0., 0., 0.],
 [0., 0., 0.]])

标量的算术运算会将标量参数传播到数组中的每个元素:

In [56]: 1 / arr
Out[56]: 
array([[1.    , 0.5   , 0.3333],
 [0.25  , 0.2   , 0.1667]])

In [57]: arr ** 2
Out[57]: 
array([[ 1.,  4.,  9.],
 [16., 25., 36.]])

相同大小的数组之间的比较会产生布尔数组:

In [58]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])

In [59]: arr2
Out[59]: 
array([[ 0.,  4.,  1.],
 [ 7.,  2., 12.]])

In [60]: arr2 > arr
Out[60]: 
array([[False,  True, False],
 [ True, False,  True]])

在不同大小的数组之间进行操作被称为广播,将在附录 A:高级 NumPy 中更详细地讨论。对广播的深入理解对本书的大部分内容并不是必要的。

基本索引和切片

NumPy 数组索引是一个深入的话题,因为有许多种方式可以选择数据的子集或单个元素。一维数组很简单;从表面上看,它们的行为类似于 Python 列表:

In [61]: arr = np.arange(10)

In [62]: arr
Out[62]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [63]: arr[5]
Out[63]: 5

In [64]: arr[5:8]
Out[64]: array([5, 6, 7])

In [65]: arr[5:8] = 12

In [66]: arr
Out[66]: array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

正如您所看到的,如果您将标量值分配给一个切片,如arr[5:8] = 12,该值将传播(或者广播)到整个选择。

注意

与 Python 内置列表的一个重要区别是,数组切片是原始数组的视图。这意味着数据没有被复制,对视图的任何修改都将反映在源数组中。

为了举例说明,我首先创建arr的一个切片:

In [67]: arr_slice = arr[5:8]

In [68]: arr_slice
Out[68]: array([12, 12, 12])

现在,当我在arr_slice中更改值时,这些变化会反映在原始数组arr中:

In [69]: arr_slice[1] = 12345

In [70]: arr
Out[70]: 
array([    0,     1,     2,     3,     4,    12, 12345,    12,     8,
 9])

“裸”切片[:]将分配给数组中的所有值:

In [71]: arr_slice[:] = 64

In [72]: arr
Out[72]: array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])

如果您是 NumPy 的新手,您可能会对此感到惊讶,特别是如果您已经使用过其他更积极复制数据的数组编程语言。由于 NumPy 被设计为能够处理非常大的数组,如果 NumPy 坚持始终复制数据,您可能会遇到性能和内存问题。

注意

如果您想要一个 ndarray 切片的副本而不是视图,您需要显式复制数组,例如arr[5:8].copy()。正如您将看到的,pandas 也是这样工作的。

对于更高维度的数组,您有更多的选择。在二维数组中,每个索引处的元素不再是标量,而是一维数组:

In [73]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

In [74]: arr2d[2]
Out[74]: array([7, 8, 9])

因此,可以递归访问单个元素。但这有点太麻烦了,所以您可以传递一个逗号分隔的索引列表来选择单个元素。因此,这些是等价的:

In [75]: arr2d[0][2]
Out[75]: 3

In [76]: arr2d[0, 2]
Out[76]: 3

请参见图 4.1 以了解如何在二维数组上进行索引的说明。我发现将轴 0 视为数组的“行”而将轴 1 视为“列”是有帮助的。

Python 数据分析(PYDA)第三版(二)-LMLPHP

图 4.1:索引 NumPy 数组中的元素

在多维数组中,如果省略后面的索引,返回的对象将是一个较低维度的 ndarray,由沿着更高维度的所有数据组成。因此,在 2×2×3 数组arr3d中:

In [77]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])

In [78]: arr3d
Out[78]: 
array([[[ 1,  2,  3],
 [ 4,  5,  6]],
 [[ 7,  8,  9],
 [10, 11, 12]]])

arr3d[0]是一个 2×3 数组:

In [79]: arr3d[0]
Out[79]: 
array([[1, 2, 3],
 [4, 5, 6]])

标量值和数组都可以分配给arr3d[0]

In [80]: old_values = arr3d[0].copy()

In [81]: arr3d[0] = 42

In [82]: arr3d
Out[82]: 
array([[[42, 42, 42],
 [42, 42, 42]],
 [[ 7,  8,  9],
 [10, 11, 12]]])

In [83]: arr3d[0] = old_values

In [84]: arr3d
Out[84]: 
array([[[ 1,  2,  3],
 [ 4,  5,  6]],
 [[ 7,  8,  9],
 [10, 11, 12]]])

类似地,arr3d[1, 0]会给您所有索引以(1, 0)开头的值,形成一个一维数组:

In [85]: arr3d[1, 0]
Out[85]: array([7, 8, 9])

这个表达式与我们分两步索引的方式相同:

In [86]: x = arr3d[1]

In [87]: x
Out[87]: 
array([[ 7,  8,  9],
 [10, 11, 12]])

In [88]: x[0]
Out[88]: array([7, 8, 9])

请注意,在所有这些选择数组的子部分的情况下,返回的数组都是视图。

注意

这种用于 NumPy 数组的多维索引语法不适用于常规的 Python 对象,例如列表的列表。

使用切片进行索引

像 Python 列表这样的一维对象一样,ndarrays 可以使用熟悉的语法进行切片:

In [89]: arr
Out[89]: array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])

In [90]: arr[1:6]
Out[90]: array([ 1,  2,  3,  4, 64])

考虑之前的二维数组arr2d。对该数组进行切片有点不同:

In [91]: arr2d
Out[91]: 
array([[1, 2, 3],
 [4, 5, 6],
 [7, 8, 9]])

In [92]: arr2d[:2]
Out[92]: 
array([[1, 2, 3],
 [4, 5, 6]])

正如您所看到的,它已经沿着轴 0 切片,即第一个轴。因此,切片选择沿着一个轴的一系列元素。阅读表达式arr2d[:2]为“选择arr2d的前两行”可能会有所帮助。

您可以像传递多个索引一样传递多个切片:

In [93]: arr2d[:2, 1:]
Out[93]: 
array([[2, 3],
 [5, 6]])

像这样切片时,您总是获得相同维数的数组视图。通过混合整数索引和切片,您可以获得较低维度的切片。

例如,我可以选择第二行,但只选择前两列,如下所示:

In [94]: lower_dim_slice = arr2d[1, :2]

在这里,虽然arr2d是二维的,lower_dim_slice是一维的,其形状是一个带有一个轴大小的元组:

In [95]: lower_dim_slice.shape
Out[95]: (2,)

同样,我可以选择第三列,但只选择前两行,如下所示:

In [96]: arr2d[:2, 2]
Out[96]: array([3, 6])

请参见图 4.2 进行说明。请注意,单独的冒号表示取整个轴,因此您可以通过以下方式仅切片更高维度的轴:

In [97]: arr2d[:, :1]
Out[97]: 
array([[1],
 [4],
 [7]])

当然,对切片表达式的分配会分配给整个选择:

In [98]: arr2d[:2, 1:] = 0

In [99]: arr2d
Out[99]: 
array([[1, 0, 0],
 [4, 0, 0],
 [7, 8, 9]])

Python 数据分析(PYDA)第三版(二)-LMLPHP

图 4.2:二维数组切片

布尔索引

让我们考虑一个例子,其中我们有一些数据在一个数组中,并且有一个包含重复名称的数组:

In [100]: names = np.array(["Bob", "Joe", "Will", "Bob", "Will", "Joe", "Joe"])

In [101]: data = np.array([[4, 7], [0, 2], [-5, 6], [0, 0], [1, 2],
 .....:                  [-12, -4], [3, 4]])

In [102]: names
Out[102]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')

In [103]: data
Out[103]: 
array([[  4,   7],
 [  0,   2],
 [ -5,   6],
 [  0,   0],
 [  1,   2],
 [-12,  -4],
 [  3,   4]])

假设每个名称对应于data数组中的一行,并且我们想要选择所有与相应名称"Bob"相对应的行。与算术运算一样,与数组进行比较(如==)也是矢量化的。因此,将names与字符串"Bob"进行比较会产生一个布尔数组:

In [104]: names == "Bob"
Out[104]: array([ True, False, False,  True, False, False, False])

此布尔数组可以在索引数组时传递:

In [105]: data[names == "Bob"]
Out[105]: 
array([[4, 7],
 [0, 0]])

布尔数组的长度必须与其索引的数组轴的长度相同。甚至可以将布尔数组与切片或整数(或整数序列)混合使用(稍后将详细介绍)。

在这些示例中,我从names == "Bob"的行中选择,并且也索引列:

In [106]: data[names == "Bob", 1:]
Out[106]: 
array([[7],
 [0]])

In [107]: data[names == "Bob", 1]
Out[107]: array([7, 0])

要选择除了"Bob"之外的所有内容,可以使用!=或使用~否定条件:

In [108]: names != "Bob"
Out[108]: array([False,  True,  True, False,  True,  True,  True])

In [109]: ~(names == "Bob")
Out[109]: array([False,  True,  True, False,  True,  True,  True])

In [110]: data[~(names == "Bob")]
Out[110]: 
array([[  0,   2],
 [ -5,   6],
 [  1,   2],
 [-12,  -4],
 [  3,   4]])

当您想要反转由变量引用的布尔数组时,~运算符可能很有用:

In [111]: cond = names == "Bob"

In [112]: data[~cond]
Out[112]: 
array([[  0,   2],
 [ -5,   6],
 [  1,   2],
 [-12,  -4],
 [  3,   4]])

使用布尔运算符如&(和)和|(或)选择三个名称中的两个来组合多个布尔条件:

In [113]: mask = (names == "Bob") | (names == "Will")

In [114]: mask
Out[114]: array([ True, False,  True,  True,  True, False, False])

In [115]: data[mask]
Out[115]: 
array([[ 4,  7],
 [-5,  6],
 [ 0,  0],
 [ 1,  2]])

通过布尔索引从数组中选择数据并将结果分配给新变量始终会创建数据的副本,即使返回的数组未更改。

注意

Python 关键字andor不能与布尔数组一起使用。请改用&(和)和|(或)。

使用布尔数组设置值的工作方式是将右侧的值或值替换到布尔数组的值为True的位置。要将data中的所有负值设置为 0,我们只需要执行:

In [116]: data[data < 0] = 0

In [117]: data
Out[117]: 
array([[4, 7],
 [0, 2],
 [0, 6],
 [0, 0],
 [1, 2],
 [0, 0],
 [3, 4]])

您还可以使用一维布尔数组设置整行或整列:

In [118]: data[names != "Joe"] = 7

In [119]: data
Out[119]: 
array([[7, 7],
 [0, 2],
 [7, 7],
 [7, 7],
 [7, 7],
 [0, 0],
 [3, 4]])

正如我们将在后面看到的,对二维数据进行这些类型的操作很方便使用 pandas。

花式索引

花式索引是 NumPy 采用的术语,用于描述使用整数数组进行索引。假设我们有一个 8×4 数组:

In [120]: arr = np.zeros((8, 4))

In [121]: for i in range(8):
 .....:     arr[i] = i

In [122]: arr
Out[122]: 
array([[0., 0., 0., 0.],
 [1., 1., 1., 1.],
 [2., 2., 2., 2.],
 [3., 3., 3., 3.],
 [4., 4., 4., 4.],
 [5., 5., 5., 5.],
 [6., 6., 6., 6.],
 [7., 7., 7., 7.]])

要按特定顺序选择行的子集,只需传递一个指定所需顺序的整数列表或 ndarray:

In [123]: arr[[4, 3, 0, 6]]
Out[123]: 
array([[4., 4., 4., 4.],
 [3., 3., 3., 3.],
 [0., 0., 0., 0.],
 [6., 6., 6., 6.]])

希望这段代码符合您的期望!使用负索引可从末尾选择行:

In [124]: arr[[-3, -5, -7]]
Out[124]: 
array([[5., 5., 5., 5.],
 [3., 3., 3., 3.],
 [1., 1., 1., 1.]])

传递多个索引数组会产生略有不同的结果;它选择与每个索引元组对应的一维数组元素:

In [125]: arr = np.arange(32).reshape((8, 4))

In [126]: arr
Out[126]: 
array([[ 0,  1,  2,  3],
 [ 4,  5,  6,  7],
 [ 8,  9, 10, 11],
 [12, 13, 14, 15],
 [16, 17, 18, 19],
 [20, 21, 22, 23],
 [24, 25, 26, 27],
 [28, 29, 30, 31]])

In [127]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]
Out[127]: array([ 4, 23, 29, 10])

要了解有关reshape方法的更多信息,请查看附录 A:高级 NumPy。

这里选择了元素(1, 0), (5, 3), (7, 1)(2, 2)。使用与轴数量相同的整数数组进行花式索引的结果始终是一维的。

在这种情况下,花式索引的行为与一些用户可能期望的有些不同(包括我自己),即通过选择矩阵的行和列的子集形成的矩形区域。以下是获得该区域的一种方法:

In [128]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
Out[128]: 
array([[ 4,  7,  5,  6],
 [20, 23, 21, 22],
 [28, 31, 29, 30],
 [ 8, 11,  9, 10]])

请记住,花式索引与切片不同,当将结果分配给新变量时,总是将数据复制到新数组中。如果使用花式索引分配值,则将修改索引的值:

In [129]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]
Out[129]: array([ 4, 23, 29, 10])

In [130]: arr[[1, 5, 7, 2], [0, 3, 1, 2]] = 0

In [131]: arr
Out[131]: 
array([[ 0,  1,  2,  3],
 [ 0,  5,  6,  7],
 [ 8,  9,  0, 11],
 [12, 13, 14, 15],
 [16, 17, 18, 19],
 [20, 21, 22,  0],
 [24, 25, 26, 27],
 [28,  0, 30, 31]])

转置数组和交换轴

转置是一种特殊的重塑形式,类似地返回基础数据的视图,而不复制任何内容。数组具有transpose方法和特殊的T属性:

In [132]: arr = np.arange(15).reshape((3, 5))

In [133]: arr
Out[133]: 
array([[ 0,  1,  2,  3,  4],
 [ 5,  6,  7,  8,  9],
 [10, 11, 12, 13, 14]])

In [134]: arr.T
Out[134]: 
array([[ 0,  5, 10],
 [ 1,  6, 11],
 [ 2,  7, 12],
 [ 3,  8, 13],
 [ 4,  9, 14]])

在进行矩阵计算时,您可能经常这样做-例如,使用numpy.dot计算内部矩阵乘积时:

In [135]: arr = np.array([[0, 1, 0], [1, 2, -2], [6, 3, 2], [-1, 0, -1], [1, 0, 1
]])

In [136]: arr
Out[136]: 
array([[ 0,  1,  0],
 [ 1,  2, -2],
 [ 6,  3,  2],
 [-1,  0, -1],
 [ 1,  0,  1]])

In [137]: np.dot(arr.T, arr)
Out[137]: 
array([[39, 20, 12],
 [20, 14,  2],
 [12,  2, 10]])

@中缀运算符是进行矩阵乘法的另一种方式:

In [138]: arr.T @ arr
Out[138]: 
array([[39, 20, 12],
 [20, 14,  2],
 [12,  2, 10]])

使用.T进行简单的转置是交换轴的特例。ndarray 具有swapaxes方法,该方法接受一对轴编号,并切换指定的轴以重新排列数据:

In [139]: arr
Out[139]: 
array([[ 0,  1,  0],
 [ 1,  2, -2],
 [ 6,  3,  2],
 [-1,  0, -1],
 [ 1,  0,  1]])

In [140]: arr.swapaxes(0, 1)
Out[140]: 
array([[ 0,  1,  6, -1,  1],
 [ 1,  2,  3,  0,  0],
 [ 0, -2,  2, -1,  1]])

类似地,swapaxes返回数据的视图而不进行复制。

4.2 伪随机数生成

numpy.random 模块通过函数有效地生成许多种概率分布的样本值的整个数组来补充内置的 Python random 模块。例如,您可以使用 numpy.random.standard_normal 从标准正态分布中获取一个 4 × 4 的样本数组:

In [141]: samples = np.random.standard_normal(size=(4, 4))

In [142]: samples
Out[142]: 
array([[-0.2047,  0.4789, -0.5194, -0.5557],
 [ 1.9658,  1.3934,  0.0929,  0.2817],
 [ 0.769 ,  1.2464,  1.0072, -1.2962],
 [ 0.275 ,  0.2289,  1.3529,  0.8864]])

相比之下,Python 的内置 random 模块一次只抽取一个值。从这个基准测试中可以看出,对于生成非常大的样本,numpy.random 的速度要快一个数量级以上:

In [143]: from random import normalvariate

In [144]: N = 1_000_000

In [145]: %timeit samples = [normalvariate(0, 1) for _ in range(N)]
490 ms +- 2.23 ms per loop (mean +- std. dev. of 7 runs, 1 loop each)

In [146]: %timeit np.random.standard_normal(N)
32.6 ms +- 271 us per loop (mean +- std. dev. of 7 runs, 10 loops each)

这些随机数并不是真正的随机(而是伪随机),而是由可配置的随机数生成器生成的,该生成器确定确定性地创建哪些值。像 numpy.random.standard_normal 这样的函数使用 numpy.random 模块的默认随机数生成器,但是您的代码可以配置为使用显式生成器:

In [147]: rng = np.random.default_rng(seed=12345)

In [148]: data = rng.standard_normal((2, 3))

seed 参数决定生成器的初始状态,每次使用 rng 对象生成数据时状态都会改变。生成器对象 rng 也与可能使用 numpy.random 模块的其他代码隔离开来:

In [149]: type(rng)
Out[149]: numpy.random._generator.Generator

查看 表 4.3 以获取类似 rng 这样的随机生成器对象上可用的部分方法列表。我将使用上面创建的 rng 对象在本章的其余部分生成随机数据。

表 4.3:NumPy 随机数生成器方法

4.3 通用函数:快速逐元素数组函数

通用函数,或者 ufunc,是在 ndarrays 中对数据执行逐元素操作的函数。您可以将它们看作是快速矢量化的简单函数的包装器,这些函数接受一个或多个标量值并产生一个或多个标量结果。

许多 ufuncs 都是简单的逐元素转换,比如 numpy.sqrtnumpy.exp

In [150]: arr = np.arange(10)

In [151]: arr
Out[151]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [152]: np.sqrt(arr)
Out[152]: 
array([0.    , 1.    , 1.4142, 1.7321, 2.    , 2.2361, 2.4495, 2.6458,
 2.8284, 3.    ])

In [153]: np.exp(arr)
Out[153]: 
array([   1.    ,    2.7183,    7.3891,   20.0855,   54.5982,  148.4132,
 403.4288, 1096.6332, 2980.958 , 8103.0839])

这些被称为一元 ufuncs。其他一些,比如 numpy.addnumpy.maximum,接受两个数组(因此是二元 ufuncs)并返回一个单一数组作为结果:

In [154]: x = rng.standard_normal(8)

In [155]: y = rng.standard_normal(8)

In [156]: x
Out[156]: 
array([-1.3678,  0.6489,  0.3611, -1.9529,  2.3474,  0.9685, -0.7594,
 0.9022])

In [157]: y
Out[157]: 
array([-0.467 , -0.0607,  0.7888, -1.2567,  0.5759,  1.399 ,  1.3223,
 -0.2997])

In [158]: np.maximum(x, y)
Out[158]: 
array([-0.467 ,  0.6489,  0.7888, -1.2567,  2.3474,  1.399 ,  1.3223,
 0.9022])

在这个例子中,numpy.maximum 计算了 xy 中元素的逐元素最大值。

虽然不常见,ufunc 可以返回多个数组。numpy.modf 就是一个例子:它是内置 Python math.modf 的矢量化版本,返回浮点数组的小数部分和整数部分:

In [159]: arr = rng.standard_normal(7) * 5

In [160]: arr
Out[160]: array([ 4.5146, -8.1079, -0.7909,  2.2474, -6.718 , -0.4084,  8.6237])

In [161]: remainder, whole_part = np.modf(arr)

In [162]: remainder
Out[162]: array([ 0.5146, -0.1079, -0.7909,  0.2474, -0.718 , -0.4084,  0.6237])

In [163]: whole_part
Out[163]: array([ 4., -8., -0.,  2., -6., -0.,  8.])

Ufuncs 接受一个可选的 out 参数,允许它们将结果分配到现有数组中,而不是创建一个新数组:

In [164]: arr
Out[164]: array([ 4.5146, -8.1079, -0.7909,  2.2474, -6.718 , -0.4084,  8.6237])

In [165]: out = np.zeros_like(arr)

In [166]: np.add(arr, 1)
Out[166]: array([ 5.5146, -7.1079,  0.2091,  3.2474, -5.718 ,  0.5916,  9.6237])

In [167]: np.add(arr, 1, out=out)
Out[167]: array([ 5.5146, -7.1079,  0.2091,  3.2474, -5.718 ,  0.5916,  9.6237])

In [168]: out
Out[168]: array([ 5.5146, -7.1079,  0.2091,  3.2474, -5.718 ,  0.5916,  9.6237])

查看 表 4.4 和 表 4.5 以获取 NumPy 的一些 ufuncs 列表。新的 ufuncs 仍在不断添加到 NumPy 中,因此查阅在线 NumPy 文档是获取全面列表并保持最新的最佳方式。

表 4.4:一些一元通用函数

表 4.5:一些二元通用函数

4.4 数组导向编程与数组

使用 NumPy 数组使您能够将许多种类的数据处理任务表达为简洁的数组表达式,否则可能需要编写循环。用数组表达式替换显式循环的这种做法被一些人称为向量化。一般来说,向量化的数组操作通常比它们纯 Python 等效的要快得多,在任何类型的数值计算中影响最大。稍后,在附录 A:高级 NumPy 中,我将解释广播,这是一种用于向量化计算的强大方法。

举个简单的例子,假设我们希望在一组常规值的网格上评估函数sqrt(x² + y²)numpy.meshgrid函数接受两个一维数组,并产生两个对应于两个数组中所有(x, y)对的二维矩阵:

In [169]: points = np.arange(-5, 5, 0.01) # 100 equally spaced points

In [170]: xs, ys = np.meshgrid(points, points)

In [171]: ys
Out[171]: 
array([[-5.  , -5.  , -5.  , ..., -5.  , -5.  , -5.  ],
 [-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
 [-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
 ...,
 [ 4.97,  4.97,  4.97, ...,  4.97,  4.97,  4.97],
 [ 4.98,  4.98,  4.98, ...,  4.98,  4.98,  4.98],
 [ 4.99,  4.99,  4.99, ...,  4.99,  4.99,  4.99]])

现在,评估函数只是写出您将用两个点写出的相同表达式的问题:

In [172]: z = np.sqrt(xs ** 2 + ys ** 2)

In [173]: z
Out[173]: 
array([[7.0711, 7.064 , 7.0569, ..., 7.0499, 7.0569, 7.064 ],
 [7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569],
 [7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
 ...,
 [7.0499, 7.0428, 7.0357, ..., 7.0286, 7.0357, 7.0428],
 [7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
 [7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569]])

作为第九章:绘图和可视化的预览,我使用 matplotlib 来创建这个二维数组的可视化:

In [174]: import matplotlib.pyplot as plt

In [175]: plt.imshow(z, cmap=plt.cm.gray, extent=[-5, 5, -5, 5])
Out[175]: <matplotlib.image.AxesImage at 0x17f04b040>

In [176]: plt.colorbar()
Out[176]: <matplotlib.colorbar.Colorbar at 0x1810661a0>

In [177]: plt.title("Image plot of $\sqrt{x² + y²}$ for a grid of values")
Out[177]: Text(0.5, 1.0, 'Image plot of $\\sqrt{x² + y²}$ for a grid of values'
)

在在网格上评估函数的绘图中,我使用了 matplotlib 函数imshow来从函数值的二维数组创建图像图。

Python 数据分析(PYDA)第三版(二)-LMLPHP

图 4.3:在网格上评估函数的绘图

如果您在 IPython 中工作,可以通过执行plt.close("all")关闭所有打开的绘图窗口:

In [179]: plt.close("all")

注意

术语矢量化用于描述其他计算机科学概念,但在本书中,我使用它来描述对整个数据数组进行操作,而不是逐个值使用 Python 的for循环。

将条件逻辑表达为数组操作

numpy.where函数是三元表达式x if condition else y的矢量化版本。假设我们有一个布尔数组和两个值数组:

In [180]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])

In [181]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])

In [182]: cond = np.array([True, False, True, True, False])

假设我们想要从cond中对应的值为True时从xarr中取一个值,否则从yarr中取一个值。一个做到这一点的列表推导可能如下所示:

In [183]: result = [(x if c else y)
 .....:           for x, y, c in zip(xarr, yarr, cond)]

In [184]: result
Out[184]: [1.1, 2.2, 1.3, 1.4, 2.5]

这有多个问题。首先,对于大数组来说速度不会很快(因为所有工作都是在解释的 Python 代码中完成的)。其次,它不适用于多维数组。使用numpy.where可以通过单个函数调用来实现这一点:

In [185]: result = np.where(cond, xarr, yarr)

In [186]: result
Out[186]: array([1.1, 2.2, 1.3, 1.4, 2.5])

numpy.where的第二个和第三个参数不需要是数组;它们中的一个或两个可以是标量。在数据分析中,where的典型用法是根据另一个数组生成一个新的值数组。假设你有一个随机生成数据的矩阵,并且你想用 2 替换所有正值和用-2 替换所有负值。这可以通过numpy.where来实现:

In [187]: arr = rng.standard_normal((4, 4))

In [188]: arr
Out[188]: 
array([[ 2.6182,  0.7774,  0.8286, -0.959 ],
 [-1.2094, -1.4123,  0.5415,  0.7519],
 [-0.6588, -1.2287,  0.2576,  0.3129],
 [-0.1308,  1.27  , -0.093 , -0.0662]])

In [189]: arr > 0
Out[189]: 
array([[ True,  True,  True, False],
 [False, False,  True,  True],
 [False, False,  True,  True],
 [False,  True, False, False]])

In [190]: np.where(arr > 0, 2, -2)
Out[190]: 
array([[ 2,  2,  2, -2],
 [-2, -2,  2,  2],
 [-2, -2,  2,  2],
 [-2,  2, -2, -2]])

在使用numpy.where时,可以将标量和数组组合在一起。例如,我可以用常数 2 替换arr中的所有正值,如下所示:

In [191]: np.where(arr > 0, 2, arr) # set only positive values to 2
Out[191]: 
array([[ 2.    ,  2.    ,  2.    , -0.959 ],
 [-1.2094, -1.4123,  2.    ,  2.    ],
 [-0.6588, -1.2287,  2.    ,  2.    ],
 [-0.1308,  2.    , -0.093 , -0.0662]])

数学和统计方法

一组数学函数,用于计算整个数组或沿轴的数据的统计信息,作为数组类的方法可访问。您可以通过调用数组实例方法或使用顶级 NumPy 函数来使用聚合(有时称为缩减)如summeanstd(标准差)。当您使用 NumPy 函数,如numpy.sum时,您必须将要聚合的数组作为第一个参数传递。

这里我生成一些正态分布的随机数据并计算一些聚合统计数据:

In [192]: arr = rng.standard_normal((5, 4))

In [193]: arr
Out[193]: 
array([[-1.1082,  0.136 ,  1.3471,  0.0611],
 [ 0.0709,  0.4337,  0.2775,  0.5303],
 [ 0.5367,  0.6184, -0.795 ,  0.3   ],
 [-1.6027,  0.2668, -1.2616, -0.0713],
 [ 0.474 , -0.4149,  0.0977, -1.6404]])

In [194]: arr.mean()
Out[194]: -0.08719744457434529

In [195]: np.mean(arr)
Out[195]: -0.08719744457434529

In [196]: arr.sum()
Out[196]: -1.743948891486906

meansum这样的函数接受一个可选的axis参数,该参数在给定轴上计算统计量,结果是一个维数少一的数组:

In [197]: arr.mean(axis=1)
Out[197]: array([ 0.109 ,  0.3281,  0.165 , -0.6672, -0.3709])

In [198]: arr.sum(axis=0)
Out[198]: array([-1.6292,  1.0399, -0.3344, -0.8203])

这里,arr.mean(axis=1)表示“计算沿着列的平均值”,而arr.sum(axis=0)表示“计算沿着行的总和”。

cumsumcumprod这样的其他方法不进行聚合,而是产生中间结果的数组:

In [199]: arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])

In [200]: arr.cumsum()
Out[200]: array([ 0,  1,  3,  6, 10, 15, 21, 28])

在多维数组中,像cumsum这样的累积函数返回一个相同大小的数组,但是根据每个较低维度切片沿着指定轴计算部分累积:

In [201]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])

In [202]: arr
Out[202]: 
array([[0, 1, 2],
 [3, 4, 5],
 [6, 7, 8]])

表达式arr.cumsum(axis=0)计算沿着行的累积和,而arr.cumsum(axis=1)计算沿着列的和:

In [203]: arr.cumsum(axis=0)
Out[203]: 
array([[ 0,  1,  2],
 [ 3,  5,  7],
 [ 9, 12, 15]])

In [204]: arr.cumsum(axis=1)
Out[204]: 
array([[ 0,  1,  3],
 [ 3,  7, 12],
 [ 6, 13, 21]])

查看表 4.6 以获取完整列表。我们将在后面的章节中看到这些方法的许多示例。

表 4.6:基本数组统计方法

布尔数组的方法

在前面的方法中,布尔值被强制转换为 1(True)和 0(False)。因此,sum经常被用作计算布尔数组中True值的计数的手段:

In [205]: arr = rng.standard_normal(100)

In [206]: (arr > 0).sum() # Number of positive values
Out[206]: 48

In [207]: (arr <= 0).sum() # Number of non-positive values
Out[207]: 52

这里表达式(arr > 0).sum()中的括号是必要的,以便能够在arr > 0的临时结果上调用sum()

另外两个方法,anyall,特别适用于布尔数组。any测试数组中是否有一个或多个值为True,而all检查是否每个值都为True

In [208]: bools = np.array([False, False, True, False])

In [209]: bools.any()
Out[209]: True

In [210]: bools.all()
Out[210]: False

这些方法也适用于非布尔数组,其中非零元素被视为True

排序

与 Python 内置的列表类型类似,NumPy 数组可以使用sort方法原地排序:

In [211]: arr = rng.standard_normal(6)

In [212]: arr
Out[212]: array([ 0.0773, -0.6839, -0.7208,  1.1206, -0.0548, -0.0824])

In [213]: arr.sort()

In [214]: arr
Out[214]: array([-0.7208, -0.6839, -0.0824, -0.0548,  0.0773,  1.1206])

您可以通过将轴编号传递给sort方法,在多维数组中对每个一维部分的值沿着轴进行原地排序。在这个例子数据中:

In [215]: arr = rng.standard_normal((5, 3))

In [216]: arr
Out[216]: 
array([[ 0.936 ,  1.2385,  1.2728],
 [ 0.4059, -0.0503,  0.2893],
 [ 0.1793,  1.3975,  0.292 ],
 [ 0.6384, -0.0279,  1.3711],
 [-2.0528,  0.3805,  0.7554]])

arr.sort(axis=0)对每列内的值进行排序,而arr.sort(axis=1)对每行进行排序:

In [217]: arr.sort(axis=0)

In [218]: arr
Out[218]: 
array([[-2.0528, -0.0503,  0.2893],
 [ 0.1793, -0.0279,  0.292 ],
 [ 0.4059,  0.3805,  0.7554],
 [ 0.6384,  1.2385,  1.2728],
 [ 0.936 ,  1.3975,  1.3711]])

In [219]: arr.sort(axis=1)

In [220]: arr
Out[220]: 
array([[-2.0528, -0.0503,  0.2893],
 [-0.0279,  0.1793,  0.292 ],
 [ 0.3805,  0.4059,  0.7554],
 [ 0.6384,  1.2385,  1.2728],
 [ 0.936 ,  1.3711,  1.3975]])

顶层方法numpy.sort返回一个数组的排序副本(类似于 Python 内置函数sorted),而不是在原地修改数组。例如:

In [221]: arr2 = np.array([5, -10, 7, 1, 0, -3])

In [222]: sorted_arr2 = np.sort(arr2)

In [223]: sorted_arr2
Out[223]: array([-10,  -3,   0,   1,   5,   7])

有关使用 NumPy 的排序方法的更多详细信息,以及更高级的技术,如间接排序,请参见附录 A:高级 NumPy。还可以在 pandas 中找到与排序相关的其他数据操作(例如,按一个或多个列对数据表进行排序)。

唯一值和其他集合逻辑

NumPy 具有一些用于一维 ndarrays 的基本集合操作。一个常用的操作是numpy.unique,它返回数组中排序的唯一值:

In [224]: names = np.array(["Bob", "Will", "Joe", "Bob", "Will", "Joe", "Joe"])

In [225]: np.unique(names)
Out[225]: array(['Bob', 'Joe', 'Will'], dtype='<U4')

In [226]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])

In [227]: np.unique(ints)
Out[227]: array([1, 2, 3, 4])

numpy.unique与纯 Python 替代方案进行对比:

In [228]: sorted(set(names))
Out[228]: ['Bob', 'Joe', 'Will']

在许多情况下,NumPy 版本更快,并返回一个 NumPy 数组而不是 Python 列表。

另一个函数numpy.in1d测试一个数组中的值在另一个数组中的成员资格,返回一个布尔数组:

In [229]: values = np.array([6, 0, 0, 3, 2, 5, 6])

In [230]: np.in1d(values, [2, 3, 6])
Out[230]: array([ True, False, False,  True,  True, False,  True])

请参见表 4.7 以获取 NumPy 中数组集合操作的列表。

表 4.7:数组集合操作

setxor1d(x, y) | 对称差集;在任一数组中但不在两个数组中的元素 |

4.5 使用数组进行文件输入和输出

NumPy 能够以一些文本或二进制格式将数据保存到磁盘并从磁盘加载数据。在本节中,我只讨论 NumPy 内置的二进制格式,因为大多数用户更倾向于使用 pandas 和其他工具来加载文本或表格数据(详见第六章:数据加载、存储和文件格式)。

numpy.savenumpy.load是在磁盘上高效保存和加载数组数据的两个主要函数。默认情况下,数组以未压缩的原始二进制格式保存,文件扩展名为*.npy*:

In [231]: arr = np.arange(10)

In [232]: np.save("some_array", arr)

如果文件路径尚未以*.npy*结尾,则会添加扩展名。然后可以使用numpy.load加载磁盘上的数组:

In [233]: np.load("some_array.npy")
Out[233]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

您可以使用numpy.savez并将数组作为关键字参数传递来保存多个数组到未压缩的存档中:

In [234]: np.savez("array_archive.npz", a=arr, b=arr)

当加载一个*.npz*文件时,您会得到一个类似字典的对象,它会延迟加载各个数组:

In [235]: arch = np.load("array_archive.npz")

In [236]: arch["b"]
Out[236]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

如果您的数据压缩效果很好,您可能希望使用numpy.savez_compressed

In [237]: np.savez_compressed("arrays_compressed.npz", a=arr, b=arr)

4.6 线性代数

线性代数运算,如矩阵乘法、分解、行列式和其他方阵数学,是许多数组库的重要组成部分。两个二维数组使用*进行元素级乘积,而矩阵乘法需要使用dot函数或@中缀运算符。dot既是一个数组方法,也是numpy命名空间中用于执行矩阵乘法的函数:

In [241]: x = np.array([[1., 2., 3.], [4., 5., 6.]])

In [242]: y = np.array([[6., 23.], [-1, 7], [8, 9]])

In [243]: x
Out[243]: 
array([[1., 2., 3.],
 [4., 5., 6.]])

In [244]: y
Out[244]: 
array([[ 6., 23.],
 [-1.,  7.],
 [ 8.,  9.]])

In [245]: x.dot(y)
Out[245]: 
array([[ 28.,  64.],
 [ 67., 181.]])

x.dot(y)等同于np.dot(x, y)

In [246]: np.dot(x, y)
Out[246]: 
array([[ 28.,  64.],
 [ 67., 181.]])

两个二维数组与适当大小的一维数组之间的矩阵乘积会得到一个一维数组:

In [247]: x @ np.ones(3)
Out[247]: array([ 6., 15.])

numpy.linalg具有一套标准的矩阵分解和逆矩阵、行列式等功能:

In [248]: from numpy.linalg import inv, qr

In [249]: X = rng.standard_normal((5, 5))

In [250]: mat = X.T @ X

In [251]: inv(mat)
Out[251]: 
array([[  3.4993,   2.8444,   3.5956, -16.5538,   4.4733],
 [  2.8444,   2.5667,   2.9002, -13.5774,   3.7678],
 [  3.5956,   2.9002,   4.4823, -18.3453,   4.7066],
 [-16.5538, -13.5774, -18.3453,  84.0102, -22.0484],
 [  4.4733,   3.7678,   4.7066, -22.0484,   6.0525]])

In [252]: mat @ inv(mat)
Out[252]: 
array([[ 1.,  0.,  0.,  0.,  0.],
 [ 0.,  1.,  0.,  0.,  0.],
 [ 0.,  0.,  1., -0.,  0.],
 [ 0.,  0.,  0.,  1.,  0.],
 [-0.,  0.,  0.,  0.,  1.]])

表达式X.T.dot(X)计算X与其转置X.T的点积。

请参见表 4.8 以获取一些最常用的线性代数函数的列表。

表 4.8:常用的numpy.linalg函数

4.7 示例:随机漫步

随机漫步的模拟提供了利用数组操作的说明性应用。让我们首先考虑一个简单的从 0 开始的随机漫步,步长为 1 和-1,发生概率相等。

这是一个使用内置的random模块实现一次包含 1,000 步的随机漫步的纯 Python 方法:

#! blockstart
import random
position = 0
walk = [position]
nsteps = 1000
for _ in range(nsteps):
 step = 1 if random.randint(0, 1) else -1
 position += step
 walk.append(position)
#! blockend

查看图 4.4 以查看这些随机漫步中前 100 个值的示例图:

In [255]: plt.plot(walk[:100])

Python 数据分析(PYDA)第三版(二)-LMLPHP

图 4.4:一个简单的随机漫步

你可能会观察到walk是随机步数的累积和,可以被评估为一个数组表达式。因此,我使用numpy.random模块一次绘制 1,000 次硬币翻转,将这些设置为 1 和-1,并计算累积和:

In [256]: nsteps = 1000

In [257]: rng = np.random.default_rng(seed=12345)  # fresh random generator

In [258]: draws = rng.integers(0, 2, size=nsteps)

In [259]: steps = np.where(draws == 0, 1, -1)

In [260]: walk = steps.cumsum()

从中我们可以开始提取统计数据,比如沿着漫步轨迹的最小值和最大值:

In [261]: walk.min()
Out[261]: -8

In [262]: walk.max()
Out[262]: 50

一个更复杂的统计量是第一次穿越时间,即随机漫步达到特定值的步数。在这里,我们可能想知道随机漫步离原点 0 至少 10 步的时间。np.abs(walk) >= 10给出一个布尔数组,指示漫步已经达到或超过 10,但我们想要第一个 10 或-10 的索引。事实证明,我们可以使用argmax来计算这个,它返回布尔数组中最大值的第一个索引(True是最大值):

In [263]: (np.abs(walk) >= 10).argmax()
Out[263]: 155

请注意,在这里使用argmax并不总是高效的,因为它总是对数组进行完整扫描。在这种特殊情况下,一旦观察到True,我们就知道它是最大值。

一次模拟多个随机漫步

如果你的目标是模拟许多随机漫步,比如说五千次,你可以通过对前面的代码进行微小修改来生成所有的随机漫步。如果传递一个 2 元组,numpy.random函数将生成一个二维数组的抽样,我们可以为每一行计算累积和,以一次性计算所有五千次随机漫步:

In [264]: nwalks = 5000

In [265]: nsteps = 1000

In [266]: draws = rng.integers(0, 2, size=(nwalks, nsteps)) # 0 or 1

In [267]: steps = np.where(draws > 0, 1, -1)

In [268]: walks = steps.cumsum(axis=1)

In [269]: walks
Out[269]: 
array([[  1,   2,   3, ...,  22,  23,  22],
 [  1,   0,  -1, ..., -50, -49, -48],
 [  1,   2,   3, ...,  50,  49,  48],
 ...,
 [ -1,  -2,  -1, ..., -10,  -9, -10],
 [ -1,  -2,  -3, ...,   8,   9,   8],
 [ -1,   0,   1, ...,  -4,  -3,  -2]])

现在,我们可以计算所有漫步中获得的最大值和最小值:

In [270]: walks.max()
Out[270]: 114

In [271]: walks.min()
Out[271]: -120

在这些漫步中,让我们计算到达 30 或-30 的最小穿越时间。这有点棘手,因为并非所有的 5000 次都达到 30。我们可以使用any方法来检查:

In [272]: hits30 = (np.abs(walks) >= 30).any(axis=1)

In [273]: hits30
Out[273]: array([False,  True,  True, ...,  True, False,  True])

In [274]: hits30.sum() # Number that hit 30 or -30
Out[274]: 3395

我们可以使用这个布尔数组来选择实际穿越绝对值 30 水平的walks的行,并在轴 1 上调用argmax来获取穿越时间:

In [275]: crossing_times = (np.abs(walks[hits30]) >= 30).argmax(axis=1)

In [276]: crossing_times
Out[276]: array([201, 491, 283, ..., 219, 259, 541])

最后,我们计算平均最小穿越时间:

In [277]: crossing_times.mean()
Out[277]: 500.5699558173785

随意尝试使用与等大小硬币翻转不同的步骤分布。你只需要使用不同的随机生成器方法,比如standard_normal来生成具有一定均值和标准差的正态分布步数:

In [278]: draws = 0.25 * rng.standard_normal((nwalks, nsteps))

注意

请记住,这种矢量化方法需要创建一个具有nwalks * nsteps元素的数组,这可能会在大型模拟中使用大量内存。如果内存更受限制,则需要采用不同的方法。

4.8 结论

尽管本书的大部分内容将集中在使用 pandas 构建数据整理技能上,我们将继续以类似的基于数组的风格工作。在附录 A:高级 NumPy 中,我们将深入探讨 NumPy 的特性,帮助您进一步发展数组计算技能。

五、使用 pandas 入门

pandas 将是本书剩余部分中的一个主要工具。它包含了专为在 Python 中快速方便地进行数据清洗和分析而设计的数据结构和数据操作工具。pandas 经常与数值计算工具(如 NumPy 和 SciPy)、分析库(如 statsmodels 和 scikit-learn)以及数据可视化库(如 matplotlib)一起使用。pandas 采用了 NumPy 的很多习惯用法,特别是基于数组的计算和对数据处理的偏好,而不使用for循环。

虽然 pandas 采用了许多来自 NumPy 的编码习惯,但最大的区别在于 pandas 是为处理表格或异构数据而设计的。相比之下,NumPy 更适合处理同质类型的数值数组数据。

自 2010 年成为开源项目以来,pandas 已经发展成一个相当庞大的库,适用于广泛的实际用例。开发者社区已经发展到超过 2500 名不同的贡献者,他们在解决日常数据问题时一直在帮助构建这个项目。充满活力的 pandas 开发者和用户社区是其成功的关键部分。

注意

很多人不知道我自 2013 年以来并没有积极参与日常 pandas 的开发;从那时起,它一直是一个完全由社区管理的项目。请务必向核心开发人员和所有贡献者传达感谢他们的辛勤工作!

在本书的剩余部分中,我使用以下的 NumPy 和 pandas 的导入约定:

In [1]: import numpy as np

In [2]: import pandas as pd

因此,每当在代码中看到pd.时,它指的是 pandas。您可能也会发现将 Series 和 DataFrame 导入到本地命名空间中更容易,因为它们经常被使用:

In [3]: from pandas import Series, DataFrame

5.1 pandas 数据结构简介

要开始使用 pandas,您需要熟悉其两个主要数据结构:SeriesDataFrame。虽然它们并非适用于每个问题的通用解决方案,但它们为各种数据任务提供了坚实的基础。

Series

Series 是一个一维数组样对象,包含一系列值(与 NumPy 类型相似的类型)和一个关联的数据标签数组,称为索引。最简单的 Series 是仅由数据数组形成的:

In [14]: obj = pd.Series([4, 7, -5, 3])

In [15]: obj
Out[15]: 
0    4
1    7
2   -5
3    3
dtype: int64

Series 的交互式显示的字符串表示在左侧显示索引,右侧显示值。由于我们没有为数据指定索引,因此会创建一个默认索引,由整数0N-1(其中N是数据的长度)组成。您可以通过其arrayindex属性分别获取 Series 的数组表示和索引对象:

In [16]: obj.array
Out[16]: 
<PandasArray>
[4, 7, -5, 3]
Length: 4, dtype: int64

In [17]: obj.index
Out[17]: RangeIndex(start=0, stop=4, step=1)

.array属性的结果是一个PandasArray,通常包装了一个 NumPy 数组,但也可以包含特殊的扩展数组类型,这将在 Ch 7.3:扩展数据类型中更详细讨论。

通常,您会希望创建一个带有标识每个数据点的索引的 Series:

In [18]: obj2 = pd.Series([4, 7, -5, 3], index=["d", "b", "a", "c"])

In [19]: obj2
Out[19]: 
d    4
b    7
a   -5
c    3
dtype: int64

In [20]: obj2.index
Out[20]: Index(['d', 'b', 'a', 'c'], dtype='object')

与 NumPy 数组相比,当选择单个值或一组值时,可以在索引中使用标签:

In [21]: obj2["a"]
Out[21]: -5

In [22]: obj2["d"] = 6

In [23]: obj2[["c", "a", "d"]]
Out[23]: 
c    3
a   -5
d    6
dtype: int64

这里["c", "a", "d"]被解释为索引列表,即使它包含字符串而不是整数。

使用 NumPy 函数或类似 NumPy 的操作,例如使用布尔数组进行过滤、标量乘法或应用数学函数,将保留索引值链接:

In [24]: obj2[obj2 > 0]
Out[24]: 
d    6
b    7
c    3
dtype: int64

In [25]: obj2 * 2
Out[25]: 
d    12
b    14
a   -10
c     6
dtype: int64

In [26]: import numpy as np

In [27]: np.exp(obj2)
Out[27]: 
d     403.428793
b    1096.633158
a       0.006738
c      20.085537
dtype: float64

将 Series 视为固定长度的有序字典的另一种方式,因为它是索引值到数据值的映射。它可以在许多上下文中使用,您可能会使用字典:

In [28]: "b" in obj2
Out[28]: True

In [29]: "e" in obj2
Out[29]: False

如果您的数据包含在 Python 字典中,可以通过传递字典来创建一个 Series:

In [30]: sdata = {"Ohio": 35000, "Texas": 71000, "Oregon": 16000, "Utah": 5000}

In [31]: obj3 = pd.Series(sdata)

In [32]: obj3
Out[32]: 
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64

Series 可以使用其to_dict方法转换回字典:

In [33]: obj3.to_dict()
Out[33]: {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}

当您只传递一个字典时,生成的 Series 中的索引将遵循字典的keys方法的键的顺序,这取决于键插入顺序。您可以通过传递一个索引,其中包含字典键的顺序,以便它们出现在生成的 Series 中的顺序来覆盖这一点:

In [34]: states = ["California", "Ohio", "Oregon", "Texas"]

In [35]: obj4 = pd.Series(sdata, index=states)

In [36]: obj4
Out[36]: 
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
dtype: float64

在这里,sdata中找到的三个值被放置在适当的位置,但由于没有找到"California"的值,它显示为NaN(不是一个数字),在 pandas 中被视为标记缺失或NA值。由于states中没有包含"Utah",因此它被排除在结果对象之外。

我将使用术语“missing”、“NA”或“null”来交替引用缺失数据。应该使用 pandas 中的isnanotna函数来检测缺失数据:

In [37]: pd.isna(obj4)
Out[37]: 
California     True
Ohio          False
Oregon        False
Texas         False
dtype: bool

In [38]: pd.notna(obj4)
Out[38]: 
California    False
Ohio           True
Oregon         True
Texas          True
dtype: bool

Series 还具有这些作为实例方法:

In [39]: obj4.isna()
Out[39]: 
California     True
Ohio          False
Oregon        False
Texas         False
dtype: bool

我将在第七章:数据清洗和准备中更详细地讨论处理缺失数据的工作。

对于许多应用程序来说,Series 的一个有用特性是它在算术运算中自动按索引标签对齐:

In [40]: obj3
Out[40]: 
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64

In [41]: obj4
Out[41]: 
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
dtype: float64

In [42]: obj3 + obj4
Out[42]: 
California         NaN
Ohio           70000.0
Oregon         32000.0
Texas         142000.0
Utah               NaN
dtype: float64

数据对齐功能将在后面更详细地讨论。如果您有数据库经验,可以将其视为类似于连接操作。

Series 对象本身和其索引都有一个name属性,它与 pandas 功能的其他区域集成:

In [43]: obj4.name = "population"

In [44]: obj4.index.name = "state"

In [45]: obj4
Out[45]: 
state
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
Name: population, dtype: float64

Series 的索引可以通过赋值来直接更改:

In [46]: obj
Out[46]: 
0    4
1    7
2   -5
3    3
dtype: int64

In [47]: obj.index = ["Bob", "Steve", "Jeff", "Ryan"]

In [48]: obj
Out[48]: 
Bob      4
Steve    7
Jeff    -5
Ryan     3
dtype: int64

DataFrame

DataFrame 表示数据的矩形表,并包含一个有序的、命名的列集合,每个列可以是不同的值类型(数值、字符串、布尔值等)。DataFrame 既有行索引又有列索引;它可以被视为共享相同索引的一系列 Series 的字典。

注意

虽然 DataFrame 在物理上是二维的,但您可以使用它来以分层索引的方式表示更高维度的数据,这是我们将在第八章:数据整理:连接、合并和重塑中讨论的一个主题,并且是 pandas 中一些更高级数据处理功能的一个组成部分。

有许多构建 DataFrame 的方法,尽管其中最常见的一种是从等长列表或 NumPy 数组的字典中构建:

data = {"state": ["Ohio", "Ohio", "Ohio", "Nevada", "Nevada", "Nevada"],
 "year": [2000, 2001, 2002, 2001, 2002, 2003],
 "pop": [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]}
frame = pd.DataFrame(data)

生成的 DataFrame 将自动分配其索引,与 Series 一样,并且列根据data中键的顺序放置(取决于字典中的插入顺序):

In [50]: frame
Out[50]: 
 state  year  pop
0    Ohio  2000  1.5
1    Ohio  2001  1.7
2    Ohio  2002  3.6
3  Nevada  2001  2.4
4  Nevada  2002  2.9
5  Nevada  2003  3.2

注意

如果您正在使用 Jupyter 笔记本,pandas DataFrame 对象将显示为更适合浏览器的 HTML 表格。请参见图 5.1 作为示例。

Python 数据分析(PYDA)第三版(二)-LMLPHP

图 5.1:Jupyter 中 pandas DataFrame 对象的外观

对于大型 DataFrame,head方法仅选择前五行:

In [51]: frame.head()
Out[51]: 
 state  year  pop
0    Ohio  2000  1.5
1    Ohio  2001  1.7
2    Ohio  2002  3.6
3  Nevada  2001  2.4
4  Nevada  2002  2.9

类似地,tail返回最后五行:

In [52]: frame.tail()
Out[52]: 
 state  year  pop
1    Ohio  2001  1.7
2    Ohio  2002  3.6
3  Nevada  2001  2.4
4  Nevada  2002  2.9
5  Nevada  2003  3.2

如果指定一系列列,DataFrame 的列将按照该顺序排列:

In [53]: pd.DataFrame(data, columns=["year", "state", "pop"])
Out[53]: 
 year   state  pop
0  2000    Ohio  1.5
1  2001    Ohio  1.7
2  2002    Ohio  3.6
3  2001  Nevada  2.4
4  2002  Nevada  2.9
5  2003  Nevada  3.2

如果传递一个字典中不包含的列,它将以缺失值的形式出现在结果中:

In [54]: frame2 = pd.DataFrame(data, columns=["year", "state", "pop", "debt"])

In [55]: frame2
Out[55]: 
 year   state  pop debt
0  2000    Ohio  1.5  NaN
1  2001    Ohio  1.7  NaN
2  2002    Ohio  3.6  NaN
3  2001  Nevada  2.4  NaN
4  2002  Nevada  2.9  NaN
5  2003  Nevada  3.2  NaN

In [56]: frame2.columns
Out[56]: Index(['year', 'state', 'pop', 'debt'], dtype='object')

DataFrame 中的列可以通过类似字典的表示法或使用点属性表示法检索为 Series:

In [57]: frame2["state"]
Out[57]: 
0      Ohio
1      Ohio
2      Ohio
3    Nevada
4    Nevada
5    Nevada
Name: state, dtype: object

In [58]: frame2.year
Out[58]: 
0    2000
1    2001
2    2002
3    2001
4    2002
5    2003
Name: year, dtype: int64

注意

提供类似属性访问(例如,frame2.year)和 IPython 中列名称的制表符补全作为便利。

frame2[column]适用于任何列名,但只有当列名是有效的 Python 变量名且不与 DataFrame 中的任何方法名冲突时,frame2.column才适用。例如,如果列名包含空格或下划线以外的其他符号,则无法使用点属性方法访问。

请注意,返回的 Series 具有与 DataFrame 相同的索引,并且它们的name属性已经适当设置。

行也可以通过特殊的ilocloc属性按位置或名称检索(稍后在使用 loc 和 iloc 在 DataFrame 上进行选择中详细介绍):

In [59]: frame2.loc[1]
Out[59]: 
year     2001
state    Ohio
pop       1.7
debt      NaN
Name: 1, dtype: object

In [60]: frame2.iloc[2]
Out[60]: 
year     2002
state    Ohio
pop       3.6
debt      NaN
Name: 2, dtype: object

列可以通过赋值进行修改。例如,可以为空的debt列分配一个标量值或一个值数组:

In [61]: frame2["debt"] = 16.5

In [62]: frame2
Out[62]: 
 year   state  pop  debt
0  2000    Ohio  1.5  16.5
1  2001    Ohio  1.7  16.5
2  2002    Ohio  3.6  16.5
3  2001  Nevada  2.4  16.5
4  2002  Nevada  2.9  16.5
5  2003  Nevada  3.2  16.5

In [63]: frame2["debt"] = np.arange(6.)

In [64]: frame2
Out[64]: 
 year   state  pop  debt
0  2000    Ohio  1.5   0.0
1  2001    Ohio  1.7   1.0
2  2002    Ohio  3.6   2.0
3  2001  Nevada  2.4   3.0
4  2002  Nevada  2.9   4.0
5  2003  Nevada  3.2   5.0

当将列表或数组分配给列时,值的长度必须与 DataFrame 的长度相匹配。如果分配一个 Series,其标签将被重新对齐到 DataFrame 的索引,插入任何不存在的索引值的缺失值:

In [65]: val = pd.Series([-1.2, -1.5, -1.7], index=[2, 4, 5])

In [66]: frame2["debt"] = val

In [67]: frame2
Out[67]: 
 year   state  pop  debt
0  2000    Ohio  1.5   NaN
1  2001    Ohio  1.7   NaN
2  2002    Ohio  3.6  -1.2
3  2001  Nevada  2.4   NaN
4  2002  Nevada  2.9  -1.5
5  2003  Nevada  3.2  -1.7

分配一个不存在的列将创建一个新列。

del关键字将像字典一样删除列。例如,首先添加一个新列,其中布尔值等于"Ohio"state列:

In [68]: frame2["eastern"] = frame2["state"] == "Ohio"

In [69]: frame2
Out[69]: 
 year   state  pop  debt  eastern
0  2000    Ohio  1.5   NaN     True
1  2001    Ohio  1.7   NaN     True
2  2002    Ohio  3.6  -1.2     True
3  2001  Nevada  2.4   NaN    False
4  2002  Nevada  2.9  -1.5    False
5  2003  Nevada  3.2  -1.7    False

警告:

不能使用frame2.eastern点属性表示法创建新列。

然后可以使用del方法删除此列:

In [70]: del frame2["eastern"]

In [71]: frame2.columns
Out[71]: Index(['year', 'state', 'pop', 'debt'], dtype='object')

注意

从 DataFrame 索引返回的列是基础数据的视图,而不是副本。因此,对 Series 的任何原地修改都将反映在 DataFrame 中。可以使用 Series 的copy方法显式复制列。

另一种常见的数据形式是嵌套字典的字典:

In [72]: populations = {"Ohio": {2000: 1.5, 2001: 1.7, 2002: 3.6},
 ....:                "Nevada": {2001: 2.4, 2002: 2.9}}

如果将嵌套字典传递给 DataFrame,pandas 将解释外部字典键为列,内部键为行索引:

In [73]: frame3 = pd.DataFrame(populations)

In [74]: frame3
Out[74]: 
 Ohio  Nevada
2000   1.5     NaN
2001   1.7     2.4
2002   3.6     2.9

您可以使用类似于 NumPy 数组的语法转置 DataFrame(交换行和列):

In [75]: frame3.T
Out[75]: 
 2000  2001  2002
Ohio     1.5   1.7   3.6
Nevada   NaN   2.4   2.9

警告:

请注意,如果列的数据类型不全都相同,则转置会丢弃列数据类型,因此转置然后再次转置可能会丢失先前的类型信息。在这种情况下,列变成了纯 Python 对象的数组。

内部字典中的键被组合以形成结果中的索引。如果指定了显式索引,则这种情况不成立:

In [76]: pd.DataFrame(populations, index=[2001, 2002, 2003])
Out[76]: 
 Ohio  Nevada
2001   1.7     2.4
2002   3.6     2.9
2003   NaN     NaN

Series 的字典以类似的方式处理:

In [77]: pdata = {"Ohio": frame3["Ohio"][:-1],
 ....:          "Nevada": frame3["Nevada"][:2]}

In [78]: pd.DataFrame(pdata)
Out[78]: 
 Ohio  Nevada
2000   1.5     NaN
2001   1.7     2.4

有关可以传递给 DataFrame 构造函数的许多内容,请参见表 5.1。

表 5.1:DataFrame 构造函数的可能数据输入

如果 DataFrame 的indexcolumns有设置它们的name属性,这些也会被显示出来:

In [79]: frame3.index.name = "year"

In [80]: frame3.columns.name = "state"

In [81]: frame3
Out[81]: 
state  Ohio  Nevada
year 
2000    1.5     NaN
2001    1.7     2.4
2002    3.6     2.9

与 Series 不同,DataFrame 没有name属性。DataFrame 的to_numpy方法将 DataFrame 中包含的数据作为二维 ndarray 返回:

In [82]: frame3.to_numpy()
Out[82]: 
array([[1.5, nan],
 [1.7, 2.4],
 [3.6, 2.9]])

如果 DataFrame 的列是不同的数据类型,则返回的数组的数据类型将被选择以容纳所有列:

In [83]: frame2.to_numpy()
Out[83]: 
array([[2000, 'Ohio', 1.5, nan],
 [2001, 'Ohio', 1.7, nan],
 [2002, 'Ohio', 3.6, -1.2],
 [2001, 'Nevada', 2.4, nan],
 [2002, 'Nevada', 2.9, -1.5],
 [2003, 'Nevada', 3.2, -1.7]], dtype=object)

索引对象

pandas 的 Index 对象负责保存轴标签(包括 DataFrame 的列名)和其他元数据(如轴名称)。在构建 Series 或 DataFrame 时使用的任何数组或其他标签序列都会在内部转换为 Index:

In [84]: obj = pd.Series(np.arange(3), index=["a", "b", "c"])

In [85]: index = obj.index

In [86]: index
Out[86]: Index(['a', 'b', 'c'], dtype='object')

In [87]: index[1:]
Out[87]: Index(['b', 'c'], dtype='object')

Index 对象是不可变的,因此用户无法修改它们:

index[1] = "d"  # TypeError

不可变性使得在数据结构之间共享 Index 对象更加安全:

In [88]: labels = pd.Index(np.arange(3))

In [89]: labels
Out[89]: Index([0, 1, 2], dtype='int64')

In [90]: obj2 = pd.Series([1.5, -2.5, 0], index=labels)

In [91]: obj2
Out[91]: 
0    1.5
1   -2.5
2    0.0
dtype: float64

In [92]: obj2.index is labels
Out[92]: True

注意

一些用户可能不经常利用 Index 提供的功能,但由于一些操作会产生包含索引数据的结果,因此了解它们的工作原理是很重要的。

除了类似数组,Index 还表现得像一个固定大小的集合:

In [93]: frame3
Out[93]: 
state  Ohio  Nevada
year 
2000    1.5     NaN
2001    1.7     2.4
2002    3.6     2.9

In [94]: frame3.columns
Out[94]: Index(['Ohio', 'Nevada'], dtype='object', name='state')

In [95]: "Ohio" in frame3.columns
Out[95]: True

In [96]: 2003 in frame3.index
Out[96]: False

与 Python 集合不同,pandas 的 Index 可以包含重复标签:

In [97]: pd.Index(["foo", "foo", "bar", "bar"])
Out[97]: Index(['foo', 'foo', 'bar', 'bar'], dtype='object')

具有重复标签的选择将选择该标签的所有出现。

每个 Index 都有一些用于集合逻辑的方法和属性,可以回答关于其包含的数据的其他常见问题。一些有用的方法总结在 Table 5.2 中。

Table 5.2: 一些索引方法和属性

| unique() | 计算索引中唯一值的数组 |

5.2 基本功能

本节将带领您了解与 Series 或 DataFrame 中包含的数据进行交互的基本机制。在接下来的章节中,我们将更深入地探讨使用 pandas 进行数据分析和操作的主题。本书不旨在作为 pandas 库的详尽文档,而是专注于让您熟悉常用功能,将不太常见的(即更神秘的)内容留给您通过阅读在线 pandas 文档来学习。

重新索引

pandas 对象上的一个重要方法是reindex,它意味着创建一个新对象,其值重新排列以与新索引对齐。考虑一个例子:

In [98]: obj = pd.Series([4.5, 7.2, -5.3, 3.6], index=["d", "b", "a", "c"])

In [99]: obj
Out[99]: 
d    4.5
b    7.2
a   -5.3
c    3.6
dtype: float64

在这个 Series 上调用reindex会根据新索引重新排列数据,如果某些索引值之前不存在,则会引入缺失值:

In [100]: obj2 = obj.reindex(["a", "b", "c", "d", "e"])

In [101]: obj2
Out[101]: 
a   -5.3
b    7.2
c    3.6
d    4.5
e    NaN
dtype: float64

对于有序数据如时间序列,当重新索引时可能需要进行一些插值或值填充。method选项允许我们使用ffill这样的方法来实现,它可以向前填充值:

In [102]: obj3 = pd.Series(["blue", "purple", "yellow"], index=[0, 2, 4])

In [103]: obj3
Out[103]: 
0      blue
2    purple
4    yellow
dtype: object

In [104]: obj3.reindex(np.arange(6), method="ffill")
Out[104]: 
0      blue
1      blue
2    purple
3    purple
4    yellow
5    yellow
dtype: object

对于 DataFrame,reindex可以改变(行)索引、列或两者。当只传递一个序列时,它会重新索引结果中的行:

In [105]: frame = pd.DataFrame(np.arange(9).reshape((3, 3)),
 .....:                      index=["a", "c", "d"],
 .....:                      columns=["Ohio", "Texas", "California"])

In [106]: frame
Out[106]: 
 Ohio  Texas  California
a     0      1           2
c     3      4           5
d     6      7           8

In [107]: frame2 = frame.reindex(index=["a", "b", "c", "d"])

In [108]: frame2
Out[108]: 
 Ohio  Texas  California
a   0.0    1.0         2.0
b   NaN    NaN         NaN
c   3.0    4.0         5.0
d   6.0    7.0         8.0

可以使用columns关键字重新索引列:

In [109]: states = ["Texas", "Utah", "California"]

In [110]: frame.reindex(columns=states)
Out[110]: 
 Texas  Utah  California
a      1   NaN           2
c      4   NaN           5
d      7   NaN           8

因为"Ohio"不在states中,所以该列的数据被从结果中删除。

重新索引特定轴的另一种方法是将新的轴标签作为位置参数传递,然后使用axis关键字指定要重新索引的轴:

In [111]: frame.reindex(states, axis="columns")
Out[111]: 
 Texas  Utah  California
a      1   NaN           2
c      4   NaN           5
d      7   NaN           8

查看 Table 5.3 以了解有关reindex参数的更多信息。

表 5.3:reindex函数参数

正如我们稍后将在使用 loc 和 iloc 在 DataFrame 上进行选择中探讨的,您也可以通过使用loc运算符重新索引,许多用户更喜欢始终以这种方式进行操作。这仅在所有新索引标签已存在于 DataFrame 中时才有效(而reindex将为新标签插入缺失数据):

In [112]: frame.loc[["a", "d", "c"], ["California", "Texas"]]
Out[112]: 
 California  Texas
a           2      1
d           8      7
c           5      4

从轴中删除条目

如果您已经有一个不包含这些条目的索引数组或列表,那么从轴中删除一个或多个条目就很简单,因为您可以使用reindex方法或基于.loc的索引。由于这可能需要一些数据处理和集合逻辑,drop方法将返回一个新对象,其中包含从轴中删除的指定值或值:

In [113]: obj = pd.Series(np.arange(5.), index=["a", "b", "c", "d", "e"])

In [114]: obj
Out[114]: 
a    0.0
b    1.0
c    2.0
d    3.0
e    4.0
dtype: float64

In [115]: new_obj = obj.drop("c")

In [116]: new_obj
Out[116]: 
a    0.0
b    1.0
d    3.0
e    4.0
dtype: float64

In [117]: obj.drop(["d", "c"])
Out[117]: 
a    0.0
b    1.0
e    4.0
dtype: float64

使用 DataFrame,可以从任一轴删除索引值。为了说明这一点,我们首先创建一个示例 DataFrame:

In [118]: data = pd.DataFrame(np.arange(16).reshape((4, 4)),
 .....:                     index=["Ohio", "Colorado", "Utah", "New York"],
 .....:                     columns=["one", "two", "three", "four"])

In [119]: data
Out[119]: 
 one  two  three  four
Ohio        0    1      2     3
Colorado    4    5      6     7
Utah        8    9     10    11
New York   12   13     14    15

使用一系列标签调用drop将从行标签(轴 0)中删除值:

In [120]: data.drop(index=["Colorado", "Ohio"])
Out[120]: 
 one  two  three  four
Utah        8    9     10    11
New York   12   13     14    15

要从列中删除标签,而不是使用columns关键字:

In [121]: data.drop(columns=["two"])
Out[121]: 
 one  three  four
Ohio        0      2     3
Colorado    4      6     7
Utah        8     10    11
New York   12     14    15

您还可以通过传递axis=1(类似于 NumPy)或axis="columns"来从列中删除值:

In [122]: data.drop("two", axis=1)
Out[122]: 
 one  three  four
Ohio        0      2     3
Colorado    4      6     7
Utah        8     10    11
New York   12     14    15

In [123]: data.drop(["two", "four"], axis="columns")
Out[123]: 
 one  three
Ohio        0      2
Colorado    4      6
Utah        8     10
New York   12     14

索引、选择和过滤

Series 索引(obj[...])的工作方式类似于 NumPy 数组索引,只是您可以使用 Series 的索引值而不仅仅是整数。以下是一些示例:

In [124]: obj = pd.Series(np.arange(4.), index=["a", "b", "c", "d"])

In [125]: obj
Out[125]: 
a    0.0
b    1.0
c    2.0
d    3.0
dtype: float64

In [126]: obj["b"]
Out[126]: 1.0

In [127]: obj[1]
Out[127]: 1.0

In [128]: obj[2:4]
Out[128]: 
c    2.0
d    3.0
dtype: float64

In [129]: obj[["b", "a", "d"]]
Out[129]: 
b    1.0
a    0.0
d    3.0
dtype: float64

In [130]: obj[[1, 3]]
Out[130]: 
b    1.0
d    3.0
dtype: float64

In [131]: obj[obj < 2]
Out[131]: 
a    0.0
b    1.0
dtype: float64

虽然您可以通过标签这种方式选择数据,但选择索引值的首选方式是使用特殊的loc运算符:

In [132]: obj.loc[["b", "a", "d"]]
Out[132]: 
b    1.0
a    0.0
d    3.0
dtype: float64

更喜欢loc的原因是因为在使用[]进行索引时,对整数的处理方式不同。如果索引包含整数,常规的[]索引将将整数视为标签,因此行为取决于索引的数据类型。例如:

In [133]: obj1 = pd.Series([1, 2, 3], index=[2, 0, 1])

In [134]: obj2 = pd.Series([1, 2, 3], index=["a", "b", "c"])

In [135]: obj1
Out[135]: 
2    1
0    2
1    3
dtype: int64

In [136]: obj2
Out[136]: 
a    1
b    2
c    3
dtype: int64

In [137]: obj1[[0, 1, 2]]
Out[137]: 
0    2
1    3
2    1
dtype: int64

In [138]: obj2[[0, 1, 2]]
Out[138]: 
a    1
b    2
c    3
dtype: int64

在使用loc时,当索引不包含整数时,表达式obj.loc[[0, 1, 2]]将失败:

In [134]: obj2.loc[[0, 1]]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
/tmp/ipykernel_804589/4185657903.py in <module>
----> 1 obj2.loc[[0, 1]]

^ LONG EXCEPTION ABBREVIATED ^

KeyError: "None of [Int64Index([0, 1], dtype="int64")] are in the [index]"

由于loc运算符仅使用标签进行索引,因此还有一个iloc运算符,它仅使用整数进行索引,以便在索引包含整数或不包含整数时始终保持一致:

In [139]: obj1.iloc[[0, 1, 2]]
Out[139]: 
2    1
0    2
1    3
dtype: int64

In [140]: obj2.iloc[[0, 1, 2]]
Out[140]: 
a    1
b    2
c    3
dtype: int64

注意

您也可以使用标签进行切片,但与正常的 Python 切片不同,终点是包含的:

In [141]: obj2.loc["b":"c"]
Out[141]: 
b    2
c    3
dtype: int64

使用这些方法分配值会修改 Series 的相应部分:

In [142]: obj2.loc["b":"c"] = 5

In [143]: obj2
Out[143]: 
a    1
b    5
c    5
dtype: int64

注意

尝试调用lociloc等函数而不是使用方括号“索引”可能是新手的常见错误。方括号表示用于启用切片操作并允许在 DataFrame 对象上的多个轴上进行索引。

在 DataFrame 中进行索引会检索一个或多个列,可以使用单个值或序列:

In [144]: data = pd.DataFrame(np.arange(16).reshape((4, 4)),
 .....:                     index=["Ohio", "Colorado", "Utah", "New York"],
 .....:                     columns=["one", "two", "three", "four"])

In [145]: data
Out[145]: 
 one  two  three  four
Ohio        0    1      2     3
Colorado    4    5      6     7
Utah        8    9     10    11
New York   12   13     14    15

In [146]: data["two"]
Out[146]: 
Ohio         1
Colorado     5
Utah         9
New York    13
Name: two, dtype: int64

In [147]: data[["three", "one"]]
Out[147]: 
 three  one
Ohio          2    0
Colorado      6    4
Utah         10    8
New York     14   12

这种索引有一些特殊情况。第一个是使用布尔数组进行切片或选择数据:

In [148]: data[:2]
Out[148]: 
 one  two  three  four
Ohio        0    1      2     3
Colorado    4    5      6     7

In [149]: data[data["three"] > 5]
Out[149]: 
 one  two  three  four
Colorado    4    5      6     7
Utah        8    9     10    11
New York   12   13     14    15

行选择语法data[:2]是作为一种便利提供的。将单个元素或列表传递给[]运算符将选择列。

另一个用例是使用布尔 DataFrame 进行索引,比如通过标量比较生成的 DataFrame。考虑一个通过与标量值比较生成的全布尔值的 DataFrame:

In [150]: data < 5
Out[150]: 
 one    two  three   four
Ohio       True   True   True   True
Colorado   True  False  False  False
Utah      False  False  False  False
New York  False  False  False  False

我们可以使用这个 DataFrame 将值为True的位置赋值为 0,就像这样:

In [151]: data[data < 5] = 0

In [152]: data
Out[152]: 
 one  two  three  four
Ohio        0    0      0     0
Colorado    0    5      6     7
Utah        8    9     10    11
New York   12   13     14    15
使用 loc 和 iloc 在 DataFrame 上进行选择

与 Series 一样,DataFrame 具有专门的属性lociloc,用于基于标签和基于整数的索引。由于 DataFrame 是二维的,您可以使用类似 NumPy 的符号使用轴标签(loc)或整数(iloc)选择行和列的子集。

作为第一个示例,让我们通过标签选择单行:

In [153]: data
Out[153]: 
 one  two  three  four
Ohio        0    0      0     0
Colorado    0    5      6     7
Utah        8    9     10    11
New York   12   13     14    15

In [154]: data.loc["Colorado"]
Out[154]: 
one      0
two      5
three    6
four     7
Name: Colorado, dtype: int64

选择单行的结果是一个带有包含 DataFrame 列标签的索引的 Series。要选择多个行,创建一个新的 DataFrame,传递一个标签序列:

In [155]: data.loc[["Colorado", "New York"]]
Out[155]: 
 one  two  three  four
Colorado    0    5      6     7
New York   12   13     14    15

您可以通过用逗号分隔选择在loc中同时选择行和列:

In [156]: data.loc["Colorado", ["two", "three"]]
Out[156]: 
two      5
three    6
Name: Colorado, dtype: int64

然后我们将使用iloc执行一些类似的整数选择:

In [157]: data.iloc[2]
Out[157]: 
one       8
two       9
three    10
four     11
Name: Utah, dtype: int64

In [158]: data.iloc[[2, 1]]
Out[158]: 
 one  two  three  four
Utah        8    9     10    11
Colorado    0    5      6     7

In [159]: data.iloc[2, [3, 0, 1]]
Out[159]: 
four    11
one      8
two      9
Name: Utah, dtype: int64

In [160]: data.iloc[[1, 2], [3, 0, 1]]
Out[160]: 
 four  one  two
Colorado     7    0    5
Utah        11    8    9

这两个索引函数都可以处理切片,除了单个标签或标签列表:

In [161]: data.loc[:"Utah", "two"]
Out[161]: 
Ohio        0
Colorado    5
Utah        9
Name: two, dtype: int64

In [162]: data.iloc[:, :3][data.three > 5]
Out[162]: 
 one  two  three
Colorado    0    5      6
Utah        8    9     10
New York   12   13     14

布尔数组可以与loc一起使用,但不能与iloc一起使用:

In [163]: data.loc[data.three >= 2]
Out[163]: 
 one  two  three  four
Colorado    0    5      6     7
Utah        8    9     10    11
New York   12   13     14    15

有许多方法可以选择和重新排列 pandas 对象中包含的数据。对于 DataFrame,表 5.4 提供了许多这些方法的简要总结。正如您将在后面看到的,还有许多其他选项可用于处理分层索引。

表 5.4:DataFrame 的索引选项

整数索引的陷阱

使用整数索引的 pandas 对象可能会成为新用户的绊脚石,因为它们与内置的 Python 数据结构(如列表和元组)的工作方式不同。例如,您可能不会期望以下代码生成错误:

In [164]: ser = pd.Series(np.arange(3.))

In [165]: ser
Out[165]: 
0    0.0
1    1.0
2    2.0
dtype: float64

In [166]: ser[-1]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/miniforge-x86/envs/book-env/lib/python3.10/site-packages/pandas/core/indexes/ra
nge.py in get_loc(self, key)
 344             try:
--> 345                 return self._range.index(new_key)
 346             except ValueError as err:
ValueError: -1 is not in range
The above exception was the direct cause of the following exception:
KeyError                                  Traceback (most recent call last)
<ipython-input-166-44969a759c20> in <module>
----> 1 ser[-1]
~/miniforge-x86/envs/book-env/lib/python3.10/site-packages/pandas/core/series.py 
in __getitem__(self, key)
 1010 
 1011         elif key_is_scalar:
-> 1012             return self._get_value(key)
 1013 
 1014         if is_hashable(key):
~/miniforge-x86/envs/book-env/lib/python3.10/site-packages/pandas/core/series.py 
in _get_value(self, label, takeable)
 1119 
 1120         # Similar to Index.get_value, but we do not fall back to position
al
-> 1121         loc = self.index.get_loc(label)
 1122 
 1123         if is_integer(loc):
~/miniforge-x86/envs/book-env/lib/python3.10/site-packages/pandas/core/indexes/ra
nge.py in get_loc(self, key)
 345                 return self._range.index(new_key)
 346             except ValueError as err:
--> 347                 raise KeyError(key) from err
 348         self._check_indexing_error(key)
 349         raise KeyError(key)
KeyError: -1

在这种情况下,pandas 可能会“回退”到整数索引,但是在不引入对用户代码中微妙错误的情况下,通常很难做到这一点。在这里,我们有一个包含012的索引,但 pandas 不想猜测用户想要什么(基于标签的索引还是基于位置的):

In [167]: ser
Out[167]: 
0    0.0
1    1.0
2    2.0
dtype: float64

另一方面,对于非整数索引,没有这种歧义:

In [168]: ser2 = pd.Series(np.arange(3.), index=["a", "b", "c"])

In [169]: ser2[-1]
Out[169]: 2.0

如果您有包含整数的轴索引,数据选择将始终是基于标签的。正如我上面所说的,如果您使用loc(用于标签)或iloc(用于整数),您将得到确切想要的结果:

In [170]: ser.iloc[-1]
Out[170]: 2.0

另一方面,使用整数进行切片始终是基于整数的:

In [171]: ser[:2]
Out[171]: 
0    0.0
1    1.0
dtype: float64

由于这些陷阱,最好始终优先使用lociloc进行索引,以避免歧义。

链式索引的陷阱

在前一节中,我们看了如何使用lociloc在 DataFrame 上进行灵活的选择。这些索引属性也可以用于就地修改 DataFrame 对象,但这样做需要一些小心。

例如,在上面的 DataFrame 示例中,我们可以按标签或整数位置分配到列或行:

In [172]: data.loc[:, "one"] = 1

In [173]: data
Out[173]: 
 one  two  three  four
Ohio        1    0      0     0
Colorado    1    5      6     7
Utah        1    9     10    11
New York    1   13     14    15

In [174]: data.iloc[2] = 5

In [175]: data
Out[175]: 
 one  two  three  four
Ohio        1    0      0     0
Colorado    1    5      6     7
Utah        5    5      5     5
New York    1   13     14    15

In [176]: data.loc[data["four"] > 5] = 3

In [177]: data
Out[177]: 
 one  two  three  four
Ohio        1    0      0     0
Colorado    3    3      3     3
Utah        5    5      5     5
New York    3    3      3     3

对于新的 pandas 用户来说,一个常见的坑是在赋值时链接选择,就像这样:

In [177]: data.loc[data.three == 5]["three"] = 6
<ipython-input-11-0ed1cf2155d5>:1: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

根据数据内容的不同,这可能会打印一个特殊的SettingWithCopyWarning,它警告您正在尝试修改一个临时值(data.loc[data.three == 5]的非空结果),而不是原始 DataFramedata,这可能是您的本意。在这里,data没有被修改:

In [179]: data
Out[179]: 
 one  two  three  four
Ohio        1    0      0     0
Colorado    3    3      3     3
Utah        5    5      5     5
New York    3    3      3     3

在这些情况下,修复的方法是重写链接赋值,使用单个loc操作:

In [180]: data.loc[data.three == 5, "three"] = 6

In [181]: data
Out[181]: 
 one  two  three  four
Ohio        1    0      0     0
Colorado    3    3      3     3
Utah        5    5      6     5
New York    3    3      3     3

一个很好的经验法则是在进行赋值时避免链接索引。还有其他情况下,pandas 会生成SettingWithCopyWarning,这与链接索引有关。我建议您查阅在线 pandas 文档中的这个主题。

算术和数据对齐

pandas 可以使处理具有不同索引的对象变得更简单。例如,当您添加对象时,如果任何索引对不相同,结果中的相应索引将是索引对的并集。让我们看一个例子:

In [182]: s1 = pd.Series([7.3, -2.5, 3.4, 1.5], index=["a", "c", "d", "e"])

In [183]: s2 = pd.Series([-2.1, 3.6, -1.5, 4, 3.1],
 .....:                index=["a", "c", "e", "f", "g"])

In [184]: s1
Out[184]: 
a    7.3
c   -2.5
d    3.4
e    1.5
dtype: float64

In [185]: s2
Out[185]: 
a   -2.1
c    3.6
e   -1.5
f    4.0
g    3.1
dtype: float64

将它们相加得到:

In [186]: s1 + s2
Out[186]: 
a    5.2
c    1.1
d    NaN
e    0.0
f    NaN
g    NaN
dtype: float64

内部数据对齐会在不重叠的标签位置引入缺失值。缺失值将在进一步的算术计算中传播。

对于 DataFrame,对齐是在行和列上执行的:

In [187]: df1 = pd.DataFrame(np.arange(9.).reshape((3, 3)), columns=list("bcd"),
 .....:                    index=["Ohio", "Texas", "Colorado"])

In [188]: df2 = pd.DataFrame(np.arange(12.).reshape((4, 3)), columns=list("bde"),
 .....:                    index=["Utah", "Ohio", "Texas", "Oregon"])

In [189]: df1
Out[189]: 
 b    c    d
Ohio      0.0  1.0  2.0
Texas     3.0  4.0  5.0
Colorado  6.0  7.0  8.0

In [190]: df2
Out[190]: 
 b     d     e
Utah    0.0   1.0   2.0
Ohio    3.0   4.0   5.0
Texas   6.0   7.0   8.0
Oregon  9.0  10.0  11.0

将它们相加返回一个 DataFrame,其索引和列是每个 DataFrame 中的索引的并集:

In [191]: df1 + df2
Out[191]: 
 b   c     d   e
Colorado  NaN NaN   NaN NaN
Ohio      3.0 NaN   6.0 NaN
Oregon    NaN NaN   NaN NaN
Texas     9.0 NaN  12.0 NaN
Utah      NaN NaN   NaN NaN

由于 DataFrame 对象中都没有找到"c""e"列,它们在结果中显示为缺失。对于标签不共同的行也是如此。

如果添加没有共同列或行标签的 DataFrame 对象,结果将包含所有空值:

In [192]: df1 = pd.DataFrame({"A": [1, 2]})

In [193]: df2 = pd.DataFrame({"B": [3, 4]})

In [194]: df1
Out[194]: 
 A
0  1
1  2

In [195]: df2
Out[195]: 
 B
0  3
1  4

In [196]: df1 + df2
Out[196]: 
 A   B
0 NaN NaN
1 NaN NaN
带有填充值的算术方法

在不同索引对象之间的算术操作中,当一个对象中找到一个轴标签而另一个对象中没有时,您可能希望填充一个特殊值,比如 0。以下是一个示例,我们通过将np.nan赋值给它来将特定值设置为 NA(null):

In [197]: df1 = pd.DataFrame(np.arange(12.).reshape((3, 4)),
 .....:                    columns=list("abcd"))

In [198]: df2 = pd.DataFrame(np.arange(20.).reshape((4, 5)),
 .....:                    columns=list("abcde"))

In [199]: df2.loc[1, "b"] = np.nan

In [200]: df1
Out[200]: 
 a    b     c     d
0  0.0  1.0   2.0   3.0
1  4.0  5.0   6.0   7.0
2  8.0  9.0  10.0  11.0

In [201]: df2
Out[201]: 
 a     b     c     d     e
0   0.0   1.0   2.0   3.0   4.0
1   5.0   NaN   7.0   8.0   9.0
2  10.0  11.0  12.0  13.0  14.0
3  15.0  16.0  17.0  18.0  19.0

将它们相加会导致不重叠位置的缺失值:

In [202]: df1 + df2
Out[202]: 
 a     b     c     d   e
0   0.0   2.0   4.0   6.0 NaN
1   9.0   NaN  13.0  15.0 NaN
2  18.0  20.0  22.0  24.0 NaN
3   NaN   NaN   NaN   NaN NaN

df1上使用add方法,我传递df2和一个参数给fill_value,它会用传递的值替换操作中的任何缺失值:

In [203]: df1.add(df2, fill_value=0)
Out[203]: 
 a     b     c     d     e
0   0.0   2.0   4.0   6.0   4.0
1   9.0   5.0  13.0  15.0   9.0
2  18.0  20.0  22.0  24.0  14.0
3  15.0  16.0  17.0  18.0  19.0

请参阅表 5.5 以获取有关算术的 Series 和 DataFrame 方法的列表。每个方法都有一个对应的方法,以字母r开头,参数顺序相反。因此,以下两个语句是等价的:

In [204]: 1 / df1
Out[204]: 
 a         b         c         d
0    inf  1.000000  0.500000  0.333333
1  0.250  0.200000  0.166667  0.142857
2  0.125  0.111111  0.100000  0.090909

In [205]: df1.rdiv(1)
Out[205]: 
 a         b         c         d
0    inf  1.000000  0.500000  0.333333
1  0.250  0.200000  0.166667  0.142857
2  0.125  0.111111  0.100000  0.090909

相关地,在重新索引 Series 或 DataFrame 时,您还可以指定不同的填充值:

In [206]: df1.reindex(columns=df2.columns, fill_value=0)
Out[206]: 
 a    b     c     d  e
0  0.0  1.0   2.0   3.0  0
1  4.0  5.0   6.0   7.0  0
2  8.0  9.0  10.0  11.0  0

表 5.5:灵活的算术方法

DataFrame 和 Series 之间的操作

与不同维度的 NumPy 数组一样,DataFrame 和 Series 之间的算术也是定义的。首先,作为一个激励性的例子,考虑一个二维数组和其一行之间的差异:

In [207]: arr = np.arange(12.).reshape((3, 4))

In [208]: arr
Out[208]: 
array([[ 0.,  1.,  2.,  3.],
 [ 4.,  5.,  6.,  7.],
 [ 8.,  9., 10., 11.]])

In [209]: arr[0]
Out[209]: array([0., 1., 2., 3.])

In [210]: arr - arr[0]
Out[210]: 
array([[0., 0., 0., 0.],
 [4., 4., 4., 4.],
 [8., 8., 8., 8.]])

当我们从arr中减去arr[0]时,减法将针对每一行执行一次。这被称为广播,并且在附录 A:高级 NumPy 中更详细地解释了它与一般 NumPy 数组的关系。DataFrame 和 Series 之间的操作类似:

In [211]: frame = pd.DataFrame(np.arange(12.).reshape((4, 3)),
 .....:                      columns=list("bde"),
 .....:                      index=["Utah", "Ohio", "Texas", "Oregon"])

In [212]: series = frame.iloc[0]

In [213]: frame
Out[213]: 
 b     d     e
Utah    0.0   1.0   2.0
Ohio    3.0   4.0   5.0
Texas   6.0   7.0   8.0
Oregon  9.0  10.0  11.0

In [214]: series
Out[214]: 
b    0.0
d    1.0
e    2.0
Name: Utah, dtype: float64

默认情况下,DataFrame 和 Series 之间的算术会将 Series 的索引与 DataFrame 的列匹配,向下广播行:

In [215]: frame - series
Out[215]: 
 b    d    e
Utah    0.0  0.0  0.0
Ohio    3.0  3.0  3.0
Texas   6.0  6.0  6.0
Oregon  9.0  9.0  9.0

如果索引值既不在 DataFrame 的列中,也不在 Series 的索引中找到,那么对象将被重新索引以形成并集:

In [216]: series2 = pd.Series(np.arange(3), index=["b", "e", "f"])

In [217]: series2
Out[217]: 
b    0
e    1
f    2
dtype: int64

In [218]: frame + series2
Out[218]: 
 b   d     e   f
Utah    0.0 NaN   3.0 NaN
Ohio    3.0 NaN   6.0 NaN
Texas   6.0 NaN   9.0 NaN
Oregon  9.0 NaN  12.0 NaN

如果您希望在列上进行广播,匹配行,您必须使用其中一个算术方法并指定匹配索引。例如:

In [219]: series3 = frame["d"]

In [220]: frame
Out[220]: 
 b     d     e
Utah    0.0   1.0   2.0
Ohio    3.0   4.0   5.0
Texas   6.0   7.0   8.0
Oregon  9.0  10.0  11.0

In [221]: series3
Out[221]: 
Utah       1.0
Ohio       4.0
Texas      7.0
Oregon    10.0
Name: d, dtype: float64

In [222]: frame.sub(series3, axis="index")
Out[222]: 
 b    d    e
Utah   -1.0  0.0  1.0
Ohio   -1.0  0.0  1.0
Texas  -1.0  0.0  1.0
Oregon -1.0  0.0  1.0

您传递的轴是要匹配的轴。在这种情况下,我们的意思是匹配 DataFrame 的行索引(axis="index")并在列之间广播。

函数应用和映射

NumPy ufuncs(逐元素数组方法)也适用于 pandas 对象:

In [223]: frame = pd.DataFrame(np.random.standard_normal((4, 3)),
 .....:                      columns=list("bde"),
 .....:                      index=["Utah", "Ohio", "Texas", "Oregon"])

In [224]: frame
Out[224]: 
 b         d         e
Utah   -0.204708  0.478943 -0.519439
Ohio   -0.555730  1.965781  1.393406
Texas   0.092908  0.281746  0.769023
Oregon  1.246435  1.007189 -1.296221

In [225]: np.abs(frame)
Out[225]: 
 b         d         e
Utah    0.204708  0.478943  0.519439
Ohio    0.555730  1.965781  1.393406
Texas   0.092908  0.281746  0.769023
Oregon  1.246435  1.007189  1.296221

另一个频繁的操作是将一个一维数组上的函数应用于每列或每行。DataFrame 的apply方法正是这样做的:

In [226]: def f1(x):
 .....:     return x.max() - x.min()

In [227]: frame.apply(f1)
Out[227]: 
b    1.802165
d    1.684034
e    2.689627
dtype: float64

这里的函数f计算 Series 的最大值和最小值之间的差异,对frame中的每列调用一次。结果是一个具有frame列作为其索引的 Series。

如果将axis="columns"传递给apply,则该函数将每行调用一次。将其视为"跨列应用"是一种有用的方式:

In [228]: frame.apply(f1, axis="columns")
Out[228]: 
Utah      0.998382
Ohio      2.521511
Texas     0.676115
Oregon    2.542656
dtype: float64

许多最常见的数组统计(如summean)都是 DataFrame 方法,因此不需要使用apply

传递给apply的函数不必返回标量值;它也可以返回具有多个值的 Series:

In [229]: def f2(x):
 .....:     return pd.Series([x.min(), x.max()], index=["min", "max"])

In [230]: frame.apply(f2)
Out[230]: 
 b         d         e
min -0.555730  0.281746 -1.296221
max  1.246435  1.965781  1.393406

也可以使用逐元素 Python 函数。假设您想要从frame中的每个浮点值计算格式化字符串。您可以使用applymap来实现:

In [231]: def my_format(x):
 .....:     return f"{x:.2f}"

In [232]: frame.applymap(my_format)
Out[232]: 
 b     d      e
Utah    -0.20  0.48  -0.52
Ohio    -0.56  1.97   1.39
Texas    0.09  0.28   0.77
Oregon   1.25  1.01  -1.30

applymap的命名原因是 Series 有一个map方法,用于应用逐元素函数:

In [233]: frame["e"].map(my_format)
Out[233]: 
Utah      -0.52
Ohio       1.39
Texas      0.77
Oregon    -1.30
Name: e, dtype: object

排序和排名

按某个标准对数据集进行排序是另一个重要的内置操作。要按行或列标签的字典顺序排序,请使用sort_index方法,该方法返回一个新的排序对象:

In [234]: obj = pd.Series(np.arange(4), index=["d", "a", "b", "c"])

In [235]: obj
Out[235]: 
d    0
a    1
b    2
c    3
dtype: int64

In [236]: obj.sort_index()
Out[236]: 
a    1
b    2
c    3
d    0
dtype: int64

对于 DataFrame,您可以在任一轴上按索引排序:

In [237]: frame = pd.DataFrame(np.arange(8).reshape((2, 4)),
 .....:                      index=["three", "one"],
 .....:                      columns=["d", "a", "b", "c"])

In [238]: frame
Out[238]: 
 d  a  b  c
three  0  1  2  3
one    4  5  6  7

In [239]: frame.sort_index()
Out[239]: 
 d  a  b  c
one    4  5  6  7
three  0  1  2  3

In [240]: frame.sort_index(axis="columns")
Out[240]: 
 a  b  c  d
three  1  2  3  0
one    5  6  7  4

默认情况下,数据按升序排序,但也可以按降序排序:

In [241]: frame.sort_index(axis="columns", ascending=False)
Out[241]: 
 d  c  b  a
three  0  3  2  1
one    4  7  6  5

要按值对 Series 进行排序,请使用其sort_values方法:

In [242]: obj = pd.Series([4, 7, -3, 2])

In [243]: obj.sort_values()
Out[243]: 
2   -3
3    2
0    4
1    7
dtype: int64

默认情况下,任何缺失值都按顺序排在 Series 的末尾:

In [244]: obj = pd.Series([4, np.nan, 7, np.nan, -3, 2])

In [245]: obj.sort_values()
Out[245]: 
4   -3.0
5    2.0
0    4.0
2    7.0
1    NaN
3    NaN
dtype: float64

缺失值也可以通过使用na_position选项将其排序到开头:

In [246]: obj.sort_values(na_position="first")
Out[246]: 
1    NaN
3    NaN
4   -3.0
5    2.0
0    4.0
2    7.0
dtype: float64

在对 DataFrame 进行排序时,可以使用一个或多个列中的数据作为排序键。为此,请将一个或多个列名传递给sort_values

In [247]: frame = pd.DataFrame({"b": [4, 7, -3, 2], "a": [0, 1, 0, 1]})

In [248]: frame
Out[248]: 
 b  a
0  4  0
1  7  1
2 -3  0
3  2  1

In [249]: frame.sort_values("b")
Out[249]: 
 b  a
2 -3  0
3  2  1
0  4  0
1  7  1

要按多个列排序,请传递一个名称列表:

In [250]: frame.sort_values(["a", "b"])
Out[250]: 
 b  a
2 -3  0
0  4  0
3  2  1
1  7  1

排名从数组中的最低值开始,为数组中的每个有效数据点分配从 1 到数据点数量的等级。Series 和 DataFrame 的rank方法是要查看的地方;默认情况下,rank通过为每个组分配平均等级来打破平局:

In [251]: obj = pd.Series([7, -5, 7, 4, 2, 0, 4])

In [252]: obj.rank()
Out[252]: 
0    6.5
1    1.0
2    6.5
3    4.5
4    3.0
5    2.0
6    4.5
dtype: float64

排名也可以根据它们在数据中观察到的顺序进行分配:

In [253]: obj.rank(method="first")
Out[253]: 
0    6.0
1    1.0
2    7.0
3    4.0
4    3.0
5    2.0
6    5.0
dtype: float64

在这里,与使用条目 0 和 2 的平均等级 6.5 不同,它们分别设置为 6 和 7,因为标签 0 在数据中位于标签 2 之前。

您也可以按降序排名:

In [254]: obj.rank(ascending=False)
Out[254]: 
0    1.5
1    7.0
2    1.5
3    3.5
4    5.0
5    6.0
6    3.5
dtype: float64

请参阅表 5.6 以获取可用的平局破解方法列表。

DataFrame 可以在行或列上计算排名:

In [255]: frame = pd.DataFrame({"b": [4.3, 7, -3, 2], "a": [0, 1, 0, 1],
 .....:                       "c": [-2, 5, 8, -2.5]})

In [256]: frame
Out[256]: 
 b  a    c
0  4.3  0 -2.0
1  7.0  1  5.0
2 -3.0  0  8.0
3  2.0  1 -2.5

In [257]: frame.rank(axis="columns")
Out[257]: 
 b    a    c
0  3.0  2.0  1.0
1  3.0  1.0  2.0
2  1.0  2.0  3.0
3  3.0  2.0  1.0

表 5.6:排名的平局破解方法

具有重复标签的轴索引

到目前为止,我们看过的几乎所有示例都具有唯一的轴标签(索引值)。虽然许多 pandas 函数(如reindex)要求标签是唯一的,但这并非强制要求。让我们考虑一个具有重复索引的小 Series:

In [258]: obj = pd.Series(np.arange(5), index=["a", "a", "b", "b", "c"])

In [259]: obj
Out[259]: 
a    0
a    1
b    2
b    3
c    4
dtype: int64

索引的is_unique属性可以告诉您其标签是否唯一:

In [260]: obj.index.is_unique
Out[260]: False

数据选择是与重复不同的主要行为之一。索引具有多个条目的标签返回一个 Series,而单个条目返回一个标量值:

In [261]: obj["a"]
Out[261]: 
a    0
a    1
dtype: int64

In [262]: obj["c"]
Out[262]: 4

这可能会使您的代码变得更加复杂,因为根据标签是否重复,索引的输出类型可能会有所不同。

相同的逻辑也适用于 DataFrame 中的行(或列)索引:

In [263]: df = pd.DataFrame(np.random.standard_normal((5, 3)),
 .....:                   index=["a", "a", "b", "b", "c"])

In [264]: df
Out[264]: 
 0         1         2
a  0.274992  0.228913  1.352917
a  0.886429 -2.001637 -0.371843
b  1.669025 -0.438570 -0.539741
b  0.476985  3.248944 -1.021228
c -0.577087  0.124121  0.302614

In [265]: df.loc["b"]
Out[265]: 
 0         1         2
b  1.669025 -0.438570 -0.539741
b  0.476985  3.248944 -1.021228

In [266]: df.loc["c"]
Out[266]: 
0   -0.577087
1    0.124121
2    0.302614
Name: c, dtype: float64

5.3 总结和计算描述性统计

pandas 对象配备了一组常见的数学和统计方法。其中大多数属于减少摘要统计的类别,这些方法从 Series 中提取单个值(如总和或均值),或者从 DataFrame 的行或列中提取一系列值。与 NumPy 数组上找到的类似方法相比,它们内置了对缺失数据的处理。考虑一个小的 DataFrame:

In [267]: df = pd.DataFrame([[1.4, np.nan], [7.1, -4.5],
 .....:                    [np.nan, np.nan], [0.75, -1.3]],
 .....:                   index=["a", "b", "c", "d"],
 .....:                   columns=["one", "two"])

In [268]: df
Out[268]: 
 one  two
a  1.40  NaN
b  7.10 -4.5
c   NaN  NaN
d  0.75 -1.3

调用 DataFrame 的sum方法会返回一个包含列和的 Series:

In [269]: df.sum()
Out[269]: 
one    9.25
two   -5.80
dtype: float64

传递axis="columns"axis=1会跨列求和:

In [270]: df.sum(axis="columns")
Out[270]: 
a    1.40
b    2.60
c    0.00
d   -0.55
dtype: float64

当整行或整列包含所有 NA 值时,总和为 0,而如果任何值不是 NA,则结果为 NA。可以使用skipna选项禁用此功能,在这种情况下,行或列中的任何 NA 值都会使相应的结果为 NA:

In [271]: df.sum(axis="index", skipna=False)
Out[271]: 
one   NaN
two   NaN
dtype: float64

In [272]: df.sum(axis="columns", skipna=False)
Out[272]: 
a     NaN
b    2.60
c     NaN
d   -0.55
dtype: float64

一些聚合,如mean,需要至少一个非 NA 值才能产生一个值结果,因此我们有:

In [273]: df.mean(axis="columns")
Out[273]: 
a    1.400
b    1.300
c      NaN
d   -0.275
dtype: float64

请参见表 5.7 以获取每种减少方法的常见选项列表。

表 5.7:减少方法的选项

一些方法,如idxminidxmax,返回间接统计信息,如达到最小值或最大值的索引值:

In [274]: df.idxmax()
Out[274]: 
one    b
two    d
dtype: object

其他方法是累积

In [275]: df.cumsum()
Out[275]: 
 one  two
a  1.40  NaN
b  8.50 -4.5
c   NaN  NaN
d  9.25 -5.8

一些方法既不是减少也不是累积。describe就是一个例子,一次生成多个摘要统计信息:

In [276]: df.describe()
Out[276]: 
 one       two
count  3.000000  2.000000
mean   3.083333 -2.900000
std    3.493685  2.262742
min    0.750000 -4.500000
25%    1.075000 -3.700000
50%    1.400000 -2.900000
75%    4.250000 -2.100000
max    7.100000 -1.300000

对于非数字数据,describe会生成替代的摘要统计信息:

In [277]: obj = pd.Series(["a", "a", "b", "c"] * 4)

In [278]: obj.describe()
Out[278]: 
count     16
unique     3
top        a
freq       8
dtype: object

请参见表 5.8 以获取摘要统计和相关方法的完整列表。

表 5.8:描述性和摘要统计

相关性和协方差

一些摘要统计信息,如相关性和协方差,是从一对参数计算得出的。让我们考虑一些股票价格和成交量的 DataFrame,最初从 Yahoo! Finance 获取,并在本书的附带数据集中以二进制 Python pickle 文件的形式提供:

In [279]: price = pd.read_pickle("examples/yahoo_price.pkl")

In [280]: volume = pd.read_pickle("examples/yahoo_volume.pkl")

现在我计算价格的百分比变化,这是一个时间序列操作,将在第十一章:时间序列中进一步探讨:

In [281]: returns = price.pct_change()

In [282]: returns.tail()
Out[282]: 
 AAPL      GOOG       IBM      MSFT
Date 
2016-10-17 -0.000680  0.001837  0.002072 -0.003483
2016-10-18 -0.000681  0.019616 -0.026168  0.007690
2016-10-19 -0.002979  0.007846  0.003583 -0.002255
2016-10-20 -0.000512 -0.005652  0.001719 -0.004867
2016-10-21 -0.003930  0.003011 -0.012474  0.042096

Series 的corr方法计算两个 Series 中重叠的、非 NA、按索引对齐的值的相关性。相关地,cov计算协方差:

In [283]: returns["MSFT"].corr(returns["IBM"])
Out[283]: 0.49976361144151166

In [284]: returns["MSFT"].cov(returns["IBM"])
Out[284]: 8.870655479703549e-05

另一方面,DataFrame 的corrcov方法分别返回完整的相关性或协方差矩阵作为 DataFrame:

In [285]: returns.corr()
Out[285]: 
 AAPL      GOOG       IBM      MSFT
AAPL  1.000000  0.407919  0.386817  0.389695
GOOG  0.407919  1.000000  0.405099  0.465919
IBM   0.386817  0.405099  1.000000  0.499764
MSFT  0.389695  0.465919  0.499764  1.000000

In [286]: returns.cov()
Out[286]: 
 AAPL      GOOG       IBM      MSFT
AAPL  0.000277  0.000107  0.000078  0.000095
GOOG  0.000107  0.000251  0.000078  0.000108
IBM   0.000078  0.000078  0.000146  0.000089
MSFT  0.000095  0.000108  0.000089  0.000215

使用 DataFrame 的corrwith方法,您可以计算 DataFrame 的列或行与另一个 Series 或 DataFrame 之间的成对相关性。传递一个 Series 会返回一个 Series,其中计算了每列的相关值:

In [287]: returns.corrwith(returns["IBM"])
Out[287]: 
AAPL    0.386817
GOOG    0.405099
IBM     1.000000
MSFT    0.499764
dtype: float64

传递一个 DataFrame 会计算匹配列名的相关性。在这里,我计算了百分比变化与成交量的相关性:

In [288]: returns.corrwith(volume)
Out[288]: 
AAPL   -0.075565
GOOG   -0.007067
IBM    -0.204849
MSFT   -0.092950
dtype: float64

传递axis="columns"会逐行执行操作。在所有情况下,在计算相关性之前,数据点都会按标签对齐。

唯一值、值计数和成员资格

另一类相关方法提取一维 Series 中包含的值的信息。为了说明这些方法,考虑以下示例:

In [289]: obj = pd.Series(["c", "a", "d", "a", "a", "b", "b", "c", "c"])

第一个函数是unique,它为您提供 Series 中唯一值的数组:

In [290]: uniques = obj.unique()

In [291]: uniques
Out[291]: array(['c', 'a', 'd', 'b'], dtype=object)

唯一的值不一定按它们首次出现的顺序返回,也不按排序顺序返回,但如果需要的话可以在之后排序(uniques.sort())。相关地,value_counts计算包含值频率的 Series:

In [292]: obj.value_counts()
Out[292]: 
c    3
a    3
b    2
d    1
Name: count, dtype: int64

Series 按值降序排序以方便起见。value_counts也作为顶级 pandas 方法可用,可与 NumPy 数组或其他 Python 序列一起使用:

In [293]: pd.value_counts(obj.to_numpy(), sort=False)
Out[293]: 
c    3
a    3
d    1
b    2
Name: count, dtype: int64

isin执行矢量化的成员检查,并且在将数据集过滤到 Series 或 DataFrame 中的值子集时可能很有用:

In [294]: obj
Out[294]: 
0    c
1    a
2    d
3    a
4    a
5    b
6    b
7    c
8    c
dtype: object

In [295]: mask = obj.isin(["b", "c"])

In [296]: mask
Out[296]: 
0     True
1    False
2    False
3    False
4    False
5     True
6     True
7     True
8     True
dtype: bool

In [297]: obj[mask]
Out[297]: 
0    c
5    b
6    b
7    c
8    c
dtype: object

isin相关的是Index.get_indexer方法,它从可能不同的值的数组中为另一个不同值的数组提供索引数组:

In [298]: to_match = pd.Series(["c", "a", "b", "b", "c", "a"])

In [299]: unique_vals = pd.Series(["c", "b", "a"])

In [300]: indices = pd.Index(unique_vals).get_indexer(to_match)

In [301]: indices
Out[301]: array([0, 2, 1, 1, 0, 2])

有关这些方法的参考,请参见表 5.9。

表 5.9:唯一值、值计数和成员资格方法

在某些情况下,您可能希望在 DataFrame 中的多个相关列上计算直方图。以下是一个示例:

In [302]: data = pd.DataFrame({"Qu1": [1, 3, 4, 3, 4],
 .....:                      "Qu2": [2, 3, 1, 2, 3],
 .....:                      "Qu3": [1, 5, 2, 4, 4]})

In [303]: data
Out[303]: 
 Qu1  Qu2  Qu3
0    1    2    1
1    3    3    5
2    4    1    2
3    3    2    4
4    4    3    4

我们可以计算单列的值计数,如下所示:

In [304]: data["Qu1"].value_counts().sort_index()
Out[304]: 
Qu1
1    1
3    2
4    2
Name: count, dtype: int64

要为所有列计算此值,请将pandas.value_counts传递给 DataFrame 的apply方法:

In [305]: result = data.apply(pd.value_counts).fillna(0)

In [306]: result
Out[306]: 
 Qu1  Qu2  Qu3
1  1.0  1.0  1.0
2  0.0  2.0  1.0
3  2.0  2.0  0.0
4  2.0  0.0  2.0
5  0.0  0.0  1.0

在这里,结果中的行标签是所有列中出现的不同值。这些值是每列中这些值的相应计数。

还有一个DataFrame.value_counts方法,但它计算考虑 DataFrame 的每一行作为元组的计数,以确定每个不同行的出现次数:

In [307]: data = pd.DataFrame({"a": [1, 1, 1, 2, 2], "b": [0, 0, 1, 0, 0]})

In [308]: data
Out[308]: 
 a  b
0  1  0
1  1  0
2  1  1
3  2  0
4  2  0

In [309]: data.value_counts()
Out[309]: 
a  b
1  0    2
2  0    2
1  1    1
Name: count, dtype: int64

在这种情况下,结果具有一个表示不同行的索引作为层次索引,这是我们将在第八章:数据整理:连接、合并和重塑中更详细地探讨的一个主题。

5.4 结论

在下一章中,我们将讨论使用 pandas 读取(或加载)和写入数据集的工具。之后,我们将深入探讨使用 pandas 进行数据清洗、整理、分析和可视化的工具。

02-04 18:44