{T}

下面进入 NumPy 入门部分,学习数据分析中常用的数学方法与处理数值数据的工具:NumPy。

完成本部分的学习后,对于数据集中的数值部分的数据,就可以用更多的分析方法(如回归分析、预测分析等),从大数据中获取更多有用的信息与结论。

在实战中,根据现有的数据对未知的数据进行预测,往往需要基于数值数据分析的方法。比如根据一个地区部分房屋交易的数据,来预测一套新房源可能会交易的价格;或者根据某只股票过去的价格来预测未来的价格走势等。

完成本部分的学习后,可以掌握分析此类问题的一系列方法和工具。除去预测分析之外,还有一些聚类的任务,比如一个班考了 10 门科目,相当于每个学生有 10 个成绩,如何判断两个学生的水平是否相近?这也需要基于数值的数据分析方法。

还有很多其他场景,下面在这个部分逐一介绍。

课前准备:安装 NumPy 工具包

第一步,安装 NumPy 工具包。打开开始菜单 → Anaconda3 → Anaconda Prompt,并输入 conda install numpy 回车执行,如下图所示。

输入 y 并回车,确认安装。

之后到课程目录创建文件夹 chapter17 并用 VS Code 打开该文件夹。创建新的 Notebook,将其保存在 chapter17 的文件夹中,同时命名为 chapter17.ipynb。

在默认的 Cell 中输入以下代码:

code
import numpy as np

执行之后若没有报错,说明 NumPy 已经安装成功。

多维数组:ndarray

ndarray 是 NumPy 中最基础的结构,地位就像 DataFrame 对于 pandas 一样。NumPy 近乎所有功能都围绕着 ndarray 展开。ndarray 的全称是 n-dimension array,如名字所示,代表多维数组。

什么是数组?Python 中学习过列表,数组和列表非常相似。在很多语言中,数组和列表基本代表同一种东西。

列表往往指的是一维的概念,类似 pandas 中的 Series,要么是一行,要么是一列。而二维的数据,比如表格,往往就没那么容易处理了,pandas 专门提供了 DataFrame 来处理二维的表格。

而多维数组,顾名思义,可以存储任意维度的数据,可以是一维、也可以是二维、三维等。二维数组除了表格之外,还有另一种常见的形式:矩阵。

三维数组怎么理解呢?由矩阵构成的一个数组就是三维,更往上的维度以此类推,不过最常用的还是三维及以下的数组。

ndarray 和列表还有一个重要区别:ndarray 只能存储同一类型的数据,比如都是整数,或者都是浮点数。

ndarray 除了能表示多维的数据,还有另一个优势:数据分析中的数值计算往往涉及较大的数据量,NumPy 的 ndarray 提供了远超列表的计算性能,使得大数据的计算能够更快地完成

下面通过几个简单的例子来感受多维数组。

code
# 初始化一个两个元素的一维数组
arr1 = np.arange(2)
print("一维数组:\n", arr1)
# 初始化一个4个元素的一维数组,并修改为 2x2 的二维数组
arr2 = np.arange(4).reshape(2,2)
print("二维数组:\n", arr2)
# 初始化一个8个元素的一维数组,并修改为 2x2x2 的三维数组
arr3 = np.arange(8).reshape(2,2,2)
print("三维数组:\n", arr3)
# 打印出三个数组的类型
print(type(arr1), type(arr2), type(arr3))

执行之后,输出如下:

code
一维数组:
 [0 1]
二维数组:
 [[0 1]
 [2 3]]
三维数组:
 [[[0 1]
  [2 3]]
 [[4 5]
  [6 7]]]
<class 'numpy.ndarray'> <class 'numpy.ndarray'> <class 'numpy.ndarray'>

从上面的例子中,可以发现维度的递进关系:

  • 两个普通元素 0 和 1,构成了一维数组;
  • 两个一维数组 [0,1]、[2,3] 构成了 2x2 的二维数组;
  • 两个二维数组 [[0,1],[2,3]]、[[4,5],[6,7]] 构成了 2x2x2 的三维数组。

在代码的最后,将三个数组的类型打印了出来,可以看到都是 numpy.ndarray 类型。

对 ndarray 有了初步认识后,下面开始学习 ndarray 的一些基础操作。

ndarray 基础操作

