Skip to content

第 6 章 NumPy

本章内容提要

  • NumPy 简介与 ndarray
  • ndarray 数组操作
  • ndarray 数组函数与方法

前几章本书介绍了 Python 基本的编程知识,从这一章开始读者将进入 Python 数据分析基础模块的学习,包括 NumPy、Matplotlib 和 Pandas 三大模块,它们将分为 3 个章节分别进行介绍。

NumPy 是 Python 数据处理最重要的基础包,绝大多数的 Python 数据分析软件包都是基于 NumPy 构建的,因此学会操作 NumPy 是读者熟练使用 Python 进行数据分析的基石。

6.1 NumPy 简介与 ndarray

6.1.1 NumPy 简介

在学习 NumPy 之前,我们先来了解一下它的历史。

Python 的面向数组计算可以追溯到 1995 年,Jim Hugunin 创建了 Numeric 库。在接下来的 10 年中,许多科学编程社区纷纷开始使用 Python 的数组编程,但是进入 21 世纪后,Numeric 库的生态系统变得碎片化了。2005 年,Travis Oliphant 从 Numeric 和 Numarray 项目整理出了 NumPy 项目,将所有 Python 计算社区都集合到了这个框架下。

NumPy 有着较漫长的演化历史,它本身关注与底层语言(如 C、C++)的交互、数组运算、数学函数运算、磁盘数据的读写等。NumPy 在计算机一个独立于其他 Python 内置对象的连续的内存块中存储数据,它内部的 C 语言算法库可以直接操作内存,可以对数组执行复杂的计算,减少内存的消耗以及提升计算效率。虽然 NumPy 本身并没有提供很高级的数学分析功能,但它所提供的底层语言接口、无需编写循环的快速运算数学函数、操作快速且节省空间的多维数组等特性让它成为 Python 数据分析最核心的计算库之一。除此之外,理解和熟练操作 NumPy 数组将有助于读者使用 Pandas 等高级库。

在数据分析时,NumPy 能为读者提供的有用功能主要有:

  • 向量化数组操作,包括数据子集的构建、数据过滤与数据转换等。
  • 常用的数组算法操作,包括排序、唯一值、集合运算等。
  • 描述性统计量和数据汇总摘要。
  • 关系型数据操作,包括数据集的合并、连接等。
  • 数组的分组运算,包括聚合、转换、函数应用等。
  • 条件循环结构的数组化,即使用数组操作替换 if-else 等结构,获得高效的计算效率。

为了让读者明白 NumPy 与 Python 本身具体的计算性能差异,这里用一个一千万整数的数组和一个等长的 Python 列表进行测试。

In [1]: import numpy as np

In [2]: np_array = np.arange(10000000)

In [3]: py_array = list(range(10000000))

In [4]: %time for i in range(10): np_array * 2
CPU times: user 136 ms, sys: 300 ms, total: 436 ms
Wall time: 435 ms

In [5]: %time for i in range(10): [ x*2 for x in py_array ]
CPU times: user 6.04 s, sys: 1.66 s, total: 7.7 s
Wall time: 7.7 s

上面代码将两个序列都分别乘以 2,计算了 10 次。很明显,NumPy 比列表推导式节省了 10 多倍的时间,另外在操作上,NumPy 更简单直接(另外 NumPy 占用的内存更少,不过这里没有体现出来)。

这里最让人惊叹的是对于一个 1000 万个元素的数组(成为向量或矢量)乘法操作,NumPy 使用方法跟单个值(称为标量)完全一致。这种高效且简易操作的 NumPy 数组其正式对象名为 ndarray。

6.1.2 创建 ndarray

ndarray(N 维数组对象)是一个快速且灵活的数据集容器,Python 用户可以利用 ndarray 对数组的整块数据或选择性数据执行批量操作,它的语法与标量运算一致。

在上一小节的介绍中,本书使用了 arange() 函数快速创建了一个含连续值的 ndarray 对象,这对应 Python 常用的内置函数 range()。除了 arange() 函数,创建 ndarray 最简单的办法是用 array() 函数,它接受序列(列表、元组等)作为输入,输出一个包含输入数据的 ndarray。

下面代码演示使用列表和元组创建 ndarray 以及一个错误示例:

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

In [9]: np.array(2, 4, 6, 8)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-1ec14a5e9a23> in <module>()
----> 1 np.array(2, 4, 6, 8)

ValueError: only 2 non-keyword arguments accepted

嵌套列表可以转换为一个多维数组,数组元素等长和不等长得到的结果是不一样类型。从下面代码输出中可以发现,当输入的嵌套列表元素等长时,得到的是一个多维数组(这里是 2 维),而元素不等长时得到的是一个一维数组,内部的子列表是 ndarray 一个维度上的两个元素。

In [10]: np.array([[1, 3, 5, 7], [2, 4, 6, 8]])
Out[10]:
array([[1, 3, 5, 7],
       [2, 4, 6, 8]])

In [11]: np.array([[1, 3, 5, 7], [2, 4, 6]])
Out[11]: array([list([1, 3, 5, 7]), list([2, 4, 6])], dtype=object)

ndarray 一旦创建,读者可以通过对象属性获取数组的一些信息。例如,ndim 属性可以获取维度,shape 属性可以获取每个维度具体的长度。

In [12]: arr1 = np.array([[1, 3, 5, 7], [2, 4, 6, 8]])
In [14]: arr1.ndim
Out[14]: 2
In [15]: arr1.shape
Out[15]: (2, 4)

ndarray 除了存储常见的数值类型数据,还可以存储 Python 其他的常见数据类型,如字符串、逻辑值。默认情况下,NumPy 的 array() 函数会自动为输入序列选择一个合适的数据类型,类型值会被保存在一个称为 dtype 的特殊对象中,它同样可以以属性的形式访问。

In [16]: arr1.dtype
Out[16]: dtype('int64')

上述结果显示输入的都是整数时,NumPy 存储的数据类型是 int64,即 64 位整数。当输入的数据包含浮点类型,NumPy 生成的 ndarray 都将为浮点数。ndarray 会强制所有的元素数据类型保持一致。

In [17]: arr2 = np.array([[1.0, 3, 5, 7], [2, 4.0, 6, 8]])
In [18]: arr2.dtype
Out[18]: dtype('float64')

