借着别人的文章(王圣元 [王的机器] ),复盘一下python的基础知识点。感谢原作者的分享!
Numpy 是 Python 专门处理高维数组 (high dimensional array) 的计算的包,学习 numpy还是遵循的 Python 里「万物皆对象」的原则,既然把数组当对象,我们就按着数组的创建、数组的存载、数组的获取、数组的变形、和数组的计算来盘一盘 NumPy。
1 数组的创建
1.1 初次印象
数组 (array) 是相同类型的元素 (element) 的集合所组成数据结构 (data structure)。numpy 数组中的元素用的最多是「数值型」元素,平时我们说的一维、二维、三维数组长下面这个样子 (对应着线、面、体)。四维数组很难被可视化。
注意一个关键字 axis,中文叫「轴」,一个数组是多少维度就有多少根轴。由于 Python 计数都是从 0 开始的,那么
- 第 1 维度 = axis 0
- 第 2 维度 = axis 1
- 第 3 维度 = axis 2
但这些数组只可能在平面上打印出来,那么它们 (高于二维的数组) 的表现形式稍微有些不同。
分析上图各个数组的在不同维度上的元素:
- 一维数组:轴 0 有 3 个元素
- 二维数组:轴 0 有 2 个元素,轴 1 有 3 个元素
- 三维数组:轴 0 有 2 个元素 (2 块),轴 1 有 2 个元素,轴 2 有 3 个元素
- 四维数组:轴 0 有 2 个元素 (2 块),轴 1 有 2 个元素 (2 块),轴 2 有 2 个元素,轴 3 有 3 个元素
1.2 创建数组
带着上面这个对轴的认识,接下来我们用代码来创建 numpy数组,有三种方式:
- 按步就班的 np.array() 用在列表和元组上
- 定隔定点的 np.arange() 和 np.linspace()
- 一步登天的 np.ones(), np.zeros(), np.eye() 和 np.random.random()
按步就班法
给了「列表」和「元组」原材料,用 np.array() 包装一下便得到 *numpy *数组。
l = [3.5, 5, 2, 8, 4.2]
np.array(l) --array([3.5, 5. , 2. , 8. , 4.2])
t = (3.5, 5, 2, 8, 4.2)
np.array(t) --array([3.5, 5. , 2. , 8. , 4.2])
注意,numpy数组的输出都带有 array() 的字样,里面的元素用「中括号 []」框住。
定隔定点法
更常见的两种创建 *numpy *数组方法:
- 定隔的 arange:固定元素大小间隔
- 定点的 linspace:固定元素个数
函数 arange 的参数为起点 , 终点 , 间隔 arange(start , stop , step)
print( np.arange(8) ) --[0 1 2 3 4 5 6 7]
print( np.arange(2,8) ) --[2 3 4 5 6 7]
print( np.arange(2,8,2)) --[2 4 6 ]
注:用函数 print 打印 numpy 数组就没有 array() 的字样了,只用其内容,而且元素之间的「逗号」也没有了。
函数 linspace 的参数为起点 , 终点 , 点数 arange(start , stop , step)
print( np.linspace(2,6,3) ) --[2. 4. 6.]
print( np.linspace(3,8,11) ) --[3. 3.5 4. 4.5 5. 5.5 6. 6.5 7. 7.5 8. ]
其中 start 和 stop 必须要有,num 没有的话默认为 50。对着这个规则看看上面各种情况的输出。
一步登天法
NumPy 还提供一次性
- 用 zeros() 创建全是 0 的 n 维数组
- 用 ones() 创建全是 1 的 n 维数组
- 用 random() 创建随机 n 维数组
- 用 eye() 创建对角矩阵 (二维数组)
对于前三种,由于输出是 n 为数组,它们的参数是一个「标量」或「元组类型的形状」,下面三个例子一看就懂了:
print( np.zeros(5) ) # 标量5代表形状(5,)
--[0. 0. 0. 0. 0.]
print( np.ones((2,3)) )
--[[1. 1. 1.]
[1. 1. 1.]]
print( np.random.random((2,3,4)) )
--[[[0.15684866 0.33684519 0.85095027 0.67827412]
[0.58041935 0.12639616 0.33509142 0.99561644]
[0.59581471 0.92043399 0.56731046 0.76811703]]
[[0.74276133 0.85278489 0.32392871 0.40553182]
[0.7718898 0.35496469 0.20061144 0.00351225]
[0.49957334 0.48449498 0.62835324 0.29610557]]]
对于函数 eye(),它的参数就是一个标量,控制矩阵的行数或列数:
np.eye(4)
--array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
此外还可以设定 eye() 里面的参数 k
- 默认设置 k = 0 代表 1 落在对角线上
- k = 1 代表 1 落在对角线右上方
- k = -1 代表 1 落在对角线左下方
np.eye(4, k=1)
--array([[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.],
[0., 0., 0., 0.]])
1.3 数组性质
还记得 Python 里面「万物皆对象」么?numpy数组也不例外,那么我们来看看数组有什么属性 (attributes) 和方法 (methods)。
一维数组
用按步就班的 np.array() 带列表生成数组 arr, 现在你应该会用 dir(arr) 来查看数组的属性了吧,看完之后我们对 type, ndim, len(), size, shape, stride, dtype 几个感兴趣,一把梭打印出来看看:
arr = np.array([3.5, 5, 2, 8, 4.2])
print( 'The type is', type(arr) ) --The type is
print( 'The dimension is', arr.ndim ) --The dimension is 1
print( 'The length of array is', len(arr) ) --The length of array is 5
print( 'The number of elements is', arr.size ) --The number of elements is 5
print( 'The shape of array is', arr.shape ) --The shape of array is (5,)
print( 'The stride of array is', arr.strides ) --The stride of array is (8,)
print( 'The type of elements is', arr.dtype ) --The type of elements is float64
根据结果我们来看看上面属性到底是啥:
- type:数组类型,当然是 numpy.ndarray
- ndim:维度个数是 1
- len():数组长度为 5 (注意这个说法只对一维数组有意义)
- size:数组元素个数为 5
- shape:数组形状,即每个维度的元素个数 (用元组来表示),只有一维,元素个数为 5,写成元组形式是 (5,)
- strides:跨度,即在某一维度下为了获取到下一个元素需要「跨过」的字节数 (用元组来表示),float64 是 8 个字节数 (bytes),因此跨度为 8
- dtype:数组元素类型,是双精度浮点 (注意和 type 区分)
注意我黄色高亮了 strides,这个概念对于解决引言的「转置高维数组」问题很重要。一图胜千言。
咦,为什么有个 Python View 和 Memory Block 啊?这两个不是一样的么?对一维数组来说,「Python 视图」看它和「内存块」存储它的形式是一样的,但对二维数组甚至高维数组呢?
二维数组
还是用按步就班的 np.array() 带二维列表生成二维数组 arr2d
l2 = [[1, 2, 3], [4, 5, 6]]
arr2d = np.array(l2)
arr2d
--array([[1, 2, 3],
[4, 5, 6]])
一把梭打印属性出来看看:
print( 'The type is', type(arr2d) ) --The type is
print( 'The dimension is', arr2d.ndim ) --The dimension is 2
print( 'The length of array is', len(arr2d) ) --The length of array is 2
print( 'The number of elements is', arr2d.size ) --The number of elements is 6
print( 'The shape of array is', arr2d.shape ) --The shape of array is (2, 3)
print( 'The stride of array is', arr2d.strides ) --The stride of array is (12, 4)
print( 'The type of elements is', arr2d.dtype ) --The type of elements is int32
同样,我们来分析一下上面属性:
- type:数组类型 numpy.ndarray
- ndim:维度个数是 2
- len():数组长度为 2 (严格定义 len 是数组在「轴 0」的元素个数)
- size:数组元素个数为 6
- shape:数组形状 (2, 3)
- strides:跨度 (12, 4) 看完下图再解释
- dtype:数组元素类型 int32
对于二维数组,Python 视图」看它和「内存块」存储它的形式是不一样的,如下图所示:
在numpy数组中, 默认的是行主序 (row-major order),意思就是每行的元素在内存块中彼此相邻,而列主序 (column-major order) 就是每列的元素在内存块中彼此相邻。
回顾跨度 (stride) 的定义,即在某一维度下为了获取到下一个元素需要「跨过」的字节数。注:每一个 int32 元素是 4 个字节数。对着上图:
- 第一维度 (轴 0):沿着它获取下一个元素需要跨过 3 个元素,即 12 = 3×4 个字节
- 第二维度 (轴 1):沿着它获取下一个元素需要跨过 1 个元素,即 4 = 1×4 个字节
因此该二维数组的跨度为 (12, 4)。
n 维数组
用 np.random.random() 来生成一个多维数组:
arr4d = np.random.random( (2,2,2,3) )
里面具体元素是什么不重要,一把梭 arr4d 的属性比较重要:
print( 'The type is', type(arr4d) ) --The type is
print( 'The dimension is', arr4d.ndim ) --The dimension is 4
print( 'The length of array is', len(arr4d) ) --The length of array is 2
print( 'The number of elements is', arr4d.size ) --The number of elements is 24
print( 'The shape of array is', arr4d.shape ) --The shape of array is (2, 2, 2, 3)
print( 'The stride of array is', arr4d.strides ) --The stride of array is (96, 48, 24, 8)
print( 'The type of elements is', arr4d.dtype ) --The type of elements is float64
除了 stride,都好理解, 回顾跨度 (stride) 的定义,即在某一维度下为了获取到下一个元素需要「跨过」的字节数。注:每一个 float64 元素是 8 个字节数
- 第一维度 (轴 0):沿着它获取下一个元素需要跨过 12 个元素,即 96 = 12×8 个字节
- 第二维度 (轴 1):沿着它获取下一个元素需要跨过 6 个元素,即 48 = 6×8 个字节
- 第三维度 (轴 2):沿着它获取下一个元素需要跨过 3 个元素,即 24 = 3×8 个字节
- 第四维度 (轴 3):沿着它获取下一个元素需要跨过 1 个元素,即 8 = 1×8 个字节
因此该四维数组的跨度为 (96, 48, 24, 8)。
2 数组的存载
本节讲数组的「保存」和「加载」,我知道它们没什么技术含量,但是很重要。假设你已经训练完一个深度神经网络,该网络就是用无数参数来表示的。比如权重都是 numpy 数组,为了下次不用训练而重复使用,将其保存成 .npy 格式或者 .csv 格式是非常重要的。
numpy 自身的 .npy 格式
用 np.save 函数将 numpy 数组保存为 .npy 格式,具体写法: np.save( ‘’文件名”,数组 )
arr_disk = np.arange(8)
np.save("arr_disk", arr_disk)
arr_disk.npy 保存在 Jupyter Notebook 所在的根目录下。要加载它也很简单,用 np.load( "文件名" ) 即可:
np.load("arr_disk.npy") --array([0, 1, 2, 3, 4, 5, 6, 7])
文本 .txt 格式
用 np.savetxt 函数将 numpy 数组保存为 .txt 格式,具体写法如下:np.save( ‘’文件名”,数组 )
arr_text = np.array([[1., 2., 3.], [4., 5., 6.]])
np.savetxt("arr_from_text.txt", arr_text)
arr_from_text.txt 保存在 Jupyter Notebook 所在的根目录下,用 Notepad 打开看里面确实存储着 [[1,2,3], [4,5,6]]。
用 np.loadtxt( "文件名" ) 即可加载该文件
np.loadtxt("arr_from_text.txt")
--array([[1., 2., 3.],
[4., 5., 6.]])
文本 .csv 格式
另外,假设我们已经在 arr_from_csv 的 csv 文件里写进去了 [[1,2,3], [4,5,6]],每行的元素是由「分号 ;」来分隔的
用 np.genfromtxt( "文件名" ) 即可加载该文件
np.genfromtxt("arr_from_csv.csv") --array([nan, nan])
奇怪的是数组里面都是 nan,原因是没有设定好「分隔符 ;」,那么函数 genfromtxt 读取的两个元素是
- 1;2;3
- 4;5;6
它们当然不是数字拉,Numpy 只能用两个 nan (Not a Number) 来代表上面的四不像了。带上「分隔符 ;」再用 np.genfromtxt( "文件名",分隔符 ) 即可加载该文件
np.genfromtxt("arr_from_csv.csv", delimiter=";")
--array([[1., 2., 3.],
[4., 5., 6.]])
3 数组的获取
获取数组是通过索引 (indexing) 和切片 (slicing) 来完成的,
- 切片是获取一段特定位置的元素, 切片写法是 arr[start : stop : step]
- 索引是获取一个特定位置的元素, 索引写法是 arr[index]
因此,切片的操作是可以用索引操作来实现的 (一个一个总能凑成一段),只是没必要罢了。为了简化,我们在本章三节标题里把切片和索引都叫做索引。
索引数组有三种形式,正规索引 (normal indexing)、布尔索引 (boolean indexing) 和花式索引 (fancy indexing)。
3.1 正规索引
虽然切片操作可以由多次索引操作替代,但两者最大的区别在于
- 切片得到的是原数组的一个视图 (view) ,修改切片中的内容会改变原数组
- 索引得到的是原数组的一个复制 (copy),修改索引中的内容不会改变原数组
请看下面一维数组的例子来说明上述两者的不同。
一维数组
arr = np.arange(10)
arr --array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
用 arr[6] 索引第 7 个元素 (记住 Python 是从 0 开始记录位置的),把它赋给变量 a,并重新给 a 赋值 1000,但是元数组 arr 第 7 个元素的值还是 6,并没有改成 1000。
arr[6] --6
a = arr[6]
a = 1000
arr --array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
用 arr[5:8] 切片第 6 到 8 元素 (记住 Python 切片包头不包尾),把它赋给变量 b,并重新给 b 的第二个元素赋值 12,再看发现元数组 arr 第 7 个元素的值已经变成 12 了
arr[5:8] --array([5, 6, 7])
b = arr[5:8]
b[1] = 12
arr --array([ 0, 1, 2, 3, 4, 5, 12, 7, 8, 9])
这就证实了切片得到原数组的视图 (view),更改切片数据会更改原数组,而索引得到原数组的复制 (copy), 更改索引数据不会更改原数组。希望用下面一张图可以明晰 view 和 copy 的关系。
了解完一维数组的切片和索引,类比到二维和多维数组上非常简单。
二维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d
--array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
索引
# 情况一:用 arr2d[2] 来索引第三行,更严格的说法是索引「轴 0」上的第三个元素。
arr2d[2] --array([7, 8, 9])
# 情况二:用 arr2d[0][2] 来索引第一行第三列,用 arr2d[0, 2] 也可以索引第一行第三列
arr2d[0][2] --3
arr2d[0,2] --3
切片
# 情况一:用 arr2d[:2] 切片前两行,更严格的说法是索引「轴 0」上的前两个元素。
arr2d[:2]
--array([[1, 2, 3],
[4, 5, 6]])
# 情况二:用 arr2d[:, [0,2]] 切片第一列和第三列
arr2d[:,[0,2]]
--array([[1, 3],
[4, 6],
[7, 9]])
# 情况三:用 arr2d[1, :2] 切片第二行的前两个元素
arr2d[1, :2]
--array([4, 5])
# 情况四:用 arr2d[:2, 2] 切片第三列的前两个元素
arr2d[:2, 2]
--array([3, 6])
3.2 布尔索引
布尔索引,就是用一个由布尔 (boolean) 类型值组成的数组来选择元素的方法。
假设我们有阿里巴巴 (BABA),脸书 (FB) 和京东 (JD) 的
- 股票代码 code 数组
- 股票价格 price 数组:每行记录一天开盘,最高和收盘价格。
code = np.array(['BABA', 'FB', 'JD', 'BABA', 'JD', 'FB'])
price = np.array([[170,177,169],[150,159,153],
[24,27,26],[165,170,167],
[22,23,20],[155,116,157]])
price
--array([[170, 177, 169],
[150, 159, 153],
[ 24, 27, 26],
[165, 170, 167],
[ 22, 23, 20],
[155, 161, 157]])
假设我们想找出 BABA 对应的股价,首先找到 code 里面是 'BABA' 对应的索引 (布尔索引),即一个值为 True 和 False 的布尔数组。
code == 'BABA'
--array([ True, False, False, True, False, False])
# 用该索引可以获取 BABA 的股价:
price[ code == 'BABA' ]
--array([[170, 177, 169], [165, 170, 167]])
# 用该索引还可以获取 BABA 的最高和收盘价格:
price[ code == 'BABA', 1: ]
--array([[177, 169], [170, 167]])
# 再试试获取 JD 和 FB 的股价:
price[ (code == 'FB')|(code == 'JD') ]
--array([[150, 159, 153], [ 24, 27, 26], [ 22, 23, 20], [155, 161, 157]])
注:这种布尔索引的操作在 Pandas 更常用也更方便,看完 pandas 那帖后就可以忽略这一节了。
3.3 花式索引
花式索引是获取数组中想要的特定元素的有效方法。考虑下面数组:
arr = np.arange(32).reshape(8,4)
arr
--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]])
假设你想按特定顺序来获取第 5, 4 和 7 行时,用 arr[ [4,3,6] ]
arr[ [4,3,6] ]
--array([[16, 17, 18, 19],
[12, 13, 14, 15],
[24, 25, 26, 27]])
假设你想按特定顺序来获取倒数第 4, 3 和 6 行时 (即正数第 4, 5 和 2 行),用 arr[ [-4,-3,-6] ]
arr[ [-4,-3,-6] ]
--array([[16, 17, 18, 19],
[20, 21, 22, 23],
[ 8, 9, 10, 11]])
此外,你还能更灵活的设定「行」和「列」中不同的索引,如下
arr[ [1,5,7,2], [0,3,1,2] ]
--array([ 4, 23, 29, 10])
检查一下,上行代码获取的分别是第二行第一列、第六行第四列、第八行第二列、第三行第三列的元素,它们确实是 4, 23, 29 和 10。如果不用花式索引,就要写下面繁琐但等价的代码:
np.array( [ arr[1,0], arr[5,3],
arr[7,1], arr[2,2] ] )
--array([ 4, 23, 29, 10])
最后,我们可以把交换列,把原先的 [0,1,2,3] 的列换成 [0,3,1,2]。
arr[:,[0,3,1,2]]
--array([[ 0, 3, 1, 2],
[ 4, 7, 5, 6],
[ 8, 11, 9, 10],
[12, 15, 13, 14],
[16, 19, 17, 18],
[20, 23, 21, 22],
[24, 27, 25, 26],
[28, 31, 29, 30]])
4 数组的变形
本节介绍四大类数组层面上的操作,具体有
- 重塑 (reshape) 和打平 (ravel, flatten)
- 合并 (concatenate, stack) 和分裂 (split)
- 重复 (repeat) 和拼接 (tile)
- 其他操作 (sort, insert, delete, copy)
4.1 重塑和打平
重塑 (reshape) 和打平 (ravel, flatten) 这两个操作仅仅只改变数组的维度
- 重塑是从低维到高维
- 打平是从高维到低维
重塑
用reshape()函数将一维数组 arr 重塑成二维数组。
arr = np.arange(12)
print( arr )
--[ 0 1 2 3 4 5 6 7 8 9 10 11]
print( arr.reshape((4,3)) )
--[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
当你重塑高维矩阵时,不想花时间算某一维度的元素个数时,可以用「-1」取代,程序会自动帮你计算出来。比如把 12 个元素重塑成 (2, 6),你可以写成 (2,-1) 或者 (-1, 6)。
print( arr.reshape((2,-1)) )
--[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]]
print( arr.reshape((-1,6)) )
--[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]]
打平
用 ravel() 或flatten() 函数将二维数组 arr 打平成一维数组。
arr = np.arange(12).reshape((4,3))
print( arr )
--[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
ravel_arr = arr.ravel()
print( ravel_arr ) --[ 0 1 2 3 4 5 6 7 8 9 10 11]
flatten_arr = arr.flatten()
print( flatten_arr ) --[ 0 1 2 3 4 5 6 7 8 9 10 11]
思考:为什么重塑后的数组不是
[[ 0 4 8]
[ 1 5 9]
[ 2 6 10]
[ 3 7 11]]
思考:为什么打平后的数组不是
[ 0 3 6 9 1 4 7 10 2 5 8 11]
要回答本节两个问题,需要了解 numpy 数组在内存块的存储方式。
行主序和列主序
行主序 (row-major order) 指每行的元素在内存块中彼此相邻,而列主序 (column-major order) 指每列的元素在内存块中彼此相邻。
在众多计算机语言中,
- 默认行主序的有 C 语言(下图 order=‘C’ 等价于行主序)
- 默认列主序的有 Fortran 语言(下图 order=‘F’ 等价于列主序)
在 *numpy *数组中, 默认的是行主序 ,即 order ='C'。现在可以回答本节那两个问题了。
如果你真的想在「重塑」和「打平」时用列主序,只用把 order 设为 'F',以重塑举例:
print( arr.reshape((4,3), order='F') )
--[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
细心的读者可能已经发现为什么「打平」需要两个函数 ravel() 或 flatten()?它们的区别在哪里?
知识点
函数 ravel() 或 flatten() 的不同之处是
- ravel() 按「行主序」打平时没有复制原数组,按「列主序」在打平时复制了原数组
- flatten() 在打平时复制了原数组
用代码验证一下,首先看 flatten(),将打平后的数组 flatten 第一个元素更新为 10000,并没有对原数组 arr 产生任何影响 (证明 flatten() 是复制了原数组)
arr = np.arange(6).reshape(2,3)
print( arr )
--[[0 1 2]
[3 4 5]]
flatten = arr.flatten()
print( flatten ) --[0 1 2 3 4 5]
flatten_arr[0] = 10000
print( arr )
--[[0 1 2]
[3 4 5]]
再看 ravel() 在「列主序」打平,将打平后的数组 ravel_F 第一个元素更新为 10000,并没有对原数组 arr 产生任何影响 (证明 ravel(order='F') 是复制了原数组)
ravel_F = arr.ravel( order='F' )
ravel_F[0] = 10000
print( ravel_F )
--[10000 3 1 4 2 5]
print( arr )
--[[0 1 2]
[3 4 5]]
最后看 ravel() 在「行主序」打平,将打平后的数组 ravel_C 第一个元素更新为 10000,原数组 arr[0][0] 也变成了 10000 (证明 ravel() 没有复制原数组)
ravel_C = arr.ravel()
ravel_C[0] = 10000
print( ravel_C )
--[10000 1 2 3 4 5]
print( arr )
--[[10000 1 2]
[ 3 4 5]]
4.2 合并和分裂
合并 (concatenate, stack) 和分裂 (split) 这两个操作仅仅只改变数组的分合
- 合并是多合一
- 分裂是一分多
合并
使用「合并」函数有三种选择
- 有通用的 concatenate
- 有专门的 vstack, hstack, dstack
- 有极简的 r_, c_
用下面两个数组来举例:
arr1 = np.array([[1, 2, 3], [4, 5, 6]])
arr2 = np.array([[7, 8, 9], [10, 11, 12]])
concatenate
np.concatenate([arr1, arr2], axis=0)
--[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]
[10 11 12]]
np.concatenate([arr1, arr2], axis=1)
--[[ 1 2 3 7 8 9]
[ 4 5 6 10 11 12]]
在 concatenate() 函数里通过设定轴,来对数组进行竖直方向合并 (轴 0) 和水平方向合并 (轴 1)。
vstack, hstack, dstack
通用的东西是好,但是可能效率不高,NumPy 里还有专门合并的函数
- vstack:v 代表 vertical,竖直合并,等价于 concatenate(axis=0)
- hstack:h 代表 horizontal,水平合并,等价于 concatenate(axis=1)
- dstack:d 代表 depth-wise,按深度合并,深度有点像彩色照片的 RGB 通道
一图胜千言:
用代码验证一下:
print( np.vstack((arr1, arr2)) )
--[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]
[10 11 12]]
print( np.hstack((arr1, arr2)) )
--[[ 1 2 3 7 8 9]
[ 4 5 6 10 11 12]]
print( np.dstack((arr1, arr2)) )
--[[[ 1 7]
[ 2 8]
[ 3 9]]
[[ 4 10]
[ 5 11]
[ 6 12]]]
和 vstack, hstack 不同,dstack 将原数组的维度增加了一维。
np.dstack((arr1, arr2)).shape --(2, 3, 2)
r_, c_
此外,还有一种更简单的在竖直和水平方向合并的函数,r_() 和 c_()。
print( np.r_[arr1,arr2] )
--[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]
[10 11 12]]
print( np.c_[arr1,arr2] )
--[[ 1 2 3 7 8 9]
[ 4 5 6 10 11 12]]
除此之外,r_() 和 c_() 有什么特别之处么?(如果完全和 vstack() 和hstack() 一样,那也没有存在的必要了)
知识点
1. 参数可以是切片。
print( np.r_[-2:2:1, [0]*3, 5, 6] ) --[-2 -1 0 1 0 0 0 5 6]
2. 第一个参数可以是控制参数,如果它用 'r' 或 'c' 字符可生成线性代数最常用的 matrix (和二维 numpy array 稍微有些不同)
np.r_['r', [1,2,3], [4,5,6]] --matrix([[1, 2, 3, 4, 5, 6]])
3. 第一个参数可以是控制参数,如果它写成 ‘a,b,c’ 的形式,其中
a:代表轴,按「轴 a」来合并;
b:合并后数组维度至少是 b;
c:在第 c 维上做维度提升看不懂吧?没事,先用程序感受一下:
print( np.r_['0,2,0', [1,2,3], [4,5,6]] )
--[[1]
[2]
[3]
[4]
[5]
[6]]
print( np.r_['0,2,1', [1,2,3], [4,5,6]] )
--[[1 2 3]
[4 5 6]]
print( np.r_['1,2,0', [1,2,3], [4,5,6]] )
--[[1 4]
[2 5]
[3 6]]
print( np.r_['1,2,1', [1,2,3], [4,5,6]] )
--[[1 2 3 4 5 6]]
没懂彻底吧?没事,我再解释下。字符串 ‘a,b,c’ 总共有四类,分别是
- '0, 2, 0'
- '0, 2, 1'
- '1, 2, 0'
- '1, 2, 1'
函数里两个数组 [1,2,3], [4,5,6] 都是一维
- c = 0 代表在「轴 0」上升一维,因此得到 [[1],[2],[3]] 和 [[4],[5],[6]]
- c = 1 代表在「轴 1」上升一维,因此得到 [[1,2,3]] 和 [[4,5,6]]
接下来如何合并就看 a 的值了
- a = 0, 沿着「轴 0」合并
- a = 1, 沿着「轴 1」合并
分裂
使用「分裂」函数有两种选择
- 有通用的 split
- 有专门的 hsplit, vsplit
用下面数组来举例:
arr = np.arange(25).reshape((5,5))
print( arr )
--[[ 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]]
split
和 concatenate() 函数一样,我们可以在 split() 函数里通过设定轴,来对数组沿着竖直方向分裂 (轴 0) 和沿着水平方向分裂 (轴 1)。
first, second, third = np.split(arr,[1,3])
print( 'The first split is', first ) --The first split is [[0 1 2 3 4]]
print( 'The second split is', second ) --The second split is [[ 5 6 7 8 9] [10 11 12 13 14]]
print( 'The third split is', third ) --The third split is [[15 16 17 18 19] [20 21 22 23 24]]
split() 默认沿着轴 0 分裂,其第二个参数 [1, 3] 相当于是个切片操作,将数组分成三部分:
- 第一部分 - :1 (即第 1 行)
- 第二部分 - 1:3 (即第 2 到 3 行)
- 第二部分 - 3: (即第 4 到 5 行)
hsplit, vsplit
vsplit() 和 split(axis=0) 等价,hsplit() 和 split(axis=1) 等价。一图胜千言:
4.3 重复和拼接
重复 (repeat) 和拼接 (tile) 这两个操作本质都是复制
- 重复是在元素层面复制
- 拼接是在数组层面复制
重复
函数 repeat() 复制的是数组的每一个元素,参数有几种设定方法:
一维数组:用标量和列表来复制元素的个数
多维数组:用标量和列表来复制元素的个数,用轴来控制复制的行和列
# 标量
# 标量参数 3 - 数组 arr 中每个元素复制 3 遍。
arr = np.arange(3)
print( arr ) --[0 1 2]
print( arr.repeat(3) ) --[0 0 0 1 1 1 2 2 2]
# 列表
# 列表参数 [2,3,4] - 数组 arr 中每个元素分别复制 2, 3, 4 遍。
print( arr.repeat([2,3,4]) ) --[0 0 1 1 1 2 2 2 2]
# 标量和轴
# 标量参数 2 和轴 0 - 数组 arr2d 中每个元素沿着轴 0 复制 2 遍。
arr2d = np.arange(6).reshape((2,3))
print( arr2d )
--[[0 1 2]
[3 4 5]]
print( arr2d.repeat(2, axis=0) )
--[[0 1 2]
[0 1 2]
[3 4 5]
[3 4 5]]
# 列表和轴
# 列表参数 [2,3,4] 和轴 1 - 数组 arr2d 中每个元素沿着轴 1 分别复制 2, 3, 4 遍
print( arr2d.repeat([2,3,4], axis=1) )
--[[0 0 1 1 1 2 2 2 2]
[3 3 4 4 4 5 5 5 5]]
拼接
函数 tile() 复制的是数组本身,参数有几种设定方法:
- 标量:把数组当成一个元素,一列一列复制
- 形状:把数组当成一个元素,按形状复制
# 标量
# 标量参数 2 - 数组 arr 按列复制 2 遍。
arr2d = np.arange(6).reshape((2,3))
print( arr2d )
--[[0 1 2]
[3 4 5]]
print( np.tile(arr2d,2) )
--[[0 1 2 0 1 2]
[3 4 5 3 4 5]]
# 形状
# 标量参数 (2,3) - 数组 arr 按形状复制 6 (2×3) 遍,并以 (2,3) 的形式展现。
print( np.tile(arr2d, (2,3)) )
--[[0 1 2 0 1 2 0 1 2]
[3 4 5 3 4 5 3 4 5]
[0 1 2 0 1 2 0 1 2]
[3 4 5 3 4 5 3 4 5]]
4.4 其他操作
本节讨论数组的其他操作,包括排序 (sort),插入 (insert),删除 (delete) 和复制 (copy)。
4.4.1 排序
排序包括直接排序 (direct sort) 和间接排序 (indirect sort)。
直接排序
arr = np.array([5,3,2,6,1,4])
print( 'Before sorting', arr ) --Before sorting [5 3 2 6 1 4]
arr.sort()
print( 'After sorting', arr ) --After sorting [1 2 3 4 5 6]
# sort()函数是按升序 (ascending order) 排列的,该函数里没有参数可以控制 order,因此你想要按降序排列的数组,只需
print( arr[::-1] ) --[6 5 4 3 2 1]
知识点
用来排序 numpy 用两种方式:
- arr.sort()
- np.sort( arr )
第一种 sort 会改变 arr,第二种 sort 在排序时创建了 arr 的一个复制品,不会改变 arr。
arr = np.random.randint( 40, size=(3,4) )
print( arr )
--[[24 32 23 30]
[26 27 28 0]
[ 9 14 24 13]]
# 第一种 arr.sort(),对第一列排序,发现 arr 的元素改变了。
arr[:, 0].sort()
print( arr )
--[[ 9 32 23 30]
[24 27 28 0]
[26 14 24 13]]
#第二种 np.sort(arr),对第二列排序,但是 arr 的元素不变。
np.sort(arr[:,1])
--array([ 14, 27, 32])
print( arr )
--[[ 9 32 23 30]
[24 27 28 0]
[26 14 24 13]]
间接排序
有时候我们不仅仅只想排序数组,还想在排序过程中提取每个元素在原数组对应的索引(index),这时 argsort() 就派上用场了。以排列下面五个学生的数学分数为例:
score = np.array([100, 60, 99, 80, 91])
idx = score.argsort()
print( idx ) --[1 3 4 2 0]
# 看一个二维数组的例子。
arr = np.random.randint( 40, size=(3,4) )
print( arr )
--[[24 32 23 30]
[26 27 28 0]
[ 9 14 24 13]]
# 对其第一行 arr[0] 排序,获取索引,在应用到所用行上。
arr[:, arr[0].argsort()]
--array([[23, 24, 30, 32],
[28, 26, 0, 27],
[24, 9, 13, 14]])
4.4.2 插入和删除
和列表一样,我们可以给 numpy 数组
- 用insert()函数在某个特定位置之前插入元素
- 用delete()函数删除某些特定元素
a = np.arange(6)
print( a ) --[0 1 2 3 4 5]
print( np.insert(a, 1, 100) ) --[ 0 100 1 2 3 4 5]
print( np.delete(a, [1,3]) ) --[0 2 4 5]
4.4.3复制
用copy()函数来复制数组 a 得到 a_copy,很明显,改变 a_copy 里面的元素不会改变 a。
a = np.arange(6)
a_copy = a.copy()
print( 'Before changing value, a is', a )
print( 'Before changing value, a_copy is', a_copy )
a_copy[-1] = 99
print( 'After changing value, a_copy is', a_copy )
print( 'After changing value, a is', a )
--
Before changing value, a is [0 1 2 3 4 5]
Before changing value, a_copy is [0 1 2 3 4 5]
After changing value, a_copy is [ 0 1 2 3 4 99]
After changing value, a is [0 1 2 3 4 5]
5 数组的计算
本节介绍四大类数组计算,具体有
- 元素层面 (element-wise) 计算
- 线性代数 (linear algebra) 计算
- 元素整合 (element aggregation) 计算
- 广播机制 (broadcasting) 计算
5.1 元素层面计算
Numpy 数组元素层面计算包括:
- 二元运算(binary operation):加减乘除
- 数学函数:倒数、平方、指数、对数
- 比较运算(comparison)
先定义两个数组 arr1 和 arr2。
arr1 = np.array([[1., 2., 3.], [4., 5., 6.]])
arr2 = np.ones((2,3)) * 2
print( arr1 )
--[[1. 2. 3.]
[4. 5. 6.]]
print( arr2 )
--[[2. 2. 2.]
[2. 2. 2.]]
# 加、减、乘、除
print( arr1 + arr2 + 1 )
print( arr1 - arr2 )
print( arr1 * arr2 )
print( arr1 / arr2 )
# 倒数、平方、指数、对数
print( 1 / arr1 )
print( arr1 ** 2 )
print( np.exp(arr1) )
print( np.log(arr1) )
# 比较
arr1 > arr2
arr1 > 3
- 「数组和数组间的二元运算」都是在元素层面上进行的
- 「作用在数组上的数学函数」都是作用在数组的元素层面上的。
- 「数组和数组间的比较」都是在元素层面上进行的
但是在「数组和标量间的比较」时,python 好像先把 3 复制了和 arr1 形状一样的数组 [[3,3,3], [3,3,3]],然后再在元素层面上作比较。上述这个复制标量的操作叫做「广播机制」,是 NumPy 里最重要的一个特点,在下一节会详细讲到。
5.2 线性代数计算
在机器学习、金融工程和量化投资的编程过程中,因为运行速度的要求,通常会向量化 (vectorization) 而涉及大量的线性代数运算,尤其是矩阵之间的乘积运算。
但是,在 NumPy 默认不采用矩阵运算,而是数组 (ndarray) 运算。矩阵只是二维,而数组可以是任何维度,因此数组运算更通用些。
如果你非要二维数组 arr2d 进项矩阵运算,那么可以通过调用以下函数来实现:
- A = np.mat(arr2d)
- A = np.asmatrix(arr2d)
下面我们分别对「数组」和「矩阵」从创建、转置、求逆和相乘四个方面看看它们的同异。
创建
创建数组 arr2d 和矩阵 A,注意它们的输出有 array 和 matrix 的关键词。
arr2d = np.array([[1,2],[3,1]])
arr2d
--array([[1, 2],
[3, 1]])
A = np.asmatrix(arr2d)
A
--matrix([[1, 2],
[3, 1]])
转置
数组用 arr2d.T 操作或 arr.tranpose() 函数,而矩阵用 A.T 操作。主要原因就是 .T 只适合二维数据,三维数组在轴 1 和轴 2 之间的转置,这时就需要用函数 arr2d.tranpose (1, 0, 2) 来实现了。
print( arr2d.T ) --[[1 3]
[2 1]]
print( arr2d.transpose() ) -- [[1 3]
[2 1]]
print( A.T ) -- [[1 3]
[2 1]]
求逆
数组用 np.linalg.inv() 函数,而矩阵用 A.I 和 A**-1 操作。
print( np.linalg.inv(arr2d) )
print( A.I )
print( A**-1 )
相乘
相乘是个很模棱两可的概念
- 数组相乘是在元素层面进行,
- 矩阵相乘要就是数学定义的矩阵相乘 (比如第一个矩阵的列要和第二个矩阵的行一样)
看个例子,「二维数组」相乘「一维数组」,「矩阵」相乘「向量」,看看有什么有趣的结果。
首先定义「一维数组」arr 和 「列向量」b:
arr = np.array([1,2])
b = np.asmatrix(arr).T
print( arr.shape, b.shape ) --(2,) (2, 1)
由上面结果看出, arr 的形状是 (2,),只含一个元素的元组只说明 arr 是一维,数组是不分行数组或列数组的。而 b 的形状是 (2,1),显然是列向量。
相乘都是用 * 符号,
print( arr2d*arr )
--[[1 4]
[3 2]]
print( A*b )
--[[5]
[5]]
由上面结果可知,
- 二维数组相乘一维数组得到的还是个二维数组,解释它需要用到「广播机制」,这是下节的重点讨论内容。现在大概知道一维数组 [1 2] 第一个元素 1 乘上 [1 3] 得到 [1 3],而第二个元素 2 乘上 [2 1] 得到 [4 2]。
- 而矩阵相乘向量的结果和我们学了很多年的线代结果很吻合。
再看一个例子,「二维数组」相乘「二维数组」,「矩阵」相乘「矩阵」
print( arr2d*arr2d )
--[[1 4]
[9 1]]
print( A*A )
--[[7 4]
[6 7]]
由上面结果可知,
- 虽然两个二维数组相乘得到二维数组,但不是根据数学上矩阵相乘的规则得来的,而且由元素层面相乘得到的。两个 [[1 2], [3,1]] 的元素相乘确实等于 [[1 4], [9,1]]。
- 而矩阵相乘矩阵的结果和我们学了很多年的线代结果很吻合。
问题来了,那么怎么才能在数组上实现「矩阵相乘向量」和「矩阵相乘矩阵」呢?用点乘函数 dot()。
print( np.dot(arr2d,arr) )
--[5 5]
print( np.dot(arr2d,arr2d) )
--[[7 4]
[6 7]]
结果对了,但还有一个小小的差异
- 矩阵相乘列向量的结果是个列向量,写成 [[5],[5]],形状是 (2,1)
- 二维数组点乘一维数组结果是个一维数组,写成 [5, 5],形状是 (2,)
由此我们来分析下 NumPy 里的 dot() 函数,计算数组和数组之间的点乘结果。
5.3 元素整合计算
在数组中,元素可以以不同方式整合 (aggregation)。拿求和 (sum) 函数来说,我们可以对数组
- 所有的元素求和
- 在某个轴 (axis) 上的元素求和
先定义数组
arr = np.arange(1,7).reshape((2,3))
arr
--array([[1, 2, 3],
[4, 5, 6]])
不难看出它是一个矩阵,分别对全部元素、跨行 (across rows)、跨列 (across columns) 求和:
print( 'The total sum is', arr.sum() ) --The total sum is 21
print( 'The sum across rows is', arr.sum(axis=0) ) --The sum across rows is [5 7 9]
print( 'The sum across columns is', arr.sum(axis=1) ) --The sum across columns is [ 6 15]
分析上述结果:
- 1, 2, 3, 4, 5, 6 的总和是 21
- 跨行求和 = [1 2 3] + [4 5 6] = [5 7 9]
- 跨列求和 = [1+2+3 4+5+6] = [6 15]
行和列这些概念对矩阵 (二维矩阵) 才适用,高维矩阵还是要用轴 (axis) 来区分每个维度。让我们抛弃「行列」这些特殊概念,拥抱「轴」这个通用概念来重看数组 (一到四维) 把。
规律:n 维数组就有 n 层方括号。最外层方括号代表「轴 0」即 axis=0,依次往里方括号对应的 axis 的计数加 1。
看一个四维数组的例子:
小节
除了 sum 函数,整合函数还包括 min, max, mean, std 和 cumsum,分别是求最小值、最大值、均值、标准差和累加,这些函数对数组里的元素整合方式和 sum 函数相同,就不多讲了。总结来说我们可以对数组
所有的元素整合
在某个轴 (axis) 上的元素整合
整合函数= {sum, min, max, mean, std, cumsum}
6 总结
本帖讨论了 NumPy 的前三节,数组创建、数组存载、数组获取数组变形和数组计算。同样把 *numpy *数组当成一个对象,要学习它,无非就是学习怎么
- 创建它:按步就班法、定隔定点法、一步登天法
- 存载它:保存成 .npy, .txt 和 .csv 格式,下次加载即用
- 获取它:一段用切片,一个用索引;有正规法、布尔法、花式法
- 变形它:重塑和打平,合并和分裂,元素重复和数组重复
- 计算它:元素层面计算,线性代数计算,广播机制计算
数组变形有以下重要操作:
- 改变维度的重塑和打平
- 改变分合的合并和分裂
- 复制本质的重复和拼接
- 其他排序****插入****删除****复制
数组计算有以下重要操作:
元素层面:四则运算、函数,比较
线性代数:务必弄懂点乘函数 dot()
元素整合:务必弄懂轴这个概念!