04-数组运算,“懒人”必备

老子道德经有云:道生一,一生二,二生三,三生万物。说的是“道”创生万物的过程,即“道”生万物从少到多,从简单到复杂的一个过程。

衍生到我们的数学学习中来,我们学习了数据的创建,好比是“道生一,一生二”的过程,数组运算则蕴含了无穷的组合,有点类似“二生三,三生万物”的感觉。确实,数组运算,极大地拓展了我们使用数组来解决实际问题的能力,让我们有了掌控数组小宇宙的能力。

数组的运算看似很神秘,其实很简单,你不需要学习复杂的数学知识,在日常的使用中,跟普通的运算并无太大差别。

1. 数组与标量之间的运算

通常我们把单独的数叫做标量,数组可以直接与标量进行计算,计算逻辑会自动传播到数组的全部元素中。我们举几个简单的例子:

# 数组与标量的加法运算
import numpy as np
arr = np.array([[1,2,3], [4,5,6]])
arr + 5
Out: 
    array([[ 6,  7,  8],
           [ 9, 10, 11]])
# 数组与标量的乘法运算
arr * 2
Out: 
    array([[ 2,  4,  6],
           [ 8, 10, 12]])
# 数组与标量的除法运算,求数组中每个元素的倒数
1 / arr
Out: 
    array([[1.        , 0.5       , 0.33333333],
       [0.25      , 0.2       , 0.16666667]])
# 数组与标量的乘方运算,求数组中每个元素的平方
arr ** 2
Out: 
    array([[ 1,  4,  9],
           [16, 25, 36]], dtype=int32)
# 求数组中每个元素的算术平方根
arr ** 0.5
Out: 
    array([[1.        , 1.41421356, 1.73205081],
           [2.        , 2.23606798, 2.44948974]])

2. 数组的通用函数运算

所谓的通用函数 ufunc ,就是能够对数组中的每个元素进行微操,也就是元素级的函数运算。

  • 四则运算

最简单的通用函数就是数组与数组的四则运算。但是在进行数组的四则运算的时候,我们需要保证二者的维数一样:

# 数组的减法:
arr - arr
Out:
    array([[0, 0, 0],
           [0, 0, 0]])
# 数组的乘法:
arr * arr
Out: 
    array([[ 1,  4,  9],
           [16, 25, 36]])

需要注意的是,这里的乘法是表示数组对应位置的元素相乘,并不是高等数学上的矩阵的乘法。当然了数组的加法和除法规则都类似,这里不一一举例了。

事实上,Numpy 也封装了针对四则运算的函数,这里我们以数组的通用乘法为例,如下:

# 数组乘法的另一种写法,效果与*星号乘法一致:
np.multiply(arr, arr)
Out: 
    array([[ 1,  4,  9],
           [16, 25, 36]])

总的来讲,Numpy 针对常见的数组之间的运算,做了一些函数封装,一起看一下:

函数说明
add, subtract, multiply, divide算术四则运算,分别对应加、减、乘、除

根据通用函数所能接纳的参数的个数,我们常常又把通用函数分为一元函数和二元函数。

  • 一元函数

对数组进行四舍五入是一个典型的一元函数,举例如下:

# 创建一个符合均值为5标准差为10的正态分布数组
arr_rnd = np.random.normal(5, 10, (3, 4))
arr_rnd
Out:
    array([[19.03116154, 13.58954268, 11.93818701,  4.85006153],
           [ 0.57122874,  4.33719914,  8.67773155, 10.15552974],
           [ 7.04757778,  6.98288594, 10.60656035, 17.95555988]])
# 对数组进行四舍五入运算。需要注意的是,结果数组仍然保留了输入数组的dtype属性
np.rint(arr_rnd)
Out:
    array([[19., 14., 12.,  5.],
           [ 1.,  4.,  9., 10.],
           [ 7.,  7., 11., 18.]])

对数组求三角函数、求平均、求幂运算等均输入一元函数的范畴,我们把常用的一元函数列举如下,供大家查阅:

函数说明
abs,fabs计算绝对值,对于非负数值,可以使用更快的 fass
sqrt,square,exp求个元素的平方根、平方、指数 ex
log,log10,log2,log1p分别计算自然对数(底数为 e)、底数为 10 的 log、底数为 2 的 log、log(1+x)
sign计算各元素的正负号:1(整数)、0(零)、-1(负数)
ceil计算各元素的、大于等于该值的最小整数
floor计算各元素的、大于等于该值的最大整数
rint将各元素值四舍五入到最接近的整数,并保留 dtype
modf把数组的小数和整数部分以两个独立的数组分别返回
isnan判断各元素是否为空 NaN,返回布尔型
cos,cosh,sin,sinh,tan,tanh普通型和双曲型三角函数
  • 二元函数

除了四则运算,对两个数组元素进行判断,是一个典型的二元函数,举例如下:

# 利用随机函数,产生2个数组
x = np.random.normal(5, 10, (3,1))
y = np.random.normal(5, 10, (3,1))
x
Out: array([ 9.5336068 ,  8.31969942, 15.20601081])
y
Out: array([22.52827938,  3.01609475,  9.03514098])
# 计算,比较元素级的最大值
np.maximum(x,y)
Out: array([22.52827938,  8.31969942, 15.20601081])
# 计算,比较元素级的最小值
np.minimum(x,y)
Out: array([9.5336068 , 3.01609475, 9.03514098])
# 计算,执行元素级的比较
np.greater(x,y)
Out: array([False,  True,  True])

我们把常用的二元函数列举如下,供大家查阅:

函数说明
maximum,fmax计算元素级的最大值,fmax自动忽略空值NaN
minimum,fmin计算元素级的最小值,fmin自动忽略空值NaN
greater,greater_equal执行元素级的比较,生产布尔型数组。效果相当于>,≥
less,less_equal执行元素级的比较,生产布尔型数组。效果相当于<,≤
equal,not_equal执行元素级的比较,生产布尔型数组。效果相当于==,!=
logical_and,logical_or,logic_xor执行元素级的逻辑运算,相当于执行运算符&、

这里要提醒朋友们的是,数组运算本身并不复杂,只是套用公式的过程。但是在使用之前,大家千万要注意数组中是否有空值,空值的存在可能会导致运算结果错误甚至是报错。判断数组是否存在空值,需要使用 isnan 函数。

3. 数组的线性代数运算

  • 矩阵乘法

线性代数(例如矩阵乘法、矩阵分解、行列式以及其他数学函数)是任何数据分析库的重要组成部分。Numpy 也提供这样的能力,例如我们上面学习了若干中矩阵的元素级乘法,那么如何进行线性代数的乘法呢?其实很方便:

# 矩阵的乘法,输入的2个数组的维度需要满足矩阵乘法的要求,否则会报错;
# arr.T表示对arr数组进行转置
# np.dot表示对输入的两个数组进行矩阵乘法运算
np.dot(arr, arr.T)
Out:
    array([[14, 32],
           [32, 77]])
  • numpy.linalg 工具

numpy.linalg 中封装了一组标准的矩阵分解运算以及诸如逆运算、行列式等功能。我们一起简单看一下。

## 利用inv函数,求解矩阵的逆矩阵(注意:矩阵可变,首先必须是方阵)
# 第一步:导包
from numpy.linalg import inv
arr_lg = np.array([[0, 1, 2], [1, 0, 3], [4, -3 ,8]])
arr_inv = inv(arr_lg)
arr_inv
Out:
    array([[-4.5,  7. , -1.5],
           [-2. ,  4. , -1. ],
           [ 1.5, -2. ,  0.5]])
# 测试:矩阵与其本身逆矩阵相乘,结果应该为单位矩阵
np.dot(arr_lg, arr_inv)
Out:
    array([[1., 0., 0.],
           [0., 1., 0.],
           [0., 0., 1.]])

numpy.linalg 中的函数 solve 可以求解形如 Ax = b 的线性方程组,其中 A 为矩阵,b 为一维数组,x 是未知变量。

# 求解如下方程组的解:
# x-2y+z=0
# 2y-8z=8
# -4x+5y+9z=-9
# 导入solve函数
from numpy.linalg import solve
A = np.array([[1, -2, 1], [0, 2, -8], [-4, 5, 9]])
b = np.array([0,8,-9])
X = solve(A,b)
X
Out:array([29., 16.,  3.])
# 测试 AX=b
np.equal(np.dot(A, X), b)
Out: array([ True,  True,  True])

numpy.linalg 中还封装了一些其它函数,这里就不一一列举了,大家可以参考下表,根据需要选择合适的函数:

函数说明
diag以一维数组的形式返回方阵的对角线(或非对角线)元素,或将一维数组转换为方阵
trace计算对角线元素的和
det计算矩阵行列式
eig计算方阵的本征值和本征向量
inv计算方阵的逆
pinv计算矩阵的 Moore-Penrose 伪逆
qr计算 QR 分解
svd计算奇异值分解(SVD)
solve解线性方程
lstsq计算 Ax=b 的最小二乘解

4. 数组的聚合函数运算

聚合函数是指对一组值(比如一个数组)进行操作,返回一个单一值作为结果的函数,比如求数组所有元素之和就是聚合函数。常见的聚合函数有:求和,求最大最小,求平均,求标准差,求中位数等。

  • 常用的聚合函数

常用的聚合函数一览表:

函数说明
sum求和运算
cumsum累积求和运算
min求最小值
max求最大值
mean求均值
median求中位数
var求方差
std求标准差

看着似乎挺简单,我们看一个案例,对上一步产生的arr_rnd进行求最大值:

np.max(arr_rdn)
Out: 19.031161536100853

但是在实际工程中,我们通常把若干个样本组成一个数组进行运算,比如arr_rnd的大小为3×4,我们可以将之视为由3个样本组成,每个样本是长度为4的水平向量。那么我希望对样本的4个维度为别求最大值,应该如何操作?