尽管对象 arr2 与 arr1 存储的数据在信息层面没有差异,但 NumPy 存储它们的方式是不一样的。

一般在数据分析操作的对象是数值型的数组,为了方便一些常用数组的创建,NumPy 提供了专门的函数。例如,zeros() 函数可以创建全 0 数组,ones() 函数可以创建全 1 数组,empty() 函数可以创建空数组。使用这些函数时只需要传入一个表示形状的数值或元组。

下面代码演示了 1 维到 3 维数组的创建。

In [19]: np.ones(5)
Out[19]: array([1., 1., 1., 1., 1.])

In [20]: np.empty((2, 5))
Out[20]:
array([[6.94152610e-310, 4.66070032e-310, 4.66070031e-310,
        6.94152610e-310, 7.35167805e+223],
       [5.40761401e-067, 1.39835953e-076, 7.01413727e-009,
        2.17150970e+214, 6.45967520e+270]])

In [21]: np.zeros((2,3,4))
Out[21]:
array([[[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]],

       [[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]]])

创建的数组默认数据类型都是浮点数,这一点可以在创建时指定 dtype 参数进行更改。在 IPython Shell 或 Jupyter Notebook 中键入 np.ones? 可以查看详细的参数说明以及示例,这几个函数的用法大体是一致的。

Signature: np.ones(shape, dtype=None, order='C')
Docstring:
Return a new array of given shape and type, filled with ones.

Parameters
----------
shape : int or sequence of ints
    Shape of the new array, e.g., ``(2, 3)`` or ``2``.
dtype : data-type, optional
    The desired data-type for the array, e.g., `numpy.int8`.  Default is
    `numpy.float64`.
order : {'C', 'F'}, optional, default: C
    Whether to store multi-dimensional data in row-major
    (C-style) or column-major (Fortran-style) order in
    memory.

Returns
-------
out : ndarray
    Array of ones with the given shape, dtype, and order.

See Also
--------
ones_like : Return an array of ones with shape and type of input.
empty : Return a new uninitialized array.
zeros : Return a new array setting values to zero.
full : Return a new array of given shape filled with value.

字符串数组存储的数据如果都是数字,我们常将它转换为数值型,这一点可以通过 ndarray 对象的 astype() 方法实现。

In [24]: num_string = np.array(['1.0', '2', '3.45'], dtype = np.string_)

In [25]: num_string
Out[25]: array([b'1.0', b'2', b'3.45'], dtype='|S4')
In [26]: num_string.astype(float)
Out[26]: array([1.  , 2.  , 3.45])

上面例子的 astype() 中使用了 float,它是 Python 内置的数据类型,NumPy 会将其自动映射到等价的 dtype 上,即 float64。如果将整数型数据转换为浮点数,信息不会丢失,但将浮点数转换为整数,小数部分将被丢失。

下面的例子在上面例子的基础上构建,除了使用 Python 内置的 float、int 等数据类型,还可以使用 NumPy 提供的更精确的数据类型,如 int32、int64、float32、float64 等。

In [28]: num_string.astype(float).astype(np.int32)
Out[28]: array([1, 2, 3], dtype=int32)
In [29]: num_string.astype(float).astype(np.int64)
Out[29]: array([1, 2, 3])

6.2 数组操作

6.2.1 数组运算

利用 NumPy 数组,Python 用户在不使用循环的情况下就可以对数据进行批量运算,该特性称为向量化计算。向量化计算意味着大小相同的数组之间任何算术运算都会应用到其成对的元素。下面代码展示了一个二维矩阵的例子。

In [30]: arr = np.array([[2, 3., 4.], [4, 5.4, 6]])
In [31]: arr
Out[31]:
array([[2. , 3. , 4. ],
       [4. , 5.4, 6. ]])

In [32]: arr * arr
Out[32]:
array([[ 4.  ,  9.  , 16.  ],
       [16.  , 29.16, 36.  ]])

In [33]: arr ** 2
Out[33]:
array([[ 4.  ,  9.  , 16.  ],
       [16.  , 29.16, 36.  ]])

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

In [35]: arr / arr
Out[35]:
array([[1., 1., 1.],
       [1., 1., 1.]])

In [36]: arr + arr
Out[36]:
array([[ 4. ,  6. ,  8. ],
       [ 8. , 10.8, 12. ]])

当上述操作的某一方是标量时(或维度更低),NumPy 会自动进行填充,将标量的维度扩展到与 ndarray 数组一致,然后进行向量化运算。该方式称为广播。

例如,我们用一个标量对一个 ndarray 进行算术运算。

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

In [38]: 1 - np.array([2, 3, 4])
Out[38]: array([-1, -2, -3])

当 NumPy 遇到这种情况时,它首先将 1 转变为 ndarray [1, 1, 1],然后进行运算,即:

In [39]: np.array([1, 1, 1]) + np.array([2, 3, 4])
Out[39]: array([3, 4, 5])

如果运算的是两个数组,NumPy 会尝试同时填充两个数组使得它们维度一致,如果不能使得维度一致,会发生什么情况呢?

In [40]: np.array([1, 1]) + np.array([2, 3, 4])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-40-1fbcd8e2dd89> in <module>()
----> 1 np.array([1, 1]) + np.array([2, 3, 4])

ValueError: operands could not be broadcast together with shapes (2,) (3,)

Python 抛出错误,这里提示无法将维度分别是 (2,) 和 (3,) 的数组广播到一起。

ndarray 数组之间除了进行算术运算,还能进行比较操作,得到的结果是相同维度的布尔型数组。注意它们比较的方式也是成对地,即按元素比较。

In [43]: np.array([5, 1, 7, 2]) > np.array([2, 3, 4, 5])
Out[43]: array([ True, False,  True, False])

上面代码中比较的结果是 5 比 2 大、1 比 3 小、7 比 4 大、2 比 5 小。

NumPy 可以让多种数据处理任务表示为简洁的数组表达式,减少了循环的编写,提高了计算的效率。通常向量化的数组运算要比等价的纯 Python 操作至少快 1 个数量级,特别是进行各种数值计算的情况下。

网格搜索算法是科学计算中寻找全局最优解的一种常用手段,它的一个计算实现基础是首先构建解的网格空间。现在假设我们要在一组网格型值上计算函数 x+y^2,首先使用 np.meshgrid() 函数产生 x 与 y 的定义域(取值区间)。

