python 数据处理基础之 numpy 的运算

上一章我们一起学习了 numpy 库的基础知识,以及 np 的主要数据结构ndarray

我们都知道,程序 = 数据结构 + 算法,这章中,我们一起学习下基于 ndarray 这个多维数组的一些基础运算。

本章所说的运算主要是 numpy 中 ndarray 的运算。在我们平常的编程工作中,数组的运算应该是用得最多的运算了,我们常常需要把数据放到数组里(也就是给数组赋值),遍历数组,对数组中数据进行处理等。在大数据处理中,数组的运算就更为重要了。下面介绍一些初级的数组运算,随着我们进一步的学习,后面会学习更多更高级运算以及结合统计学或线性代数的知识学习更多运用的场合。

学习时,可以边对照着下面的导图边学习各章内容,这样思路会更清晰。

1 导图

(建议看每一章时,先把此导图打印出来对着看,好让自己对整体内容有个大概的把握与知道自己学习到哪个位置:)我自己做此笔记也是这样,把导图都打印出来,然后对着自己的笔记回顾,看完哪里就去哪里打个勾表示复习过一遍了)

2 简单运算

2.1 算术运算

numpy 中数组的算术运算异常地强大,不信?请你想想看如果使用 py 中基本的 list,或是使用 c 语言的数组等,怎么计算两个数组元素两两相乘?你必须对它们分别遍历对吧?那么看下下面的代码:

多维数 组相乘
1
2
3
4
5
6
7
8
9
10
11
# 注意,就是普通的相乘,不是点积。
# [1,2,3] [1,2,3] [1*1, 2*2, 3*3] [ 1 4 9]
# [4,5,6] x [4,5,6] = [4*4, 5*5, 6*6] = [16 25 36]
arr = np.array([[1,2,3], [4,5,6]]) # 创建一个二维数组 2*3
arr = arr * arr
print(arr)

==> out:
[[ 1 4 9]
[16 25 36]]
----------

根本不需要遍历,就这么简单。以此类推,数组的加、减运算也是这么轻松可以完成。

2.2 逻辑运算

对 ndarray 作逻辑运算,得到的结果也是 ndarray 每个元素计算之后的结果组成的结果数组。描述运算总是太抽象,还是看看具体示例吧:

多维数组的逻辑运算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arr = np.array([[1,2,3], [4,5,6]]) # 创建一个二维数组 2*3
arr = arr > 3
print(arr)

==> out:
1,2,3 [[False False False]
> 3 -->
4,5,6 [ True True True]]
----------

arr = np.array([1,4,8])
arr2 = np.array([5,3,7])
print(arr < arr2) # 这样也是两两元素进行对比,输出结果
==> out:
[ True False False]
----------

2.3 索引与切片

索引与切片是数组求子串的一种非常优雅的运算方式

  • 切片不包括末位元素(如:arr[1:3] 为 arr[1]、arr[2],不包括 arr[3])
切片不包括末位元素
1
2
3
arr = np.arange(10) # 生成 0-9 的数组 [0 1 2 3 4 5 6 7 8 9]
print(arr[5]) # 输出 5
print(arr[3:8]) # [3 4 5 6 7], 注意,切片不包括末位元素
  • 切片具有”广播”效应

这是因为 numpy 为了效率,一般情况下不会对数据进行复制!就是切下来组成新数组的元素有可能会被原数组修改(因为 numpy 设计来是用于处理大数据的,所以这种通常来说不需要的复制它都能免则免了)。

切片具有”广播”效应
1
2
3
arr2 = arr[3:8] # [3 4 5 6 7]
arr[5] = 100
print(arr2) # [3 4 100 6 7] ,这里可见,arr 的修改影响到了 arr2!
  • 使用 copy() 解决广播问题

如果不想被影响,可以显示地复制数据 -> arr2 = arr[3:5].copy()

2.3.1 布尔型索引

布尔型索引可以用于对数组元素进行过滤或选取。

  • 过滤或选取

