工具学习 -python -02 Numpy

借着别人的文章(王圣元 [王的机器] ),复盘一下python的基础知识点。感谢原作者的分享!

Numpy 是 Python 专门处理高维数组 (high dimensional array) 的计算的包,学习 numpy还是遵循的 Python 里「万物皆对象」的原则,既然把数组当对象,我们就按着数组的创建、数组的存载、数组的获取、数组的变形、和数组的计算来盘一盘 NumPy。


1 数组的创建

1.1 初次印象

数组 (array) 是相同类型的元素 (element) 的集合所组成数据结构 (data structure)。numpy 数组中的元素用的最多是「数值型」元素,平时我们说的一维、二维、三维数组长下面这个样子 (对应着线、面、体)。四维数组很难被可视化。

image.png

注意一个关键字 axis,中文叫「轴」,一个数组是多少维度就有多少根轴。由于 Python 计数都是从 0 开始的,那么

  • 第 1 维度 = axis 0
  • 第 2 维度 = axis 1
  • 第 3 维度 = axis 2

但这些数组只可能在平面上打印出来,那么它们 (高于二维的数组) 的表现形式稍微有些不同。

image.png

分析上图各个数组的在不同维度上的元素:

  • 一维数组:轴 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数组,有三种方式:

  1. 按步就班的 np.array() 用在列表和元组上
  2. 定隔定点的 np.arange() 和 np.linspace()
  3. 一步登天的 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,这个概念对于解决引言的「转置高维数组」问题很重要。一图胜千言。


image.png

咦,为什么有个 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 视图」看它和「内存块」存储它的形式是不一样的,如下图所示:

image.png

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 数组的变形

本节介绍四大类数组层面上的操作,具体有

  1. 重塑 (reshape) 和打平 (ravel, flatten)
  2. 合并 (concatenate, stack) 和分裂 (split)
  3. 重复 (repeat) 和拼接 (tile)
  4. 其他操作 (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’ 等价于列主序)
image.png

在 *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() 的不同之处是

  1. ravel() 按「行主序」打平时没有复制原数组,按「列主序」在打平时复制了原数组
  2. 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) 这两个操作仅仅只改变数组的分合

  • 合并是多合一
  • 分裂是一分多

合并

使用「合并」函数有三种选择

  1. 有通用的 concatenate
  2. 有专门的 vstack, hstack, dstack
  3. 有极简的 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 通道

一图胜千言:

image.png

用代码验证一下:

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」合并

分裂

使用「分裂」函数有两种选择

  1. 有通用的 split
  2. 有专门的 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) 等价。一图胜千言:

image.png

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 用两种方式:

  1. arr.sort()
  2. 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 数组的计算

本节介绍四大类数组计算,具体有

  1. 元素层面 (element-wise) 计算
  2. 线性代数 (linear algebra) 计算
  3. 元素整合 (element aggregation) 计算
  4. 广播机制 (broadcasting) 计算

5.1 元素层面计算

Numpy 数组元素层面计算包括:

  1. 二元运算(binary operation):加减乘除
  2. 数学函数:倒数、平方、指数、对数
  3. 比较运算(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) 来区分每个维度。让我们抛弃「行列」这些特殊概念,拥抱「轴」这个通用概念来重看数组 (一到四维) 把。

image.png

规律:n 维数组就有 n 层方括号。最外层方括号代表「轴 0」即 axis=0,依次往里方括号对应的 axis 的计数加 1。
看一个四维数组的例子:

image.png

小节
除了 sum 函数,整合函数还包括 min, max, mean, std 和 cumsum,分别是求最小值、最大值、均值、标准差和累加,这些函数对数组里的元素整合方式和 sum 函数相同,就不多讲了。总结来说我们可以对数组

  • 所有的元素整合

  • 在某个轴 (axis) 上的元素整合

整合函数= {sum, min, max, mean, std, cumsum}


6 总结

本帖讨论了 NumPy 的前三节,数组创建、数组存载、数组获取数组变形和数组计算。同样把 *numpy *数组当成一个对象,要学习它,无非就是学习怎么

  • 创建它:按步就班法、定隔定点法、一步登天法
  • 存载它:保存成 .npy, .txt 和 .csv 格式,下次加载即用
  • 获取它:一段用切片,一个用索引;有正规法、布尔法、花式法
  • 变形它:重塑和打平,合并和分裂,元素重复和数组重复
  • 计算它:元素层面计算,线性代数计算,广播机制计算

数组变形有以下重要操作:

  • 改变维度的重塑打平
  • 改变分合的合并分裂
  • 复制本质的重复拼接
  • 其他排序****插入****删除****复制

数组计算有以下重要操作:
元素层面:四则运算、函数,比较
线性代数:务必弄懂点乘函数 dot()
元素整合:务必弄懂轴这个概念!

你可能感兴趣的:(工具学习 -python -02 Numpy)