In [7]: x = np.arange(-5, 2, 0.01)
In [8]: y = np.arange(-20, -10, 0.2)
In [9]: xspace, yspace = np.meshgrid(x, y)

In [10]: xspace
Out[10]:
array([[-5.  , -4.99, -4.98, ...,  1.97,  1.98,  1.99],
       [-5.  , -4.99, -4.98, ...,  1.97,  1.98,  1.99],
       [-5.  , -4.99, -4.98, ...,  1.97,  1.98,  1.99],
       ...,
       [-5.  , -4.99, -4.98, ...,  1.97,  1.98,  1.99],
       [-5.  , -4.99, -4.98, ...,  1.97,  1.98,  1.99],
       [-5.  , -4.99, -4.98, ...,  1.97,  1.98,  1.99]])
In [11]: yspace
Out[11]:
array([[-20. , -20. , -20. , ..., -20. , -20. , -20. ],
       [-19.8, -19.8, -19.8, ..., -19.8, -19.8, -19.8],
       [-19.6, -19.6, -19.6, ..., -19.6, -19.6, -19.6],
       ...,
       [-10.6, -10.6, -10.6, ..., -10.6, -10.6, -10.6],
       [-10.4, -10.4, -10.4, ..., -10.4, -10.4, -10.4],
       [-10.2, -10.2, -10.2, ..., -10.2, -10.2, -10.2]])

In [12]: xspace + yspace ** 2
Out[12]:
array([[395.  , 395.01, 395.02, ..., 401.97, 401.98, 401.99],
       [387.04, 387.05, 387.06, ..., 394.01, 394.02, 394.03],
       [379.16, 379.17, 379.18, ..., 386.13, 386.14, 386.15],
       ...,
       [107.36, 107.37, 107.38, ..., 114.33, 114.34, 114.35],
       [103.16, 103.17, 103.18, ..., 110.13, 110.14, 110.15],
       [ 99.04,  99.05,  99.06, ..., 106.01, 106.02, 106.03]])

这里 meshgrid() 函数接受两个一维 ndarray,产生两个二维矩阵,它们分别对应两个 ndarray 的元素对。

为了帮助读者理解,下面生成两个短小的矩阵解释上述过程。

In [13]: x = np.arange(-5, -2, 1)
In [16]: y = np.arange(-20, -15, 1)
In [17]: xspace, yspace = np.meshgrid(x, y)

In [18]: x
Out[18]: array([-5, -4, -3])
In [19]: y
Out[19]: array([-20, -19, -18, -17, -16])

In [20]: xspace
Out[20]:
array([[-5, -4, -3],
       [-5, -4, -3],
       [-5, -4, -3],
       [-5, -4, -3],
       [-5, -4, -3]])
In [21]: yspace
Out[21]:
array([[-20, -20, -20],
       [-19, -19, -19],
       [-18, -18, -18],
       [-17, -17, -17],
       [-16, -16, -16]])

数组 x 有 3 个元素,数组 y 有 5 个元素,它们组合起来就有 15 种情况,即 [(-5, -20), (-5, -19), …],我们可以称为 xy 对,meshgrid() 函数返回结果时将 xy 对拆开,我们就可以得到 xy 对中 15 个 x 值,15 个 y 值。

6.2.2 索引与切片

在列表章节读者已经学习过 Python 索引,它是非常丰富的主题,同一个目的可以用不同的方法达成。本小节专注于介绍 ndarray 的索引与切片操作,它在原理上与列表索引与切片基本一致,但在实际操作时更为复杂,且具有更多的特性。

一维数组的索引非常简单,跟列表索引差不多。下面代码演示了一些基本的索引操作,包括单值索引、范围索引和重新赋值。再次注意,Python 索引从 0 开始,涉及范围操作时用的区间都是左闭右开,如 2:5 表示第 3 到第 5 个元素,包含 2(第 3 个元素)不包含 5(第 6 个元素)。

In [44]: arr = np.arange(20)
In [45]: arr
Out[45]:
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
       17, 18, 19])

In [46]: arr[5]          # 单个值索引, 提取第 6 个元素
Out[46]: 5
In [47]: arr[2:5]        # 范围索引,取第 3 个到第 5 个元素
Out[47]: array([2, 3, 4])
In [48]: arr[2:5] = 10   # 将第 3 到第 5 到元素重新赋值为 10
In [49]: arr
Out[49]:
array([ 0,  1, 10, 10, 10,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
       17, 18, 19])

In [50]: arr[10:13] = [111, 222, 333]  # 将第 11、12、13 个元素分别赋值为 111、222、333
In [51]: arr
Out[51]:
array([  0,   1,  10,  10,  10,   5,   6,   7,   8,   9, 111, 222, 333,
        13,  14,  15,  16,  17,  18,  19])

NumPy 数组切片与数组切片有一个重要的区别,NumPy 切片后赋值给变量不会产生数组子集的拷贝,它依旧指向原始数据,所以读者在获取数组子集时需要特别注意。下面用一个实例来演示 ndarray 切片和 list 切片的区别。

先分别创建一个 list 和一个 ndarray,它们都保存 0 到 9 这 10 个数字。

In [52]: ls = [i for i in range(10)]
In [53]: ls
Out[53]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [54]: arr = np.arange(10)
In [55]: arr
Out[55]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

现在对第 5 到 8 个元素切片并赋值给新的变量。

In [56]: ls2 = ls[4:8]
In [57]: ls2
Out[57]: [4, 5, 6, 7]

In [60]: arr2 = arr[4:8]
In [61]: arr2
Out[61]: array([4, 5, 6, 7])

现在更改新的变量第 1 个元素,然后查看原始变量是否会被影响。

In [67]: ls2[0] = 100  # 修改列表第 1 个元素值为 100
In [68]: ls2
Out[68]: [100, 5, 6, 7]
In [69]: ls
Out[69]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [70]: arr2[0] = 100 # 修改 ndarray 第 1 个元素值为 100
In [71]: arr2
Out[71]: array([100,   5,   6,   7])
In [72]: arr
Out[72]: array([  0,   1,   2,   3, 100,   5,   6,   7,   8,   9])

