本文首发于“生信补给站”公众号 https://mp.weixin.qq.com/s/0-9zUTxZC6qefyjLyw1SSA
本文是 Python 系列的第四篇
- Python 入门篇 (上)
- Python 入门篇 (下)
- 数组计算之 NumPy (上)
- 数组计算之 NumPy (下)
- 科学计算之 SciPy
- 数据结构之 Pandas
- 基本可视化之 Matplotlib
- 统计可视化之 Seaborn
- 交互可视化之 Bokeh
- 炫酷可视化之 PyEcharts
- 机器学习之 Sklearn
- 深度学习之 TensorFlow
- 深度学习之 Keras
- 深度学习之 PyTorch
- 深度学习之 MXnet
接着上篇继续后面两个章节,数组变形和数组计算。
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 )print( arr.reshape((4,3)) )
[ 0 1 2 3 4 5 6 7 8 9 10 11] [[ 0 1 2] [ 3 4 5] [ 6 7 8] [ 9 10 11]]
思考:为什么重塑后的数组不是
[[ 0 4 8] [ 1 5 9] [ 2 6 10] [ 3 7 11]]
当你重塑高维矩阵时,不想花时间算某一维度的元素个数时,可以用「-1」取代,程序会自动帮你计算出来。比如把 12 个元素重塑成 (2, 6),你可以写成 (2,-1) 或者 (-1, 6)。
print( arr.reshape((2,-1)) )print( arr.reshape((-1,6)) )
[[ 0 1 2 3 4 5] [ 6 7 8 9 10 11]] [[ 0 1 2 3 4 5] [ 6 7 8 9 10 11]]
打平
用 ravel() 或flatten() 函数将二维数组 arr 打平成一维数组。
arr = np.arange(12).reshape((4,3))print( arr ) ravel_arr = arr.ravel()print( ravel_arr ) flatten_arr = arr.flatten()print( flatten_arr )
[[ 0 1 2] [ 3 4 5] [ 6 7 8] [ 9 10 11]] [ 0 1 2 3 4 5 6 7 8 9 10 11] [ 0 1 2 3 4 5 6 7 8 9 10 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 )flatten = arr.flatten()print( flatten )flatten_arr[0] = 10000print( arr )
[[0 1 2] [3 4 5]] [0 1 2 3 4 5] [[0 1 2] [3 4 5]]
再看 ravel() 在「列主序」打平,将打平后的数组 ravel_F 第一个元素更新为 10000,并没有对原数组 arr 产生任何影响 (证明 ravel(order='F') 是复制了原数组)
ravel_F = arr.ravel( order='F' )ravel_F[0] = 10000print( ravel_F )print( arr )
[10000 3 1 4 2 5] [[0 1 2] [3 4 5]]
最后看 ravel() 在「行主序」打平,将打平后的数组 ravel_C 第一个元素更新为 10000,原数组 arr[0][0] 也变成了 10000 (证明 ravel() 没有复制原数组)
ravel_C = arr.ravel()ravel_C[0] = 10000print( ravel_C )print( arr )
[10000 1 2 3 4 5] [[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)np.concatenate([arr1, arr2], axis=1)
[[ 1 2 3] [ 4 5 6] [ 7 8 9] [10 11 12]] [[ 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)) )print( np.hstack((arr1, arr2)) )print( np.dstack((arr1, arr2)) )
[[ 1 2 3] [ 4 5 6] [ 7 8 9] [10 11 12]] ----------------------- [[ 1 2 3 7 8 9] [ 4 5 6 10 11 12]] ----------------------- [[[ 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] )print( np.c_[arr1,arr2] )
[[ 1 2 3] [ 4 5 6] [ 7 8 9] [10 11 12]] [[ 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:合并后数组维度至少是 bc:在第 c 维上做维度提升
看不懂吧?没事,先用程序感受一下:
print( np.r_['0,2,0', [1,2,3], [4,5,6]] )print( np.r_['0,2,1', [1,2,3], [4,5,6]] )print( np.r_['1,2,0', [1,2,3], [4,5,6]] )print( np.r_['1,2,1', [1,2,3], [4,5,6]] )
[[1] [2] [3] [4] [5] [6]] ---------------- [[1 2 3] [4 5 6]] ---------------- [[1 4] [2 5] [3 6]] ---------------- [[1 2 3 4 5 6]]
还看不懂吧 (但至少知道完事后的维度是 2,即字符串 ‘a,b,c’ 的 b 起的作用 )?没事,我再画个图。
还没懂彻底吧?没事,我再解释下。
字符串 ‘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」合并