20  NumPy:高性能向量运算

Note
  1. 需要讲前6节
  2. 时间充足可以副本和视图等

NumPy是什么:这个包提供的组件是Python自带的List的强大的替代品:速度飞快,操作便利(后面都会说到)。特别是处理长数组的时候,典型的比如各种时间序列数据。

# 导入numpy
import numpy as np

我们用一个List来创建一个NumPy的一维数组(array)

numpy.array()函数可以接受一个List作为参数,返回给你一个numpy的ndarray。

注意:array一般称为“数组”,你可以理解为“数据类型相同的列表”,比如股价序列,其中所有元素都是浮点数。 那么ndarray,可以理解为n维数组,比如1维数组(向量),2维数组(矩阵),3维数组等等 …

x = np.array([1,2,3,4,5]) 
# 这里的np是我们前面定义的numpy的别名
print('x的类型:',type(x)) # np.ndarray
print('x的长度:', x.shape[0])
print('x的第一个元素:', x[0])
x的类型: <class 'numpy.ndarray'>
x的长度: 5
x的第一个元素: 1

20.1 广播(Broadcast)

这和直接用Python中的List有什么区别?

列举2个例子:

比如我们要把一个序列里的元素全部乘以2。如果把数据用List的形式保存,我们可以用列表推导式:

a = [1,2,3,4,5]
b = [i * 2 for i in a]
print(b)
[2, 4, 6, 8, 10]

如果用array保存,那直接*2即可:

a = np.array([1,2,3,4,5])
b = a * 2
print(b)
[ 2  4  6  8 10]

我们两个序列中的数值对位相加。

如果用List,我们可能用循环:

a = [1,2,3,4]
b = [2,3,4,5]

c = []
for i in range(len(a)):
  c.append(a[i]+b[i])

print(c)
[3, 5, 7, 9]

如果用ndarray,直接相加即可。

a = np.array([1,2,3,4])
b = np.array([2,3,4,5])

c = a + b

print(c)
[3 5 7 9]

这个特性叫“广播Boardcast”。

使用广播,你可以把一个序列视为“一个变量”:如果你不看定义,c = a + b和2个变量相加没什么区别。 并且对一般的操作都成立。

print(a - b * 4) # 复合运算
print(a <= 3) # 逻辑运算
[ -7 -10 -13 -16]
[ True  True  True False]

因此: 1. 不用手写循环或者列表推导式,是采用ndarray最直接的一个好处。 2. 任何数值型序列数据都可以考虑采用ndarray,很多时候你可以把一个序列看出一个变量来操作。 3. ndarray处理速度飞快。

20.2 构造ndarray

ndarray的构造方式很多,这里列举一些

a = np.array([1,2,3,4,5,6]) # 从列表构造
print(a)

b = np.zeros(3) # n个0
print(b)

c = np.ones(4) # n个1
print(c)

d = np.ones_like(a) # 和另一个数组同样长度的,由1构成的数组
print(d) #(a有6个元素,因此会得到6个1)

# 同样长度,但是0组成
print(np.zeros_like(a))

# 同样长度,但填充你指定的数字
print(np.full_like(a,9))
[1 2 3 4 5 6]
[0. 0. 0.]
[1. 1. 1. 1.]
[1 1 1 1 1 1]
[0 0 0 0 0 0]
[9 9 9 9 9 9]

np.arange()可以按范围创造数组,类比Python自带的range()函数,用法基本一致。

注:

range()返回的是可迭代对象:可以取值,可以切片,不是List但可以转为List。(考虑你要保存一个巨大的序列,比如全体自然数,但只要用到其中几个值)

np.arange()返回的就是一维的ndarray对象。

print(list(range(5))) # [0,1,2,3,4]
print(list(range(2,8,2))) # 2到7,步长为2: [2,4,6]

print(np.arange(5)) # [0 1 2 3 4]
print(np.arange(2,8,2)) # 同样是[2 4 6]
[0, 1, 2, 3, 4]
[2, 4, 6]
[0 1 2 3 4]
[2 4 6]

20.3 取值和切片:几乎和Python的List一样