代码结果直观展示了两个 Python 列表看起来互不干扰,但两个 ndarray 是相互影响的。这里产生两种不同结果的原因是,当 Python 列表切片并赋值时,Python 会对切片的数据(这里是 4 到 7)重新生成一份拷贝,然后将新变量 ls2 指向这个新的数据拷贝。而 NumPy 数组进行切片时它只是把原始数据对应的位置(4 到 7)指向新变量 arr2,所以一改则全改。

从简单的使用来看列表更令人感觉舒适,使用 NumPy 数组一不小心就可能犯错。但 NumPy 这样做是有潜在的好处的,符合它处理大数据的目的,当存在大数据处理时需要多次进行没必要的拷贝操作会占用大量的内存,降低计算性能。

高维度的 ndarray 能存储更广泛的数据,每一个元素都是低一维的数组,如矩阵的元素是一维数组、三维数组的元素是矩阵。对高维度 ndarray 进行索引取值、赋值需要使用 1 到多个索引符、切片符以及它们的组合。

下面以二维数组和三维数组为对象进行一些常见的索引和切片操作。二维数组可以想象为一个大的正方形或矩形,内部的小正方形是它的元素。三维矩阵可以想象为一个大的正方体(魔方)或长方体,构成它的小正方体是它的元素。更高维度的数组操作原理相通,但高纬度太过抽象不方便理解,因此本书不举例介绍。

In [1]: import numpy as np
In [2]: arr2d = np.array([[1, 2, 3], [4, 5, 6]])  # 初始化一个二维数组,即矩阵

In [3]: arr2d[0]      # 提取矩阵的第 1 行
Out[3]: array([1, 2, 3])

In [4]: arr2d[:,0]    # 提取矩阵的第 1 列
Out[4]: array([1, 4])

In [5]: arr2d[0, 0]   # 提取矩阵位于第 1 行第 1 列的元素
Out[5]: 1

In [6]: arr2d[0:2, 0] # 提取矩阵第 1 列前两个元素
Out[6]: array([1, 4])

In [7]: arr2d[0:2]    # 提取矩阵的前两行,这跟 arr2d[0:2, :] 结果是一致的
Out[7]:
array([[1, 2, 3],
       [4, 5, 6]])

In [8]: arr2d[:2, 1:] = 0  # 矩阵前两行的第 2 列开始往右元素值全为 0

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

In [10]: arr3d = np.array([[[1,2,3],[4,5,6],[7,8,9]]])  # 创建一个简单的 3 维数组

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

In [12]: arr3d[0]  # 这是沿着第 0 轴(第一个轴)切片的结果,注意与 arr3d 的区别,这里是一个 3x3 数组(矩阵)
Out[12]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [13]: new_array = arr3d[0].copy()  # 创建矩阵新的拷贝

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

In [15]: arr3d[0] = 42  # 对原始 3 维数组第 1 个子维度重新赋值

In [16]: arr3d          # 此处 42 进行了广播,矩阵全部元素都为 42
Out[16]:
array([[[42, 42, 42],
        [42, 42, 42],
        [42, 42, 42]]])

In [17]: arr3d[0] = new_array  # 将存储在 new_array 的原始值重新赋值回去

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

6.2.3 布尔型索引

除了整数索引和切片,NumPy 还可以使用布尔值型的索引。布尔值一般由数组的比较运算得到,该过程也是向量化的,可广播的。

沿用上面使用的 arr3d 变量,下面使用它与数值 5 进行比较,得到的结果是与 arr3d 维度一致的数组。

In [19]: arr3d > 5
Out[19]:
array([[[False, False, False],
        [False, False,  True],
        [ True,  True,  True]]])

利用逻辑数组,读者可以选择性地提取出想要的数据。下面代码创建了一个一维的数组和一个二维的随机矩阵,一维数组的元素个数与矩阵的行数一致。

In [20]: subject = np.array(['chinese', 'math', 'chinese', 'english', 'history'])
In [21]: df = np.random.randn(5, 3)
In [22]: df
Out[22]:
array([[ 0.50025766, -0.4625053 , -1.85743193],
       [ 0.63757593,  0.55624546, -1.7669166 ],
       [-0.18061614, -0.71896639, -0.26744936],
       [ 1.37094842, -0.21829646, -0.34926808],
       [-0.90192432, -0.2821726 ,  0.54411861]])

这里假设每个 subject 都对应随机矩阵的一行,而我们想要提取出与 chinese 相对应的行,这里利用数组 subject 与字符 ‘chinese’ 的比较结果进行数组索引。

In [23]: subject == 'chinese'
Out[23]: array([ True, False,  True, False, False])
In [24]: df[subject == 'chinese']
Out[24]:
array([[ 0.50025766, -0.4625053 , -1.85743193],
       [-0.18061614, -0.71896639, -0.26744936]])

注意,布尔索引数组的长度与被索引的轴长度一致,在不加英文逗号的情况下默认比较的都是 0 轴,即第一个维度。

布尔索引数组可以和切片、整数索引等搭配使用,从而高度自定义想要获取的数组子集。

In [25]: df[subject == 'chinese', 1:] # 满足 chinese 对应行,去除第 1 列
Out[25]:
array([[-0.4625053 , -1.85743193],
       [-0.71896639, -0.26744936]])

In [26]: df[subject == 'chinese', 2:] # 满足 chinese 对应行,去除第 1、2 列
Out[26]:
array([[-1.85743193],
       [-0.26744936]])

In [27]: df[subject != 'chinese', 2:] # 不满足 chinese 对应行,去除第 1、2 列
Out[27]:
array([[-1.7669166 ],
       [-0.34926808],
       [ 0.54411861]])

除了使用不等号 != 还表示否定,还可以使用波浪号 ~ 表示对条件结果的取反。

In [29]: df[~ (subject == 'chinese'), 2:] # 该代码行与上一个代码行结果一致
Out[29]:
array([[-1.7669166 ],
       [-0.34926808],
       [ 0.54411861]])

如果需要应用多个逻辑判断,可以使用和(&)、或(|)等布尔操作符。读者需要注意,当存在多个判断时一定要用括号将每一个逻辑比较运算括起来,否则会报错。

In [31]: df[subject == "chinese" | subject == "math"]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-ec9c993564c8> in <module>()
----> 1 df[subject == "chinese" | subject == "math"]

正确的写法如下。

