Python-数据分析之 Numpy 初步
-
NumPy 是 Python 中一个基本的科学计算库,包含以下特性:
- 强大的 N 维数组对象;
- 精巧的广播(broadcasting)功能;
- C/C++ 和 Fortran 代码集成工具;
- 实用的线性代数、傅里叶变换、随机数生成等功能。
其中,N 维数组是 NumPy 最为核心的特性。
除了显而易见的科学计算用途,NumPy 还可以用作一般数据类型的多维容器,并且是任何数据类型均可;但有一点:一个数组中必须是同种数据类型。这一特性使得 NumPy 可以高效、无缝地与各种数据库进行集成。
NumPy 基于开源许可协议 BSD license 发布,对于二次分发使用几乎没有限制。
-
安装 NumPy
由于 NumPy 并非 Python 的内置模块,因此我们直接从 Python 官网下载安装的发行版是不包含 NumPy 这个模块的。这个时候你要是想
import numpy
,显然是会无功而返的。因此我们需要额外安装 NumPy 模块。安装 NumPy 有好几种方式,我们这里推荐的是:1)使用
pip
进行安装;2)安装Anaconda。 -
这种方式推荐给已经从 Python 官网下载了某个 Python 发行版的读者,或是已经通过其它方式获得了 Python 环境,但却没有 NumPy 这个模块的读者。
安装命令:
pip install numpy
或:
python -m pip install numpy
均可。
当然,实际上 NumPy 模块本身也有很多依赖,也需要其他一些模块才能够真正发挥出它强大的功能,因此我们推荐一次安装多个模块:
python -m pip install --user numpy scipy matplotlib ipython jupyter pandas symp
-
N 维数组
N 维数组,也称“多维数组”,这里的“N”代表的就是数组的维数。
- 在之前已经学过了 Python 中的内置数据类型,其中有一种在表现形式上跟多维数组很像——就是序列(sequence)型的列表(list)数据:二者都是由中括号括起来的数据类型。
-
多维数组和列表是完全不同的两种数据类型,我们在使用的时候一定要严格地将二者区分开来。具体地讲,二者有以下不同点:
- NumPy 的多维数组在创建之初形状大小就已经固定;而列表的大小则是动态变化的。改变一个多维数组的大小,实际上会导致一个新的数值被创建,旧的数组的删除。
- NumPy 多维数组中的元素要求是同种数据类型,因此在内存中实际的大小也是相同的。只有一个例外:数组由 Python 的各种对象组成,这样数组中元素的大小才会不同。
- NumPy 的多维数组使得对大量数据进行高级的数学运算或其他操作变得更加方便快捷。对同样一个运算来说,使用多维数组会比使用 Python 内置的序列类型更加高效,代码量也更少。
- 许许多多的基于 Python 的科学计算库、数学应用库都使用 NumPy 的多维数组进行实现,并且这些库的数量还在日渐增多。就算有些库也同样支持 Python 的序列作为输入,它们也会优先将序列转换为多维数组,再进行处理;此外,这些库的输出一般也是多维数组。换而言之,要无障碍地使用当今大多数基于 Python 的科学/数学计算软件,仅仅掌握 Python 内置的序列数据类型是不够的,还应当学会如何使用 NumPy 的多维数组。
-
两个重要的特性
在上面的例子中,支撑起 NumPy 强大功能的主要有两个很重要的特性:矢量化(vectorization)和广播(broadcasting)。
-
矢量化
其中,矢量化描述的是这么个情形:凡是在遇到需要显式循环、索引的地方,都可以省略掉这些细枝末节,由预编译好的、优化过的 C 代码在后台默默耕耘。这么一来就带来了几个好处:
- 矢量化的代码更加简洁易读;
- 代码行的减少也意味着更少的 bug;
- 代码中表达式更加接近数学公式的本来面目;
- 矢量化也使得代码更加 Pythonic。没有“矢量化”,我们的代码将会充斥着低效、难懂的
for
循环。
-
广播
“广播”这个特性对于掌握 NumPy 非常重要,文章后面会单独讲解一下,希望读者能够用心理解
“广播”是一个 NumPy 的术语,描述的则是这么一个情况:NumPy 中的运算都是默认逐元素进行的。这些运算不仅限于常见的算术运算,也包括不那么常见的逻辑运算、位运算,以及函数,等等,凡是用到这些运算,或者说操作,NumPy 都默认是逐元素进行的;这就叫“广播”。
从前面的例子来看,进行逐元素乘法的 a、b 两个数组,既可以是大小相同的多维数组,也可以其中一个是标量、另一个是数组,甚至可以是两个大小不同的数组,为了使得“广播”的语意明确、结果清晰,其中较小的那个数组就可以被扩展为较大数组的大小。
-
认识数组
创建数组
本小节代码来自《NumPy 快速入门》
创建数组(array)的方式有不少,其中最自然的一种方式就是通过列表来生成数组。
要使用 NumPy 模块,首先我们要在当前的 Python 环境中导入 NumPy,同时为了便于之后的引用,我们将其重命名为
np
: - import numpy as np
-
通过列表创建数组
文章前面我们提到过,NumPy 的多维数组和 Python 内置的列表长得很像,估计是表亲还是啥的。并且嵌套的列表在一定程度上也能够实现多维数组的功能,因此 NumPy 也很人性化的提供了接口,可以将现成的列表转换为我们要的多维数组。
-
>>> first_array = np.array([2,3,4]) >>> first_array array([2, 3, 4]) >>> first_array.dtype dtype('int32')
-
其中,多维数组的
dtype
属性指明了多维数组中元素的类型。当然也可以将列表变量作为参数: -
>>> example_list = [2.0,3.0,4.0] >>> second_array = np.array(example_list) >>> second_array array([2., 3., 4.]) >>> second_array.dtype dtype('float64')
-
要注意的是,通过这种方式创建数组,经常犯的一个错误是缺少了列表的方括号,这样参数就不再是一个列表,而是好几个独立的参数了:
-
>>> a = np.array(1,2,3,4) # 这是错的 >>> a = np.array([1,2,3,4]) # 这样才是对的
- 实际上不仅是列表,同为 Python 中的序列类型,列表的亲兄弟元组也可以起到相同的作用:
-
>>> a = np.array((2,3,4)) >>> a array([2, 3, 4])
- 同时,使用
array
创建数组时,如果提供的序列对象是嵌套的,NumPy 还可以直接据此生成二维、三维甚至更高维的多维数组: -
>>> a = np.array(((2,3,4,5),(4,5,6,7))) >>> a array([[2, 3, 4, 5], [4, 5, 6, 7]]) >>> >>> a = np.array([(2,3,4,5),(4,5,6,7)]) >>> a array([[2, 3, 4, 5], [4, 5, 6, 7]])
- 还能在创建数组的同时显式指定数据类型:
-
>>> complex_array = np.array([[1,2],[3,4]], dtype='complex') >>> complex_array array([[1.+0.j, 2.+0.j], [3.+0.j, 4.+0.j]]) >>> >>> float_array = np.array([[1,2],[3,4]], dtype='float64') >>> float_array array([[1., 2.], [3., 4.]])
-
元素未知的创建方式
一般来说,很多时候我们都是知道多维数组的大小,但不知道其元素具体的值,因此 NumPy 提供了一些函数,可以创建以占位符初始化的固定大小的多维数组。其中,
zeros
创建的是全为 0 的多维数组,ones
创建的时候全为 1 的多维数组,而empty
创建的则是随机初始值的多维数组,数组大小由一个序列(即列表或元组,建议使用元组)参数给定。并且这几种方式的默认类型都是flloat64
。 -
>>> np.zeros((3,4)) array([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]]) >>> >>> np.ones((3,4)) array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]]) >>> >>> np.empty((3,4)) # empty 的结果就是一块没有初始化的内存。这里由于形状相同,是直接取了上一个数组的值 array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]]) >>> >>> np.empty((3,5)) # 增大数组的大小就可以得到预期的随机结果了 array([[1.40546330e-311, 1.40546113e-311, 1.37961302e-306, 6.23053614e-307, 8.45593934e-307], [7.56593017e-307, 8.01097889e-307, 1.78020169e-306, 7.56601165e-307, 1.02359984e-306], [2.04719290e-306, 1.00132653e-307, 1.78021527e-306, 1.66889876e-307, 3.49694131e-317]])
-
等差序列的数组
NumPy 还提供了一个类似于 Python 内置的
range
的函数arange
,用以创建一个由等差序列组成的数组: -
>>> np.arange(10,30,5) array([10, 15, 20, 25]) >>> >>> np.arange( 0, 2, 0.3 ) array([ 0. , 0.3, 0.6, 0.9, 1.2, 1.5, 1.8]) >>> >>> np.arange(0.2,0.3,0.01) array([0.2 , 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29])
-
元素数量固定的浮点数组
由于浮点精度的原因,使用
arange
创建浮点数组时,我们不能保证得到我们预期大小的数组,因此这个时候就建议使用linspace
这个函数。linspace
与arange
的区别就在于它们的第三个参数:前者指定的是最终得到的元素个数,而后者指定的则是元素间的步长。 -
np.linspace(0.2,0.3,9) array([0.2 , 0.2125, 0.225 , 0.2375, 0.25 , 0.2625, 0.275 , 0.2875, 0.3 ])
-
多维数组的基本属性
多维数组是 NumPy 中最主要的对象。其实就是一个由同种元素组成的元素表,可以由元组进行索引。在 NumPy 中,维度又被称作“轴(axe)”。
注意,在继续介绍数组的各种属性之前,我们要区别开“数组的维度”和“数组某个轴上的维度”。
“数组的维度”指的是数组的“轴”数,用维度空间的概念来理解,也就是数组能够在多少个方向上具有坐标。比如一维的线性空间中,数组就只能在 x 方向上具有坐标;对于二维的平面空间,数组就在 x 和 y 两个方向上具有坐标。
而“数组轴的维度”则是指的在某个特定的方向上,数组可以有几个刻度,或者说“层次”。比如一维数组
[0,1,2]
就是在 x 方向上具有 3 个层次;二维数组[[0,1,2],[2,3,4]]
则是在 x 方向上具有 2 个层次,每个层次都是一个在 y 方向上的三个层次的一维数组,在 y 方向上具有 3 个层次,每个层次都是一个在 x 方向上具有两个层次的一维数组。 -
下面介绍 NumPy 多维数组的基本属性:
- ndim
ndim
即“n dimension”的简写。该属性指示的是多维数组的维数,或者说是“轴数”。- shape
字面意思。这个属性指示的是多维数组整体的维度,或者说是多维数组的“形状”。是一个整型元组,每一个元素都对应与相应轴上的维数。对 n 行 m 列的矩阵而言,它的
shape
就是(n,m)
。shape
的元素个数等于多维数组的轴数。- size
多维数组中元素的总个数。等于
shape
中各元素之积。- dtype
dtype
实际上是“data type”的简写,意味着它指示的是多维数组中元素的数据类型。- itemsize
多维数组中,每个元素的字节大小。等效于
dtype.itemsize
。- data
指示的是包含多维数组中元素实际内存的缓冲区。通常用不到。
>>> print(np.zeros((2,))) # 一维数组 [0. 0.] >>> >>> print(np.zeros((2,3))) # 二维数组 [[0. 0. 0.] [0. 0. 0.]] >>> >>> print(np.zeros((2,3,4))) # 三维数组 [[[0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.]] [[0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.]]] >>> >>> print(np.zeros((2,3,4,5))) # 四维数组 [[[[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] [[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] [[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]]] [[[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] [[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] [[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]]]]
-
重整多维数组的大小
另外,为了更灵活地使用多维数组,NumPy 还提供了
reshape
方法,可以将多维数组重整为某个大小。比如在图像识别领域,就需要图像作为机器学习输入数据,而实用的机器学习应用图像来源又是不确定的,因此图像的像素阵列大小不一定一致。
使用
reshape
要求参数乘积与被重整的数组元素个数相同: -
>>> np.arange(12) array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) >>> >>> np.arange(12).reshape(2,6) array([[ 0, 1, 2, 3, 4, 5], [ 6, 7, 8, 9, 10, 11]]) >>> >>> np.arange(12).reshape(6,2) array([[ 0, 1], [ 2, 3], [ 4, 5], [ 6, 7], [ 8, 9], [10, 11]])
- 并且
reshape
还允许缺省 1 个参数(用-1
占位),它会根据数组元素的总数和提供的其他参数自动求出一个合适的值,从而得到新的大小的数组: -
>>> np.arange(12).reshape(2,3,2) array([[[ 0, 1], [ 2, 3], [ 4, 5]], [[ 6, 7], [ 8, 9], [10, 11]]]) >>> >>> np.arange(12).reshape(2,-1,2) array([[[ 0, 1], [ 2, 3], [ 4, 5]], [[ 6, 7], [ 8, 9], [10, 11]]])
-
多维数组的索引
本节参考自《NumPy 索引》。示例代码多来自《NumPy 索引》。
所谓索引,在 NumPy 中指的是任何用中括号来获取数组元素值的行为。
NumPy 中,索引的方式有很多,这既使得 NumPy 更加强大灵活,也带来了难于辨析的问题。
常用方式
最简单的一种索引方式就是单个索引。对于一维数组,我们可以像对 Python 中的序列一样进行索引:
-
>>> x = np.arange(10) >>> x[2] 2 >>> x[-2] 8
- 对于二维和更高维的数组,我们可以在同一个中括号内直接索引:
-
>>> x.shape = (2,5) # 等同于 x = x.reshape(2,5) >>> x[1,3] 8 >>> x[1,-1] 9
- 而不必像对嵌套序列一样,用多个中括号分别索引:
-
>>> y = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]] # 这是一个列表,不是 NumPy 多维数组! >>> y [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]] >>> y[1][3] 8
- 当给出的索引少于数组维数(轴数)时,得到的会是一个数组对象:
-
>>> x[0]
array([0, 1, 2, 3, 4])
-
而对于返回的这个数组,我们又可以继续索引,因此对于多维数组而言,也可以使用嵌套序列的索引方式,即使用多个中括号(但这种方式比一次索引要更低效,因此不推荐):
>>> x[0][2]
2
此外,NumPy 多维数组又可以像列表一样,进行切片(用冒号“:”):
>>> x = np.arange(10)
>>> x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> x[2:5]
array([2, 3, 4])
>>> x[1:7:2]
array([1, 3, 5])
数组也可以用另一个数组来索引:
>>> x = np.arange(10,1,-1)
>>> x
array([10, 9, 8, 7, 6, 5, 4, 3, 2])
>>> x[np.array([3, 3, 1, 8])]
array([7, 7, 9, 2])
4.2 不常用方式
也可以用多个数组来进行索引:
>>> y = np.arange(35).reshape(5,7)
>>> y
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, 32, 33, 34]])
>>> y[np.array([0,2,4]), np.array([0,1,2])]
array([ 0, 15, 30])
>>>
>>> y[np.array([0,2,4]), 1]
array([ 1, 15, 29])
还可以用布尔数组作为掩码,筛选数组元素:
>>> b = y>20
>>> b
array([[False, False, False, False, False, False, False],
[False, False, False, False, False, False, False],
[False, False, False, False, False, False, False],
[ True, True, True, True, True, True, True],
[ True, True, True, True, True, True, True]])
>>> y[b]
array([21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34])
对应于布尔数组中为“真”的元素就被筛选出来了。
5. NumPy 中的广播
本小节参考自《NumPy 广播》和《NumPy 广播机制详解》。图片来自《NumPy 广播机制详解》。
NumPy 中,各种运算默认都是“逐元素”进行的。最简单的例子就是两个明显大小相同的数组运算:
>>> a = np.array([1.0, 2.0, 3.0])
>>> b = np.array([2.0, 2.0, 2.0])
>>> a * b
array([ 2., 4., 6.])
但是逐元素计算有一个问题:对于形状不太像的数组怎么办呢?比如下面这个数组和标量的乘法运算:
>>> multi_array = np.array([1.0,2.0,3.0])
>>> scalar = 2.0
>>> multi_array * scalar # array([2., 4., 6.])
按“逐元素”运算的预期,显然我们是期望把这个标量与数组的每一个元素相乘,来得到一个新的数组。但是 NumPy 懂得这其中的逻辑吗?诶?好像还真的懂。
既然我们号称 NumPy 可以用自然的方式来表达数学公式,肯定不能把一个简单的标量乘向量弄得太复杂,同样是直接乘就可以了,得到的结果与之前两个大小相同的数组直接相乘是一样的:
>>> multi_array * scalar
array([2., 4., 6.])
在这里边,标量
scalar
就好像被扩展为了一个跟multi_array
大小相当的数组一样。 -
-
广播的前提:相容的形状
在 NumPy 的广播机制中,有一个很重要的概念叫做“相容的形状”。只有当两个数组具有“相容的形状”时,“广播”才能起作用;否则抛出异常
ValueError: operands could not be broadcast together with shapes xx yy
。所谓“相容的形状”,指的是参与运算的这两个数组各个维度要么 1)相等;要么 2)其中一个数组的对应维度为 1(不存在的维度也是 1)。
而 NumPy 比较各个维度的顺序是从后往前,一次比较,就相当于把参与运算的数组形状右对齐,然后若相等就再往前看,若其中一个为 1 就将其在这个维度上扩展到更高的维度,直到第一个维度。
下面是对于上述规则一个更清晰的表述描述:
-
Image (3d array): 256 x 256 x 3 Scale (1d array): 3 Result (3d array): 256 x 256 x 3 A (4d array): 8 x 1 x 6 x 1 B (3d array): 7 x 1 x 5 Result (4d array): 8 x 7 x 6 x 5
- 但是像这样的两个数组就无法通过“广播”实现逐元素运算了:
-
A (1d array): 3 B (1d array): 4 # 最后一个维度无法匹配 A (2d array): 2 x 1 B (3d array): 8 x 4 x 3 # 倒数第二个维度无法匹配
-
广播的实例
对于一个多维数组和一个一维数组的运算,实例如下:
-
>>> a = np.array([[ 0.0, 0.0, 0.0], ... [10.0, 10.0, 10.0], ... [20.0, 20.0, 20.0], ... [30.0, 30.0, 30.0]]) >>> b = np.array([1.0, 2.0, 3.0]) >>> a.shape (4, 3) >>> b.shape (3,) >>> a + b array([[ 1., 2., 3.], [11., 12., 13.], [21., 22., 23.], [31., 32., 33.]])
-
- 对齐之后,维
>>> a = np.array([[ 0.0, 0.0, 0.0], ... [10.0, 10.0, 10.0], ... [20.0, 20.0, 20.0], ... [30.0, 30.0, 30.0]]) >>> b = np.array([1.0, 2.0, 3.0]) >>> a.shape (4, 3) >>> b.shape (4,) >>> a + b Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: operands could not be broadcast together with shapes (4,3) (4,)
-
-
- 当然我
>>> b.shape (4,) >>> b = b.reshape(4,1) >>> b.shape (4, 1) >>> b array([[1.], [2.], [3.], [4.]]) >>> a + b array([[ 1., 1., 1.], [12., 12., 12.], [23., 23., 23.], [34., 34., 34.]])
-
reshape
重整b
数组的形状,以适应广播的要求: -
多维数组切片之图像裁剪
看了那么多枯燥的原理,我们接下来轻松一下,看看 NumPy 还能干什么。
喜欢拍照的同学都知道,图片是由“像素(pixel)”构成的。所谓“像素”,英文 pixel 就是“picture element”的简写,指的是“构成图像的元素”。
实际上我们可以把一张图片纵横切分成很多小块,这些最基本的小方格就是构成多姿多彩的数字图像世界的一砖一瓦。作为二维的图像,它们像素的排布是不是跟二维数组很像?诶~ 对了,我们可以用一个很常用的图形库
matplotlib
来读取图像,得到的实际上就是一个 NumPy 二维数组: -
>>> import matplotlib.pyplot as plt >>> image = plt.imread("python-logo.png") >>> image.shape (600, 800, 3)
- 取下一半图像:
-
>>> image_crop = image[300:,::,::] >>> plt.imshow(image_crop) <matplotlib.image.AxesImage object at 0x0000019C783EA2B0> >>> plt.show()
- 取右边一半的图像:
-
>>> image_crop = image[:,400:,:] >>> plt.imshow(image_crop) <matplotlib.image.AxesImage object at 0x0000019C78BC0BE0> >>> plt.show()