我们采用索引(index)或者下标来取值,第一个元素的索引是0。

a = np.arange(10)*2
print(a)

print(a[4]) # 取值

print(a[3:6:2]) #切片: 3号元素(包含)到6号元素(不包含),步长为2

print(a[::-1]) # 逆序
[ 0  2  4  6  8 10 12 14 16 18]
8
[ 6 10]
[18 16 14 12 10  8  6  4  2  0]

还可以按条件筛选,例如选择一个序列里的所有奇数:

a = np.arange(5) 
a % 2 == 1 # 广播求奇数,这是个布尔值序列 [False,True,False .... ] 

print(a % 2 == 1)

print(a)
print(a[a % 2 == 1])  # a[某个布尔值序列],就可以把True位置上的值取出
[False  True False  True False]
[0 1 2 3 4]
[1 3]

20.4 缺失值

ndarray中缺失值由np.nan来表示。

a = np.array([1,2,np.nan,5,np.nan])
print(a)
[ 1.  2. nan  5. nan]

判断哪个元素是缺失值?用函数np.isnan(),返回每一个元素是不是np.nan

print(np.isnan(a)) # 得到一个bool型的ndarray:[False, False,  True, False]
[False False  True False  True]

20.5 布尔序列和逻辑运算

缺失值的索引是(第几个是)?np.where()函数。

print(np.where(np.isnan(a))) # [2,4],2号和4号(第3和第5个)
(array([2, 4]),)

np.where()可以获得任何布尔序列True的位置,例如

a = np.arange(10)*2
print(a)
print(np.where(a == 8)) # [4], 
[ 0  2  4  6  8 10 12 14 16 18]
(array([4]),)

逻辑运算符:与&,或|,非~,和四则运算,><=类似。

例如,与运算 &

a = np.array([1,2,3,4,5])

a > 3 # 这是一个bool序列:array([False, False, False,  True,  True])

mask = (a > 3) & (a & 2 == 0)  
# a > 3 且(与运算) a 是偶数:这也是一个bool序列

# bool序列,可以作为筛选的条件

a[mask] # 选出 a中大于3且是偶数的元素
array([4, 5])

或运算同理。

非运算 ~

# 对mask取反,即求 "非(> 3 且 为偶数)"的元素 
a[~mask]
array([1, 2, 3])

布尔序列的逻辑运算,也可以用逻辑函数 np.logical_and(),np.logical_or(),np.logical_not()

# 把2个布尔序列,进行对位的逻辑与操作
print(np.logical_and([True,False],[True, True])) # [True, False] 

# 或和非类似
[ True False]

例子:选取1-100 之间,可以被7整除,且不能被5整除的数字:

用运算符写:

a = np.arange(1,101)

mask = (a % 7 == 0) & (a % 5 != 0)

a[mask]
array([ 7, 14, 21, 28, 42, 49, 56, 63, 77, 84, 91, 98])

用逻辑函数写:

a = np.arange(100) + 1 # [1 2 3 ...]

mask = np.logical_and(a % 7 == 0, a % 5 != 0)  # mask:蒙版,可以理解为“过滤器”
print(mask[:10]) # mask的前10个

print(a[mask]) # 把a用mask过滤一下。
[False False False False False False  True False False False]
[ 7 14 21 28 42 49 56 63 77 84 91 98]

20.6 一些numpy函数

数量太多,无法列举。请善于搜索引擎,如搜索”numpy 正弦函数”。

应用numpy的另一个好处:内置极大量的数学函数,可以直接调用。对比List:求和也要手写循环。

a = np.array([1,2,3,4])
print(np.sin(a))
[ 0.84147098  0.90929743  0.14112001 -0.7568025 ]

取对数

print(np.log(a))
[0.         0.69314718 1.09861229 1.38629436]

保留n位小数

b = np.log(a)
print(np.round(b,2))
print(b.round(2))
[0.   0.69 1.1  1.39]
[0.   0.69 1.1  1.39]

最大值最小值

print(np.max(a))
print(a.max())

print(np.min(a))
print(a.min())
4
4
1
1

求和,均值,中位数,标准差

print(np.sum(a))
print(np.mean(a))
print(np.median(a))
print(np.std(a).round(2))