In [33]: df[(subject == "chinese") | (subject == "math")]
Out[33]:
array([[ 0.50025766, -0.4625053 , -1.85743193],
       [ 0.63757593,  0.55624546, -1.7669166 ],
       [-0.18061614, -0.71896639, -0.26744936]])

通过布尔型数组索引可以非常方便地对符合条件的数据进行重赋值,这是数据分析最常用的操作之一。

In [34]: df[df < 0] = 0
In [35]: df
Out[35]:
array([[0.50025766, 0.        , 0.        ],
       [0.63757593, 0.55624546, 0.        ],
       [0.        , 0.        , 0.        ],
       [1.37094842, 0.        , 0.        ],
       [0.        , 0.        , 0.54411861]])

除了上述利用逻辑组合选取子集进行重新赋值,NumPy 提供的 where() 函数可以实现整个数组的条件逻辑运算,它是三元表达式 x if condition else y 的向量化版本。

例如,解决这样一个问题,将一个随机数据数组中的正数替换为 1,而负数为 0,可以用如下代码轻松实现。

```python In [22]: arr_random = np.random.randn(4, 4) In [23]: arr_random Out[23]: array([[-1.48064102, 1.4408966 , -0.13313057, 1.09683071], [ 0.44698237, 0.01854261, 0.56719151, -1.03926198], [ 1.45070221, 0.04421898, 0.787423 , -1.28715644], [ 2.27759091, -0.06808282, -0.99294482, -0.39755302]]) In [24]: arr_random > 0 Out[24]: array([[False, True, False, True], [ True, True, True, False], [ True, True, True, False], [ True, False, False, False]])

In [25]: np.where(arr_random > 0, 1, 0) Out[25]: array([[0, 1, 0, 1], [1, 1, 1, 0], [1, 1, 1, 0], [1, 0, 0, 0]]) ```

where() 函数中替换的 1 和 0 可以是一个与 arr_random 等维度的数组。

6.2.3 数组转置与轴转换

转置是一种常见的数学矩阵操作,是将矩阵的行列进行互换,如位于第 1 行第 2 列的元素转置后位于第 2 行第 1 列,第 2 行第 1 列的元素转置后位于第 1 行第 2 列。NumPy 将这一概念应用到了多维数组,ndarray 不仅有转置方法,还有一个特殊的 T 属性。

In [2]: import numpy as np
In [3]: arr = np.arange(12).reshape((3, 4))
In [4]: arr
Out[4]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

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

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

为了简便地对数组形状进行重塑,NumPy 提供了reshape()方法。上面代码中,为了快速生成一个 3 行 4 列的样例矩阵,我们使用 arange 方法先生成一个长度为 12 的一维数组,然后使用 reshape() 方法重塑为 3 行 4 列的 ndarray,使用 T 属性或者 transpose() 方法都可以获得转置数组。这里 reshape() 方法中要使用元组提供数组维度,数组的维度可以通过 shape 属性获取。

In [10]: arr.shape
Out[10]: (3, 4)

有一些操作在进行矩阵计算时经常用到,如计算矩阵的内积可以使用 np.dot() 函数。

In [11]: np.dot(arr, arr.T)
Out[11]:
array([[ 14,  38,  62],
       [ 38, 126, 214],
       [ 62, 214, 366]])

如果处理的是高维数组,进行转置时需要通过轴序指定互换的维度。

In [12]: arr = np.arange(24).reshape((2,3,4))
In [13]: arr
Out[13]:
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]]])

In [14]: arr.transpose((1,0,2)) # 0表示第1轴、1表示第2轴、2表示第3轴
Out[14]:
array([[[ 0,  1,  2,  3],
        [12, 13, 14, 15]],

       [[ 4,  5,  6,  7],
        [16, 17, 18, 19]],

       [[ 8,  9, 10, 11],
        [20, 21, 22, 23]]])

默认的轴序是 (0, 1, 2),上述代码 transpose() 方法中指定的是 (1, 0, 2),因此转置的结果是数组第 1 个轴变成了第 2 个轴、第 2 个轴变成了第 1 个轴,而第 3 个轴不变。只要理清楚需要互换的轴,相同的操作可以应用到更高维度。

另外,NumPy 还提供了一个 swapaxes() 方法转换轴,该方法需要输入互换的一对轴编号。

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

       [[12, 16, 20],
        [13, 17, 21],
        [14, 18, 22],
        [15, 19, 23]]])

In [16]: arr.swapaxes(0, 1)
Out[16]:
array([[[ 0,  1,  2,  3],
        [12, 13, 14, 15]],

       [[ 4,  5,  6,  7],
        [16, 17, 18, 19]],

       [[ 8,  9, 10, 11],
        [20, 21, 22, 23]]])

6.3 数组函数与方法

6.3.1 通用函数

NumPy 中除了 ndarray 数组操作是操作符(算术、比较等)运算是元素级别的,可以操作 ndarray 的各类通用函数也是执行元素级运算。NumPy 的简便在于用户操作向量化计算跟标量计算的操作方法基本相同。

NumPy 提供的通用数学运算函数很多是 Python 元素级函数(来自内置模块、math模块等)的变体,如开方函数 sqrt()、指数函数 exp()。

In [17]: arr = np.arange(10).reshape((2, 5))
In [18]: arr
Out[18]:
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]])

In [19]: np.sqrt(arr)
Out[19]:
array([[0.        , 1.        , 1.41421356, 1.73205081, 2.        ],
       [2.23606798, 2.44948974, 2.64575131, 2.82842712, 3.        ]])
In [20]: np.exp(arr)
Out[20]:
array([[1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
        5.45981500e+01],
       [1.48413159e+02, 4.03428793e+02, 1.09663316e+03, 2.98095799e+03,
        8.10308393e+03]])

有一些函数接收 2 个或多个数组,返回一个数组。

In [21]: arr2 = np.random.randn(10).reshape((2,5))
In [22]: arr2
Out[22]:
array([[-0.81547072,  0.02248639, -0.3004805 ,  1.53433534,  0.59514916],
       [ 1.60022692, -0.68780704,  0.79007821,  0.72034177, -1.33966745]])

In [23]: np.add(arr, arr2)      # 对应元素相加
Out[23]:
array([[-0.81547072,  1.02248639,  1.6995195 ,  4.53433534,  4.59514916],
       [ 6.60022692,  5.31219296,  7.79007821,  8.72034177,  7.66033255]])