将布尔值组成的矩阵作为索引代入原矩阵,会输出由 True 索引出的元素组成的新矩阵。还是看看下面的例子,比如我们要选出一个数组中的所有偶数值,这相当于一个过滤器方法(过滤出源中的偶数值):

布尔型索引,选出数组中的偶数元素
1
2
3
a = np.arange(10)     # [0,1,...,9]
print(a % 2 == 0) # [ True False True False True False True False True False]
print(a[a % 2 == 0]) # 将布尔矩阵作为原矩阵索引,只选取 True 位置的元素
  • 使用布尔索引来进行数据清洗

后面我们会详细介绍数据清洗,现在我们简单说下,比如你有一个由随机数组成的矩阵,已知负值为不合法的输入,现在你想把矩阵中所有负值都置为 0,我们看看怎么做:

使用布尔索引来进行数据清洗,将负值元素置为 0
1
2
3
4
5
6
7
8
data = np.random.randn(4, 4) # 随机生成一个 4*4 的-1~1 随机数矩阵
data[data < 0] = 0

output: ===>
[ 0.34164317 -1.11070441 -0.63823478 0.80319368] [ 0.34164317 0 0 0.80319368]
[ 0.50016268 -0.54812653 0.71050926 0.05316833] [ 0.50016268 0 0.71050926 0.05316833]
[-0.63185555 -0.54865819 0.99373464 0.32828296] [ 0 0 0.99373464 0.32828296]
[ 0.55370335 -0.44756017 -1.11465901 0.14144133] -> [ 0.55370335 0 0 0.14144133]

2.3.2 多维数组的索引问题

多维数组的索引,总是优先从外层往内层访问,这是什么意思呢?比如一个 3 维数组

多维数组的索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arr3d = np.arange(24).reshape(2,3,4) #2*3*4 的 3 维数组
my_print(arr3d)
my_print(arr3d[0])

==> arr3d:
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]

[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]

==> arr3d[0]:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
----------

我们现在来看看什么叫从外层到内层,由于我们思维不常接触多维数据,所以对于多维的想像力是不够的。所以我们怎么看代如 3 维或更高维的数组呢?我有个办法就是从外到内层层剥离:比如这个2*3*4的数组可以看成是 2 个3*4的数组组成的数组;那么最外层就是这个2,所以arr3d[0]表示的就是这 2 个3*4的数组中的第一个。

这样想之后,我们就能轻松地理解上面的代码段了。作为思考,我们想想arr3d[1,2]会输出什么呢? 同样,多维数组也可以使用切片运算如arr3d[x:y]。更高级一些arr[x:y, z:s]表示对 arr 第一层使用 [x:y] 切片后,再对第二层使用 [z:s] 切片。

多维数组多次切片示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arr2d = np.arange(20).reshape(4,5) # 4*5 的 2 维数组
arr = arr2d[1:3, 3:]
print(arr2d)
print(arr)

==> arr2d: 由 0-19 组成的 4*52 维数组
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]
[15 16 17 18 19]]

==> arr:
[[ 8 9]
[13 14]]
----------

我们分析下上面操作的结果,最后输出结果是加了阴影的几个(为了对齐,我在单位数字前加了个 0)
00 01 02 03 04
05 06 07 08 09
10 11 12 13 14
15 16 17 18 19

首先,arr2d[1:3, 3:] 的第一个切片 [1:3] 选取了矩阵的第 2 行和第 3 行 (index 从 0 开始)。然后第 2 个切片从列开始切,[3:] 表示选取了 3-4 列 (index 也是 0 开始),也就是后两列。这样就“切剩下”结果了。

2.3.3 花式索引 (Fancy indexing)

花式过引有点像 SQL 里的 select 语句,往索引选取符号[]里传入一个数组,以之为 index,可以选出你要想的行子集:

花式索引
1
2
3
4
5
6
7
8
arr = np.arange(20).reshape(4,5) #4*5 的 2 维数组
print(arr[1, 3]) # 8
print(arr[[1, 3]])

