Numpy入门
写在开始前,本系列将使用jupyter notebook作为工具,如果你是windows系统,为了方便请下载Anaconda3-5.1.0-Windows-x86_64,这集成了所有你可以用到或者用不到的各种工具。如果你是mac或者linux系统,可以下载Anaconda3-5.1.0的对应版本。
在计算机中,可以将图像(尤其是数字图像)简单地看作二维数字数组,这些数字数组代表各区域的像素值;声音片段可以看作时间和强度的一维数组;文本也可以通过各种方式转换成数值表示,一种可能的转换是用二进制数表示特定单词或单词对出现的频率。
不管数据是何种形式,第一步都是将这些数据转换成数值数组形式的可分析数据,正因如此,有效地存储和操作数值数组是数据科学中绝对的基础过程。 Python中专门用来处理这些数值数组的工具: NumPy 包和 Pandas 包,首先来介绍NumPy包。
一、NumPy数组
Python 中的数据操作几乎等同于 NumPy 数组操作,甚至新出现的 Pandas 工具也是构建在 NumPy 数组的基础之上的,因此这是最重要的也是要理解的。
1.1 从Python列表创建数组
首先,可以用 np.array 从 Python 列表创建数组:不同于 Python 列表, NumPy 要求数组必须包含同一类型的数据。如果类型不匹配, NumPy 将会向上转换(如果可行),优先级:str>float>int 。
In[1]: import numpy as np
In[2]: # 整型数组:
np.array([1, 4, 2, 5, 3])
Out[2]: array([1, 4, 2, 5, 3])
如果希望明确设置数组的数据类型,可以用 dtype 关键字:
In[3]: np.array([1, 2, 3, 4], dtype='float32')
Out[3]: array([ 1., 2., 3., 4.], dtype=float32)
最后,不同于 Python 列表, NumPy 数组可以被指定为多维的。以下是用列表的列表初始化多维数组的一种方法:
In[11]: # 嵌套列表构成的多维数组
np.array([range(i, i + 3) for i in [2, 4, 6]])
Out[11]: array([[2, 3, 4],[4, 5, 6],[6, 7, 8]]
内层的列表被当作二维数组的行。
1.2 从头创建数组
面对大型数组的时候,用 NumPy 内置的方法从头创建数组是一种更高效的方法。以下是几个示例:
In[12]: # 创建一个长度为10的数组,数组的值都是0
np.zeros(10, dtype=int)
Out[12]: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
In[13]: # 创建一个3×5的浮点型数组,数组的值都是1
np.ones((3, 5), dtype=float)
Out[13]: array([[ 1., 1., 1., 1., 1.],[ 1., 1., 1., 1., 1.],[ 1., 1., 1., 1., 1.]])
In[14]: # 创建一个3×5的浮点型数组,数组的值都是3.14
np.full((3, 5), 3.14)
Out[14]: array([[ 3.14, 3.14, 3.14, 3.14, 3.14],[ 3.14, 3.14, 3.14, 3.14, 3.14],[ 3.14, 3.14, 3.14, 3.14, 3.14]])
1.3 NumPy数组基础
1.3.1 数组的属性
确定数组的大小、形状、存储大小、数据类型。
每个数组有 nidm(数组的维度)、 shape(数组每个维度的大小)和 size(数组的总大小)属性:
In[1]: x3 = np.random.randint(10, size=(3, 4, 5)) # 三维数组
In[2]: print("x3 ndim: ", x3.ndim)
print("x3 shape:", x3.shape)
print("x3 size: ", x3.size)
x3 ndim: 3
x3 shape: (3, 4, 5)
x3 size: 60
另外一个有用的属性是 dtype,它是数组的数据类型:
In[3]: print("dtype:", x3.dtype)
dtype: int32
其他的属性包括表示每个数组元素字节大小的 itemsize,以及表示数组总字节大小的属性nbytes:
In[4]: print("itemsize:", x3.itemsize, "bytes")
print("nbytes:", x3.nbytes, "bytes")
itemsize: 4 bytes
nbytes: 240 bytes
一般来说,可以认为 nbytes 跟 itemsize 和 size 的乘积大小相等。
1.3.2 数组的索引
获取和设置数组各个元素的值。
如果你熟悉 Python 的标准列表索引,那么你对 NumPy 的索引方式也不会陌生。和 Python列表一样,在一维数组中,你也可以通过中括号指定索引获取第 i 个值(从 0 开始计数):
x1 = np.random.randint(10, size=6)
In[5]: x1
Out[5]: array([5, 0, 3, 3, 7, 9])
In[6]: x1[0]
Out[6]: 5
为了获取数组的末尾索引,可以用负值索引:
In[8]: x1[-1]
Out[8]: 9
In[9]: x1[-2]
Out[9]: 7
在多维数组中,可以用逗号分隔的索引元组获取元素:
x2 = np.random.randint(10, size=(3, 4)) # 二维数组
In[10]: x2
Out[10]: array([[3, 5, 2, 4],[7, 6, 8, 8],[1, 6, 7, 7]])
In[11]: x2[0, 0]
Out[11]: 3
In[12]: x2[2, 0]
Out[12]: 1
In[13]: x2[2, -1]
Out[13]: 7
也可以用以上索引方式修改元素值,和 Python 列表不同, NumPy 数组是固定类型的。这意味着当你试图将一个浮点值插入一个整型数组时,浮点值会被截短成整型。并且这种截短是自动完成的,不会给你提示或警告,所以需要特别注意这一点!
In[15]: x1[0] = 3.14159 # 这将被截短
x1
Out[15]: array([3, 0, 3, 3, 7, 9])
1.3.3 数组的切分: 获取子数组
在大的数组中获取或设置更小的子数组。
NumPy 切片语法和 Python 列表的标准切片语法相同。为了获取数组 x 的一个切片,可以用以下方式:
x[start:stop:step]
一维子数组:
In[16]: x = np.arange(10)
x
Out[16]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In[17]: x[:5] # 前五个元素
Out[17]: array([0, 1, 2, 3, 4])
In[18]: x[5:] # 索引五之后的元素
Out[18]: array([5, 6, 7, 8, 9])
In[19]: x[4:7] # 中间的子数组
Out[19]: array([4, 5, 6])
In[20]: x[::2] # 每隔一个元素
Out[20]: array([0, 2, 4, 6, 8])
In[21]: x[1::2] # 每隔一个元素,从索引1开始
Out[21]: array([1, 3, 5, 7, 9])
多维子数组:多维切片也采用同样的方式处理,用冒号分隔。
In[24]: x2
Out[24]: array([[12, 5, 2, 4],[ 7, 6, 8, 8],[ 1, 6, 7, 7]])
In[25]: x2[:2, :3] # 两行,三列
Out[25]: array([[12, 5, 2],[ 7, 6, 8]])
In[26]: x2[:3, ::2] # 所有行,每隔一列
Out[26]: array([[12, 2],[ 7, 8],[ 1, 7]])
In[27]: x2[::-1, ::-1] # 子数组维度也可以同时被逆序
Out[27]: array([[ 7, 7, 6, 1],[ 8, 8, 6, 7],[ 4, 2, 5, 12]])
获取数组的行和列:一种常见的需求是获取数组的单行和单列。你可以将索引与切片组合起来实现这个功能,用一个冒号(:)表示空切片:
In[28]: print(x2[:, 0]) # x2的第一列
[12 7 1]
In[29]: print(x2[0, :]) # x2的第一行
[12 5 2 4]
In[30]: print(x2[0]) # 等于x2[0, :]
[12 5 2 4]
关于数组切片有一点很重要也非常有用,那就是数组切片返回的是数组数据的视图,而不是数值数据的副本。这一点也是 NumPy 数组切片和 Python 列表切片的不同之处:在Python 列表中,切片是值的副本。
它意味着在处理非常大的数据集时,可以获取或处理这些数据集的片段,而不用复制底层的数据缓存。如果需要创建数组的副本,可以使用copy(),如果修改这个子数组,原始的数组不会被改变。
1.3.4 数组的变形
改变给定数组的形状。
数组变形最灵活的实现方式是通过 reshape() 函数来实现。
In[38]: grid = np.arange(1, 10).reshape((3, 3))
print(grid)
[[1 2 3]
[4 5 6]
[7 8 9]]
原始数组的大小必须和变形后数组的大小一致。你也可以在切片操作中使用 newaxis 关键字。
In[39]: x = np.array([1, 2, 3])# 通过变形获得的行向量
x.reshape((1, 3))
Out[39]: array([[1, 2, 3]])
In[40]: # 通过newaxis获得的行向量
x[np.newaxis, :]
Out[40]: array([[1, 2, 3]])
1.3.5 数组的拼接和分裂
以上所有的操作都是针对单一数组的,但有时也需要将多个数组合并为一个,或将一个数组分裂成多个。
数组的拼接:
拼接或连接 NumPy 中的两个数组主要由 np.concatenate、 np.vstack 和 np.hstack 例程实现。 np.concatenate 将数组元组或数组列表作为第一个参数,如下所示:
In[43]: x = np.array([1, 2, 3])
y = np.array([3, 2, 1])
np.concatenate([x, y])
Out[43]: array([1, 2, 3, 3, 2, 1])
也可以一次性拼接两个以上数组:
In[44]: z = [99, 99, 99]
print(np.concatenate([x, y, z]))
[ 1 2 3 3 2 1 99 99 99]
np.concatenate 也可以用于二维数组的拼接:
In[45]: grid = np.array([[1, 2, 3],[4, 5, 6]])
In[46]: # 沿着第一个轴拼接
np.concatenate([grid, grid])
Out[46]: array([[1, 2, 3],[4, 5, 6],[1, 2, 3],[4, 5, 6]])
In[47]: # 沿着第二个轴拼接(从0开始索引)
np.concatenate([grid, grid], axis=1)
Out[47]: array([[1, 2, 3, 1, 2, 3],[4, 5, 6, 4, 5, 6]])
沿着固定维度处理数组时,使用 np.vstack(垂直栈)和 np.hstack(水平栈)函数会更简洁:
In[48]: x = np.array([1, 2, 3])
grid = np.array([[9, 8, 7],[6, 5, 4]])
# 垂直栈数组
np.vstack([x, grid])
Out[48]: array([[1, 2, 3],[9, 8, 7],[6, 5, 4]])
In[49]: # 水平栈数组
y = np.array([[99],[99]])
np.hstack([grid, y])
Out[49]: array([[ 9, 8, 7, 99],[ 6, 5, 4, 99]])
与之类似, np.dstack 将沿着第三个维度拼接数组。
数组的分裂:
与拼接相反的过程是分裂。分裂可以通过 np.split、 np.hsplit 和 np.vsplit 函数来实现。可以向以上函数传递一个索引列表作为参数,索引列表记录的是分裂点位置:
In[50]: x = [1, 2, 3, 99, 99, 3, 2, 1]
x1, x2, x3 = np.split(x, [3, 5])
print(x1, x2, x3)
[1 2 3] [99 99] [3 2 1]
值得注意的是, N 分裂点会得到 N + 1 个子数组。相关的 np.hsplit 和 np.vsplit 的用法也类似:
In[51]: grid = np.arange(16).reshape((4, 4))
grid
Out[51]: array([[ 0, 1, 2, 3],[ 4, 5, 6, 7],[ 8, 9, 10, 11],[12, 13, 14, 15]])
In[52]: upper, lower = np.vsplit(grid, [2])
print(upper)
print(lower)
[[0 1 2 3]
[4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]
In[53]: left, right = np.hsplit(grid, [2])
print(left)
print(right)
[[ 0 1]
[ 4 5]
[ 8 9]
[12 13]]
[[ 2 3]
[ 6 7]
[10 11]
[14 15]]
同样, np.dsplit 将数组沿着第三个维度分裂。