In [24]: np.maximum(arr, arr2)  # 返回对应元素较大的那个
Out[24]:
array([[0., 1., 2., 3., 4.],
       [5., 6., 7., 8., 9.]])

另外还有一些函数可以返回多个数组,如 modf() 函数可以返回浮点 ndarray 数组的小数与整数部分。

In [25]: part1, part2 = np.modf(arr2)
In [26]: part1
Out[26]:
array([[-0.81547072,  0.02248639, -0.3004805 ,  0.53433534,  0.59514916],
       [ 0.60022692, -0.68780704,  0.79007821,  0.72034177, -0.33966745]])
In [27]: part2
Out[27]:
array([[-0.,  0., -0.,  1.,  0.],
       [ 1., -0.,  0.,  0., -1.]])

下表列出了常见通用函数。NumPy 提供的通用函数非常多,本书不再一一举例介绍,读者在实际使用时可以参考对应的帮助文档。

表6-1 NumPy 常见通用函数

函数 说明
abs、fabs 计算绝对值
sqrt 计算元素的平方根,等价于 arr ** 0.5
square 计算元素的平方,等价于 arr ** 2
exp 计算元素的指数
log、log2、log10 分别计算自然对数、底数是 2 的对数、底数是 10 的对数
sign 计算元素的正负号:结果 1 表示正数、0 表示零、-1 表示负数
floor 计算小于元素的最大整数,如 floor(5.3) 结果为 5
ceil 计算大于元素的最大正数,如 ceil(5.3) 结果为 6
rint 元素四舍五入,并保留 dtype
modf 将数组的小数与整数部分以两个独立数组返回
isnan 返回数组元素是否为 NaN 的布尔型数组
isfinite 返回数组元素是否为有限值的布尔型数组
isinf 返回数组元素是否为无穷值的布尔型数组
cos、cosh、sin、sinh、tan、tanh 普通和双曲型三角函数
arccos、arccosh、arcsin、arcsinh、arctan、arctanh 反三角函数
logical_not 计算逻辑型元素的反,如 arr([True, False]) 的结果是 arr([False, True])
add 将数组对应的元素相加
subtract 将数组对应的元素相减
multiply 将数组对应的元素相乘
divide、floor_divide 除法和整除法(丢掉余数)
power 第一个数组元素为底,第二个数组元素为幂
maximum、fmax 最大值计算,fmax 会忽略 NaN
minimum、fmin 最小值计算,fmin 会忽略 NaN
mod 求模计算(除法的余数)
copysign 将第二个数组中元素值的符号复制给第一个数组中的值
greater、greater_equal、less、less_equal、equal、not_equal 执行元素级比较运算,产生布尔型数组,等价于操作符 >、>=、<、<=、==、!=
logical_and、logical_or、logical_xor 元素级逻辑运算,等价于操作符 &、!、^

6.3.2 基本统计

NumPy 除了提供了很多方便使用的通用函数,还提供了一些数学统计的基本方法,用于计算常见的统计量,如均值、方差、标准差。这些统计方法可以处理整个数组或者以特定的数轴为方向的数组子集。

为了展示基本统计方法的使用,下面创建一个正态分布随机矩阵作为示例数据。

首先对整个数组计算基本统计量:均值、求和、方差以及标准差。

In [2]: import numpy as np
In [3]: arr = np.random.randn(5, 5)
In [4]: arr
Out[4]:
array([[-0.51132191, -0.88525544, -1.10119999, -2.3272623 ,  0.24502215],
       [ 0.22767771, -0.43164608,  0.62262033, -1.68672377, -0.19473212],
       [-0.65820486, -1.62823718,  0.0798516 ,  0.1056899 , -0.45333499],
       [ 0.86035323,  1.79121647,  0.75648603,  0.56113024, -1.57487612],
       [ 0.90551266, -2.35820418,  0.34951423, -1.23775123, -0.62627856]])

In [5]: arr.mean()  # 求均值
Out[5]: -0.36679816721832453
In [6]: arr.sum()   # 求和
Out[6]: -9.169954180458113
In [7]: arr.var()   # 求方差
Out[7]: 1.0772072348109176
In [8]: arr.std()   # 求标准差
Out[8]: 1.037885944991509

针对整个数组的计算结果最后都会是标量,不过,如果指定一个轴向参数 axis,最终的结果会是一个少一维的数组。

In [9]: arr.mean(axis=0)   # 计算列的平均值
Out[9]: array([ 0.16480337, -0.70242528,  0.14145444, -0.91698343, -0.52083993])
In [10]: arr.mean(axis=1)  # 计算行的平均值
Out[10]: array([-0.9160035 , -0.29256078, -0.51084711,  0.47886197, -0.59344142])

这类方法计算的结果比输入维度小,所以常被称为聚合(aggregation)计算。

当然,也存在一些非聚合计算的方法,如累计和(即累加)cumsum()。

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

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

累加得到的数组第 i 个元素都是输入数组前 i-1 个元素之和。

除了上面提到的 mean()、sum()、var()、std()、cumsum() 方法,还有几个常用的数组统计方法:min() 用于计算最小值、max() 用于计算最大值、argmin() 用于获取最小元素索引、argmax() 用于获取最大元素索引、cumprod() 用于计算累计积。

6.3.3 排序与集合操作

数据处理离不开排序和集合操作,NumPy 也提供了相应的操作方法或函数。

sort() 方法可以实现 ndarray 的就地排序。

In [2]: arr = np.random.randn(10)
In [3]: arr
Out[3]:
array([-1.03434834, -0.1066477 , -0.18138105, -0.02874672,  0.37446326,
       -0.19669119,  0.00594903,  0.19048595,  0.14961745,  0.5749973 ])

In [4]: arr.sort()
In [5]: arr
Out[5]:
array([-1.03434834, -0.19669119, -0.18138105, -0.1066477 , -0.02874672,
        0.00594903,  0.14961745,  0.19048595,  0.37446326,  0.5749973 ])

如果操作的是多维数组,想按照某个轴进行排序,只需要指定轴的编号。