==> out:
[[ 5 6 7 8 9]
[15 16 17 18 19]]
----------

上面这个例子很好地解释了花式索引了。我们看,arr[1,3] 表示取第 2 行第 4 列元素,从之前我们输出过的 arr 的那个加阴影的矩阵可以看出,就是数字 8。

而 arr[[1, 3]],注意哦,[] 里面给的不是一个元组,而是一个数组 [1,3],这样就表示我要获取第 2 行和第 4 行,结果返回的是切出来的新矩阵。
上面的花式索引示例,[[1, 3]] 中两层[[]],表示的是对原矩阵进行切片,而不是降维索引。

  • 布尔矩阵 mask 应用

花式索引还有一种 mask 应用的:

花式索引之 mask 应用
1
2
3
4
a = np.arange(10)
print(a % 2 == 0) # [ True False True False True False True False True False]
b = a[a % 2 == 0] # 只选取了偶数元素
print(b) # [0 2 4 6 8]

这里选使用数组的算术运算a % 2 == 0,得出一个布尔数组,再使用这个布尔数组对原数组进行花式索引!从而选出了只有值为 True 的元素,即偶数元素。

2.3.4 遍历

nditer 方法,C – 广度优先,F – 深度优先

1
2
3
4
5
6
7
8
arr = np.arange(0, 60, 5).reshape(3, 4)
log('原矩阵', arr)
print('广度优先遍历:')
for x in np.nditer(arr, order='C'):
print(x, end=',')
print('深度优先遍历:')
for x in np.nditer(arr, order='F'):
print(x, end=',')

2.4 规整化、扁平化

2.4.1 重整数据结构 reshape

1
2
# 将一维数组,重整型至 2x4 矩阵
np.arange(8).reshape(2,4)

2.4.2 扁平化

  1. ndarray.reshape(-1)
  2. ndarray.ravel()
  3. ndarray.flatten()

上面已经说过,使用 ndarray.reshape(-1) 可将数组重整为一维数组,相当于把数据压平。

还有一个方法 ndarray.ravel() 与之效果是一样的。

ravel()reshape(-1) 返回的是原始数组的视图,而不是其副本。因此,如果修改新数组中的任何元素,原始数组也会受到影响。
如果需要返回一个数组副本,可以使用flatten()函数。

2.4.3 旋转、翻转

  1. ndarray.rot90() - 逆时针旋转90 度
  2. ndarray.flip() - 用于翻转数组中的元素

ndarray.rot90() 的作用是将一个数组逆时针旋转90 度。默认情况下,这个函数会将数组的前两个维度 axes=(0, 1) 进行旋转。此外,还可以利用参数k (正整数) 逆时针旋转k × 90 度。默认,k = 1。

注意,ndarray.rot90() 的结果也是返回原始数组的视图,而不是副本。

ndarray.flip(axis=None) 函数用于翻转数组中的元素,即将数组沿着一个或多个轴翻转。如果不指定 axis,则默认将整个数组沿着所有的轴进行翻转。

3 小结

作为小结和对本章的练习,大家可以试着在纸上算算下面程序的输出,并亲自复制到源文件中执行对比看自己对了多少:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def log(msgOrOjb, obj=None):
"""一个实用的 log 方法。"""
if isinstance(msgOrOjb, str):
if obj is not None:
print('\n==> ' + msgOrOjb + ':' , type(obj))
print(obj)
else:
print('\n==> ' + msgOrOjb)
else:
print('\n==> ', type(msgOrOjb))
print(msgOrOjb)
print('----------')

def main():
#### ndarray ####
# 打印 ndarray 类型
ndarray = np.array([1, 2, 3])
print(type(ndarray)) # <class 'numpy.ndarray'>

# ndarray 的两个重要属性
ndarray = np.random.rand(2, 3)
print(ndarray)
print(ndarray.shape)
print(ndarray.dtype)
# ==> output:
# [[0.14819278 0.72268965 0.29513592]
# [0.76169912 0.29356355 0.8358218 ]]
# (2, 3)
# float64

