# 导入numpy
import numpy as np
20 NumPy:高性能向量运算
- 需要讲前6节
- 时间充足可以副本和视图等
NumPy是什么:这个包提供的组件是Python自带的List的强大的替代品:速度飞快,操作便利(后面都会说到)。特别是处理长数组的时候,典型的比如各种时间序列数据。
我们用一个List来创建一个NumPy的一维数组(array)
numpy.array()
函数可以接受一个List作为参数,返回给你一个numpy的ndarray。
注意:array一般称为“数组”,你可以理解为“数据类型相同的列表”,比如股价序列,其中所有元素都是浮点数。 那么ndarray,可以理解为n维数组,比如1维数组(向量),2维数组(矩阵),3维数组等等 …
= np.array([1,2,3,4,5])
x # 这里的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的形式保存,我们可以用列表推导式:
= [1,2,3,4,5]
a = [i * 2 for i in a]
b print(b)
[2, 4, 6, 8, 10]
如果用array保存,那直接*2即可:
= np.array([1,2,3,4,5])
a = a * 2
b print(b)
[ 2 4 6 8 10]
我们两个序列中的数值对位相加。
如果用List,我们可能用循环:
= [1,2,3,4]
a = [2,3,4,5]
b
= []
c for i in range(len(a)):
+b[i])
c.append(a[i]
print(c)
[3, 5, 7, 9]
如果用ndarray,直接相加即可。
= np.array([1,2,3,4])
a = np.array([2,3,4,5])
b
= a + b
c
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的构造方式很多,这里列举一些
= np.array([1,2,3,4,5,6]) # 从列表构造
a print(a)
= np.zeros(3) # n个0
b print(b)
= np.ones(4) # n个1
c print(c)
= np.ones_like(a) # 和另一个数组同样长度的,由1构成的数组
d 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。
= np.arange(10)*2
a 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]
还可以按条件筛选,例如选择一个序列里的所有奇数:
= np.arange(5)
a % 2 == 1 # 广播求奇数,这是个布尔值序列 [False,True,False .... ]
a
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
来表示。
= np.array([1,2,np.nan,5,np.nan])
a 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
的位置,例如
= np.arange(10)*2
a print(a)
print(np.where(a == 8)) # [4],
[ 0 2 4 6 8 10 12 14 16 18]
(array([4]),)
逻辑运算符:与&
,或|
,非~
,和四则运算,><=
类似。
例如,与运算 &
= np.array([1,2,3,4,5])
a
> 3 # 这是一个bool序列:array([False, False, False, True, True])
a
= (a > 3) & (a & 2 == 0)
mask # a > 3 且(与运算) a 是偶数:这也是一个bool序列
# bool序列,可以作为筛选的条件
# 选出 a中大于3且是偶数的元素 a[mask]
array([4, 5])
或运算同理。
非运算 ~
# 对mask取反,即求 "非(> 3 且 为偶数)"的元素
~mask] a[
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整除的数字:
用运算符写:
= np.arange(1,101)
a
= (a % 7 == 0) & (a % 5 != 0)
mask
a[mask]
array([ 7, 14, 21, 28, 42, 49, 56, 63, 77, 84, 91, 98])
用逻辑函数写:
= np.arange(100) + 1 # [1 2 3 ...]
a
= np.logical_and(a % 7 == 0, a % 5 != 0) # mask:蒙版,可以理解为“过滤器”
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:求和也要手写循环。
= np.array([1,2,3,4])
a print(np.sin(a))
[ 0.84147098 0.90929743 0.14112001 -0.7568025 ]
取对数
print(np.log(a))
[0. 0.69314718 1.09861229 1.38629436]
保留n位小数
= np.log(a)
b 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
排序
x.sort()
:原地排序,会改变变量的值np.sort(x)
:获得新的排序后的数组,不会改变原值
# 两种方法都可以
= np.array([5,4,3,2,1])
a
a.sort() print(a) # a的值已经改变
= np.array([5,4,3,2,1])
a = np.sort(a) # 不改变原值
b 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()
方法和切片,产生的都是视图,修改视图会改变原数组:
= np.array([1,2,3,4,5])
a = a.view() # 产生了一个视图:此时b和a共享数据
b
0] = 999
b[print(a)
print(type(b))
[999 2 3 4 5]
<class 'numpy.ndarray'>
= a[:2] # 切片也产生了一个视图:此时b和a的前2个元素共享数据
b
-1] = -999 # 改变了原数组
b[print(a)
[ 999 -999 3 4 5]
数组的base
属性会指示出一个视图所指向的数据。
print(b)
print(b.base)
[ 999 -999]
[ 999 -999 3 4 5]
按条件选取(你提供一个布尔序列[True,False,... ]
作为筛选条件)再赋值,产生的是副本,改变副本不影响原数组。
= np.array([1,2,3,4,5])
a print(a % 2 == 1)
= a[a % 2 == 1] # b是一个副本
b 0] = 999
b[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一样。
= a.copy()
b 0] = 12345
b[print(a)
[1 2 3 4 5]
% 2 == 1] = -1
a[ a print(a)
[-1 2 -1 4 -1]
20.9 reshape
.reshape()
方法可以改变数组的形状,比如把6个元素的一维数组,变成2x3的二维数组。
= np.arange(6)
a print('a:')
print(a)
print('b:')
= a.reshape((2,3)) # 2行3列
b 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。
0,0] = 999
b[print(a)
[999 1 2 3 4 5]
排序获得的是副本还是视图?
= np.array([5,4,3,2,1])
a = np.sort(a)
b print(b)
print(b.base) # b.base is None,b是一个拷贝,没有和其他数组共享数据。
[1 2 3 4 5]
None
20.10 视图和变量对变量的赋值
视图,和数组对另一个变量赋值有什么区别?
- 视图是一个单独的变量,和原数组共享底层的数据,但是某些属性(比如行列shape)是各自独立的。
- 数组直接给另一个变量名赋值,等于原数组的一个别名(变量名只是标签),两个变量指向相同的实体。
= np.array([1,2,3,4,5,6])
a print(a)
# b是a的一个视图,是另一个变量,只是共享数据
= a.view()[::-1] # 获得一个逆序的视图
b
print(b)
# c是a的别名,a和c指向同样的实体,
= a
c print(c)
[1 2 3 4 5 6]
[6 5 4 3 2 1]
[1 2 3 4 5 6]