In [8]: arr = np.random.randn(3, 4)
In [9]: arr
Out[9]:
array([[-1.36520054, -1.61647551, -1.19945064,  1.37181547],
       [-0.10126557, -0.39124394, -0.34307793, -0.8307224 ],
       [ 0.76972754,  1.10906676, -0.17070844,  0.06256465]])

In [10]: arr.sort(1) # 每行按升序排列
In [11]: arr
Out[11]:
array([[-1.61647551, -1.36520054, -1.19945064,  1.37181547],
       [-0.8307224 , -0.39124394, -0.34307793, -0.10126557],
       [-0.17070844,  0.06256465,  0.76972754,  1.10906676]])

另外 NumPy 提供的 sort() 函数(使用方式是 np.sort(arr))也可以实现相同的功能,两者的区别是 sort() 方法(使用方式是 arr.sort())是实现被操作数组的就地修改,而 sort() 函数返回的是一个拷贝。

NumPy 提供的集合运算主要针对的是一维 ndarray。函数 unique()、intersect1d()、union1d()、setdiff1d() 分别实现唯一化、交集、并集以及差集操作,其中唯一化最为常用。

unique() 函数不仅会找出数组的唯一值,还会对结果进行排序。

In [14]: arr_int = np.array([3, 4, 5, 8, 4, 3, 2, 1, 6, 10])
In [15]: arr_int
Out[15]: array([ 3,  4,  5,  8,  4,  3,  2,  1,  6, 10])

In [16]: np.unique(arr_int)
Out[16]: array([ 1,  2,  3,  4,  5,  6,  8, 10])

等效的纯 Python 需要调用两个函数实现该操作。

In [17]: sorted(set(arr_int))
Out[17]: [1, 2, 3, 4, 5, 6, 8, 10]

交集、并集与差集操作分别举例如下。

In [18]: arr_int2 = np.array([1, 3, 22, 5, 6])  # 另外新建一个一维数组

In [19]: np.intersect1d(arr_int, arr_int2)  # 交集
Out[19]: array([1, 3, 5, 6])
In [20]: np.union1d(arr_int, arr_int2)      # 并集
Out[20]: array([ 1,  2,  3,  4,  5,  6,  8, 10, 22])
In [21]: np.setdiff1d(arr_int, arr_int2)    # 差集
Out[21]: array([ 2,  4,  8, 10])

集合操作后的结果都自动进行了排序。

6.3.4 线性代数操作

线性代数的主要内容是矩阵乘法、矩阵分解以及行列式等,由于矩阵运算方法与基本的数值计算有极大不同,NumPy 提供了专门的方法或函数进行线性代数操作。

In [22]: x = np.array([[1, 2, 3], [4, 5, 6]])
In [23]: y = np.array([[3, 5, 6], [7, 8, 9]])
In [24]: x * y
Out[24]:
array([[ 3, 10, 18],
       [28, 40, 54]])

如果使用乘法运算符,得到的是两个数组元素级别的相乘,而不是矩阵的点积,点积操作需要使用 dot() 函数(或方法)。

In [25]: np.dot(x, y)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-25-c3a58f1d73f8> in <module>()
----> 1 np.dot(x, y)

ValueError: shapes (2,3) and (2,3) not aligned: 3 (dim 1) != 2 (dim 0)

输入两个矩阵并不代表函数能够成功运行,进行点积的两个矩阵需要满足特殊的要求,假设矩阵 x 维度是 m 行 n 列,那么矩阵y必须为 n 行 p 列,即矩阵 x 的列数与矩阵 y 的行数必须相等,得到的矩阵维度是 m 行 p 列。

In [27]: np.dot(x, y)
Out[27]:
array([[ 39,  46],
       [ 90, 109]])

这里矩阵 x 维度是 2 行 3 列,矩阵y维度是 3 行 2 列,所以点积得到的矩阵是 2 行 2 列。

np.dot(x, y) 函数操作可以使用等价的 x.dot(y) 方法操作实现,另外还可以使用中缀运算符 @。

In [28]: x.dot(y)
Out[28]:
array([[ 39,  46],
       [ 90, 109]])

In [29]: x @ y
Out[29]:
array([[ 39,  46],
       [ 90, 109]])

NumPy 的子模块中提供了一组实现标准的矩阵分解运算、求逆、求行列式等的函数,下面列出了最常用的线性代数函数。

表6-2 常用线性代数操作

函数 说明
diag 返回方阵的对角线(或非对角线)元素
dot 矩阵乘法
trace 求对角线元素和
det 求行列式
eig 求方阵的本征值、本征向量
inv 求方阵的逆
pinv 求 Moore-Penrose 伪逆
qr QR 分解
svd 奇异值分解
solve 解线性方程
lstsq 求线性方程最小二乘解

6.3.5 伪随机数生成

NumPy 的 random 子模块提供了一系列高效生成多种概率分布的函数。在前面的章节里,本书已经利用过其中的 randn() 函数创建随机数据,本小节将对它们进行更详细的介绍。

使用 normal() 函数可以创建标准的正态分布抽样数据,下面代码创建一个 5x5 的数组。

In [30]: np.random.normal(size=(5,5))
Out[30]:
array([[ 0.03488939, -1.58459629, -1.46781029, -1.13217542, -1.06312407],
       [ 0.83678804,  1.21880709, -0.90811673, -1.71748912,  0.92877163],
       [-0.49898785, -0.17523296, -1.73258953, -0.47749123, -1.49576169],
       [ 0.15254935,  0.46308905,  0.1221845 , -2.15762674,  2.23510318],
       [-0.70557981,  0.96598878,  0.43192638, -0.2049251 ,  0.23281444]])

randn() 函数可以产生平均值为 0,标准差为 1 的正态分布样本。

In [31]: np.random.randn(10)
Out[31]:
array([-0.559106  ,  1.36880898, -0.24559224, -0.16668403,  2.42001793,
        0.39617551, -1.06446839,  1.02696512,  0.08217648,  1.07538155])
In [36]: np.random.randn(5,5)
Out[36]:
array([[-0.77315232,  0.4786622 , -1.38927237, -0.20433972, -2.43830605],
       [ 0.34922348,  0.87849643,  1.5239394 , -0.73135812,  2.21068918],
       [ 0.12944191,  1.01207972, -0.57685143,  2.63207061, -0.74326986],
       [ 0.73286193,  0.42616076, -0.42334269, -0.98384705, -0.02632024],
       [-0.6184617 ,  0.40202667, -0.3722806 ,  0.16819083,  0.55132166]])