#### 创建 ndarray ####
# 创建 ndarray 的各种方法
ndarray = np.array([[1,2,3], [4,5,6]])
log(ndarray)
# ==> output:
# <class 'numpy.ndarray'>
# [[1 2 3]
# [4 5 6]]
# ----------

ndarray = np.zeros((2,3))
log(ndarray)
# ==> output:
# <class 'numpy.ndarray'>
# [[0. 0. 0.]
# [0. 0. 0.]]
# ----------

ndarray = np.full((2,3), 10)
log(ndarray)
# ==> output:
# <class 'numpy.ndarray'>
# [[10 10 10]
# [10 10 10]]
# ----------

# 创建一维数组
# arange(start = 0, end, step = 1)
arr = np.arange(1, 100, 2)
log(arr)
# ==> output:
# [ 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47
# 51 53 55 57 59 61 63 65 67 69 71 73 75 77 79 81 83 85 87 89 91 93 95
# 99 ]

#### ndarray 的运算 ####
# ndarray 的算术运算
arr = np.array([[1,2,3], [4,5,6]]) # 创建一个二维数组 2*3
arr = arr * arr
log(arr)

# ndarray 逻辑运算
arr = np.array([[1,2,3], [4,5,6]]) # 创建一个二维数组 2*3
arr = arr > 2
log(arr)

arr = np.array([1,4,8]) # 创建一个二维数组 2*3
arr2 = np.array([5,3,7]) # 创建一个二维数组 2*3
log(arr < arr2)

# 索引与切片
arr = np.arange(10) # 生成 0-9 的数组 [0 1 2 3 4 5 6 7 8 9]
print(arr[5]) # 输出 5
log(arr[3:8]) # [3 4 5 6 7], 注意,切片不包括末位元素
arr2 = arr[3:8] # [3 4 5 6 7]
arr3 = arr[3:8].copy() # 使用 copy 避免 "广播问题"
arr[5] = 100
log(arr2) # [3 4 100 6 7]
log(arr3) # [3 4 5 6 7]

# 多维数组的索引
arr3d = np.arange(24).reshape(2,3,4) #2*3*4 的 3 维数组
log(arr3d)
log(arr3d[0])

# 多维数组多次切片示例
arr2d = np.arange(20).reshape(4,5) #4*5 的 2 维数组
log(arr2d)
arr = arr2d[1:3, 3:]
log(arr)

# 花式索引
arr = np.arange(20).reshape(4,5) #4*5 的 2 维数组
log(arr[1, 3])
log(arr[[1, 3]])

# 布尔矩阵 mask 应用
a = np.arange(10)
print(a % 2 == 0) # [ True False True False True False True False True False]
b = a[a % 2 == 0] # 只选取了偶数元素
log(b) # [0 2 4 6 8]

# 使用布尔索引来进行数据清洗
data = np.random.randn(4, 4) # 随机生成一个 4*4 的-1~1 随机数矩阵
log(data)
data[data < 0] = 0
log(data)

#### ndarray 矩阵运算 ####
matrix = np.arange(12).reshape(3, 4) # 快速建立一个 3*4 的矩阵
log(matrix)
log(matrix.T)

# 轴对换
arr = np.arange(24).reshape((2,3,4)) # 创建一个 2*3*4 的 3 维数组
transposeArr = arr.transpose((1,0,2))
log('原数组', arr)
log('transpose((1,0,2) 之后)', transposeArr)
log('transpose((0,2,1) 之后)', arr.transpose((0,2,1)))
log('arr.swapaxes(0, 1)', arr.swapaxes(0, 1))

4 引用

  1. 《利用 python 进行数据分析》 - WesMcKinney 著,SeanCheney 译 - WesMcKinney 是 pandas 的开发者,所以必须是讲解 numpy、pandas 等数据处理工具最权威的专家了,他的这本书写的很简洁很易读,SeanCheney 翻译的也是非常完美。十分值得推荐的。