返回介绍

4.1 The NumPy ndarray 多维数组对象

发布于 2023-07-20 22:13:11 字数 17224 浏览 0 评论 0 收藏 0

NumPy Basics: Arrays and Vectorized Computation

在数值计算领域,说 Numpy 是 python 最重要的包也不为过。在 numpy 中有下面这些东西:

  • ndarray, 一个有效的多维数组,能提供以数组为导向的快速数值计算和灵活的广播功能(broadcasting)
  • 便利的数学函数
  • 用于读取/写入(reading/writing)数据到磁盘的便利工具
  • 线性代数,随机数生成,傅里叶变换能力
  • 可以用 C API 来写 C,C++,或 FORTRAN

通过学习理解 numpy 中数组和数组导向计算,能帮我们理解 pandas 之类的工具。

4.1 The NumPy ndarray: A Multidimensional Array Object(ndarray: 多维数组对象)

N-dimensional array object(n 维数组对象), or ndarray,这是 numpy 的关键特征。先来尝试一下,生成一个随机数组:

import numpy as np
# Generate some random data
data = np.random.randn(2, 3)
data
array([[-0.35512366, -0.63779545,  0.14137933],
       [ 0.36642056,  0.30898139, -0.87040292]])

进行一些数学运算:

data * 10
array([[-3.55123655, -6.37795453,  1.41379333],
       [ 3.66420556,  3.0898139 , -8.70402916]])
data + data
array([[-0.71024731, -1.27559091,  0.28275867],
       [ 0.73284111,  0.61796278, -1.74080583]])

每一个数组都有一个 shape,来表示维度大小。而 dtype,用来表示 data type:

data.shape
(2, 3)
data.dtype
dtype('float64')

1 Greating ndarrays (创建 n 维数组)

最简单的方法使用 array 函数,输入一个序列即可,比如 list:

data1 = [6, 7.5, 8, 0, 1]
arr1 = np.array(data1)
arr1
array([ 6. ,  7.5,  8. ,  0. ,  1. ])

嵌套序列能被转换为多维数组:

data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
arr2 = np.array(data2)
arr2
array([[1, 2, 3, 4],
       [5, 6, 7, 8]])

因为 data2 是一个 list of lists, 所以 arr2 维度为 2。我们能用 ndim 和 shape 属性来确认一下:

arr2.ndim
2
arr2.shape
(2, 4)

除非主动声明,否则 np.array 会自动给 data 搭配适合的类型,并保存在 dtype 里:

arr1.dtype
dtype('float64')
arr2.dtype
dtype('int64')

除了 np.array,还有一些其他函数能创建数组。比如 zeros,ones,另外还可以在一个 tuple 里指定 shape:

np.zeros(10)
array([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.])
np.zeros((3, 6))
array([[ 0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.]])
np.empty((2, 3, 2))
array([[[  0.00000000e+000,   0.00000000e+000],
        [  2.16538378e-314,   2.16514681e-314],
        [  2.16511832e-314,   2.16072529e-314]],

       [[  0.00000000e+000,   0.00000000e+000],
        [  2.14037397e-314,   6.36598737e-311],
        [  0.00000000e+000,   0.00000000e+000]]])

np.empty 并不能保证返回所有是 0 的数组,某些情况下,会返回为初始化的垃圾数值,比如上面。

arange 是一个数组版的 python range 函数:

np.arange(15)
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

这里是一些创建数组的函数:

2 Data Types for ndarrays

dtype 保存数据的类型:

arr1 = np.array([1, 2, 3], dtype=np.float64)
arr2 = np.array([1, 2, 3], dtype=np.int32)
arr1.dtype
dtype('float64')
arr2.dtype
dtype('int32')

dtype 才是 numpy 能灵活处理其他外界数据的原因。

类型表格:

可以用 astype 来转换类型:

arr = np.array([1, 2, 3, 4, 5])
arr.dtype
dtype('int64')
float_arr = arr.astype(np.float64)
float_arr.dtype
dtype('float64')

上面是把 int 变为 float。如果是把 float 变为 int,小数点后的部分会被丢弃:

arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
arr
array([  3.7,  -1.2,  -2.6,   0.5,  12.9,  10.1])
arr.astype(np.int32)
array([ 3, -1, -2,  0, 12, 10], dtype=int32)

还可以用 astype 把 string 里的数字变为实际的数字:

numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)
numeric_strings
array([b'1.25', b'-9.6', b'42'], 
      dtype='|S4')
numeric_strings.astype(float)
array([  1.25,  -9.6 ,  42.  ])

要十分注意 numpy.string_ 类型,这种类型的长度是固定的,所以可能会直接截取部分输入而不给警告。

如果转换(casting)失败的话,会给出一个 ValueError 提示。

