ndarray in Numpy

ndarray 数组是我们用python进行科学计算时常用的数据类型,对它的深入了解是非常必要的。本文回顾之前学习的有关adarray的知识。

内存结构

ndarray 的内存结构如下图所示:

  • data:指向数组中元素的二进制数据块。
  • dtype:定义了数组中存放的对象的数据类型,通过它可以知道如何将元素的二进制数据转换为可用的值。如上图每32位表示一个有用数据
  • dim count: 表示数组维数,上图为2维数组 (对应ndarray的属性ndim)
  • dimmension: 数组的形状,3×3给出数组的(对应ndarray的属性shape)
  • strides: 保存的是当每个轴的下标增加1时,数据存储区中的指针所增加的字节数。例如图中的strides为12,4,即第0轴的下标增加1时,数据的地址增加12个字节:即a[1,0]的地址比a[0,0]的地址要高12个字节,正好是3个单精度浮点数的总字节数;第1轴下标增加1时,数据的地址增加4个字节,正好是单精度浮点数的字节数。

dtype

dtype(数据类型)是一个特殊的对象,它含有ndarray将一块内存解释为特定数据类型所需的信息。

可以通过ndarray的astype方法显式地转换其dtype。

1
2
3
4
5
6
7
8
9
>>> import numpy as np
>>> arr = np.array([1, 2, 3], dtype=np.int32)
>>> arr
array([1, 2, 3], dtype=int32)
>>> arr.dtype
dtype('int32')
>>> float_arr = arr.astype(np.float128)
>>> float_arr.dtype
dtype('float128')

numpy 有多种预定义的dtype(用的比较多的还是np.float64),需要的时候可以自行查找

还可以自定义数组的dtype。

strides再说明

有时ndarray中的数据并不一定都是连续储存的,通过下标范围得到新的数组是原始数组的视图(类似于引用),即它和原始视图共享数据存储区域, 对于新数组来说,其中的数据不一定是连续存储的。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> import numpy as np
>>> a = np.array([[0,1,2],[3,4,5],[6,7,8]], dtype=np.float32)
>>> a
array([[ 0., 1., 2.],
[ 3., 4., 5.],
[ 6., 7., 8.]], dtype=float32)
>>> a.strides
(12, 4)
>>> b = a[::2,::2]
>>> b
array([[ 0., 2.],
[ 6., 8.]], dtype=float32)
>>> b.strides
(24, 8)

由于数组b和数组a共享数据存储区,而b中的第0轴和第1轴都是数组a中隔一个元素取一个,因此数组b的strides变成了24,8,正好都是数组a的两倍。

元素在数据存储区中的排列格式有两种:C语言格式和Fortan语言格式。在C语言中,多维数组的第0轴是最上位的,即第0轴的下标增加1时,元素的地址增加的字节数最多;而Fortan语言的多维数组的第0轴是最下位的,即第0轴的下标增加1时,地址只增加一个元素的字节数。在NumPy中,元素在内存中的排列缺省是以C语言格式存储的,如果你希望改为Fortan格式的话,只需要给数组传递order=”F”参数:

1
2
3
4
5
6
>>> c = np.array([[0,1,2],[3,4,5],[6,7,8]], dtype=np.float32, order="C")
>>> c.strides
(12, 4)
>>> f = np.array([[0,1,2],[3,4,5],[6,7,8]], dtype=np.float32, order="F")
>>> f.strides
(4, 12)

ndarray的创建

创建ndarray对象的常用函数如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
np.array
np.asarray
np.arange # 通过开始值、终值和步长 创建一维数组
np.linspace # 通过开始值、终值和元素个数创建一维数组
np.logspace # 产生等比数列
np.ones
np.ones_like
np.zeros
np.zeros_like
np.empty
np.empty_like
np.eye
np.identity
np.fromfunction # 根据下标生成数组

示例代码如下:

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
>>> np.arange(0,1,0.1)
array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
>>> np.linspace(0, 1, 12)
array([ 0. , 0.09090909, 0.18181818, 0.27272727, 0.36363636,
0.45454545, 0.54545455, 0.63636364, 0.72727273, 0.81818182,
0.90909091, 1. ])
>>> np.logspace(0, 2, 20)
array([ 1. , 1.27427499, 1.62377674, 2.06913808,
2.6366509 , 3.35981829, 4.2813324 , 5.45559478,
6.95192796, 8.8586679 , 11.28837892, 14.38449888,
18.32980711, 23.35721469, 29.76351442, 37.92690191,
48.32930239, 61.58482111, 78.47599704, 100. ])

>>> def func(i):
... return i%4+1
...
>>> np.fromfunction(func, (10,))
array([ 1., 2., 3., 4., 1., 2., 3., 4., 1., 2.])

>>> def func2(i, j):
... return (i+1) * (j+1)
...
>>> np.fromfunction(func2, (4, 4))
array([[ 1., 2., 3., 4.],
[ 2., 4., 6., 8.],
[ 3., 6., 9., 12.],
[ 4., 8., 12., 16.]])

ndarray元素的索引

ndarray提供了三种索引方法。

切片索引