ndarray 的创建

ndarray 的创建有多种方式,不同的创建方式适用于不同的场合。下面逐一介绍。

(1)从已有的列表中创建

使用 np.array 函数可以将 Python 的列表转换为 NumPy 的 ndarray。

code
a = [1,3,5,7]
arr = np.array(a)
print(arr, type(arr))

输出如下:

code
[1 3 5 7] <class 'numpy.ndarray'>

(2)生成 0 到 N 的一个 ndarray

使用 np.arange 函数,传入参数 N,可以直接生成一个 N 个元素、内容是 0,1,2...N-1 的 ndarray。这也是上一节中使用的方法。

code
arr1 = np.arange(10)
print(arr1, type(arr1))

输出如下:

code
[0 1 2 3 4 5 6 7 8 9] <class 'numpy.ndarray'>

(3)重新调整 ndarray 的形状(也就是维度)

使用 reshape 函数可以将 ndarray 的维度重新调整。用法是 array.reshape(D1,D2,...DN),代表将数组重新调整为 N 维数组,第一维的长度为 D1,第二维的长度为 D2,以此类推。

reshape 函数参数的数量取决于维度数,比如要调整为二维数组,则需要两个参数,如下所示:

code
# 将 10 个元素的 arr1 调整为两个五个元素的一维数组组成的二维数组
arr1.reshape(2,5)

arr1 是刚才创建的一维数组 [0 1 2 3 4 5 6 7 8 9],在经过 reshape 后,输出为:

code
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]])

调整之后,变为了 2 个 5 个元素的一维数组,对应的形状为 2x5。

另外,使用 reshape 时,注意调整前后的元素个数应该一致。D1D2D3...DN 要等于数组的元素个数。比如 arr1 有 10 个元素,可以 reshape 成 (2,5) 或 (5,2) 或 (1,2,5),但不能是 (3,4) 这样。

(4)创建全是 0 的数组

创建全是 0 的一维数组:

code
np.zeros(5)

输出如下:

code
array([0., 0., 0., 0., 0.])

创建全是 0 的数组,并指定形状:

code
# 形状以列表的形式传入,[3,4] 代表 3x4 的二维数组
np.zeros([3,4])

输出如下:

code
array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])

(5)创建全是 1 的数组

用法类似 np.zeros,如下所示。

code
print("一维数组:\n", np.ones(3))
print("二维数组:\n", np.ones([3,4]))

输出如下:

code
一维数组:
 [1. 1. 1.]