可以用其他数组的 dtype 直接来制定类型:

int_array = np.arange(10)

calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
int_array.astype(calibers.dtype)
array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9.])

还可以利用类型的缩写,比如 u4 就代表 unit32:

empty_unit32 = np.empty(8, dtype='u4')
empty_unit32
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=uint32)

记住,astype 总是会返回一个新的数组

3 Arithmetic with NumPy Arrays(数组计算)

数组之所以重要,是因为不用写 for 循环就能表达很多操作,这种特性叫做 vectorization(向量化)。任何两个大小相等的数组之间的运算,都是 element-wise(点对点):

arr = np.array([[1., 2., 3.], [4., 5., 6.]])
arr
array([[ 1.,  2.,  3.],
       [ 4.,  5.,  6.]])
arr * arr
array([[  1.,   4.,   9.],
       [ 16.,  25.,  36.]])
arr - arr
array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.]])

element-wise 我翻译为点对点,就是指两个数组的运算,在同一位置的元素间才会进行运算。

这种算数操作如果涉及标量(scalar)的话,会涉及到数组的每一个元素:

1 / arr
array([[ 1.        ,  0.5       ,  0.33333333],
       [ 0.25      ,  0.2       ,  0.16666667]])
arr ** 0.5
array([[ 1.        ,  1.41421356,  1.73205081],
       [ 2.        ,  2.23606798,  2.44948974]])

两个数组的比较会产生布尔数组:

arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
arr2
array([[  0.,   4.,   1.],
       [  7.,   2.,  12.]])
arr2 > arr
array([[False,  True, False],
       [ True, False,  True]], dtype=bool)

4 Basic Indexing and Slicing(基本的索引和切片)

一维的我们之前已经在 list 部分用过了,没什么不同:

arr = np.arange(10)
arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arr[5]
5
arr[5:8]
array([5, 6, 7])
arr[5:8] = 12
arr
array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

这里把 12 赋给 arr[5:8] ,其实用到了 broadcasted(我觉得应该翻译为广式转变)。这里有一个比较重要的概念需要区分,python 内建的 list 与 numpy 的 array 有个明显的区别,这里 array 的切片后的结果只是一个 views(视图),用来代表原有 array 对应的元素,而不是创建了一个新的 array。但 list 里的切片是产生了一个新的 list:

arr_slice = arr[5:8]
arr_slice
array([12, 12, 12])

如果我们改变 arr_slice 的值,会反映在原始的数组 arr 上:

arr_slice[1] = 12345
arr
array([    0,     1,     2,     3,     4,    12, 12345,    12,     8,     9])

[:] 这个赋值给所有元素:

arr_slice[:] = 64
arr
array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])

之所以这样设计是出于性能和内存的考虑,毕竟如果总是复制数据的话,会很影响运算时间。当然如果想要复制,可以使用 copy()方法,比如 arr[5:8].copy()

在一个二维数组里,单一的索引指代的是一维的数组:

arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d[2]
array([7, 8, 9])

有两种方式可以访问单一元素:

arr2d[0][2]
3
arr2d[0, 2]
3

我们可以把下图中的 axis0 看做 row(行),把 axis1 看做 column(列):

对于多维数组,如果省略后面的索引,返回的将是一个低纬度的多维数组。比如下面一个 2 x 2 x 3 数组:

arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
arr3d
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

arr3d[0]是一个 2x3 数组:

arr3d[0]
array([[1, 2, 3],
       [4, 5, 6]])

标量和数组都能赋给 arr3d[0]:

old_values = arr3d[0].copy()

arr3d[0] = 42

arr3d
array([[[42, 42, 42],
        [42, 42, 42]],

       [[ 7,  8,  9],
        [10, 11, 12]]])
arr3d[0] = old_values
arr3d
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

arr3d[1, 0] 会给你一个(1, 0)的一维数组:

arr3d[1, 0]
array([7, 8, 9])

上面的一步等于下面的两步:

x = arr3d[1]
x
array([[ 7,  8,  9],
       [10, 11, 12]])
x[0]
array([7, 8, 9])

一定要牢记这些切片后返回的数组都是 views

Indexing with slices(用切片索引)

一维的话和 python 里的 list 没什么差别:

arr
array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])
arr[1:6]
array([ 1,  2,  3,  4, 64])

二维的话,数组的切片有点不同:

arr2d[:2]
array([[1, 2, 3],
       [4, 5, 6]])

可以看到,切片是沿着 axis 0(行)来处理的。所以,数组中的切片,是要沿着设置的 axis 来处理的。我们可以把 arr2d[:2]理解为“选中 arr2d 的前两行”。

当然,给定多个索引后,也可以使用复数切片:

arr2d
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
arr2d[:2, 1:] # 前两行,第二列之后
array([[2, 3],
       [5, 6]])

记住,选中的是 array view。通过混合整数和切片,能做低维切片。比如,我们选中第二行的前两列:

arr2d[1, :2]
array([4, 5])

选中第三列的前两行:

arr2d[:2, 2]
array([3, 6])

冒号表示提取整个 axis(轴):

arr2d[:, :1]
array([[1],
       [4],
       [7]])

看图示有助于理解:

赋值也很方便:

arr2d[:2, 1:] = 0
arr2d
array([[1, 0, 0],
       [4, 0, 0],
       [7, 8, 9]])

5 Boolean Indexing (布尔索引)

假设我们的数组数据里有一些重复。这里我们用 numpy.random 里的 randn 函数来随机生成一些离散数据:

names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
names
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], 
      dtype='<U4')
data = np.random.randn(7, 4)
data
array([[ 0.02584271, -1.53529621,  0.73143988, -0.34086189],
       [ 0.40864782,  0.53476799,  1.09620596,  0.4846564 ],
       [ 1.95024076, -0.37291038, -0.40424703,  0.30297059],
       [-0.48632936,  0.63817756, -0.40792716, -1.48037389],
       [-0.81976335, -1.10162466, -0.59823212, -0.10926744],
       [-0.5212113 ,  0.29449179,  2.0568032 ,  2.00515735],
       [-2.36066876, -0.3294302 , -0.24464646, -0.81432884]])

假设每一个 name 对应 data 数组中的一行,我们想要选中 name 为'Bob'的所有行。就像四则运算,用比较运算符(==):

names == 'Bob'
array([ True, False, False,  True, False, False, False], dtype=bool)

然后用这个布尔数组当做索引:

data[names == 'Bob']
array([[ 0.02584271, -1.53529621,  0.73143988, -0.34086189],
       [-0.48632936,  0.63817756, -0.40792716, -1.48037389]])

注意:布尔数组和 data 数组的长度要一样。

我们可以选中 names=='Bob'的行,然后索引列:

data[names == 'Bob', 2:]
array([[ 0.73143988, -0.34086189],
       [-0.40792716, -1.48037389]])
data[names == 'Bob', 3]
array([-0.34086189, -1.48037389])

选中除了'Bob'外的所有行,可以用 != 或者 ~

names != 'Bob'
array([False,  True,  True, False,  True,  True,  True], dtype=bool)
data[~(names == 'Bob')]
array([[ 0.40864782,  0.53476799,  1.09620596,  0.4846564 ],
       [ 1.95024076, -0.37291038, -0.40424703,  0.30297059],
       [-0.81976335, -1.10162466, -0.59823212, -0.10926744],
       [-0.5212113 ,  0.29449179,  2.0568032 ,  2.00515735],
       [-2.36066876, -0.3294302 , -0.24464646, -0.81432884]])

当想要反转一个条件时,用 ~ 操作符很方便:

cond = names == 'Bob'
data[~cond]
array([[ 0.40864782,  0.53476799,  1.09620596,  0.4846564 ],
       [ 1.95024076, -0.37291038, -0.40424703,  0.30297059],
       [-0.81976335, -1.10162466, -0.59823212, -0.10926744],
       [-0.5212113 ,  0.29449179,  2.0568032 ,  2.00515735],
       [-2.36066876, -0.3294302 , -0.24464646, -0.81432884]])

选中 2 个或 3 个名字,组合多个布尔条件,用布尔运算符&,|,另外 python 中的关键词 and 和 or 不管用:

names
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], 
      dtype='<U4')
mask = (names == 'Bob') | (names == 'Will')
mask
array([ True, False,  True,  True,  True, False, False], dtype=bool)
data[mask]
array([[ 0.02584271, -1.53529621,  0.73143988, -0.34086189],
       [ 1.95024076, -0.37291038, -0.40424703,  0.30297059],
       [-0.48632936,  0.63817756, -0.40792716, -1.48037389],
       [-0.81976335, -1.10162466, -0.59823212, -0.10926744]])

用布尔索引总是会返回一份新创建的数据,原本的数据不会被改变。

更改值的方式也很直觉。比如我们想让所有负数变为 0:

data[data < 0] = 0
data
array([[ 0.02584271,  0.        ,  0.73143988,  0.        ],
       [ 0.40864782,  0.53476799,  1.09620596,  0.4846564 ],
       [ 1.95024076,  0.        ,  0.        ,  0.30297059],
       [ 0.        ,  0.63817756,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.29449179,  2.0568032 ,  2.00515735],
       [ 0.        ,  0.        ,  0.        ,  0.        ]])

用一维的布尔数组也能更改所有行或列:

names
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], 
      dtype='<U4')
data[names != 'Joe'] = 7
data
array([[ 7.        ,  7.        ,  7.        ,  7.        ],
       [ 0.40864782,  0.53476799,  1.09620596,  0.4846564 ],
       [ 7.        ,  7.        ,  7.        ,  7.        ],
       [ 7.        ,  7.        ,  7.        ,  7.        ],
       [ 7.        ,  7.        ,  7.        ,  7.        ],
       [ 0.        ,  0.29449179,  2.0568032 ,  2.00515735],
       [ 0.        ,  0.        ,  0.        ,  0.        ]])

6 Fancy Indexing(花式索引)

通过整数数组来索引。假设我们有一个 8 x 4 的数组:

arr = np.empty((8, 4))
for i in range(8):
    arr[i] = i
arr
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.]])

想要按一定顺序选出几行,可以用一个整数 list 或整数 ndarray 来指定顺序:

arr[[4, 3, 0, 6]]
array([[ 4.,  4.,  4.,  4.],
       [ 3.,  3.,  3.,  3.],
       [ 0.,  0.,  0.,  0.],
       [ 6.,  6.,  6.,  6.]])

用符号来从后选择 row:

arr[[-3, -5, -7]]
array([[ 5.,  5.,  5.,  5.],
       [ 3.,  3.,  3.,  3.],
       [ 1.,  1.,  1.,  1.]])

用多维索引数组,能选出由一维数组中的元素,通过在每个 tuple 中指定索引:

arr = np.arange(32).reshape((8, 4))
arr
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]])
arr[[1, 5, 7, 2], [0, 3, 1, 2]]
array([ 4, 23, 29, 10])

可以看到 [ 4, 23, 29, 10] 分别对应 (1, 0), (5, 3), (7, 1), (2, 2) 。不论数组有多少维,fancy indexing 的结果总是一维。

对于长方形区域,有下面的方法来截取:

arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
array([[ 4,  7,  5,  6],
       [20, 23, 21, 22],
       [28, 31, 29, 30],
       [ 8, 11,  9, 10]])

上面的意思是,先从 arr 中选出[1, 5, 7, 2]这四行:

array([[ 4,  5,  6,  7],
       [20, 21, 22, 23],
       [28, 29, 30, 31],
       [ 8,  9, 10, 11]])
       

然后[:, [0, 3, 1, 2]]表示选中所有行,但是列的顺序要按 0,3,1,2 来排。于是得到:

array([[ 4,  7,  5,  6],
       [20, 23, 21, 22],
       [28, 31, 29, 30],
       [ 8, 11,  9, 10]])
       

要记住,fancy indexing 和切片不同,得到的是一个新的 array。

7 Transposing Arrays and Swapping Axes(数组转置和轴交换)

转置也是返回一个 view,而不是新建一个数组。有两种方式,一个是 transpose 方法,一个是 T 属性:

arr = np.arange(15).reshape((3, 5))
arr
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])
arr.T
array([[ 0,  5, 10],
       [ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14]])

做矩阵计算的时候,这个功能很常用,计算矩阵乘法的时候,用 np.dot:

arr = np.arange(8).reshape((4, 2))
print(arr.T)
print(arr)
[[0 2 4 6]
 [1 3 5 7]]
[[0 1]
 [2 3]
 [4 5]
 [6 7]]
np.dot(arr.T, arr)
array([[56, 68],
       [68, 84]])

上面的例子是 (2x4) x (4x2) = (2x2)。得到的结果是 2x2 维,就是普通的矩阵乘法。

对于多维数组,transpose 会接受由轴数字组成的 tuple,来交换轴:

arr = np.arange(16).reshape((2, 2, 4))
arr
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7]],

       [[ 8,  9, 10, 11],
        [12, 13, 14, 15]]])
arr.transpose((1, 0, 2))
array([[[ 0,  1,  2,  3],
        [ 8,  9, 10, 11]],

       [[ 4,  5,  6,  7],
        [12, 13, 14, 15]]])

这里,secode axis(1)被设为第一个,first axis(0)第二个,最后的 axis 没边。

使用 .T 来转置 swapping axes(交换轴)的一个特殊情况。ndarray 有方法叫做 swapaxes, 这个方法取两个 axis 值,并交换这两个轴:

arr
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7]],

       [[ 8,  9, 10, 11],
        [12, 13, 14, 15]]])
arr.swapaxes(1, 2) # 直交换 second axis 和 last axis
array([[[ 0,  4],
        [ 1,  5],
        [ 2,  6],
        [ 3,  7]],

       [[ 8, 12],
        [ 9, 13],
        [10, 14],
        [11, 15]]])

swapaxes 也是返回 view,不生成新的 data。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文