这里我们引入运算方向的概念。

5. Numpy 运算方向 axis 详解

为了简化问题,我们主要考虑二维数组的场景,毕竟这已经可以覆盖我们绝大部分的场景了。

在前一小节,为了可视化切片过程,我们把二维数组的垂直方向定义为 axis 0 轴,水平方向为 axis 1 轴。当我们在对 Numpy 进行运算时,我们把 axis=0 指定为垂直方向的计算,axis=1 指定为水平方向的运算。

那么,显然,如果我们希望解决上面的疑问,规定求最大值的方向为垂直方向即可,即 axis=0

np.max(arr_rnd, axis=0)
Out: array([19.03116154, 13.58954268, 11.93818701, 17.95555988])

6. 小试牛刀:面向数组的编程方式

经过上面的步骤,朋友们对数组极高的运算效率有了初步理解,利用Numpy中封装好的函数,可以省去我们写循环的繁杂步骤,我们把这种利用数组执行批量计算,而省去编写循环的过程,叫做矢量化。

  • 利用函数解决一些问题

在实际应用中,我们面临的场景很可能不是一两个简单的函数能够解决的。比如投票,为了降低个人偏见的影响以保证比赛结果的客观公平,通常我们会去除一个最高分、去除一个最低分,并把剩下的打分的均值作为选手的最后成绩。

# 利用自带的随机数生成函数生成5位选手的评委打分结果,一共有7位评委。打分结果用5×7大小的数组表示
votes = np.random.randint(1, 10, (5, 7))
votes
Out: array([[8, 6, 4, 1, 6, 1, 5],
           [7, 6, 1, 9, 2, 1, 5],
           [9, 6, 8, 9, 3, 5, 4],
           [8, 5, 8, 4, 7, 9, 7],
           [6, 2, 1, 2, 1, 8, 3]])
# 总分-最高分-最低分,再求平均,即可求得最终结果
(np.sum(votes, axis=1)-np.max(votes, axis=1)-np.min(votes, axis=1))/5
Out: array([4.4, 4.2, 6.4, 7. , 2.8])

那么对于无法通过简单函数解决的呢?我们简单介绍两个方法:

  • 利用Numpy实现条件判断

条件判断是在计算领域非常常见的一种场景。例如我希望对上面产生的 arr_rnd的数据网格进行判断,如果数据元素小于等于5,则替换成 NaN。其实利用 np.where 函数轻松实现:

# where 函数中输入3个参数,分别是判断条件、为真时的值,为假时的值
# 在Numpy中,空值是一种新的数据格式,我们用np.nan产生空值
np.where(arr_rnd<5, np.nan, arr_rnd)
Out: array([[19.03116154, 13.58954268, 11.93818701,         nan],
           [        nan,         nan,  8.67773155, 10.15552974],
           [ 7.04757778,  6.98288594, 10.60656035, 17.95555988]])
  • np.frompyfunc

如果还是找不到合适的函数来实现自己的目的,那不妨自己写一个,也很简单。我们只需要利用 frompyfunc 函数,将计算单元素的函数转换成,能对数组的每个元素进行操作的函数即可。

举个简单栗子:

假设某淘宝店做批发生意,店里的人气产品 A 原价为 20 元。购买 100 件及以上,打 6 折;购买50件及以上,不到 100 件,打 8 折;购买 10 件及以上,不满 50 件,打 9 折;不满 10 件不打折。已知某天 5 位客户的具体购买量,求当天的营业额。方法有很多,那么今天这里给大家推荐一种基于数组的计算方法:

# 定义函数,购买x件订单,返回订单金额
def order(x):
    if x>=100:
        return 20*0.6*x
    if x>=50:
        return 20*0.8*x
    if x>=10:
        return 20*0.9*x
    return 20*x


# frompyfunc函数有三个输入参数,分别是待转化的函数、函数的输入参数的个数、函数的返回值的个数
income = np.frompyfunc(order, 1, 1)
# order_lst 为5位顾客的下单量
order_lst = [600, 300, 5, 2, 85]
# 计算当天的营业额
np.sum(income(order_lst))
Out: 12300.0

7. 总结

学完了数组的运算,Numpy 的初阶内容基本学完了。本章节主要是对数组运算中常用的函数做了系统的简介,包括:数组的通用函数、线性代数、聚合函数等,并有若干个实战 demo 供朋友们学习。

本节涉及的函数较多,在学习中大家切勿以死记硬背的心态介入学习。在数据分析学习阶段,应该是以理解加实践为主,至于函数太多记不住,那就交给时间去解决吧,用多了自然就记住了。对于偏门的函数,在应用的时候查阅即可。

在下一节,将给大家就数组的切片、视图、拷贝等概念进行初步的探究,并为大家在后续的学习中打好基础。请参考下一小节:《进阶必读:引用、拷贝与视图》。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI悦创|编程1v1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值