文章目录
NumPy 入门
Numpy (Numerical Python 的简称) 是数据科学学习中最重要的工具之一,它提供了高效存储和操作密集数据缓存的接口。Numpy 数组和 Python 内置的列表类型非常相似。但是随着数组维度变大,Numpy 数组提供了更高效的存储和数据操作。Numpy 数组几乎是整个 Python 数据科学工具生态系统的核心。
切记:Numpy 数组不同于 Python 列表,Numpy 要求数组必须包含同一类型的数据,如果类型不匹配,Numpy将会向上转换 (如果可行) ,这里整型会被转换为浮点型:
遵循传统,一般大多数人导入 Numpy 时会将 np 作为别名,更方便操作。
如果想获取更详细的文档及教程和其它资源,可以访问 http://www.numpy.org
1. Numpy 数组基础
1.1 Numpy 数组的属性
我们先用 Numpy 的随即数生成器设置一组种子,以确保每次程序执行时都可以生成同样的随即数组:
一般来说,可以认为 nbytes 数值等于 itemsize 与 size 的乘积大小
1.2 数组索引:获取单个元素
1.通过括号中指定的索引获取第 i 个值,(从0开始计数)
2.获取数组末尾的数值,可以用负值索引
3.在多维数组中,可以用逗号分隔的索引元组获取元素
4.也可以用以上索引方式修改元素值
和 Python列表不同,Numpy 数组是固定类型的,这意味着当你试图将一个浮点型值插入到一个整型数组时,浮点型浮点型会被截短成整型,并且这种截短是自动完成的,不会给你提示和警告!
1.3 数组切片:获取子数组
Numpy 切片语法和 Python 列表的标准切片语法相同,获取数组 x 的一个切片,可以用一下公式:x[start:stop:step]
,如果三个参数都没有被指定,那么它们分别会被设置成默认参数 start = 0,stop = 维度的大小,step = 1。
1.一维子数组
数组逆序切片操作,start 和 stop 参数默认是被交换的
2.多维子数组
3.获取数组的行和列(索引和切片组合实现)
用一个冒号(:)表示空切片:
在获取行时,为了使语法更简介,可以省略空的切片:
4.非副本视图的子数组
在上面我们每次进行数组切片操作后,其实返回的是数组数据的视图,而不是数值数据的副本。这一点也是 Numpy 数组切片和 Python 列表切片的不同之处,在 Python 列表中,切片返回的是值的副本,比如下面的二维数组:
如果要修改这个子数组,将会看到原始数组也会被修改!
这种默认的处理方式在实际中非常有用,因为这意味着在处理非常大的数据集时,可以在获取或处理其中的一些数据集片段的时候,不用去复制底层大量的数据缓存,从而提高效率!
5.创建数组的副本
虽然数组视图有些很好的特性,但是有时候明确的要复制数组里的数据或子数组,可以通过 copy() 方法实现:
1.4 数组的变形
数组变形最灵活的方式是通过 reshape() 函数来实现,例如将一维数组中的数字 1~9 放入一个 3x3 的矩阵中:
另外一个变形模式是将一个一维数组转变成二维的行或列的矩阵,既可以通过 reshape 实现,也可以更简单的在一个切片操作中利用 newaxis 关键字,利用 np.newaxis 可以增加数组的一个维度:
1.5 数组的拼接与分裂
1.数组的拼接
拼接或连接 Numpy 中的两个数组主要由 np.concatenate, np.vstack 和 np.hstack 实现。
np.concatenate 将数组元组或数组列表作为第一个参数:
np.concatenate 也可用于二维数组的拼接:
当沿着固定维度处理数组时,使用 np.vstack (垂直栈) 和 np.hstack (水平栈) 函数会更简洁:
同样 np.dstack() 将会沿着第三个维度拼接数组
2.数组的分裂
分裂可通过 np.split(), np.hsplit(), np.vsplit() 函数来实现,可以向以上函数传递一个索引列表作为参数,索引列表记录的是分裂点的位置,且 N 个分裂点会得到 N+1 个数组,比如下面 2 个分裂点得到了三个子数组:
np.vsplit() 和 np.hsplit() 的用法类似:
同样 np.dsplit() 会将数组沿着第三个维度进行分割
2. Numpy 数组的计算:通用函数
2.1 通用函数的介绍
Numpy 为很多类型的操作提供了非常方便的,静态类型,可编译程序的接口,也被称作
向量操作。你可以通过简单地对数组执行操作来实现,这里对数组的操作将会被用于数组中的每一个元素。这种向量方法被用于将循环推送至 Numpy 之下的编译层,这样会取得更快的执行效率。Numpy 中的向量操作是通过函数 通用函数 实现的。通用函数的主要的目的是对 Numpy 数组中的值执行更快操作。它非常的灵活,前面我们看过标量和数组的运算,但是也是对两个数组的运算:
通用函数并不仅限于一维数组的运算,它们也可以进行多维数组的运算:
通过 Numpy 的通用函数使用向量的方式进行计算几乎总比用 Python 循环实现的计算更加有效,尤其是当数组很大的时候,只要你看到 Python 脚本中有这样的循环,就应该考虑是否能用向量的方式替换这个循环。
2.2 探索 Numpy 的通用函数
通用函数有两种存在形式:一元通用函数 对单个输入操作,二元通用函数 对两个输入操作,以下将会对两种类型的操作进行示范。
1. 数组的运算
Numpy 通用函数的使用方式非常自然,因为和 Python 的原生运算符相同,标准的加,减,乘,除都可以使用:
表1.1: Numpy 实现的算术运算符
2. 绝对值
绝对值函数对应的函数是 np.absolute(), 该函数也可以用别名 np.abs()来访问:
3. 三角函数
Numpy 提供了大量好用的通用函数,其中对于数据科学家最有用的就是三角函数。首先定义一个角度数组:
反三角函数:
这些值是在机器精度内计算的,所以有些应该是 0 的值并没有精确到 0
4. 指数和对数
指数运算:
对数运算:
其中有些特殊的版本,对于非常小的输入值可以保持较好的精度。当 x 的数值很小时,以下函数计算出的值比 np.log() 和 np,exp() 的计算更精确:
5. 专用的通用函数
上面介绍的是平时用的比较多的基础通用函数,如果你想要进行一些晦涩的数学计算,可以使用 scipy.special 模块。该模块中包含了非常多的计算函数,你所需要的计算函数可能就在里面,下面会列举一些可能在统计学中会用到的函数:
2.3 通用函数的高级特性
1. 指定输出
在进行大量计算时,有时候指定一个用于存放运算结果的数组是非常有用的。不同于创建一个临时数组,你可以用这个特性将计算结果直接写入到你期望的储存位置 。所有的通用函数都可以通过 out 参数来指定计算结果的存放位置:
数组视图也可以用这个特性:
如果这里写的是 y = 2 ** x ,那么结果就是创建一个临时数组,该数组存放的是 2**x 的结果,并且接下来会将这些值复制到 y 数组中。对于上述计算量比较小的数组来说,这辆两种方式的差别并不大。但是对于较大的数组,通过慎重使用 out 参数将能够有效节约内存。
2. 聚合
二元通用函数有些非常有趣的聚合功能,这些聚合可以直接在对象上计算。什么意思呢?举个例子,如果我们希望用一个特定的运算 reduce 一个数组,那么可以用任何通用函数的 reduce 方法。一个 reduce 方法会对给定的元素和操作重复执行,直至得到单个的结果,什么意思呢?(通俗点讲,你可以根据自己的需要,通过函数方法将一行或一列元素,聚合成一个元素。二维数组聚合成一维数组,甚至聚合成一个元素)请看下面的例子:
在一些特殊情况中,Numpy 提供了专用的函数 (np.sum, np.prod, np.cumsum, np.cumprod ),它们也可以实现上面 reduce 方法,下面一节中将具体介绍。
3. 外积 (线性代数中一般指两个向量的张量积,结果是个矩阵)
任何通用函数都可以用 outer 方法获得两个不同输入数组所有元素对的函数运算结果,这意味着你也可以用一行代码实现一个乘法表:
2.4 聚合 (多维度聚合)
上一小节已经简单介绍了聚合的概念和使用,这一节将会详细介绍。
一种常见的聚合操作是沿着一行或一列聚合。例如,你有一些数据储存在二维数组中,默认情况下,每一个 Numpy 聚合函数将会返回对整个数组的聚合结果:
聚合函数还有一个参数,用于指定沿哪个维度的方向进行聚合。例如,可以通过指定 axis=0 找到每一列的最小值,axis=1 找到每一行的最大值:
其它语言的用户可能会对维度的指定方式比较困惑。axis 关键字指定的是 数组将会被折叠的维度, 而不是将要返回的维度。因此指定 axis=0 意味着第一个维度将要被折叠(对于二维数组,这意味着每一列的值将会被聚合)。
Numpy 中可用的聚合函数:
2.5 数组的计算:广播
前面我们介绍了 Numpy 如何通过通用函数的向量化操作来减少缓慢的 Python 循环,另一种向量化操作的方法是利用 Numpy 的广播功能。广播可以简单理解为用于不同大小数组的二进制通用函数(加,减,乘等)的一组规则。
1. 广播的介绍
对于同样大小的数组,二进制操作,是对相应的元素逐个计算:
广播允许这些二进制操作可以用于不同大小的数组。例如,可以简单地将一个标量(可以认为是一个 0 维的数组)和一个数组想加:
我们可以认为这个操作是将数值 5 扩展或复制到数组 [5, 5, 5],然后执行加法。Numpy 广播功能的好处是,这种对值的重复实际上并没有发生,但是这是一种很好用的理解广播功能的模型。
我们同样也可以将这个原理扩展到更高的维度。观察以下将一个一维数组和一个二维数组相加的结果:
这里这个一维数组就被扩展了或者说是广播了。它沿着第二维度扩展,扩展到匹配 M 数组的形状,再进行相加。
以上的这些例子理解起来相对容易,更复杂的情况会涉及到对两个数组的同时广播,例如以下示例:
正如之前将一个值扩展或者广播以匹配另外一个数组的形状,这里将 a 和 b都进行了扩展来匹配一个公共的形状,最终的结果是一个二维数组,下面是这些例子的广播几何可视化图:
浅色的盒子表示广播值。同时需要注意的是,这些额外的内存并没有在实际操作中进行分配,但是这样的思考方式更方便我们从概念上理解广播的含义。
2. 广播的规则
NumPy中的广播遵循一套严格的规则来确定两个数组之间的交互:
规则1:如果两个数组的维数不同,则维数较少的数组的形状将在其前(左侧)填充。
规则2:如果两个数组的形状在任何维度上都不匹配,则将在该维度上形状等于1的数组拉伸以匹配其他形状。
规则3:如果尺寸在任何维度上都不相同,且都不等于1,则会引发错误。
广播示例1
将一个二维数组与一个一维数组相加:
M = np.ones((2, 3))
a = np.arange(3)
接下来看两个数组的加法操作,两个数组的形状如下:
M.shape = (2, 3)
a.shape = (3,)
可以看到,根据规则 1 ,数组 a 的维度数更小,所以在其左边补 1 :
M.shape -> (2, 3)
a.shape -> (1, 3)
然后根据规则 2 ,第一个维度不匹配,因此扩展这个维度以匹配数组:
M.shape -> (2, 3)
a.shape -> (2, 3)
现在两个数组的形状匹配了,可以看到它们最终的形状都为 (2, 3):
广播示例2
下面我们来看一下两个数组均需要广播的示例:
a = np.arange(3).reshape(3, 1)
b = np.arange(3)
同样,首先写出两个数组的形状:
a.shape = (3, 1)
b.shape = (3,)
规则1告诉我们,需要用 1 将 b 的形状补全:
a.shape = (3, 1)
b.shape = (1, 3)
规则2告诉我们,需要更新这两个数组的维度来互相匹配:
a.shape = (3, 3)
b.shape = (3, 3)
因为结果匹配,所以这两个形状兼容的,可以看到以下结果:
广播示例3
现在来看两个数组不兼容的示例:
M = np.ones((3, 2))
a = np.arange(3)
和第一个示例略有不同的情况:矩阵 M 已转置。这如何影响计算?数组的形状是:
M.shape = (3, 2)
a.shape = (3,)
同样规则 1 告诉我们,a 数组的形状必须用 1 进行补全:
M.shape -> (3, 2)
a.shape -> (1, 3)
根据规则 2 ,a 数组的第一个维度进行扩展以匹配 M 的维度:
M.shape -> (3, 2)
a.shape -> (3, 2)
现在你需要用到规则 3 (最终形状还是不匹配),因此这两个数组是不兼容的。当我们执行运算时会看到以下结果:
请注意此处可能存在的混淆:你可能想通过在 a 数组的右边补 1,而不是左边补 1,从而让 a 和 M 的维度变得兼容。但这是广播规则不允许的!这种灵活性在某些情况下可能有用,但这也可能会导致歧义。如果您想要右侧填充,则可以通过数组变形来明确地做到这一点 (将会用到关键字 np.newaxis )
另外需要注意的是,这里仅用到 + 运算符,而这些广播规则对于 任意二进制通用函数 都是适用的。
2.6 比较 掩码和布尔逻辑
本节将会介绍使用布尔掩码来检查和操作 NumPy 数组中的值。当您要基于某些条件提取,修改,计数或以其他方式操纵数组中的值时,掩码就会派上用场。例如,您可能希望对大于某个值的所有值进行计数,或者可能删除高于某个值的所有异常值阈。在 NumPy 中,布尔掩码通常是完成这些类型任务的最有效方法。
1. 和通用函数类似的比较操作
这些比较的结果是一个布尔数据类型:
另外,利用符合表达式实现对两个数组的逐个元素比较也是可行的:
与算术运算符一样,比较运算符在 NumPy 中也是借助通用函数来实现的。例如,在编写时 x < 3,NumPy 在内部使用 np.less(x, 3)。这些比较运算符和其对应的通用函数如下:
和算术运算通用函数一样,这些比较运算通用函数也可以用于任意形状,大小的数组。下面是一个二维数组的例子:
2. 操作布尔数组
统计记录个数
**sum()**有着 和其它 Numpy 聚合函数一样的好处,可以沿着行或列进行
np.any 或 np.all 可以快速检查任意或者所有这些值是否为 True
np.all 和 np.any可以沿着特定的坐标轴
3. 将布尔数组作为掩码
2.7 花式索引
在前面的部分中,我们看到了如何使用简单索引(例如arr[0]),切片(例如arr[:5])和布尔掩码(例如arr[arr > 0])访问和修改数组的部分。在本节中,我们将介绍另一种样式的数组索引,即 花式索引 (fancy indexing)。花式索引就像我们已经看到的简单索引一样,但是我们传递索引数组来代替单个标量。这使我们能够非常快速地访问和修改数组值的复杂子集。
1. 探索花式的索引
花式索引可以通过传递一个索引数组来一次性获得多个数组元素。
方法一:
方法二 (通过传递索引的单个列表或数组来获得同样的结果):
利用花式索引,结果的形状与 索引数组 的形状一致,而不是与 被索引数组 的形状一致:
花式索引也对多个维度数组适用 (第一个索引指行,第二个索引指列):
结果中的第一个值为X[0, 2],第二个为X[1, 1],第三个为X[2, 3]。花式索引中的索引配对遵循“阵列计算:广播”中提到的所有广播规则。因此,例如,如果我们在索引内合并列向量和行向量,则将获得二维结果 (每个行值都与每个列向量匹配):
2. 组合索引
花式索引和简单的索引组合使用:
花式索引和切片的组合使用:
花式索引和掩码的组合使用:
3. 花式索引修改值
不过需要注意,操作中重复索引会导致一些出乎意料的结果产生
4去了哪里?此操作的结果是先分配 x[0] = 4,然后是 x[0] = 6。结果当然是 x[0] 包含值6。
以上还算合理,但是设想以下操作:
您可能希望其中 x[3] 的值为 2,并且 x[4] 的值为3,因为这是每个索引重复的次数。为什么不是这样?从概念上讲,这是因为 x[i] += 1 是 x[i] = x[i] + 1 的简写。x[i] + 1进行计算后,然后将结果赋值给 x 中的索引。记住这个原理后,我们却发现数组并没有发生多次累加,而是发生了赋值,显然这不是我们想要的结果。
那么,如果您想要重复操作的其他行为怎么办?为此,您可以使用 at() 方法(自NumPy 1.8起可用),然后执行以下操作:
at() 函数中 x 为被操作的数组,i 为给定的索引值,1 为执行操作的值。另一个可以实现该功能的类似方法是通用函数中的 reduceat() 函数,您可以在 NumPy 文档中阅读该方法。
2.8 数组排序
到目前为止,我们主要关注的是使用 NumPy 访问和处理数组数据的工具。本节介绍与 NumPy 数组中的值排序有关的算法,这些算法是计算机科学入门课程中最喜欢的主题。
1.Numpy 中的快速排序:np.sort 和 np.argsort
尽管 Python 具有内置功能 sort 和 sorted 可使用列表的功能,但由于NumPy 的 np.sort 功能对于我们的目的而言更加高效和有用,因此我们在此不进行讨论。默认情况下,np.sort 使用的是 快速排序,其算法的时间复杂度为 O(N log N),另外也可以选择 归并排序 和 堆排序 。
如果想在不修改原始输入数组的基础上返回一个排序好的数组,可以使用 np.sort :
如果希望用排序好的数组替代原始数组,可以使用数组的 sort 方法:
另外一个相关的函数是 argsort,该函数返回的是原始数组排序好的索引值:
此结果的第一个元素给出最小元素的索引,第二个值给出第二个最小元素的索引,依此类推。然后可以根据需要使用这些索引(通过花式索引)来构造排序后的数组:
(沿着行或列排序)
NumPy 排序算法的一个有用功能是能够使用axis参数沿多维数组的特定行或列进行排序。例如:
请记住,这会将每一行或每一列视为一个独立的数组,并且行或列值之间的任何关系都将丢失!
2. 部分排序:分区
有时我们不希望对整个数组进行排序,而只是想找到数组中的 k 个最小值。NumPy 在np.partition 函数中提供了此功能。np.partition函数的输入是一个数组和一个数字 K ; 结果是一个新的数组,最左边是第 K 小的值,往右是任意顺序的其它值:
请注意,结果数组中的前三个值是该数组中的三个最小值,其余数组位置包含其余值。在两个分区中,元素具有任意顺序。
沿着多维数组任意轴进行分隔
输出结果是一个数组,该数组每一行的前两个元素是该行的最小的两个值,每行的其它值分布在剩下的位置。
最后,就像有一个 np.argsort 计算排序的索引一样,有一个np.argpartition 计算分区的索引。
数据科学手册 Github 地址:https://jakevdp.github.io/PythonDataScienceHandbook/