二维数组:
 [[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]

(6)创建随机数填充的数组

code
# 生成 20 个 0到1之间的随机数
print(np.random.rand(20))
# 生成 20 个 0 到10 之间的随机数
print(np.random.randint(0,10,20))

输出如下:

code
[0.4041234  0.54505458 0.78659903 0.81566742 0.19598521 0.66532724
 0.35192288 0.83556087 0.90010909 0.24167259 0.20850511 0.93573772
 0.51754404 0.98113317 0.9792484  0.74293827 0.64741681 0.29030238
 0.81008087 0.15342404]
[1 5 9 7 5 1 7 2 0 1 8 2 8 1 1 3 4 5 3 5]

(7)生成单位矩阵

code
print("2x2 的单位矩阵:\n",np.identity(2))
print("3x3 的单位矩阵:\n", np.identity(3))

输出如下:

code
2x2 的单位矩阵:
 [[1. 0.]
 [0. 1.]]
3x3 的单位矩阵:
 [[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

ndarray 的基础属性

ndarray 提供了一些基本的属性可以获得数组的相关信息。

  • ndim,获取数组的维度数,比如 1 维,还是 2 维。
  • shape,获取数组的形状,具体就是每一维的元素个数。
  • size,获取数组的总元素个数。

用法如下:

code
arr = np.ones([3,4])
print("维度数:", arr.ndim)
print("形状:", arr.shape)
print("元素数:", arr.size)

输出如下:

code
维度数: 2
形状: (3, 4)
元素数: 12

多维数组索引

多维数组支持多种索引方式来访问数组中的局部数据。下面逐一学习。

(1)一维数组索引

一维数组的索引方式和普通的 Python 列表一致,直接使用 [] 即可,如下所示。

code
arr = np.arange(12)
print("arr:",arr)
print("第二个元素:",arr[1])
print("第十一个元素:",arr[10])

输出如下:

code
arr: [ 0  1  2  3  4  5  6  7  8  9 10 11]
第二个元素: 1
第十一个元素: 10

(2)多维数组索引

多维数组的索引和一维数组类似,只需将每一维的索引都写在中括号中,并用逗号隔开即可。

比如 a[3,1,0] 代表访问 a 数组的第 4 个元素(对应索引 3),这个元素是一个二维数组;然后访问这个二维数组的第 2 个元素(对应索引 1),这个元素是一个一维数组;然后访问这个一维数组的第 1 个元素(对应索引 0)。

代码示例:

code
# 二维数组索引
arr0 = np.arange(12).reshape(3,4)
print("arr0:\n", arr0)
print("第一维第三个,第二维第四个元素:", arr0[2,3])
# 打印空行
print("\n")
# 三维数组索引
arr1 = np.arange(12).reshape(2,2,3)
print("arr1:", arr1)
print("第一维第二个,第二维第一个,第三维第三个数:", arr1[1,0,2])

输出如下:

code
arr0:
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
第一维第三个,第二维第四个元素: 11
arr1: [[[ 0  1  2]
  [ 3  4  5]]
 [[ 6  7  8]
  [ 9 10 11]]]
第一维第二个,第二维第一个,第三维第三个数: 8

上面的例子中,首先打印了多维数组,然后索引了其中一个元素作为例子。

如果是多维数组,但传入的索引数量和维度数不一致会怎么样?比如是二维数组,但只传了一个维度。这种情况会返回一个低一维度的数组。

可以这样理解:假设有 N 维数组,提供了 N 个索引,返回的就是 N - N = 0 维数组,也就是单个元素;而如果只提供了 1 个索引,返回的就是 N - 1 维的数组。

代码示例:

code
arr = np.arange(12).reshape(2,6)
print("原始二维数组:\n",arr)
print("提供一个索引后,得到一维数组:\n", arr[0])

输出如下:

code
原始二维数组:
 [[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]]
提供一个索引后,得到一维数组:
 [0 1 2 3 4 5]

(3)范围索引

类似 pandas 的索引机制,NumPy 的 ndarray 也支持通过冒号分割的形式来选取其中一部分连续的元素。比如 a[:] 代表选取全部元素,a[3:5] 代表选择索引 >=3 且小于 5 的元素,也就是 a[3]、a[4]。

还有一个点需要注意:对 N 维数组使用范围选择时,所返回的结果仍然是 N 维数组。代码示例:

code
arr = np.arange(10)
print("arr:\n", arr)
print("从索引2到索引4:",arr[2:4])
print("从索引1到索引5:", arr[1:5])

输出如下:

code
arr:
 [0 1 2 3 4 5 6 7 8 9]
从索引2到索引4: [2 3]
从索引1到索引5: [1 2 3 4]

范围索引同样支持多维数组,并且范围语法可以出现在任何一维中。比如假设如下二维数组:

code
[[0 1 2]
 [3 4 5]
 [6 7 8]]

取 3 和 6,那逻辑就是:首先从第一维中取出第二个和第三个元素,分别是 [3 4 5] 和 [6 7 8] 这两个一维数组,然后再从这两个一维数组中分别取第一个元素。实现如下:

code
arr = np.arange(9).reshape(3,3)
print(arr)
print(arr[1:3,0])

输出如下:

code
[[0 1 2]
 [3 4 5]
 [6 7 8]]
array([3, 6])

实现批量计算

回应到本章的开头,NumPy 最显著的作用就是可以做数据的批量计算,也就是数值计算的基础。下面学习这个部分。

数组与单一元素的计算

当数组和单一元素进行运算时,计算规则就是让数组的每一个元素都与这个元素做计算,然后返回的还是一个数组。比如某个班获得荣誉班集体称号,学生的成绩可以整体加 10 分,就会用到类似的操作。代码示例如下:

code
arr = np.arange(12).reshape(3,4)
print("原始数组:\n", arr)
print("数组+2 :\n", arr+2)
print("数组*3 :\n", arr*3)
print("数组/5:\n", arr / 5)

输出如下:

code
原始数组:
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
数组+2 :
 [[ 2  3  4  5]
 [ 6  7  8  9]
 [10 11 12 13]]
数组*3 :
 [[ 0  3  6  9]
 [12 15 18 21]
 [24 27 30 33]]
数组/5:
 [[0.  0.2 0.4 0.6]
 [0.8 1.  1.2 1.4]
 [1.6 1.8 2.  2.2]]

数组对数组的计算

数组对数组的运算分为两种情况。第一种是参与运算的两个数组的 shape 一致,则运算就是将两个数组对应位置的元素分别进行运算,并返回存储运算结果的多维数组,结果的 shape 和参与计算的数组一致。

数组和数组的运算也比较常见。还是基于上面学生成绩的例子,假设每个班不同学生的加分不一样,那一般会把每个学生的加分也存储为一个数组,加分数组和分数数组一一对应,然后直接将两个数组相加就得到了每个学生的最终成绩。代码示例如下:

code
arr = np.arange(12).reshape(3,4)
arr1 = np.random.randint(1,20, [3,4])
print("原始数组1:\n", arr)
print("原始数组2:\n", arr1)
print("数组相加的结果:\n", arr + arr1)
print("数组相乘的结果:\n", arr * arr1)

输出如下:

code
原始数组1:
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
原始数组2:
 [[ 6  8  8  2]
 [ 4 11 15  1]
 [19 10  5 13]]
数组相加的结果:
 [[ 6  9 10  5]
 [ 8 16 21  8]
 [27 19 15 24]]
数组相乘的结果:
 [[  0   8  16   6]
 [ 16  55  90   7]
 [152  90  50 143]]

可以看到结果就是两个数组对应位置相加的结果(实际试验时数字可能不同,因为 arr1 是随机生成的)。

数组之间运算还有第二种情况:虽然两个数组 shape 不一致,但满足广播条件,则可以将维度低的数组逐个与高维度数组对应的部分进行计算。举个例子,将一个 3x4 的二维数组和一个 4 个元素的一维数组进行运算。

看以下代码示例:

code
arr = np.arange(12).reshape(3,4)
arr1 = np.arange(4)
print("原始数组1:\n", arr)
print("原始数组2:\n", arr1)
print("数组相加的结果:\n", arr + arr1)
print("数组相乘的结果:\n", arr * arr1)

输出如下:

code
原始数组1:
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
原始数组2:
 [0 1 2 3]
数组相加的结果:
 [[ 0  2  4  6]
 [ 4  6  8 10]
 [ 8 10 12 14]]
数组相乘的结果:
 [[ 0  1  4  9]
 [ 0  5 12 21]
 [ 0  9 20 33]]

可以看到,结果就是 arr1 的 [0 1 2 3] 分别同 arr 的三个子元素 [0 1 2 3]、[4 5 6 7] 和 [8 9 10 11] 进行分别计算,最后将结果重新汇总成 3x4 的二维数组。

对数组应用常见数学函数

NumPy 提供了一系列常见的数学函数,比如开根、绝对值、求平方、求正余弦等。这些函数可以直接处理数组的数据,类似数组和单个元素的计算:对数组应用数学函数就是将每个元素都传入函数,计算出每一个元素的结果之后重新组成形状一致的多维数组。代码示例:

code
arr = np.arange(12).reshape(3,4)
print("原始数组:\n", arr)
print("求平方:\n", np.square(arr))
print("开根:\n", np.sqrt(arr))
print("正弦函数:\n", np.sin(arr))

输出如下:

code
原始数组:
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
求平方:
 [[  0   1   4   9]
 [ 16  25  36  49]
 [ 64  81 100 121]]
开根:
 [[0.         1.         1.41421356 1.73205081]
 [2.         2.23606798 2.44948974 2.64575131]
 [2.82842712 3.         3.16227766 3.31662479]]
正弦函数:
 [[ 0.          0.84147098  0.90929743  0.14112001]
 [-0.7568025  -0.95892427 -0.2794155   0.6569866 ]
 [ 0.98935825  0.41211849 -0.54402111 -0.99999021]]

计算两个数组是否接近

最初举的例子中,计算学生的成绩是否相近,本质就是比较两个数组是否相近。NumPy 提供了 allclose 函数,可以通过指定误差的方式来判断两个数组的接近度:如果两个数组在误差范围内相等,则返回 True,否则返回 False。代码示例如下:

code
arr = np.arange(4)
arr1 = np.arange(2,6)
print("数组1:", arr)
print("数组2:", arr1)
print("不允许误差:", np.allclose(arr, arr1))
print("允许误差1:", np.allclose(arr, arr1, atol=1))
print("允许误差2:", np.allclose(arr, arr1, atol=2))

输出如下:

code
数组1: [0 1 2 3]
数组2: [2 3 4 5]
不允许误差: False
允许误差1: False
允许误差2: True

上述代码中模拟了两个数组,每个元素差 2,然后调用了 allclose 函数。不传 atol 参数时代表不允许误差,也就是判断两个数组是否相等,这个分支返回 False,因为数组确实不相等。允许 1 的误差也是 False,因为实际上差了 2。

通过 allclose 函数,可以很方便地比较学生的考试水平是否接近,比如可以设置 5 分的误差:如果两个学生所有科目的相差都在 5 分以内,则可以认为是接近的。

小结

本文初步完成了 NumPy 基础的学习,接下来的章节会基于本文学习的内容学习更多的数值分析方法和工具。

回顾一下本文的内容。

  • 首先,学习了 NumPy 中的多维数组 ndarray 以及基本概念。

  • 然后学习了 ndarray 的基本创建,主要有以下的形式: 通过现有列表创建:np.array

  • 生成一个连续元素的数组:np.arange

  • 重新调整数组的形状:reshape

  • 全 0:np.zeros

  • 全 1:np.ones

  • 随机数:np.random.rand

  • 之后是 ndarray 的索引方式。 一维数组元素索引:[index]

  • 多维数组元素索引:[index1, index2, ... indexN]

  • 范围索引:[开始索引 : 结束索引]

  • 最后,学习了 ndarray 的计算规则。 单一元素与数组计算:将元素和数组的每个元素分别计算并返回数组

  • 数组与数组的计算:形状一致的数组对应位置的元素分别计算

  • 对数组应用数学函数:对每一个元素应用数学函数,并返回同样形状的数组

  • 计算两个数组是否接近:allclose 函数

下一篇将介绍基于 NumPy 的常见的统计分析方法。

课后习题

如下面的代码所示,列表 a 中存储了学生小 A 的各科成绩,列表 b 中存储了学生小 B 的各科成绩,是 150 分制。要求:

  • 将两人的成绩都转换为百分制,并打印;
  • 如果各科分数差 10 分以内,则认为水平相近,计算小 A 和小 B 的水平是否相近,并打印。
code
a = [100, 122, 123,  94, 147]
b = [ 94, 120,  131, 103, 140]

答案:

code
a = [100, 122, 123,  94, 147]
b = [ 94, 120,  131, 103, 140]
arr_a = np.array(a)
arr_b = np.array(b)
arr_a = arr_a * 100 / 150
arr_b = arr_b * 100 / 150
print("小A百分制成绩", arr_a)
print("小B百分制成绩", arr_b)
print("小A和小B水平是否接近:", np.allclose(arr_a, arr_b, atol=10))

输出如下:

code
小A百分制成绩 [66.66666667 81.33333333 82.         62.66666667 98.        ]
小B百分制成绩 [62.66666667 80.         87.33333333 68.66666667 93.33333333]
小A和小B水平是否接近: True

版本差异(NumPy 1.x/2.0 → 2.3.x)

特性本文编写时当前(NumPy 2.3.x)
版本基线1.x2.x 系列(2.3 为最新稳定版);2.0 起要求 Python 3.10+
标量类型np.int_/np.float_2.0 起移除或重命名(np.int64/float64 为准),np.NaNnp.nan
字符串类型np.str_ / np.bytes_2.0 起弃用,改用 Python str / bytes
复制行为各处行为不一2.0 起默认更严格(缩进显式化),copy= 语义统一
数值精度默认 float64不变;2.0 引入 NEP 50 类型提升规则,int32 + float32 结果更符合直觉
Python 版本3.8+2.x 要求 3.10+,建议 3.13/3.14

本文讲解的 ndarray 核心概念(广播、索引、ufunc)在 NumPy 2.x 中完全成立;升级时主要关注类型别名移除(np.float_float64)与 NEP 50 提升规则。