# 也可以
print(a.sum())
print(a.mean())
# print(a.median()) 没有median()方法,要用np.median()函数
10
2.5
2.5
1.12
10
2.5

排序

  1. x.sort():原地排序,会改变变量的值
  2. np.sort(x):获得新的排序后的数组,不会改变原值
# 两种方法都可以
a = np.array([5,4,3,2,1])
a.sort() 
print(a) # a的值已经改变


a = np.array([5,4,3,2,1])
b = np.sort(a) # 不改变原值
print('原值a是:',a)
print('排序后的b是:',b)
[1 2 3 4 5]
原值a是: [5 4 3 2 1]
排序后的b是: [1 2 3 4 5]

善用搜索引擎

20.7 矩阵运算

20.8 副本和视图(view)

所谓副本:就是数组另一个拷贝,对副本的修改不会影响原来的数组。

所谓视图:和原数组共享数据,或者共享部分数据,对视图的修改会改变原数组。

.view()方法和切片,产生的都是视图,修改视图会改变原数组:

a = np.array([1,2,3,4,5])
b = a.view() # 产生了一个视图:此时b和a共享数据

b[0] = 999
print(a)
print(type(b))
[999   2   3   4   5]
<class 'numpy.ndarray'>
b = a[:2]  # 切片也产生了一个视图:此时b和a的前2个元素共享数据

b[-1] = -999 # 改变了原数组
print(a)
[ 999 -999    3    4    5]

数组的base属性会指示出一个视图所指向的数据。

print(b)
print(b.base) 
[ 999 -999]
[ 999 -999    3    4    5]

按条件选取(你提供一个布尔序列[True,False,... ]作为筛选条件)再赋值,产生的是副本,改变副本不影响原数组。

a = np.array([1,2,3,4,5])
print(a % 2 == 1)

b = a[a % 2 == 1] # b是一个副本
b[0] = 999
print(a)
[ True False  True False  True]
[1 2 3 4 5]

如果b是一个副本,那么b.base则是None

print(b)
print(b.base) 
[999   3   5]
None

要明确地获得一个副本,可以用.copy()方法,和List一样。

b = a.copy()
b[0] = 12345
print(a)
[1 2 3 4 5]
a[ a % 2 == 1] = -1
print(a)
[-1  2 -1  4 -1]

20.9 reshape

.reshape()方法可以改变数组的形状,比如把6个元素的一维数组,变成2x3的二维数组。

a = np.arange(6)
print('a:')
print(a)

print('b:')
b = a.reshape((2,3)) # 2行3列
print(b)
a:
[0 1 2 3 4 5]
b:
[[0 1 2]
 [3 4 5]]

.reshape()获得的是什么呢?其实是一个视图。

这里可以琢磨一下视图的含义:视图b,只是数组a中的数据的另一种呈现方式。

同样共享了一片数据[0 1 2 3 4 5],a的以一维的方式呈现,b的以二维的方式呈现。

print(b.base) # b.base != None
[0 1 2 3 4 5]

显然我们改变b中的值,也会改变a。

b[0,0] = 999
print(a)
[999   1   2   3   4   5]

排序获得的是副本还是视图?

a = np.array([5,4,3,2,1])
b = np.sort(a)
print(b)
print(b.base) # b.base is None,b是一个拷贝,没有和其他数组共享数据。
[1 2 3 4 5]
None

20.10 视图和变量对变量的赋值

视图,和数组对另一个变量赋值有什么区别?

  1. 视图是一个单独的变量,和原数组共享底层的数据,但是某些属性(比如行列shape)是各自独立的。
  2. 数组直接给另一个变量名赋值,等于原数组的一个别名(变量名只是标签),两个变量指向相同的实体。
a = np.array([1,2,3,4,5,6])
print(a)

# b是a的一个视图,是另一个变量,只是共享数据
b = a.view()[::-1]  # 获得一个逆序的视图

print(b)

# c是a的别名,a和c指向同样的实体,
c = a
print(c)
[1 2 3 4 5 6]
[6 5 4 3 2 1]
[1 2 3 4 5 6]