从本章开始,将正式进入本书的核心内容(其实前面的内容是面向什么都不懂的新手用户的,但是我非常怀疑,如果是真的什么都不懂的新手,是完全看不懂这本书前面内容的)。
说明一下:因为专业术语是缩写或者要求大写,但是频繁切换又比较麻烦,所以有些名词我会用小写的方式。
还有,从本章开始,我将进一步规范笔记里的格式,带来更好的阅读体验。
——————————————–
Numpy是目前Python数值计算中最为重要的基础包。大多数计算包都提供了基于numpy的科学函数功能,将numpy的数组对象作为数据交换的通用语。
以下内容将会出现在numpy中:
1、ndarray,一种高效多维数组,提供了基于数组的编写算数操作以及灵活的广播功能。
2、对所有数据进行快速的矩阵计算,而无需编写循环程序。
3、对硬盘中数组数据进行读写的工具,并对内存映射文件进行操作。
4、线性代数、随机数生成以及傅里叶变换功能。
5、用于连接numpy到C、C++和fortran语言类库的C语言API。
由于numpy提供了一个非常易用的C语言API,这使得,将数据传递给用底层语言编写的外部类库,再由外部类库将计算结果按照numpy数组的方式返回,编的非常简单。这个特征使得Python可以对存量C/C++/Fortran代码库进行封装,并未这些代码提供动态、易用的接口。
Numpy本身并不提供建模和科学函数,理解numpy的数组以及基于数组的计算将帮助你更高效地使用基于数组的工具,比如pandas。由于numpy是一个很大的话题,我将在后续章节讲解numpy的一些高级特性。
对于大多数的数据分析应用,我主要关注的内容为:
1、在数据处理、清晰、构造子集、过滤、变换以及其他计算中进行快速的向量化计算。
2、常见的数组算法,比如sort、unique以及set操作等。
3、高效地描述性统计和聚合/概述数据。
4、数据排列和相关数据操作,例如对异构数据进行merge和join。
5、使用数组表达式来表明条件逻辑,代替if-elif-else条件分支的循环。
6、分组数据的操作(聚合、变换以及函数式操作)。
虽然numpy提供了数值数据操作的计算基础,但大多数读者还是想把pandas作为统计分析的基石,尤其是针对表哥数据。pandas提供了更多的针对特定场景的函数功能,例如时间序列操作等numpy并不包含的功能。
numpy之所以如此重要,其中一个原因就是它的设计对于含有大量数组的数据非常有效。此外还有如下原因:
1、numpy在内部将数据存储在连续的内存块上,这与其他的python内建数据结构是不同的。numpy的算法库是用C语言写的,所以在操作数据库时,不需要任何类型检查或者其他管理操作。numpy数组使用的内存量也小于其他Python内建序列。
2、numpy可以针对全量数组进行复杂计算而不需要写python循环。
第4章
4.1 NumPy ndarray:多维数组对象
Numpy的核心特征之一就是N-维数组对象——ndarray。ndarray是Python中一个快速,灵活的大型数据集容器。数组允许你使用类似于标量的操作语法在整块数据上进行数学计算。
为了让你感受下Numpy如何使用类似于Python内建对象的标量计算语法进行批量计算,我首先导入numpy,再生成一个小的随机数组:
In [1]: import numpy as np
In [2]: data = np.random.randn(2,3)
In [3]: data
Out[3]:
array([[ 0.485639 , 1.07251377, -1.30833252],
[-0.18263245, -0.39578942, 0.52570894]])
In [4]: data * 10
Out[4]:
array([[ 4.85638999, 10.72513772, -13.08332518],
[ -1.82632445, -3.95789424, 5.2570894 ]])
In [5]: data + data
Out[5]:
array([[ 0.971278 , 2.14502754, -2.61666504],
[-0.36526489, -0.79157885, 1.05141788]])
一个ndarray是一个通用的多维同类数据容器,也就是说,它包含的每一个元素均为相同类型。每一个数组都有一个shape属性,用来表征数组每一维度的数量,每一个数组都有一个dtype属性,用来描述数组的数据类型:
In [6]: data.shape
Out[6]: (2, 3)
In [7]: data.dtype
Out[7]: dtype('float64')
4.1.1 生成ndarray
生成数组最简单的方式就是使用array函数。array函数接受任意的序列型对象(当然也包括其他的数组),生成一个新的包含传递数据的numpy数组。例如,列表的转换:
In [8]: data1 = [6,7.5,8,0,1]
In [9]: arr1 = np.array(data1)
In [10]: arr1
Out[10]: array([6. , 7.5, 8. , 0. , 1. ])
嵌套序列,例如同等长度的列表,将会自动转换成多维数组:
In [11]: data2 = [[1,2,3,4],[5,6,7,8]]
In [12]: arr2 = np.array(data2)
In [13]: arr2
Out[13]:
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
因为data2是一个包含列表的列表,所以numpy数组arr2形成了二维数组。我们可以通过检查ndim和shape属性来确认这一点:
In [14]: arr2.ndim
Out[14]: 2
In [15]: arr2.shape
Out[15]: (2, 4)
除非显式地指定,否则np.array会自动推断生成数组的数据类型。数据类型被存储在一个特殊的元数据dtype中。
In [17]: arr2.dtype
Out[17]: dtype('int64')
ndim用来表示这是个几维数组,更简单一点,就是.shape里有几个数字,ndim里面就是几。
除了np.array,还有很多其他函数可以创建新数组。例如,给定长度及形状后,zeros可以一次性创建全0数组,ones可以一次性创造全1数组。empty则可以创建一个没有初始化数值的数组。想要创建高纬数组,则需要为shape传递一个元组:
In [25]: np.zeros(10)
Out[25]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [26]: np.zeros((3,6))
Out[26]:
array([[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]])
In [27]: np.empty((2,3,2))
Out[27]:
array([[[ 3.10503618e+231, 2.00389292e+000],
[ 2.96439388e-323, 0.00000000e+000],
[ 2.12199579e-314, 2.12199579e-314]],
[[ 3.10503618e+231, -1.49457162e-154],
[ 2.15981457e-314, 2.15984407e-314],
[ 2.15984407e-314, 8.34402697e-309]]])
arange是Python内建函数range的数组版:
In [34]: np.arange(15)
Out[34]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
4.1.2 ndarray 的数据类型
数据类型,即dtype,是一个特殊的对象,它包含了ndarray需要为某一种类型数据所申明的内存块信息(也称为元数据,即表示数据的数据):
In [6]: arr1 = np.array([1,2,3],dtype=np.float64)
In [7]: arr2 = np.array([1,2,3],dtype=np.int32)
In [8]: arr1.dtype
Out[8]: dtype('float64')
In [9]: arr2.dtype
Out[9]: dtype('int32')
dtype是numpy能够与其他系统数据灵活交互的原因。通常,其他系统提供一个硬盘或内存与数据的对应关系。使得利用C或fortran等底层语言读写数据变得十分方便。数据的dtype通常都是按照一个方式命名:类型名,比如float和int,后面在街上表明每个元素位数的数字。一个标准的双精度浮点值(即浮点数float),将使用8字节或64位。因此,这个类型在numpy中成为float64。
你可以使用astype方法显式地转换数组的数据类型:
In [10]: arr = np.array([1,2,3,4,5])
In [11]: arr.dtype
Out[11]: dtype('int64')
In [12]: float_atr = arr.astype(np.float64)
In [13]: float_arr.dtype
In [14]: float_atr.dtype
Out[14]: dtype('float64')
在上面例子中,整数倍转换成了浮点数。如果我把浮点数转换成整数,则小数点后面的部分将被消除。
In [15]: arr = np.array([1.2,2.3,3.4,4.5,5.6])
In [16]: arr.dtype
Out[16]: dtype('float64')
In [17]: arr.astype(np.int32)
Out[17]: array([1, 2, 3, 4, 5], dtype=int32)
如果你有一个数组,里面的元素都是表达数字含义的字符串,也可以通过astype将字符串胡在哪换位数字:
In [20]: a = np.array(['1.1','1.2','1.3'],dtype=np.string_)
In [21]: a.astype(float)
Out[21]: array([1.1, 1.2, 1.3])
在numpy中,当使用numpy.string_类型做字符串数据要小心,因为numpy会修正它的大小或删除输入且不发出警告。pandas在处理非数值数据时有更直观的开箱型操作。
如果因为某些原因导致转换类型失败(比如字符串无法转换为float64位时),将会跑出一个valueerror。这里我偷懒地使用float来代替np.float64,是因为numpy可以使用相同别名来表征与Python精度相同的Python数据类型。
In [26]: a = np.arange(10)
In [27]: b = np.array([.22,.270,.357,.380],dtype=np.float64)
In [28]: a.astype(b.dtype)
Out[28]: array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
In [29]: a.dtype
Out[29]: dtype('int64')
也可以使用类型代码来传入数据类型:
In [30]: a = np.empty(8,dtype="u4")
In [31]: a.dtype
Out[31]: dtype('uint32')
In [32]: a
Out[32]:
array([ 0, 4026531840, 3785738036, 268437501, 2,
0, 8, 131072], dtype=uint32)
4.1.3 numpy数组算数
数组之所以重要是因为它允许你进行批量操作而无需任何for循环。numpy用户称这种特性为向量化。任何在两个等尺寸数组之间的算数操作都应用了逐元素操作的方式:
In [33]: arr = np.array([[1.,2.,3.],[4.,5.,6.,]])
In [34]: arr
Out[34]:
array([[1., 2., 3.],
[4., 5., 6.]])
In [35]: arr * arr
Out[35]:
array([[ 1., 4., 9.],
[16., 25., 36.]])
In [36]: arr - arr
Out[36]:
array([[0., 0., 0.],
[0., 0., 0.]])
带有标量计算的算数操作,会把计算参数传递给数组的每一个元素:
In [37]: 1 / arr
Out[37]:
array([[1. , 0.5 , 0.33333333],
[0.25 , 0.2 , 0.16666667]])
同尺寸数组之间的比较,会产生一个布尔值数组:
In [38]: arr2 = np.array([[0.,4.,1.,],[7.,2.,12.]])
In [39]: arr2
Out[39]:
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
In [40]: arr2 > arr1
Out[40]:
array([[False, True, False],
[ True, False, True]])
4.1.4 基础索引与切片
Numpy数组索引是一个大话题,有很多种方式可以让你选中数据的子集或某个单个元素。以为数组比较简单,看起来和Python的列表很类似:
In [53]: arr = np.arange(10)
In [43]: arr
Out[43]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [54]: arr[5:8]
Out[54]: array([5, 6, 7])
In [55]: arr[5]
Out[55]: 5
In [56]: arr[-1:] = 12
In [57]: arr
Out[57]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 12])
如你所见,如果你传入了一个数值给数组的切片,例如arr[5:8]=12,数值被传递给了整个切片。区别于Python的内建列表,数组的切片是原数组的视图。这意味着数据并不是被复制了,任何对于视图的修改都会反映到原数组上。
Python内建列表:
In [92]: a =a [0, 1, 2, 3, 4, 5, 6, 7, 13, 13]
In [93]: c = a[3:6]
In [94]: c.append(250)
In [95]: a
Out[95]: [0, 1, 2, 3, 4, 5, 6, 7, 13, 13]
In [96]: c
Out[96]: [3, 4, 5, 250]
数组切片:
In [98]: a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 13, 13])
In [99]: b = a[3:6]
In [100]: b
Out[100]: array([3, 4, 5])
In [101]: b[1] = 666
In [102]: b
Out[102]: array([ 3, 666, 5])
In [103]: a
Out[103]: array([ 0, 1, 2, 3, 666, 5, 6, 7, 13, 13])
假如你是numpy新手,你可能会感到惊讶,因为其他的数组编程语言都是更为急切地复制数据。由于Numpy被设计成适合处理非常大的数组,你可以想象如果numpy持续复制数据会引起多少内存问题。
如果你还是想要一份数组切片的拷贝而不是视图的话,你就必须显式地复制这个数组,例如arr[5:8].copy()
对更高维度的数组,你会有更多选择。在一个二维数组中,每个索引值对应的元素不再是一个值,而是一个一维数组:
In [105]: arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
In [106]: arr2d[2]
Out[106]: array([7, 8, 9])
因此,单个元素可以通过递归的方式获得。但是要多写点代码,你可以通过传递一个索引的逗号分隔列表去选择单个元素,以下两种方式效果一样:
In [109]: arr2d[1][1]
Out[109]: 5
In [110]: arr2d[1,1]
Out[110]: 5
在多维数组中,你可以省略后续索引值,返回的对象将是降低一个维度的数组。因此在一个2X2X3的数组arr3d中:
In [117]: arr3d = np.array([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]])
In [118]: arr3d[0]
Out[118]:
array([[1, 2, 3],
[4, 5, 6]])
标量和数组都可以传递给arr3d[0]:
In [10]: arr3d = np.array([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]])
In [11]:
In [11]: old = arr3d[0].copy()
In [12]: arr3d[0] = 42
In [13]: arr3d
Out[13]:
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [14]: arr3d[0] = old
In [15]: arr3d
Out[15]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
类似的,arr3d[1,0]返回的是一个一维数组:
In [16]: arr3d[1,0]
Out[16]: array([7, 8, 9])
上面的表达式可以分解为下面两步:
x = arr3d[1]
x[0]
需要注意的是,以上的数组子集选择中,返回的数组都是视图。
4.1.4.1 数组的切片索引
与Python列表的一维对象类似,数组可以通过类似的语法进行切片:
In [18]: arr = np.array([0,1,2,3,4,5,6,7,8,9])
In [19]: arr
Out[19]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [20]: arr[1:6]
Out[20]: array([1, 2, 3, 4, 5])
再回想下前面的二维数组,arr2d,对数组进行切片略有不同:
In [21]: arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
In [22]: arr2d[:2]
Out[22]:
array([[1, 2, 3],
[4, 5, 6]])
如你所见,数组沿着轴0进行了切片。表达式arrzd[:2]的含义为选择arr2d的前两行。
你可以进行多组切片,与多组索引类似:
In [23]: arr2d[:2,1:]
Out[23]:
array([[2, 3],
[5, 6]])
这里如果理解不好,可能会有点乱。简单来说,:2和1:是分开进行的,先进行arr2d[:2],这样可以得到[[1,2,3,],[4,5,6]]两组数据。然后进行arr2d[1:],只取1和2位置上的数据。
需要注明的是:[:3]是不包含位置3的,[3:]是包含位置3的。
例如:
a = [0, 1, 2, 3, 4, 5, 6, 7]
c = a[3:]
print(c)
c = a[:3]
print(c)
当你像上面这个例子中那样切片时,你需要按照原数组的维度进行切片。如果将索引和切片混合,就可以得到低纬度的切片。
In [25]: arr2d[1,:2]
Out[25]: array([4, 5])
类似地,我也可以选择第三列,但是只选择前两行:
In [26]: arr2d[:2,2]
Out[26]: array([3, 6])
需要注意的是,单独一个冒号表示选择整个轴上的数组,因此你可以按照下面的方式在更高维度上进行切片:
In [27]: arr2d[:,:1]
Out[27]:
array([[1],
[4],
[7]])
当然对切片表达式赋值时,整个切片都会重新赋值:
In [28]: arr2d[:2,1] = 0
In [29]: arr2d
Out[29]:
array([[1, 0, 3],
[4, 0, 6],
[7, 8, 9]])
做数组切片,非常考验大脑的构造能力,最好是能在脑海里建一个N维图标。特别上升到3维数组以上时,纯看数字实在是很容易晕。
4.1.5 布尔索引
让我们考虑以下例子,假设我们的数据都在数组中,并且数组中的数据是一些存在重复的人名。我会使用numpy.random中的randn函数来生成一些随机正态分布的数据:
In [31]: name = np.array(['xinyuanjieyi','quanxiaosheng','piaoxiaomin','piaozhiyan','jinzhenxi','quanxiaosheng','piaozhiyan'])
In [32]: data = np.random.randn(7,4)
In [33]: name
Out[33]:
array(['xinyuanjieyi', 'quanxiaosheng', 'piaoxiaomin', 'piaozhiyan',
'jinzhenxi', 'quanxiaosheng', 'piaozhiyan'], dtype='<U13')
In [34]: data
Out[34]:
array([[ 1.6315863 , 0.50699035, 0.51158519, 0.48401104],
[ 1.12748399, 1.05808631, 0.13778461, -1.858049 ],
[ 0.03941804, -0.19496509, -0.30485908, 1.61633262],
[ 1.40152675, -1.35439393, 2.09354603, 0.09019296],
[-1.05720089, 0.84242955, -0.42479353, -0.72882557],
[ 0.02676118, -1.80843011, -1.51472835, -2.61188652],
[-0.79323013, 0.5822709 , 0.06517463, -1.34054588]])
假设每个人名都喝data数组中的一行相对应,并且我们想要选中所有‘piaozhiyan’对应的行。与数学操作类似,数组的比较操作(比如==)也是可以向量化的。因此,比较name数组和字符串‘piaozhiyan’会产生一个布尔值数组:
In [39]: name == 'piaozhiyan'
Out[39]: array([False, False, False, True, False, False, True])
在索引数组时可以传入布尔值数组:
In [41]: data[name == 'piaozhiyan']
Out[41]:
array([[ 1.40152675, -1.35439393, 2.09354603, 0.09019296],
[-0.79323013, 0.5822709 , 0.06517463, -1.34054588]])
布尔值数组的长度必须和数组轴索引长度一致。你甚至还可以用切片或整数值对布尔值数组进行混合好匹配。
当布尔值数组的长度不正确时,布尔值选择数据的方法并不会报错,因此,我建议在使用该特性的时候要小心。
在这些例子中,我选择了name == ‘piaozhiyan’的行,并索引了各个列:
In [42]: data[name == 'piaozhiyan',2:]
Out[42]:
array([[ 2.09354603, 0.09019296],
[ 0.06517463, -1.34054588]])
In [43]: data[name == 'piaozhiyan',3]
Out[43]: array([ 0.09019296, -1.34054588])
为了选择除了‘piaozhiyan’以外的其他数据,你可以使用!=或在条件表达式前使用~对条件取反:
In [45]: name != 'piaozhiyan'
Out[45]: array([ True, True, True, False, True, True, False])
In [46]: data[~(name == 'piaozhiyan')]
Out[46]:
array([[ 1.6315863 , 0.50699035, 0.51158519, 0.48401104],
[ 1.12748399, 1.05808631, 0.13778461, -1.858049 ],
[ 0.03941804, -0.19496509, -0.30485908, 1.61633262],
[-1.05720089, 0.84242955, -0.42479353, -0.72882557],
[ 0.02676118, -1.80843011, -1.51472835, -2.61188652]])
~符号可以在你想要对一个通用条件进行取反时使用:
In [47]: cond = name == 'piaozhiyan'
In [48]: data[~cond]
Out[48]:
array([[ 1.6315863 , 0.50699035, 0.51158519, 0.48401104],
[ 1.12748399, 1.05808631, 0.13778461, -1.858049 ],
[ 0.03941804, -0.19496509, -0.30485908, 1.61633262],
[-1.05720089, 0.84242955, -0.42479353, -0.72882557],
[ 0.02676118, -1.80843011, -1.51472835, -2.61188652]])
当要选择的三个名字中的两个来组合多个布尔值条件时,需要使用布尔算数运算符,如&和|:
In [49]: mask = (name == 'piaozhiyan') | (name == 'jinzhenxi ')
In [50]: mask
Out[50]: array([False, False, False, True, False, False, True])
In [51]: data[mask]
Out[51]:
array([[ 1.40152675, -1.35439393, 2.09354603, 0.09019296],
[-0.79323013, 0.5822709 , 0.06517463, -1.34054588]])
python的关键字and和or对布尔值数组并没有用,请使用&(and)和|(or)代替。
基于常识来设置布尔值数组的值也是可行的。将data中所有的负值设置为0,我们要做:
In [53]: data
Out[53]:
array([[1.6315863 , 0.50699035, 0.51158519, 0.48401104],
[1.12748399, 1.05808631, 0.13778461, 0. ],
[0.03941804, 0. , 0. , 1.61633262],
[1.40152675, 0. , 2.09354603, 0.09019296],
[0. , 0.84242955, 0. , 0. ],
[0.02676118, 0. , 0. , 0. ],
[0. , 0.5822709 , 0.06517463, 0. ]])
利用一维布尔值数组对每一行或每一列设置数值也是非常简单的:
In [54]: data[name != 'quanxiaosheng'] = 7
In [55]: data
Out[55]:
array([[7. , 7. , 7. , 7. ],
[1.12748399, 1.05808631, 0.13778461, 0. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[0.02676118, 0. , 0. , 0. ],
[7. , 7. , 7. , 7. ]])
这块书中加入了random.randn之后看的异常复杂,都不知道是在讲什么。我这里大概改一下,首先先把data改成0-6的整数(包含0),因为name一共6个数据。
然后再使用data[name == ‘piaozhiyan’],就会得到array([3, 6])。意思就是在name的位置3和位置6的地方,有符合布尔值True的的数据。
4.1.6 神奇索引
神奇索引是numpy中的术语,用于描述使用整数数组进行数据索引。
假设我们有一个8 X 4的数组。
In [70]: arr = np.empty((8,4))
In [71]: for i in range(8):
...: arr[i] = i
...:
In [72]: arr
Out[72]:
array([[0., 0., 0., 0.],
[1., 1., 1., 1.],
[2., 2., 2., 2.],
[3., 3., 3., 3.],
[4., 4., 4., 4.],
[5., 5., 5., 5.],
[6., 6., 6., 6.],
[7., 7., 7., 7.]])
为了选出一个符合特定顺序的子集,你可以简单地通过传递一个包含指明所需顺序的列表或数组来完成:
In [73]: arr[[1,3,5]]
Out[73]:
array([[1., 1., 1., 1.],
[3., 3., 3., 3.],
[5., 5., 5., 5.]])
如果使用负的索引,将从尾部进行选择:
In [74]: arr[[-1,-3,-5]]
Out[74]:
array([[7., 7., 7., 7.],
[5., 5., 5., 5.],
[3., 3., 3., 3.]])
传递多个索引数组时情况有些许不同,这样会根据每个索引元组对应的元素选出一个一维数组:
In [75]: arr = np.arange(32).reshape((8,4))
In [76]: arr
Out[76]:
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]])
In [77]: arr[[1,5,7,2],[0,3,1,2]]
Out[77]: array([ 4, 23, 29, 10])
这里也比较乱,首先np.arange(32).reshape((8,4))的意思是从生成一个从0到31的数字,reshape设定这些数字分成每4个数字为一个列表,一共8个列表的数组(或者说是8行4列)。
arr[[1,5,7,2],[0,3,1,2]]:前面的[1,5,7,2]是行,后面的[0,3,1,2]是索引值。例如第一个值4,源自arr的[1]行的第[0]个。
注意,这里的行和列都有0行0列的概念(也就是第一行及第一列),所以算的时候要从0开始数。
在本例中,神奇索引的行为和一些用户所设想的并不相同。通常情况下,我们所摄像的结果是通过选择矩阵中行列的子集所形成的矩形区域。下面是实现我们想法的一种方式:
In [78]: arr[[1,5,7,2]][:,[0,3,1,2]]
Out[78]:
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
请牢记神奇索引与切片不同,它总是将数据复制到一个新的数组中。
这个也不是很好理解,但实际上跟上面的逻辑是一样的,只不过变得更复杂了点。我们先拆一下:
In [79]: arr[:,[0,3,1,2]]
Out[79]:
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]])
arr[:]就是指0-31,8X4的整个数组。后面的是索引值[0,3,1,2]。因为是完整的整个数组,所以一开始就是从0行开始。0行的[0]就是0,0行的[3]就是3,0行的[1]就是1,0行的[2]就是2。所以得出第一行的数据[0,3,1,2]。第二行、第三行及后面的都是以此类推,一直到8行结束。
把这个值当做索引,再去从arr的[1]\[5]\[7]\[2]行找对应的数字。
arr[[1,5,7,2]]的数组:
array([[ 4, 5, 6, 7],
[20, 21, 22, 23],
[28, 29, 30, 31],
[ 8, 9, 10, 11]])
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,5,6,7]数值的[0,3,1,2]索引是[4,7,5,6]
[20,21,22,23]数值的[4,7,5,6]索引是[20,23,21,22]
[28,29,30,31]数值的[8,11,9,10]索引是[28,31,29,30]
后面的索引的逻辑是:当前面索引完结后,重复数值进入第二轮索引。比如索引是5,值[4,5,6,7]是0,1,2,3四个索引。当数值完结后,进入第二轮[4,5,6,7]的索引,即4,5,6,7四个索引值,原来的索引是5,这里就对应了5这个值。如果是索引是12,那就以此类推,0,1,2,3,4,5,6,7,8,9,10,11,12,这里的12就是4。
4.1.7 数组转置和换轴
转置是一种特殊的数据重组形式,可以返回底层数据的视图而不需要复制任何内容。数组拥有transpose方法,也有特殊的T属性。
In [83]: arr = np.arange(15).reshape((3,5))
In [84]: arr
Out[84]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [85]: arr.T
Out[85]:
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
不得不吐槽一下,书上真的是太粗略了,T属性是啥意思也不说。基本只能靠百度或者靠猜。
T属性就是转置的意思,从3行5列,变成5行3列,原来的横向数据变成了竖向数据。
当进行矩阵计算时,你可能会经常进行一些特定操作,比如,当计算矩阵内积会使用np.dot:
In [83]: arr = np.arange(15).reshape((3,5))
In [84]: arr
Out[84]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [85]: arr.T
Out[85]:
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
In [86]: old = arr.copy()
In [87]: np.dot(arr.T,arr)
Out[87]:
array([[125, 140, 155, 170, 185],
[140, 158, 176, 194, 212],
[155, 176, 197, 218, 239],
[170, 194, 218, 242, 266],
[185, 212, 239, 266, 293]])
矩阵内积就是一行与一列对应相乘,加和。属于线性代数范畴,看不懂,再见~
对于更高维度的数组,transpose方法可以接收包含轴编号的元组,用于置换轴:
In [88]: arr = np.arange(16).reshape((2,2,4))
In [89]: arr
Out[89]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [90]: arr.transpose((1,0,2))
Out[90]:
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
在这里,轴已经被重新排序,使得原来的第二个轴变成第一个,原来的第一个轴变成了第二个。最后一个轴没有改变。
使用.T进行转置是换轴的一个特殊案例,ndarry有一个swapaxes方法,该方法接受一对轴编号作为参数,并对轴进行调整用于重组数据:
In [96]: arr.swapaxes(1,2)
Out[96]:
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])
这里的核心就是换轴,怎么理解呢。轴是固定的,0轴(可以理解为X)轴,1轴可以理解为Y轴,2轴可以理解为Z轴。
如arr里的4,坐标位置是(0,1)(第一个列表的第二个位置),那么0轴和1轴互换之后,变成了(1,0)(也就是第二个位置的第0个位置。)
In [89]: arr
Out[89]:
array([[[ 0, 1, 2, 3],
[ 4(我的位置是[0][1]), 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [90]: arr.transpose((1,0,2))
Out[90]:
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4(我的位置是[1][0]), 5, 6, 7],
[12, 13, 14, 15]]])
这个是真的不太好理解(矩形内积就更别提了)。
如果还有不懂的,建议参考以下这篇文章:
胭惜雨
2021年03月23日