参考书籍:《Python数据科学手册》
NumPy库笔记精选
0.引言
不同类型间的数据存在明显的异构性,将所有数据简单地看作数字数组非常有助于我们理解和处理数据。 不管数据是何种形式,第一步都可将这些数据转换成数值数组形式的可分析数据。
正因如此,有效地存储和操作数值数组是数据科学中绝对的基础过程。而Python中专门用来处理这些数值数组的工具有:NumPy 包和 Pandas 包。
NumPy(Numerical Python 的简称)提供了高效存储和操作密集数据缓存的接口。在某些方面,NumPy 数组与 Python 内置的列表类型非常相似。但是随着数组在维度上变大,NumPy 数组提供了更加高效的存储和数据操作。NumPy 数组几乎是整个 Python 数据科学工具生态系统的核心。
1.Python中的数据类型
首先应了解Python 语言中数据数组是如何被处理的,并对比 NumPy 所做的改进,理解这个不同之处。
Python动态推断数据类型。Python 变量不仅是它们的值,还包括了关于值的类型的一些额外信息。
1.1 整型
标准的 Python 实现是用 C 语言编写的。这意味着每一个 Python 对象都是一个聪明的伪 C语言结构体。例如,定义 x = 1 时,x 是一个指针,指向一个 C 语言的复合结构体,结构体里包含了一些值。
C 语言整型本质上是对应某个内存位置的标签,里面存储的字节会编码
成整型。 而 Python 的整型其实是一个指针,指向包含这个 Python 对象所有信息的某个内存位置,其中包括可以转换成整型的字节。由于 Python 的整型结构体里面还包含了大量额外的信息,所以 Python 可以自由、动态地编码。 但是,Python 类型中的这些额外信息也会成为负担,在多个对象组合的结构体中尤其明显。
1.2 列表
为了获得灵活的类型,Python列表中的每一项必须包含各自的类型信息、引用计数和其他信息;也就是说,每一项都是一个完整的Python 对象。 一个特殊的例子,如果列表中的所有变量都是同一类型的,那么很多信息都会显得多余——将数据存储在固定类型(NumPy 式)的数组中应该会更高效。
Python 列表包含一个指向指针块的指针,这其中的每一个指针对应一个完整的 Python 对象。固定类型的 NumPy 式数组缺乏这种灵活性,但是能更有效地存储和操作数据。
1.3 Python中的固定类型数组
可通过array(内置模块)、numpy库实现,更实用的是 NumPy 包中的 ndarray 对象。
1.4 从Python列表创建数组
不同于 Python 列表,NumPy 要求数组必须包含同一类型的数据。 如果类型不匹配,NumPy 将会向上转换(如果可行)。此外,NumPy 数组可以被指定为多维。
1.5 从头创建数组(很多方法需要了解)
面对大型数组的时候,用 NumPy 内置的方法从头创建数组是一种更高效的方法。
如:zeros、arange、linspace、random.normal、empty……
1.6 NumPy标准数据类型
2.NumPy数组基础
基本的数组操作:属性、索引、切分、变形、拼接分裂。
2.1 属性
.ndim维度,.shape每个维度的大小,.size数组的总大小(维度相乘), itemsize每个数组元素字节大小的,nbytes数组总字节大小。
2.2 索引
如 name[2, -1] 或 name[2][-1] 索引多维。
当试图将一个浮点值插入一个整型数组时,浮点值会被截短成整型。并且这种截短是自动完成的,不会给你提示或警告。
2.3 切片
NumPy 切片语法和 Python 列表的标准切片语法相似。x[start:stop:step]。(步长可为负)
多维用逗号分开维度
数组切片返回的是数组数据的视图,而不是数值数据的副本。 这一点也是 NumPy 数组切片和 Python 列表切片的不同之处:在Python 列表中,切片是值的副本。 NumPy中,如果修改子数组,将会看到原始数组也被修改。
这种默认的处理方式实际上非常有用:它意味着在处理非常大的数据集时,可以获取或处理这些数据集的片段,而不用复制底层的数据缓存。
当然,也可以很简单地通过 copy() 方法创建数组的副本。
2.4 数组的变形
数组变形最灵活的实现方式是通过 reshape() 函数来实现。
如果希望该方法可行,那么原始数组的大小必须和变形后数组的大小一致。如果满足这个条件,reshape 方法将会用到原始数组的一个非副本视图。但实际情况是,在非连续的数据缓存的情况下,返回非副本视图往往不可能实现。
将一个一维数组转变为二维的行或列的矩阵,可以通过reshape 方法来实现,或者更简单地在一个切片操作中利用newaxis 关键字。
2.5 数组拼接和分裂(多个数组)
拼接:np.concatenate、np.vstack(垂直栈) 和 np.hstack(水平栈)。
分裂:np.split、np.hsplit 和 np.vsplit。(np.dsplit 将数组沿着第三个维度分裂。)
可以向以上函数传递一个索引列表作为参数,索引列表记录的是分裂点位置。
3.NumPy数组的计算:通用函数
3.1 缓慢的loop
Python 的默认实现(被称作 CPython)处理起某些操作时非常慢,一部分原因是该语言的动态性和解释性——数据类型灵活的特性决定了序列操作不能像 C 语言和 Fortran 语言一样被编译成有效的机器码。
CPython 在每次循环时必须做数据类型的检查和函数的调度。 如果我们在编译代码时进行这样的操作,那么就能在代码执行之前知晓类型的声明, 结果的计算也会更加有效率。
3.2 通用函数介绍
NumPy 为很多类型的操作提供了非常方便的、静态类型的、可编译程序的接口,也被称作向量操作。 这种向量方法被用于将循环推送至 NumPy 之下的编译层,这样会取得更快的执行效率。
NumPy 中的向量操作是通过通用函数实现的。通用函数的主要目的是对数组中的值执行更快的重复操作。
3.3 通用函数基本运算
一元通用函数对单个输入操作,二元通用函数对两个输入操作。
NumPy 能理解 Python 内置的运算操作符。
通用函数也可以处理复数。当处理复数时,绝对值返回的是该复数的幅度。
此外,还能进行三角函数、指数、对数等运算。
还有一个更加专用,也更加晦涩的通用函数优异来源是子模块scipy.special。如果希望对自己的数据进行一些更晦涩的数学计算,scipy.special 可能包含了需要的计算函数。(from scipy import special)
3.4 高级的通用函数特性
指定输出、聚合、外积、广播。
指定输出:在进行大量运算时,有时候指定一个用于存放运算结果的数组是非常有用的。所有的通用函数都可以通过 out 参数来指定计算结果的存放位置。由于可能创建临时数组储存计算结果,慎重使用 out 参数将能够有效节约内存。
聚合:reduce 方法会对给定的元素和操作重复执行,直至得到单个的结果。如果需要存储每次计算的中间结果,可以使用 accumulate。专用函数也可实现reduce功能。
外积:任何通用函数都可以用 outer 方法获得两个不同输入数组所元素对的函数运算结果。这意味着你可以用一行代码实现一个运算表。
广播:操作不同大小和形状的数组。
4.聚合
大多数的聚合都有对 NaN 值的安全处理策略(NaN-safe),即计算时忽略所有的缺失值,这些缺失值即特殊的 IEEE 浮点型 NaN 值.
5.广播
5.1 介绍
广播可以简单理解为用于不同大小数组的二进制通用函数(加、减、乘等)的一组规则。
可以理解为数组做了数值重复的拓展,但实际上并未发生。(额外的内存并没有在实际操作中进行分配)
5.2 规则
• 1:如果两个数组的维度数不相同,那么小维度数组的形状将会在最左边补 1。
• 2:如果两个数组的形状在任何一个维度上都不匹配,那么数组的形状会沿着维度为 1 的维度扩展以匹配另外一个数组的形状。
• 3:如果两个数组的形状在任何一个维度上都不匹配并且没有任何一个维度等于 1,那么会引发异常。
(如果希望实现右边补全,可以通过变形数组来实现)
5.3 应用
如便于数组归一化、画制二维函数……
6.比较、掩码和布尔逻辑
当你想基于某些准则来抽取、修改、计数或对一个数组中的值进行其他操作时,掩码就可以派上用场了。 例如你可能希望统计数组中有多少值大于某一个给定值,或者删除所有超出某些门限值的异常点。 在 NumPy 中,布尔掩码通常是完成这类任务的最高效方式。
6.1 统计记录的个数
x=[[5 0 3 3]
[7 9 3 5]
[2 4 7 6]]
# 有多少值小于6?
np.count_nonzero(x < 6)
True
# 另外一种实现方式是利用 np.sum,
np.sum(x < 6)
True
# 有没有值大于8?
np.any(x > 8)
False
# 是否所有值都小于10?
np.all(x < 10)
True
np.sum() ,np.all() 和 np.any()的好处是,和其他 NumPy 聚合函数一样,可以沿着行或列 (axis)进行
Python 有内置的 sum()、any() 和 all() 函数,这些函数在 NumPy 中有不同的语法版本。如果在多维数组上混用这两个版本,会导致失败或产生不可预知的错误结果。因此,确保在以上的示例中用的都是 np.sum()、np.any()和 np.all() 函数。
6.2 布尔运算符
np.sum((inches > 0.5) & (inches < 1))
# 降水量在 0.5 英寸 ~1 英寸间的天数
# 括号是非常重要的,因为有运算优先级规则
逐位的布尔运算符和其对应的通用函数:
6.3 将布尔数组作为掩码
In[26]: x
Out[26]: array([[5, 0, 3, 3],
[7, 9, 3, 5],
[2, 4, 7, 6]])
In[27]:x < 5
Out[27]: array([[False, True, True, True],
[False, False, True, False],
[ True, True, False, False]], dtype=bool)
In[28]: x[x < 5]
Out[28]: array([0, 3, 3, 3, 2, 4])
# 返回的是一个一维数组,它包含了所有满足条件的值。所有的这些值是掩码数组对应位置为 True 的值。
and 和 or 判断整个对象是真或假,而 & 和 | 是指每个对象中的比特位。
当你使用 and 或 or 时,就等于让 Python 将这个对象当作整个布尔实体; 当你对整数使用 & 和 | 时,表达式操作的是元素的比特,将 and 或 or 应用于组成该数字的每个比特.
and 和 or 对整个对象执行单个布尔运算,而 & 和 | 对一个对象的内容(单个比特或字节)执行多个布尔运算。对于 NumPy 布尔数组,后者是常用的操作
7.花哨索引
即传递一个 索引数组 来一次性获得多个数组元素。
利用花哨的索引,结果的形状与索引数组的形状一致,而不是与被索引数组的形状一致。
In[6]: row = np.array([0, 1, 2])
col = np.array([2, 1, 3])
X[row, col]
# 结果的第一个值是 X[0, 2],第二个值是 X[1, 1],第三个值是 X[2, 3]。
在花哨的索引中,索引值的配对遵循广播的规则。
7.1 组合索引
以上方法通常用于快速分割数据,即需要分割训练 / 测试数据集以验证统计模型时,以及在解答统计问题时的抽样方法中使用。
7.2 花哨索引的修改
i = [2, 3, 3, 4, 4, 4]
x = np.zeros(10)
x[i] += 1
print(x)
[ 0. 0. 1. 1. 1. 0. 0. 0. 0. 0.]
x = np.zeros(10)
np.add.at(x, i, 1)
print(x)
[ 0. 0. 1. 2. 3. 0. 0. 0. 0. 0.]
# at() 函数在这里对给定的操作、给定的索引(这里是 i)以及给定的值(这里是 1)执行的是就地操作。
# 另一个可以实现该功能的类似方法是通用函数中的 reduceat() 函数,
拓;算法效率并不是一个简单的问题。一个对大数据集非常有效的算法并不总是小数据集的最佳选择,反之同理。(相关:大 O 标记)
8.数组排序
8.1 NumPy中的快速排序:np.sort和np.argsort
如果想在不修改原始输入数组的基础上返回一个排好序的数组,可以使用 np.sort; 如果希望用排好序的数组替代原始数组,可以使用数组的 sort 方法。
argsort函数返回的是原始数组排好序的索引值,这些索引值可以被用于(通过花哨的索引)创建有序的数组。
也可通过 axis 参数,沿着多维数组的行或列进行排序:
要注意,这种处理方式是将行或列当作独立的数组,任何行或列的值之间的关系将会丢失。
8.2 部分排序:分隔
np.partition:函数的输入是数组和数字 K,输出结果是一个新数组,其中左边是前 K 小的值,往右是其他值。在这两个分隔区间中,元素都是任意排列的。 与排序类似,也可以沿着多维数组任意的轴进行分隔。
np.argpartition 函数计算的是分隔的索引值。
*9.结构化数据:NumPy的结构化数组
(有点像C语言结构体)
结构化数组的方便之处在于,你可以通过索引或名称查看相应的值。
利用布尔掩码,还可以做一些更复杂的操作,如按条件筛选。
如果你希望实现比上面更复杂的操作,可考虑使用 Pandas 包。
9.1 生成结构化数组
除了前面的字典方法,
拓:NumPy 的 dtype 直接映射到 C 结构的定义,因此包含数组内容的缓存可以直接在 C 程序中使用。如果你想写一个Python 接口与一个遗留的 C 语言或 Fortran 库交互,从而操作结构化数据,你将会发现结构化数组非常有用。