从本章开始,将正式进入本书的核心内容(其实前面的内容是面向什么都不懂的新手用户的,但是我非常怀疑,如果是真的什么都不懂的新手,是完全看不懂这本书前面内容的)。

说明一下:因为专业术语是缩写或者要求大写,但是频繁切换又比较麻烦,所以有些名词我会用小写的方式。

还有,从本章开始,我将进一步规范笔记里的格式,带来更好的阅读体验。

——————————————–

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轴。

图片转载自CSDN博客:November丶Chopin

如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]]])

这个是真的不太好理解(矩形内积就更别提了)。

如果还有不懂的,建议参考以下这篇文章:

《Python numpy.transpose 详解》

胭惜雨

2021年03月23日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据