通过下标范围获取的新的数组是原始数组的一个视图。它与原始数组共享同一块数据空间,所以对 索引数组的修改 就是对原始数组的修改。

如果你想要得到的是ndarray切片的一份副本而非视图,就需要显式地进行复制操作,例如arr[5:8].copy()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> a = np.arange(10)
>>> a[5] # 用整数作为下标可以获取数组中的某个元素
5
>>> a[3:5] # 用范围作为下标获取数组的一个切片,包括a[3]不包括a[5]
array([3, 4])
>>> a[:5] # 省略开始下标,表示从a[0]开始
array([0, 1, 2, 3, 4])
>>> a[:-1] # 下标可以使用负数,表示从数组后往前数
array([0, 1, 2, 3, 4, 5, 6, 7, 8])
>>> a[2:4] = 100,101 # 下标还可以用来修改元素的值
>>> a
array([ 0, 1, 100, 101, 4, 5, 6, 7, 8, 9])
>>> a[1:-1:2] # 范围中的第三个参数表示步长,2表示隔一个元素取一个元素
array([ 1, 101, 5, 7])
>>> a[::-1] # 省略范围的开始下标和结束下标,步长为-1,整个数组头尾颠倒
array([ 9, 8, 7, 6, 5, 4, 101, 100, 1, 0])
>>> a[5:1:-2] # 步长为负数时,开始下标必须大于结束下标
array([ 5, 101])

花式索引(整数序列)

当使用整数序列对数组元素进行存取时,将使用整数序列中的每个元素作为下标,整数序列可以是列表或者数组。

使用整数序列作为下标获得的数组不和原始数组共享数据空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> x = np.arange(10,1,-1)
>>> x
array([10, 9, 8, 7, 6, 5, 4, 3, 2])
>>> x[[3, 3, 1, 8]] # 获取x中的下标为3, 3, 1, 8的4个元素,组成一个新的数组
array([7, 7, 9, 2])
>>> b = x[np.array([3,3,-3,8])] #下标可以是负数
>>> b[2] = 100
>>> b
array([ 7, 7, 100, 2])
>>> x # 由于b和x不共享数据空间,因此x中的值并没有改变
array([10, 9, 8, 7, 6, 5, 4, 3, 2])
>>> x[[3,5,1]] = -1, -2, -3 # 整数序列下标也可以用来修改元素的值
>>> x
array([10, -3, 8, -1, 6, -2, 4, 3, 2])

布尔索引(布尔数组 & 布尔列表)

当使用布尔数组b作为下标存取数组x中的元素时,将收集数组x中所有在数组b中对应下标为True的元素。

使用布尔数组作为下标获得的数组和原始数组共享数据空间,注意这种方式只对应于布尔数组

不能使用布尔列表, 否则会把True当作1, False当作0,按照整数序列方式获取原始中的元素,不共享内存

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> x = np.arange(5,0,-1)
>>> x
array([5, 4, 3, 2, 1])
>>> x[np.array([True, False, True, False, False])] # 布尔数组中下标为0,2的元素为True,因此获取x中下标为0,2的元素
array([5, 3])
>>> x[[True, False, True, False, False]] # 如果是布尔列表,则把True当作1, False当作0,按照整数序列方式获取x中的元素
array([5, 3])
>>> x[np.array([True, False, True, True])] # 布尔数组的长度不够时,不够的部分都当作False
__main__:1: VisibleDeprecationWarning: boolean index did not match indexed array along dimension 0; dimension is 5 but corresponding boolean dimension is 4
array([5, 3, 2])
>>> x[np.array([True, False, True, True])] = -1, -2, -3 # 布尔数组下标也可以用来修改元素
>>> x
array([-1, 4, -2, -3, 1])

布尔数组 索引可以使用逻辑运算, 分别为 |, &, ~ (或 与 非)。不能使用 and or not

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe']) # 七个人 ,有重复
>>> data = np.random.randn(7, 4) # 7个人的成绩,每行一个人
>>> data[~(names == 'Bob')] # 查看除了 Bob 之外 其他六个人的成绩 , 等价于 data[(names != 'Bob')]
array([[ 0.07820045, -1.71734855, -0.34533472, 0.13199966],
[ 1.44077541, 0.599357 , 0.02741339, -0.27952545],
[ 0.40324184, -1.14696069, -2.63616404, -0.89460574],
[-1.07763886, 1.38956886, -0.51076578, 0.22787857],
[-0.56223972, -0.4285839 , 1.00189764, -1.27962868]])
>>> data[names == 'Bob'] # 查看 Bob 的成绩
array([[-0.20510993, 1.16346024, 1.0559742 , -0.17387184],
[ 0.08864056, 0.49012893, -0.27613655, -0.89549472]])
>>> mask = (names == 'Bob') | (names == 'Will')
>>> data[mask] # 查看 Bob 和 Will 的成绩
array([[-0.20510993, 1.16346024, 1.0559742 , -0.17387184],
[ 1.44077541, 0.599357 , 0.02741339, -0.27952545],
[ 0.08864056, 0.49012893, -0.27613655, -0.89549472],
[ 0.40324184, -1.14696069, -2.63616404, -0.89460574]])

参考资料

用Python做科学计算

利用python进行数据分析