快速上手NumPy
NumPy is the fundamental package for scientific computing in Python.
NumPy是一个开源的Python科学计算库。
对于相同的数值计算任务,使用NumPy比直接使用Python要简洁、高效的多。
NumPy使用ndarray
来处理多维数组。
NumPy provides an N-dimensional array type, the ndarray, which describes a collection of “items” of the same type. The items can be indexed using for example N integers.
NumPy提供了一个N维数组类型ndarray
,它描述了相同类型的items
的集合。
比如下面的学生成绩:
语文 | 数学 | 英语 | 物理 | 化学 |
---|---|---|---|---|
92 | 99 | 91 | 85 | 90 |
95 | 85 | 88 | 81 | 88 |
85 | 81 | 80 | 78 | 86 |
用ndarray
进行存储:
import numpy as np
score = np.array(
[[92, 99, 91, 85, 90],
[95, 85, 88, 81, 88],
[85, 81, 80, 78, 86]])
score
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
用数据说话。所以,这里先通过几行代码来比较ndarray
与Python原生的list
的执行效率。
import random
import time
import numpy as np
list = [random.random() for i in range(10000000)]
array = np.array(list)
# 使用%time魔法方法, 可查看当前行的代码运行一次所花费的时间
%time sum_array = np.sum(array)
%time sum_list = sum(list)
CPU times: user 4.59 ms, sys: 3 µs, total: 4.59 ms Wall time: 4.6 ms CPU times: user 37.2 ms, sys: 155 µs, total: 37.4 ms Wall time: 37.2 ms
可以看到ndarray
的计算速度要快很多。机器学习通常有大量的数据运算,如果没有一个高效的运算方案,很难流行起来。
NumPy对ndarray
的操作和运算进行了专门的设计,所以数组的存储效率和输入输出性能远优于Python中的嵌套列表,数组越大,NumPy的优势就越明显。
那么自然要问了,ndarray
为什么这么快?
- 在内存分配上,
ndarray
相邻元素的地址是连续的,而python原生list
是通过二次寻址方式找到下一个元素的具体位置。如下图所示:
其中图片来自:
https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/
一个ndarray
占用内存中一个连续块,并且元素的类型都是相同的。所以一旦确定了ndarray
的元素类型以及元素个数,它的内存占用就确定了。而原生list
则不同,它的每个元素在list
中其实是一个地址引用,这个地址指向存储实际元素数据的内存空间,也就是说指向的内存不一定是连续的。
- numpy底层使用
C
语言编写,内部解除了GIL
(全局解释器锁)限制。
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
- numpy支持并行运算,系统有多个核时,条件允许的话numpy会自动发挥多核优势。
a1 = np.array([1, 2, 3])
a1
array([1, 2, 3])
# 数组维度
a1.ndim
1
# 数组形状
a1.shape
(3,)
# 数组元素个数
a1.size
3
# 数组元素的类型
a1.dtype
dtype('int64')
# 一个数组元素的长度(字节数)
a1.itemsize
8
ndarray
的元素类型如下表所示:
类型 | 描述 | 简写 |
---|---|---|
bool | 用一个字节存储的布尔类型(True或False) | 'b' |
int8 | 一个字节大小,-128 至 127 | 'i' |
int16 | 整数,-32768 至 32767 | 'i2' |
int32 | 整数,-2^31 至 2^31 -1 | 'i4' |
int64 | 整数,-2^63 至 2^63 - 1 | 'i8' |
uint8 | 无符号整数,0 至 255 | 'u' |
uint16 | 无符号整数,0 至 65535 | 'u2' |
uint32 | 无符号整数,0 至 2^32 - 1 | 'u4' |
uint64 | 无符号整数,0 至 2^64 - 1 | 'u8' |
float16 | 半精度浮点数:16位,正负号1位,指数5位,精度10位 | 'f2' |
float32 | 单精度浮点数:32位,正负号1位,指数8位,精度23位 | 'f4' |
float64 | 双精度浮点数:64位,正负号1位,指数11位,精度52位 | 'f8' |
complex64 | 复数,分别用两个32位浮点数表示实部和虚部 | 'c8' |
complex128 | 复数,分别用两个64位浮点数表示实部和虚部 | 'c16' |
object_ | python对象 | 'O' |
string_ | 字符串 | 'S' |
unicode_ | unicode类型 | 'U' |
创建数组的时候可指定元素类型。若不指定,整数默认int64
,小数默认float64
。
np.array([1, 2, 3.0]).dtype
dtype('float64')
np.array([True, True, False]).itemsize
1
np.array(['Python', 'Java', 'Golang'], dtype=np.string_).dtype
dtype('S6')
a2 = np.array([
[1, 2, 3],
[1, 2, 3]
])
a2
array([[1, 2, 3], [1, 2, 3]])
# 数组维度
a2.ndim
2
# 数组形状
a2.shape
(2, 3)
# 数组元素个数
a2.size
6
# 数组元素类型
a2.dtype
dtype('int64')
# 一个数组元素的长度(字节数)
a2.itemsize
8
a3 = np.array([
[[1, 2, 3], [1, 2, 3]],
[[1, 2, 3], [1, 2, 3]],
[[1, 2, 3], [1, 2, 3]],
[[1, 2, 3], [1, 2, 3]]
])
a3
array([[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]])
# 数组维度
a3.ndim
3
# 数组形状
a3.shape
(4, 2, 3)
# 数组元素个数
a3.size
24
# 数组元素类型
a3.dtype
dtype('int64')
# 一个数组元素的长度(字节数)
a3.itemsize
8
score
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
# 相当于深拷贝
arr1 = np.array(score)
arr1
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
# 相当于浅拷贝, 并没有copy完整的array对象
arr2 = np.asarray(score)
arr2
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
score[0, 0] = 100
score
array([[100, 99, 91, 85, 90], [ 95, 85, 88, 81, 88], [ 85, 81, 80, 78, 86]])
arr1
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
arr2
array([[100, 99, 91, 85, 90], [ 95, 85, 88, 81, 88], [ 85, 81, 80, 78, 86]])
从上面的结果可以看出:传入ndarray
时,np.array()
会copy完整的ndarray
,而np.asarray()
不会。
注意:传入的参数是ndarray
,并非Python原生的list
。这两种情况不能混淆。下面看下传入list
是啥结果。
nums = [1, 2, 3]
nums
[1, 2, 3]
array1 = np.array(nums)
array1
array([1, 2, 3])
array2 = np.asarray(nums)
array2
array([1, 2, 3])
现在修改list
中的元素:
nums[0] = 10
nums
[10, 2, 3]
array1
array([1, 2, 3])
array2
array([1, 2, 3])
- 生成元素全为0的数组
np.zeros([3, 2], dtype=np.int64)
array([[0, 0], [0, 0], [0, 0]])
# Return an array of zeros with the same shape and type as a given array.
np.zeros_like(score)
array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]])
- 生成元素全为1的数组
np.ones([3, 2], dtype=np.int64)
array([[1, 1], [1, 1], [1, 1]])
# Return an array of ones with the same shape and type as a given array.
np.ones_like(score)
array([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1]])
- 创建等差数组(指定步长, 即等差数列中的公差)
# Return evenly spaced values within a given interval.
np.arange(10, 50, 5)
array([10, 15, 20, 25, 30, 35, 40, 45])
- 创建等差数组(指定元素个数)
np.linspace(2.0, 3.0, num=5)
array([2. , 2.25, 2.5 , 2.75, 3. ])
# If endpoint True, 3.0 is the last sample. Otherwise, 3.0 is not included.
np.linspace(2.0, 3.0, num=5, endpoint=False)
array([2. , 2.2, 2.4, 2.6, 2.8])
- 创建等比数列
np.logspace(2, 5, num=4, dtype=np.int64)
array([ 100, 1000, 10000, 100000])
np.logspace(2, 5, num=4, base=3, dtype=np.int64)
array([ 9, 27, 81, 243])
默认base=10.0
,第一个例子中生成num=4
个元素的等比数列,起始值是10^2
,终止值是10^5
,所以等比数列为[100, 1000, 10000, 100000]
同理,第二个例子中也是生成num=4
个元素的等比数列,不过base=3
,起始值是3^2
,终止值是3^5
,所以等比数列为[9, 27, 81, 243]
实际生产中的数据大多可能是随机数值,而这些随机数据往往又符合某些规律。下面会涉及到概率论的一点点知识,无需畏惧,其实初高中数学就或多或少接触过。
# 生成均匀分布的随机数
x1 = np.random.uniform(0, 10, 100000)
x1
array([5.76720988, 5.32880068, 7.58561359, ..., 7.59316418, 8.30197616, 4.38992042])
所谓均匀分布
,指的是相同间隔内的分布概率是等可能的。直方图可用于较直观地估计一个连续变量的概率分布。 下面简要回顾一下画直方图的步骤,也能加深对使用场景的理解。
(1) 收集数据(数据一般应大于50个)
(2) 确定数据的极差(用数据的最大值减去最小值)
(3) 确定组距。先确定直方图的组数,然后以此组数去除极差,可得直方图每组的宽度,即组距
(4) 确定各组的界限值。为避免出现数据值与组界限值重合而造成频数据计算困难,组的界限值单位应取最小测量单位的1/2
(5) 编制频数分布表。把多个组上下界限值分别填入频数分布表内,并把数据表中的各个数据列入相应的组,统计各组频数据
(6) 按数据值比例画出横坐标
(7) 按频数值比例画纵坐标。以观测值数目或百分数表示
(8) 画直方图。按纵坐标画出每个长方形的高度,它代表取落在此长方形中的数据数。
下面用matplotlib
帮助我们画图。
import matplotlib.pyplot as plt
# 创建画布
plt.figure(figsize=(10, 5), dpi=100)
# 画直方图, x代表要使用的数据,bins表示要划分区间数
plt.hist(x=x1, bins=20)
# 设置坐标轴刻度
plt.xticks(np.arange(0, 10.5, 0.5))
plt.yticks(np.arange(0, 6000, 500))
# 添加网格显示
plt.grid(True, linestyle='--', alpha=0.8)
# 显示图像
plt.show()
从上图可以直观的看到,100000
个[0, 10)
范围内的样本数据,落在区间[0,0.5)
、[0.5,1.0)
、...、[9.0,9.5)
、[9.5,10.0)
内频数都近乎5000
,符合均匀分布
规律。
正态分布也是一种概率分布。正态分布是具有两个参数μ
和σ
的连续型随机变量的分布,参数μ
是随机变量的期望(即均值),决定了其位置; 参数σ
是随机变量的标准差,决定了其分布的幅度。
若随机变量X服从一个数学期望为μ、方差为σ^2的正态分布,记为N(μ,σ^2)。当μ = 0,σ = 1时的正态分布是标准正态分布。
类似上面的均匀分布
,我们通过生成样本数据,画图观察正态分布状况。
已知某地区成年男性身高近似服从正态分布。下面生成均值为170,标准差为5的100000个符合正态分布规律的样本数据。
x2 = np.random.normal(170, 5, 100000)
x2
array([177.45732513, 171.49250483, 159.53980655, ..., 156.38843943, 172.38350177, 164.87975538])
同样使用matplotlib
帮助我们画图。
import matplotlib.pyplot as plt
# 创建画布
plt.figure(figsize=(10, 5), dpi=100)
# 画直方图
plt.hist(x=x2, bins=100)
# 添加网格显示
plt.grid(True, linestyle='--', alpha=0.8)
# 显示图像
plt.show()
从图中我们可以看出,大多人身高都集中在170左右。讲到这里,不知道你有没有回想起高中数学讲过的3σ
原则:
P(μ-σ < X ≤ μ+σ) = 68.3%
P(μ-2σ < X ≤μ+2σ) = 95.4%
P(μ-3σ < X ≤μ+3σ) = 99.7%
即:
- 数值分布在(μ-σ, μ+σ)中的概率为68.3%
- 数值分布在(μ-2σ, μ+2σ)中的概率为95.4%
- 数值分布在(μ-3σ, μ+3σ)中的概率为99.7%
可以认为,取值几乎全部集中在(μ-3σ, μ+3σ)区间,超出这个范围的可能性仅到0.3%
其实,生活、生产与科学实验中很多随机变量的概率分布都可以近似地用正态分布来描述。
数组的索引与切片类似Python中的list。下面演示一下即可。
score
array([[100, 99, 91, 85, 90], [ 95, 85, 88, 81, 88], [ 85, 81, 80, 78, 86]])
score[0]
array([100, 99, 91, 85, 90])
score[0, 1]
99
score[0, 2:4]
array([91, 85])
score[0, :-2]
array([100, 99, 91])
score[:-1, :-3]
array([[100, 99], [ 95, 85]])
score[:-1, :-3] = 100
score[:-1, :-3]
array([[100, 100], [100, 100]])
操作数据非常方便。
还记得数组的形状是什么吗?
score.shape
(3, 5)
(3, 5)
表示这是3行5列的二维数组。
如果现在想得到一个5行3列的二维数组呢?
# Returns an array containing the same data with a new shape
score.reshape([5, 3])
array([[100, 100, 91], [ 85, 90, 100], [100, 88, 81], [ 88, 85, 81], [ 80, 78, 86]])
score本身的形状有变化吗,看看此时的score啥样?
score
array([[100, 100, 91, 85, 90], [100, 100, 88, 81, 88], [ 85, 81, 80, 78, 86]])
如果想就地修改score的形状,应该使用resize()
:
# Change shape and size of array in-place
score.resize([5, 3])
score
array([[100, 100, 91], [ 85, 90, 100], [100, 88, 81], [ 88, 85, 81], [ 80, 78, 86]])
如果想转置数组呢(即数组的行、列进行互换)?
score.T
array([[100, 85, 100, 88, 80], [100, 90, 88, 85, 78], [ 91, 100, 81, 81, 86]])
主意:调用数组的转置后,score
本身并没有改变,如下:
score
array([[100, 100, 91], [ 85, 90, 100], [100, 88, 81], [ 88, 85, 81], [ 80, 78, 86]])
# Find the unique elements of an array
# Returns the sorted unique elements of an array
np.unique(score)
array([ 78, 80, 81, 85, 86, 88, 90, 91, 100])
score.dtype
dtype('int64')
# Copy of the array, cast to a specified type.
score.astype(np.float64)
array([[100., 100., 91.], [ 85., 90., 100.], [100., 88., 81.], [ 88., 85., 81.], [ 80., 78., 86.]])
如果想操作符合某些条件的数据,应该怎么做?
# 成绩是否及格(60分及以上为及格)
score >= 60
array([[ True, True, True], [ True, True, True], [ True, True, True], [ True, True, True], [ True, True, True]])
# 成绩是否优秀(90分及以上为优秀)
score >= 90
array([[ True, True, True], [False, True, True], [ True, False, False], [False, False, False], [False, False, False]])
给满足条件的数据赋值。
# 给及格的同学都加上5分
score[score >= 60] += 5
score
array([[105, 105, 96], [ 90, 95, 105], [105, 93, 86], [ 93, 90, 86], [ 85, 83, 91]])
# 分数不允许超过满分(即100)
score[score > 100] = 100
score
array([[100, 100, 96], [ 90, 95, 100], [100, 93, 86], [ 93, 90, 86], [ 85, 83, 91]])
# 前面2个同学是否都满分
np.all(score[:2] >= 100)
False
# 前面2个同学是否有满分的
np.any(score[:2] >= 100)
True
# 分数大于90且小于95的置为1,否则为0
np.where(np.logical_and(score > 90, score < 95), 1, 0)
array([[0, 0, 0], [0, 0, 0], [0, 1, 0], [1, 0, 0], [0, 0, 1]])
# 分数为100或者小于90置为1,否则为0
np.where(np.logical_or(score == 100, score < 90), 1, 0)
array([[1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 0, 1], [1, 1, 0]])
如果想统计分数的最大值、最小值、平均值、方差,该怎么做?
上面演示过程中,把成绩都弄乱了。这里先恢复一下最开始的数据。
score = np.array(
[[92, 99, 91, 85, 90],
[95, 85, 88, 81, 88],
[85, 81, 80, 78, 86]])
score
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
# 每门课的最高分(axis=0表示按照列的维度去统计)
np.max(score, axis=0)
array([95, 99, 91, 85, 90])
# 每个学生的最高分(axis=1表示按照行的维度去统计)
np.max(score, axis=1)
array([99, 95, 86])
如果想知道每门课最高分对应的是哪个同学,怎么办?
# 每门课的最高分对应的学生(即下标)
np.argmax(score, axis=0)
array([1, 0, 0, 0, 0])
其他统计函数也都类似。
# 每门课的最低分
np.min(score, axis=0)
array([85, 81, 80, 78, 86])
# 每门课的平均分
np.mean(score, axis=0)
array([90.66666667, 88.33333333, 86.33333333, 81.33333333, 88. ])
# 每门课的中位数
np.median(score, axis=0)
array([92., 85., 88., 81., 88.])
# 每门课的方差
np.var(score, axis=0)
array([17.55555556, 59.55555556, 21.55555556, 8.22222222, 2.66666667])
# 每门课的标准差
np.std(score, axis=0)
array([4.18993503, 7.7172246 , 4.64279609, 2.86744176, 1.63299316])
arr = np.array([[1, 2, 3], [11, 22, 33]])
arr
array([[ 1, 2, 3], [11, 22, 33]])
arr * 10 + 1
array([[ 11, 21, 31], [111, 221, 331]])
通常对于两个numpy数组的相加、相减以及相乘都是对应元素之间的操作。
arr + arr
array([[ 2, 4, 6], [22, 44, 66]])
arr - arr
array([[0, 0, 0], [0, 0, 0]])
arr / arr
array([[1., 1., 1.], [1., 1., 1.]])
arr * arr
array([[ 1, 4, 9], [ 121, 484, 1089]])
数组在进行矢量化运算时,要求数组的形状是相等的。当两个数组的形状不相同的时候,可以通过扩展数组的方法来实现相加、相减、相乘等操作,这种机制叫做广播(broadcasting
)。
大学线性代数课程中讲过矩阵的知识。矩阵在这里可以看成二维数组。
矩阵乘法:(M行, N列) * (N行, L列) = (M行, L列)。计算过程如下图所示:
下面举一个简单的例子说明矩阵乘法的应用。
很多学科的最终成绩都是综合平时成绩与期末成绩得到的,即:
平时成绩 * 0.3 + 期末成绩 * 0.7 = 最终成绩
用矩阵乘法来计算就是:
看下在NumPy中如何计算矩阵乘法:
a = np.array([[80, 86],
[82, 80],
[85, 78],
[90, 90],
[86, 82],
[82, 90],
[78, 80],
[92, 94]])
b = np.array([[0.3], [0.7]])
np.matmul(a, b)
array([[84.2], [80.6], [80.1], [90. ], [83.2], [87.6], [79.4], [93.4]])
另外,np.dot
也可以计算矩阵乘法,如下:
np.dot(a, b)
array([[84.2], [80.6], [80.1], [90. ], [83.2], [87.6], [79.4], [93.4]])
与np.matmul
不同的是,np.dot
还可以与标量进行乘法运算:
np.dot(a, 2)
array([[160, 172], [164, 160], [170, 156], [180, 180], [172, 164], [164, 180], [156, 160], [184, 188]])