《利用Python进行数据分析》附录A 高阶NumPy
A.1 ndarray对象内幕
ndarray如何灵活的部分原因使每个数组对象都是一个数据块的分步视图。例如,数组arr[::2,::-1]如何做到不复制任何数据。
原因是ndarray不仅仅是一块内存和一个type,它还具有"跨步"信息,使数组能够以不同的步长在内部中移动。
更准确的说,ndarray内部包含以下内容:
指向数据的指针-即RAM中或内存映射文件中的数据块
数据类型或dtype,描述数组中固定大小的值单元格
表示数组形状(shape)的元祖
步长元祖,表示要"步进"的字节数的整数以便沿维度推进一个元素
步幅这里介绍比较好
计算步幅:如果要在维度0方向上移动到下一个数组的相同位置,则需要移动三个元素。 每个元素的大小为2个字节。 因此,维度0中的步幅是2个字节x3个元素= 6个字节。
同样,如果要在维度1中移动一个单位,则需要移动1个元素。 因此,维度1中的步幅是2个字节x 1个元素= 2个字节。 最后一个维度的步幅始终等于元素大小。
我们可以使用.strides
检查数组的步幅:
In [41]: n Out[41]: array([[3, 4, 5], [6, 7, 8], [0, 1, 2]]) In [42]: n.dtype Out[42]: dtype('int64') In [43]: n.strides Out[43]: (24, 8)
A1.1NumPy dtype层次结构
In [44]: ints = np.ones(10,dtype=np.uint16) In [45]: floats = np.ones(10,dtype=np.float32) In [46]: np.issubdtype(ints.dtype, np.integer) Out[46]: True In [47]: np.issubdtype(floats.dtype, np.floating) Out[47]: True In [48]: np.float64.mro() Out[48]: [numpy.float64, numpy.floating, numpy.inexact, numpy.number, numpy.generic, float, object] In [49]:
重点记住一个是否使子类的方法issubdtype
A2 高阶数组操作
A.2.1重塑数组
In [50]: n = np.zeros(9).reshape((3,3)) In [51]: n Out[51]: array([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]) In [52]: n = np.zeros(9).reshape((3,-1)) In [53]: n Out[53]: array([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]) In [54]:
reshape内部参数可以是元祖,也可以直接去掉那个括号,第二个参数可以使用-1,将直接根据第一个参数自动计算。
还有这个np.ravel以及np.flatten
两者的功能是一致的,将多维数组降为一维,但是两者的区别是返回拷贝还是返回视图,np.flatten(0返回一份拷贝,对拷贝所做修改不会影响原始矩阵,而np.ravel()返回的是视图,修改时会影响原始矩阵
In [55]: n = np.arange(4).reshape(2,-1) In [56]: n Out[56]: array([[0, 1], [2, 3]]) In [57]: n_ravel = n.ravel() In [58]: n_flatten = n.flatten() In [59]: n_ravel Out[59]: array([0, 1, 2, 3]) In [60]: n_ravel[:] =9 In [61]: n Out[61]: array([[9, 9], [9, 9]]) In [62]: n_flatten[:]=10 In [63]: n Out[63]: array([[9, 9], [9, 9]])
A2.2 C顺序和Fortran顺序
由于历史原因,行和列方向的顺序也分别称为C顺序和Fortran顺序。在FORTRAN77语言中,矩阵都使列方向的
In [64]: arr = np.arange(12).reshape(3,4) In [65]: arr Out[65]: array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) In [69]: arr.ravel('F') Out[69]: array([ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11]) In [70]: arr.T.ravel() Out[70]: array([ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11]) In [71]:
A2.3连接和分隔数据
连接前面已经学过了,再复习一下,用concatenate来完成。
In [80]: arr1 Out[80]: array([[1, 2, 3], [4, 5, 6]]) In [81]: arr2 Out[81]: array([[ 7, 8, 9], [10, 11, 12]]) In [82]: np.concatenate([arr1,arr2]) Out[82]: array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) In [83]: np.vstack((arr1,arr2)) Out[83]: array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) In [84]: np.concatenate([arr1,arr2],axis=1) Out[84]: array([[ 1, 2, 3, 7, 8, 9], [ 4, 5, 6, 10, 11, 12]]) In [85]: np.hstack((arr1,arr2)) Out[85]: array([[ 1, 2, 3, 7, 8, 9], [ 4, 5, 6, 10, 11, 12]])
其实只要记住了concatenate就可以了
In [87]: arr = np.random.randn(5,2) In [88]: arr Out[88]: array([[ 1.33138714, 2.07803625], [-0.93913673, -0.73406683], [-0.72445096, -0.76812243], [-1.14585571, -1.65073635], [ 0.7386325 , -0.22569135]]) In [89]: first,second,third = np.split(arr,[1,3]) In [90]: first Out[90]: array([[1.33138714, 2.07803625]]) In [91]: second Out[91]: array([[-0.93913673, -0.73406683], [-0.72445096, -0.76812243]]) In [92]: third Out[92]: array([[-1.14585571, -1.65073635], [ 0.7386325 , -0.22569135]])
切的话是split,内部需要传入切割的点,n个点生成n+1条线段
A2.3.1堆叠助手:r_和c_
这个前面一本书也有介绍,今天仔细的看了一下,感觉功能还是很强大的
In [100]: arr = np.arange(6) In [101]: arr1 = np.arange(6).reshape(3,2) In [102]: arr2 = np.random.randn(3,2) In [103]: np.r_[arr1,arr2] Out[103]: array([[ 0. , 1. ], [ 2. , 3. ], [ 4. , 5. ], [-1.21541112, -2.00013006], [-0.80744917, -1.539052 ], [-1.39408604, 0.84356217]]) In [104]: arr.shape Out[104]: (6,) In [105]: np.c_[np.r_[arr1,arr2],arr] Out[105]: array([[ 0. , 1. , 0. ], [ 2. , 3. , 1. ], [ 4. , 5. , 2. ], [-1.21541112, -2.00013006, 3. ], [-0.80744917, -1.539052 , 4. ], [-1.39408604, 0.84356217, 5. ]])
这个c_太牛了,arr本是一维的数组,1行6个元素,直接当扩展的column了。
这个函数还可以将切片转换未数组:
In [113]: np.c_[5:0:-1,0:5] Out[113]: array([[5, 0], [4, 1], [3, 2], [2, 3], [1, 4]]) In [114]:
A2.4重复元素:tile和repaet
repeat和tile函数是用于重复或复制数组的两个有用的工具。repeat函数按照给定次数对数组中的每个元素进行复制,生成一个更大的数组:
In [114]: arr = np.arange(3) In [115]: arr.repeat(3) Out[115]: array([0, 0, 0, 1, 1, 1, 2, 2, 2]) In [116]: arr.repeat([2,3]) --------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-116-a0c86ecd16ca> in <module> ----> 1 arr.repeat([2,3]) ValueError: operands could not be broadcast together with shape (3,) (2,) In [117]: arr.repeat([2,3,2]) Out[117]: array([0, 0, 1, 1, 1, 2, 2])
可以指定重复的次数,也可以指定单个元素的重复次数,但需要内部套一个数组,数量要对repert的元素数量一样
多维数组可以在指定的轴向上对它们的元素进行重复:
In [121]: arr Out[121]: array([[-1.40245973, -0.04140971], [-1.37313291, 0.7491575 ]]) In [122]: arr.repeat(2) Out[122]: array([-1.40245973, -1.40245973, -0.04140971, -0.04140971, -1.37313291, -1.37313291, 0.7491575 , 0.7491575 ]) In [123]: arr.repeat(2,axis=0) Out[123]: array([[-1.40245973, -0.04140971], [-1.40245973, -0.04140971], [-1.37313291, 0.7491575 ], [-1.37313291, 0.7491575 ]]) In [124]: arr.repeat([2,3],axis=1) Out[124]: array([[-1.40245973, -1.40245973, -0.04140971, -0.04140971, -0.04140971], [-1.37313291, -1.37313291, 0.7491575 , 0.7491575 , 0.7491575 ]]) In [125]:
另一方面,tile是一种快捷方法,它可以沿着轴向堆叠副本。在视觉上,你可以把它看做类似于"铺设瓷砖":
In [127]: np.tile(arr,2) Out[127]: array([[-1.40245973, -0.04140971, -1.40245973, -0.04140971], [-1.37313291, 0.7491575 , -1.37313291, 0.7491575 ]]) In [128]: np.tile(arr,(2,1)) Out[128]: array([[-1.40245973, -0.04140971], [-1.37313291, 0.7491575 ], [-1.40245973, -0.04140971], [-1.37313291, 0.7491575 ]]) In [129]: np.tile(arr,(3,2)) Out[129]: array([[-1.40245973, -0.04140971, -1.40245973, -0.04140971], [-1.37313291, 0.7491575 , -1.37313291, 0.7491575 ], [-1.40245973, -0.04140971, -1.40245973, -0.04140971], [-1.37313291, 0.7491575 , -1.37313291, 0.7491575 ], [-1.40245973, -0.04140971, -1.40245973, -0.04140971], [-1.37313291, 0.7491575 , -1.37313291, 0.7491575 ]]) In [130]: np.tile(arr,(1,2)) Out[130]: array([[-1.40245973, -0.04140971, -1.40245973, -0.04140971], [-1.37313291, 0.7491575 , -1.37313291, 0.7491575 ]]) In [131]:
np.tile将内部的元素的打包成一个副本,可以在行于列的进行复制。默认一个参数是在column方向进行复制。
如果填写一个array,会对row和column进行复制。
A2.5神奇索引的等价方法:take和put
In [131]: arr = np.arange(10)*10 In [132]: arr = np.arange(10)*100 In [133]: inds = [7,1,2,6] In [134]: arr[inds] Out[134]: array([700, 100, 200, 600]) In [135]:
上面的神奇索引有介绍过.
put拿来赋值,take拿来取值,切put不知道axis参数,而是将数组索引到扁平版本(一维,C顺序)。因此,当您需要使用其他轴上的索引数组设置元素是,通常最容易使用神奇索引。
In [143]: arr = np.arange(10) *100 In [144]: arr.take(inds) Out[144]: array([700, 100, 200, 600]) In [145]: arr.put(inds,42) In [146]: arr Out[146]: array([ 0, 42, 42, 300, 400, 500, 42, 42, 800, 900]) In [147]: arr[inds] = 42 In [148]: arr Out[148]: array([ 0, 42, 42, 300, 400, 500, 42, 42, 800, 900]) In [149]: arr.put(inds,[40,41,42,43]) In [150]: arr Out[150]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900]) In [151]: arr[inds] = np.arange(40,44) In [152]: arr Out[152]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900])
所有的操作都可以用魔法索引赋值进行操作。
take可以传递axis值
In [154]: arr = np.random.rand(2,4) In [155]: arr Out[155]: array([[0.12023118, 0.4302048 , 0.28467179, 0.88792358], [0.2420648 , 0.21527482, 0.10031776, 0.57313978]]) In [156]: arr.take(inds,axis=1) Out[156]: array([[0.28467179, 0.12023118, 0.28467179, 0.4302048 ], [0.10031776, 0.2420648 , 0.10031776, 0.21527482]]) In [158]: arr[:,inds] Out[158]: array([[0.28467179, 0.12023118, 0.28467179, 0.4302048 ], [0.10031776, 0.2420648 , 0.10031776, 0.21527482]])
A3 广播
广播秒速了算法如何在不同形状的数组之间进行运算。它是一个强大的功能,但可能会导致混淆,即使对于有经验的用户也是如此。最简单的广播示例发生在将标量值于数组组合的时候:
In [171]: arr = np.arange(5) In [172]: arr Out[172]: array([0, 1, 2, 3, 4]) In [173]: arr * 4 Out[173]: array([ 0, 4, 8, 12, 16])
这里我们说标量4已经被广播给乘法运算中的其他元素。
个人理解,一个标量,无论于几维的数组进行操作,都可以完美的进行广播,我把标量想象成0维,就好比杭州麻将的百搭,可以完美适配任何的维度【也就是所谓的广播】
例如,我们可以通过减去列均值来降低数组中的每一列的数值。
In [174]: arr = np.random.randn(4,3) In [175]: arr Out[175]: array([[-0.21606821, -0.16318088, -0.19862113], [ 0.35614129, -1.2219687 , -2.05856434], [ 0.21446578, -0.6502341 , 1.33610149], [ 0.0314505 , 0.24780944, 0.36204802]]) In [176]: arr.mean(0) Out[176]: array([ 0.09649734, -0.44689356, -0.13975899]) In [177]: demeaned = arr - arr.mean(0) In [178]: demeaned Out[178]: array([[-0.31256555, 0.28371268, -0.05886214], [ 0.25964395, -0.77507514, -1.91880535], [ 0.11796844, -0.20334054, 1.47586048], [-0.06504684, 0.694703 , 0.50180701]]) In [179]: demeaned.mean(0) Out[179]: array([6.93889390e-18, 5.55111512e-17, 5.55111512e-17]) In [180]: demeaned.mean(0).shape Out[180]: (3,) In [181]:
重点🏁重点 如果每个结尾的维度(即从尾部开始的),轴长度都匹配或者长度都是1,两个二维数组就是可以兼容广播的。之后,广播会在丢失的或长度为1的轴上进行。
这句话真的是金句,翻译的也很好,把另一本书上几句话说明的事情,抽象成了一句话,太棒了。
下面演示列广播,也就是行匹配了。
In [181]: arr Out[181]: array([[-0.21606821, -0.16318088, -0.19862113], [ 0.35614129, -1.2219687 , -2.05856434], [ 0.21446578, -0.6502341 , 1.33610149], [ 0.0314505 , 0.24780944, 0.36204802]]) In [183]: row_meas = arr.mean(1) In [184]: row_meas.shape Out[184]: (4,) In [185]: row_meas.reshape(4,1) Out[185]: array([[-0.19262341], [-0.97479725], [ 0.30011106], [ 0.21376932]]) In [186]: arr.shape Out[186]: (4, 3) In [187]: demeaned = arr - row_meas.reshape(4,1) In [188]: demeaned Out[188]: array([[-0.0234448 , 0.02944252, -0.00599772], [ 1.33093854, -0.24717145, -1.08376709], [-0.08564527, -0.95034516, 1.03599043], [-0.18231882, 0.03404012, 0.1482787 ]]) In [189]: demeaned.mean(1) Out[189]: array([-9.25185854e-18, -7.40148683e-17, 0.00000000e+00, 0.00000000e+00]) In [190]:
A3.1在其他轴上进行广播
在三维情况下,在三个维度中的任何一个维度上进行广播,只是将数据重塑为形状兼容的问题。书中的图简直太棒了。
因此,一个常见的问题是需要添加一个长度为1的新轴,专门用于广播目的。
使用reshpe当然可以,当需要构建一个元祖太麻烦了。因此,NumPy数组提供了一种通过索引插入新轴的特殊语法。
使用np.newaxis属性和"完整"切片来插入新轴:
In [193]: arr = np.zeros((4,4)) In [194]: arr Out[194]: array([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]]) In [195]: arr.shape Out[195]: (4, 4) In [196]: arr.ndim Out[196]: 2 In [197]: arr3d = arr[:, np.newaxis,:] In [198]: arr3d Out[198]: array([[[0., 0., 0., 0.]], [[0., 0., 0., 0.]], [[0., 0., 0., 0.]], [[0., 0., 0., 0.]]]) In [199]: arr3d.ndim Out[199]: 3 In [200]: arr3d.shape Out[200]: (4, 1, 4)
这是二维数组添加了一个维度,变成了三维数组
In [201]: arr_1d = np.random.normal(size=3) In [202]: arr_1d[:,np.newaxis] Out[202]: array([[-0.070474 ], [ 0.80928887], [ 1.04804804]]) In [203]: arr_1d[np.newaxis,:] Out[203]: array([[-0.070474 , 0.80928887, 1.04804804]])
因此,如果我有一个三维数组并想在轴2上减去均值,需要这么写