注意上面 normal() 函数与 randn() 函数在产生 5x5 数组上的不同之处:normal() 需要传入一个元组,而 randn() 只需要传入一系列维度值。

除了生成随机的数据,最常用的操作是设置随机种子。上述所看到的随机数实际上是计算机依据随机数生成器在确定性条件下生成的数据,我们一般也常称为伪随机数。通过设置随机种子,我们可以重复之前生成的随机数据,这为重复同样的分析结果(该分析使用到了随机数据)提供了帮助。

In [37]: np.random.seed(1234)

此处使用 random 子模块的 seed() 函数设定的是一个全局的随机种子。如果想要避免全局状态,读者可以使用 RandomState() 函数创建一个隔离的随机数生成器。

In [37]: r = np.random.RandomState(123456)
In [38]: r.randn(10)
Out[38]:
array([ 0.4691123 , -0.28286334, -1.5090585 , -1.13563237,  1.21211203,
       -0.17321465,  0.11920871, -1.04423597, -0.86184896, -2.10456922])

下表给出了 random 子模块中常用的随机函数。

表6-3 常用随机函数

函数 说明
seed 设定全局随机数生成器种子
RandomState 设定局部随机数生成器种子
permutation 随机排列输入序列
shuffle 对输入序列就地随机排列(洗牌)
rand 生成随机值
randint 根据指定范围随机选取整数
randn 从标准正态分布中随机抽样
binomial 二项分布取样
normal 正态分布取样
beta 贝塔分布取样
chisquare 卡方分布取样
gamma 伽马分布取样
uniform 均匀分布取样

6.3.6 数组文件输入与导出

Numpy 除了提供多维数组对象 ndarrary、高效的数组操作函数和方法,它还支持磁盘文本或二进制数据的读写操作。本小节主要介绍 NumPy 内置的数据存储二进制格式,数据处理常见的文本格式的导入与保存将在 Pandas 章节进行介绍。

Numpy 提供了两个主要的函数分别用于将 ndarray 写入磁盘和从磁盘中读入保存的数组数据文件。

默认情况下,数组会以未压缩的二进制格式保存在文件扩展名为 .npy 的文件中。

In [2]: arr1 = np.random.randn(10)  # 创建一些数组
In [3]: arr2 = np.random.randn(10)
In [4]: arr_res =  arr1 + arr2      # 操作数组
In [5]: np.save('result', arr_res)  # 保存结果数组

np.save() 函数的第一个参数是一个文件名(可以包含指定路径),第二个参数是要保存的数组。如果用户没有指定文件扩展名时,NumPy 会在实际保存时将 .npy 加上。。

这样,数据就被保存到了计算机磁盘上,当需要导入该数据时,只需要调用 np.load() 函数并传入文件路径。

In [6]: np.load('result.npy')
Out[6]:
array([-1.62141731,  0.22330449,  0.52851935, -0.34489954,  0.00938235,
        3.27527395, -0.83738875,  0.45741888, -0.12050226, -0.90452199])

注意,导入时需要完整输入文件名及文件扩展名,否则 load() 函数将找不到文件所在。

In [7]: np.load('result')
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
<ipython-input-7-f17638fbbc1f> in <module>()
----> 1 np.load('result')

~/anaconda3/lib/python3.7/site-packages/numpy/lib/npyio.py in load(file, mmap_mode, allow_pickle, fix_imports, encoding)
    382     own_fid = False
    383     if isinstance(file, basestring):
--> 384         fid = open(file, "rb")
    385         own_fid = True
    386     elif is_pathlib_path(file):

FileNotFoundError: [Errno 2] No such file or directory: 'result'

在实际进行数组操作时往往会产生多个需要保存的数据结果,np.savez() 函数可以将多个 ndarray 保存到二进制文件中。该函数需要的第一个参数与 np.save() 一样也是保存的文件名,不过以 .npz 作为文件扩展名,后面的参数可以是任意多个用户指定的关键字参数。

下面的代码将之前生成的 3 个数组保存到一个文件中。

In [8]: np.savez('array_save.npz', input1=arr1, input2=arr2, result=arr_res)

如果直接将其导入,返回的结果是一个不可查看的 NpzFile 对象。

In [9]: np.load('array_save.npz')
Out[9]: <numpy.lib.npyio.NpzFile at 0x7f917c6caba8>

我们需要将其赋值给一个变量,然后通过保存时设定的关键字参数进行索引查看单个的数组数据。

In [10]: arr_save =  np.load('array_save.npz')
In [11]: arr_save['input1']
Out[11]:
array([-0.79417709,  0.57095314,  1.59839779, -0.96875458, -1.35098779,
        2.5673315 ,  0.64841217,  0.28681969,  0.26718872,  0.26876572])
In [12]: arr_save['input2']
Out[12]:
array([-0.82724022, -0.34764864, -1.06987844,  0.62385504,  1.36037014,
        0.70794245, -1.48580092,  0.17059919, -0.38769099, -1.17328771])
In [13]: arr_save['result']
Out[13]:
array([-1.62141731,  0.22330449,  0.52851935, -0.34489954,  0.00938235,
        3.27527395, -0.83738875,  0.45741888, -0.12050226, -0.90452199])

上述保存的文件都是未压缩的,如果想要将数据压缩,读者不妨使用 np.savez_compressed() 函数,该函数的使用方法与 np.savez() 一致,因此不再赘述。

6.4 章末小结

NumPy 模块中 ndarray 数据存储对象和一系列高效数据操作方法、函数的引入为更高级别的数据处理、统计建模提供了坚实的底层实现。

通过本章的学习,读者应当对 NumPy 的长处有所认知:NumPy 模块提供了独立于 Python 内置对象的 ndarray 多维数组对象,并将其存储在一个连续的内存块中,利用 C 语言实现的算法库操作和修改数据,减少对内存资源的消耗;NumPy 可以对整个数组执行复杂的数值计算,避免了 for 循环的使用,提升了计算的速度。相应地,读者在处理大数据集时,应当尽量利用 NumPy 的优势,一方面在数组操作时避免创建过多的中间变量,另一方面熟练使用 NumPy 提供的向量化计算函数和方法。