NumPy是Numerial Python(数字化Python)的简写,是Python中数值计算的基础包之一。许多提供科学功能的计算包使用Numpy的数组对象作为一个数据交换的标准接口通用语言。关于Numpy的大部分知识也可以转移到pandas上。
以下是你将在NumPy中找到的一些内容:
- ndarray,一种高效的多维数组,提供快速的面向数组的算术运算和灵活的广播功能
- 用于对整个数组数据进行操作的数学函数,无需编写循环
- 用于将数组数据读取/写入磁盘以及处理内存-映射文件的工具
- 线性代数、随机数生成和傅立叶变换功能
- 用于将NumPy与C、C++或FORTRAN编写的库连接起来的C API
由于NumPy提供了一个全面且具有良好文档的C API,因此将数据传递到用底层语言编写的外部库,以及外部库将数据作为NumPy数组返回到Python是简单的。这个特性使得Python成为包装旧版C,C++或FORTRAN代码库并为其提供动态且可访问界面的首选语言。
虽然NumPy本身不提供建模或科学计算,但了解NumPy数组和面向数组的计算将帮助你更有效地使用具有数组计算语义的工具,如pandas。由于NumPy是一个很大的主题,我将在稍后更深入地介绍许多高级NumPy功能(见附录A:高级NumPy),例如广播。
对大多数数据分析应用程序,将重点介绍的主要功能有:
- 基于数组的快速操作,用于数据整理和清理、子集化和过滤、转换以及任何其它类型的计算
- 常见数组算法,如排序、唯一化和集合操作
- 高效的描述性统计和聚合/汇总数据
- 用于合并和连接异构数据集的数据对齐和关系数据操作
- 将条件逻辑表达为数组表达式,而不是带有if-elif-else的循环
- 按组进行数据操作(聚合、转换和函数应用)
虽然NumPy为一般的数值数据提供了计算基础,但很多读者希望使用pandas作为很多统计和分析的基础,特别是在表格数据上。此外,pandas还提供了一些特定领域的函数,如时间序列操作,这在NumPy中是不存在的。
NumPy对于Python中的数值计算如此重要的原因之一是,它旨在提高大型大型数组数据的效率。造成这样情况的原因有很多:
- NumPy在内部将数据存储在连续的内存块中,独立于其他内置的Python对象。NumPy中的用C语言编写的算法库可以对此内存进行操作,而无需任何类型检查或其他开销。NumPy数组使用的内存也比其他内置的Python序列少得多
- NumPy操作对整个数组执行复杂的计算,而无需Python循环,这对大型序列来说可能很慢。NumPy比常规Python代码更快,因为它基于C的算法避免了常规解释Python代码存在的开销。
为了了解性能差异,考虑一个包含一百万整数的NumPy数组以及等效的Python列表:
import numpy as np
my_arr = np.arrange(1_000_000)
my_list = list(range(1_000_000))
# 将两个序列都乘以2
# 利用ipython中的%timeit魔术方法,可以方便地计算单个执行语句(或方法)的效率
%timeit my_arr2 = my_arr * 2
%timeit my_list2 = [x * 2 for x in my_list]
可以发现,基于NumPy的算法通常比纯Python算法快10到100倍(或更多),并且使用的内存要少得多。
4.1NumPy ndarray:一个多维数组对象
NumPy的主要功能之一是它的N维数组对象,ndarray,它是Python中的大型数据集的快速,灵活的容器。数组使你能够使用与标量元素之间的运算类似的语法对整个数据块执行数学运算。
# 导入NumPy并创建一个数组
import numpy as np
data = np.array([[1.5, -0.1, 3], [0, -3, 6.5]])
# 对数组执行数学运算
data * 10
data + data
# ndarray的元素必须是同一类型。
# 每一个数组都有属性shape,它是一个指示每个维度大小的元组。
# 每一个数组都有属性dtype,它是描述数组内元素类型的对象
data.shape # 返回元组(2,3)
data.dtype # 返回dtype('float64')
本书所指的所有’数组’、‘NumPy数组’或’ndarray’,多数情况下都是指ndarray对象。
创建ndarray
# 创建数组的最简单方法就是使用array函数。它接受任何类似序列的对象(包括其他数组),并生成包含传递数据的新NumPy数组。例如,列表就是好的候选项:
data1 = [6, 7.5, 8, 0, 1] # 创建列表
arr1 = np.array(data1) # 使用array函数将列表转化为ndarray
# 嵌套序列(如等长列表的列表)将被转换为多维数组:
data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
arr2 = np.array(data2) # 被转化为二维数组
# 可以通过ndim属性和shape属性来查看ndarray的维度数量和各维度大小
arr2.ndim # 返回2
arr2.shape # 返回(2, 4)
# 除非显式指定(在ndarrays的数据类型中将会被讨论),否则numpy.array会尝试推断它所创建的数组元素的好的数据类型
arr1.dtype # 返回dtype('float64')
arr2.dtype # 返回dtype('int64')
# 还有别的方法可以创建新数组:numpy.zeros和numpy.ones分别用于创建给定长度或形状的全为0或全为1的数组。numpy.empty创建一个数组,而不将其值初始化为任何特定值
np.zeros(10) # 创建长度为10的全为0的数组
np.zeros((3, 6)) # 创建3*6的全为0的数组
np.empty((2, 3, 2)) # 创建2*3*2的数组,其中的值可能为任意值
# 注意:假设numpy.empty会返回一个全为0的数组是不安全的。它会返回未初始化的内存,因此可能包含非零'垃圾值'。应当在打算用数据填充新数组时候才使用这个函数,
# numpy.arrange是内置的Python的range函数的数组值版本:
np.arrange(15) # 返回array([0, 1, 2, 3, 4, ... 14])
下表是有关标准数组创建函数的简短列表。由于NumPy专注于数值计算,因此数据类型(如果未指定)在许多情况下将为float64(浮点)。
ndarrays的数据类型
数据类型或dtype是一个特殊对象,其中包含ndarray需要将内存块解释为特定类型数据所需要的信息(或元数据、有关数据的数据)
arr1 = np.array([1, 2, 3], dtype=np.float64)
arr2 = np.array([1, 2, 3], dtype=np.int32)
arr1.dtype # dtype('float64')
arr2.dtype # dtype('int32')
数据类型是NumPy与来自其他系统的数据进行交互的灵活性的来源。在大多数情况下,它们提供直接到底层磁盘或内存表示的映射,这使得在磁盘上读取和写入二进制数据流成为可能,并连接到用C或FORTRAN等低级语言编写的代码。数值数据类型以相同的方式命名:一个类型名,如float或int,后跟一个数字,指示每个元素对应的比特位。标准的双精度浮点数值(Python的float对象)占用8个字节或64比特位。因此,此类型在NumPy中被称为float64。
不要担心记忆NumPy数据类型。通常只需要关心你正在处理的一般数据类型,无论是浮点数,复数,布尔类型,字符串还是一般Python对象。当你需要更好地控制数据在内存和磁盘上的存储方式时(尤其是大型数据集),最好确定你能控制存储的数据类型。
下表是NumPy支持的数据类型的完整列表。
# 用astype方法将数组从一种数据类型转化为另一种数据类型
arr = np.array([1, 2, 3, 4, 5])
arr.dtype # 返回dtype('int64')
floar_arr = arr.astype(np.float64) # 将arr的数据类型转化为np.float64
float_arr # 返回arrary([1., 2., 3., 4., 5.])
float_arr.dtype # 返回dtype('float64')
# 注意:如果是将浮点数转化为整数数据列星,小数部分会被截断
# 使用astype将表示数字的字符串数组,转换为数字形式
numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)
numeric_strings.astype(float) # 返回array([1.25, -9.6, 42.])
# 使用numpy.string_要小心,因为NumPy中的字符串数据是固定大小的,并且可能在没有警告的情况下截断输入。pandas对非数字数据具有更直观的开箱即用行为
# 如果转换由于某些原因(如一个字符串无法转换为float64)而失败,将会引发ValueError。
# 也可以使用别的数组的dtype属性来作为astypede的参数
# 还有速记类型代码字符串也可以被用来指代dtype:
zeros_units32 = np.zeros(8, dtype='u4') # 创建长为8的值全为0的ndarray,数值类型为32位无符号整型
注意:调用astype函数始终会创建一个新数组(数据的副本),即使新数据类型与旧数据类型相同也是如此。
使用NumPy数组的算术
我们能够在不写入任何for循环的情况下对数组进行批处理操作。NumPy用户称之为“矢量化”。大小相同的数组之间的任何算术运算都会逐元素应用该运算:
arr = np.array([[1., 2., 3.], [4., 5., 6.]])
arr # 返回array([[1., 2., 3.], [4., 5., 6.]])
arr * arr # 返回array([[1., 4., 9.], [16., 25., 36.]])
arr - arr # 返回array([[0., 0., 0.], [0., 0., 0.]])
# 使用标量的算术运算符能将标量参数传播到数组中的每个元素
1 / arr # 每个元素取倒数
arr ** 2 # 每个元素求平方
# 相同大小的数组之间的比较产生布尔数组
arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
arr2 > arr
不同大小的数组之间的操作称为“广播”,它将在附录A:高级NumPy中被更详细地讨论。
基本索引和切片
# 对一维数组的索引和切片
arr = np.arrange(10)
arr[5] # 索引
arr[5:8] # 切片
arr[5:8] = 12 # 将标量赋值给切片,会将标量的值广播到整个选区
注意:与Python内置列表的切片不同的是:数组切片是原始数组上的视图。这意味着不会复制数据生成新的对象,而且对视图的任何修改都将直接反应在原数组中。
arr_slice = arr[5:8] # 返回array([12, 12, 12])
arr_slice[1] = 12345 # 修改视图中的值
arr # 返回array([0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])
# [:]将分配给数组中的所有值:
arr_slice[:] = 64
arr # 返回array([0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
如果NumPy始终复制数据,其性能和内存将有巨大问题。正是它采用视图的方式,使得NumPy可以处理非常大的数组。
如果需要ndarray的切片的副本而不是视图,则需要显式复制数组-例如,arr[5:8].copy()。正如你所见,pandas也是这么工作的。
# 使用高维数组,则索引处的元素不再是标量,而是一维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d[2] # 返回第2行(从0开始记)对应的数组:array([7, 8, 9])
# 可以通过递归方式访问更细粒度的元素。也可以传递逗号分隔的索引列表来访问
arr2d[0][2] # arr2d[0, 2]是等效的,功能都是访问第0行第二列的元素3
NumPy数组的这种多维索引语法不适用于常规Python对象,例如列表。
使用切片索引
# 传递多个切片,arr2d为3*3的数组
arr2d[:2, 1:] # 返回相同数量维度的数组视图:这里返回第0-1行、第1-2列的二维数组,array([[2, 3], [5, 6]])
# 通过混合整数和切片,可以获得较低维度的数组
lower_dim_slice = arr2d[1, :2] # 返回第1行、第0-1列的一维数组:array([4, 0])
布尔索引
names = np.array(['Bob', ...])
data = np.array([[4, 7], ...])
names == 'Bob' # 返回由True和False组成的布尔数组
data[names == 'Bob'] # 在数组中传递此布尔数组会对数组进行选择
data[names == 'Bob', 1:] # 将布尔数组与切片混合进行选择:返回二维数组:array([[7], [0]])
data[names == 'Bob', 1] # 将布尔数组与整数混合进行选择:返回一维数组:array([7, 0])
# 要选择除了'Bob'外的所有内容,可以使用!=或者~:
names != 'Bob'
~(names == 'Bob')
data[~(names == 'Bob')]
# 注意:通过布尔索引从数组中选择数据并将结果分配给新变量, 则始终会创建数据的副本,即使返回的数组未更改也是如此。
Python关键字and和or并不能与布尔数组一起使用,必须改用&(代替 and)和|(代替or)。
data[data < 0] = 0 # 将data中的所有负值赋值为0
data[names != 'Joe'] = 7 # 将布尔数组为True的行的元素全赋值7
花式索引(Fancy Indexing)
arr = np.zeros((8, 4)) # 创建8行4列元素全为0的数组
for i in range(8): # 第i行的元素赋值为i(下标从0开始)
arr[i] = i
# 特定顺序选择行的子集
arr[[4, 3, 0, 6]] # 返回第4行、第三行、第0行、第六行组成的数组
arr[[-3, -5, -7]]
# 传递多个索引数组:选择相应的索引数组元素组成的元组对应的元素组成的一维数组
arr = np.arange(32).reshape((8, 4)) # 创建8行4列的数组,更多reshape方法的信息见附录A
arr[[1, 5, 7, 2], [0, 3, 1, 2]] # 返回第1行第0列、第5行第3列、第7行第1列,第2行第2列的元素组成的数组
arr[[1 ,5, 7, 2]][:, [0, 3, 1, 2]] # 先通过arr[[1 ,5, 7, 2]]获得第1、5、7、2行组成的数组,再通过[:, [0, 3, 1, 2]]将数组按照第0、3、1、2列排列
# 注意:使用和维数一样多的整数数组进花式索引的结果始终是一维的
# 注意:与切片不同,花式索引总是在讲结果分配给新变量时将复制数据到新数组中。如果使用花式索引分配值,则索引对应的值将被修改
转置数组和交换轴
arr = np.arange(15).reshape((3, 5)) # 创建3行5列的数组
# 转置数组
arr.T # 将数组转置为5行3列的数组
# 利用numpy.dot计算矩阵乘法,也可以用@来进行矩阵乘法
np.dot(arr.T, arr)
arr.T @ arr
# 用.T简单转置是交换轴的特殊情况。ndarray有方法swapaxes,它以一对轴号为输入并切换指示的轴来重新排列数据
arr # 5行3列的数组
arr.swapaxes(0, 1) # 将0-轴与1-轴互换
# 注意:swapaxes同样返回数据的视图而不进行复制
4.2伪随机数生成
numpy.random模块,在从多种概率分布中有效生成整个样本值的数组方面,补充了Python内置的random模块。
# 使用numpy.random.standard_normal函数从标准正态分布中获取4*4的样本数组
samples = np.random.standard_normal(size=(4, 4))
相比之下,Python的内置random模块一次只能对一个值进行采样。从下面的测试可以看出,numpy.random在生成非常大的样本的速度要比random快一个数量级
from random import normalvariate
N = 1_000_000
%timeit samples = [normalvariate(0, 1) for _ in range(N)] # 约492ms
%timeit np.random.standard_normal(N) # 约27.7ms
这些随机数是伪随机数,是由可配置的随机数生成器生成的,通过生成器确定要创建的值。函数numpy.random.standard_normal使用numpy.random模块的默认随机数生成器,但是我们传递seed参数配置使用显式的生成器:
rng = np.random.default_rng(seed=12345) # 创建seed为12345的随机数生成器rng:seed参数决定了生成器的初始状态
type(rng) # 返回随机数生成器:numpy.random._generator.Generator
data = rng.standard_normal((2, 3))
4.3通用函数:快速点对点数组函数
一个通用函数,或者ufunc,是对数组中数据进行点对点操作的函数。可以将它们视为简单函数的快速矢量化包装器,它们以一个或者多个标量为输入并产生一个或多个标量。
许多ufunc函数是简单的点对点转化,例如numpy.sqrt或numpy.exp
# 一元的ufunc,如numpy.sqrt或numpy.exp
arr = np.arange(10) # 生成长为10的数组
np.sqrt(arr) # 逐元素求根
np.exp(arr) # 关于e为底,逐元素求指数
# 二元的ufunc,如numpy.add或numpy.maximum,以两个数组为输入并返回单个数组作为结果
x = rng.standard_normal(8) # 用rng生成器从标准正态分布中抽取8个样本
y = rng.standard_normal(8) # 用rng生成器从标准正态分布中抽取8个样本
np.maximum(x, y) # 逐个元素比较大小并返回较大的值组成的数组
# 虽然不常见,但有的ufunc可以返回多个数组。如numpy.modf函数(它是Python内置函数math.mof的矢量化版本)能以浮点数为输入,返回小数和整数部分对应的数组
arr = rng.standard_normal(7) * 5 # 用rng生成器从标准正态分布中抽取8个样本,并对每个元素乘以5
remainder, whole_part = np.modf(arr) # 返回小数部分和整数部分对应的数组
# ufuncs接受一个可选参数out,该参数允许将结果分配到现有数组中,而不是创建一个新数组
arr # 长为7的数组
out = np.zeros_like(arr) # 生成形状和arr相同的值全为0的数组out,用于后续存放结果数组
np.add(arr, 1)
np.add(arr, 1, out=out) # 将np.add(arr, 1)的结果保存到现有数组out中
下表是一些一元通用函数
4.4使用数组进行面向数组编程
使用NumPy数组可将许多数据处理任务表达为简洁的数组表达式,否则可能需要编写循环。这种用数组表达式代替显式循环的做法被称为“矢量化(vectorization)”。通常,矢量化数组操作比纯Python等效项快得多。稍后,在附录A中,将解释广播,这是一种强大的矢量化计算方法。
# 假设希望在常规的值网络中计算函数sqrt(x^2+y^2)。则使用numpy.meshgrid函数以两个一维数组为输入,并生成两个二维数组,对应于所有(x, y)对
points = np.arange(-5, 5, 0.01) # -5到4.99之间的100个等距点
xs, ys = np.meshgrid(points, points) # 两个一维数组作为输入,生成两个100*100的数组,xs中的元素对应(x, y)的x,ys中的元素对应(x, y)中的y
z = np.sqrt(xs ** 2 + ys ** 2) # 计算函数
# 利用第九章中介绍的matplotlib函数来创建此二维数组的可视化
import matplotlib.pyplot as plt
plt.imshow(z, cmap=plt.cm.gray, extent=[-5, 5, -5, 5]) # 利用matplotlib中的imshow函数创建一个二维数组的函数值图像
plt.colorbar()
plt.title('Image ploot of $\sqrt{x^2 + y^2}$ for a grid of values')
# 注意:如果你在IPython中工作,则可以通过执行plt.close('all')命令关闭所有打开的绘图窗口
注意:矢量化在本书中用于描述一次对整个数据数组的操作,而不是使用Python的for循环逐个值进行操作。
将条件逻辑表示为数组操作
numpy.where函数是三元表达式"x if condition else y"的矢量化版本。
# 创建两个值数组和一个布尔数组,我们想创建一个数组,当cond数组对应的值为True时,新数组的对应值来自xarr,否则来自yarr
xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
cond = np.array([True, False, True, True, False])
# 列表生成式写法(这种写法的问题是:1.速度慢; 2.不适合多维数组)
result = [(x if c else y) for x, y, c in zip(xarr, yarr, cond)]
# 使用numpy.where函数实现同样的效果
result = np.where(cond, xarr, yarr)
# 注意:numpy.where的第二个参数和第三个参数不需要是数组;其中的一个或者两个都可以是标量。
# 假设你有一个随机生成数的矩阵,并且你想要将所有的正数代替为2,所有的负数代替为-2。则可以用numpy.where函数:
arr = rng.standard_normal((4, 4))
np.where( arr > 0, 2, -2)
# 只将所有正数替换为2
np.where(arr > 0, 2, arr)
数学和统计方法
一组用于计算有关整个数组或沿轴的数据的统计信息的数学函数,可以作为数组类的方法被访问。你可以通过调用数组实例方法或使用NumPy函数来使用如sum,mean和std等的聚合(有时候称为reductions(约简))函数。当你使用像numpy.sum这样的NumPy函数,你必须传递你想要聚合的数组作为第一个参数。
arr = rng.standard_normal((5, 4)) # 从标准正态分布中抽样,生成5行4列的数组
arr.mean() # 利用数组实例方法获得均值
np.mean(arr) # 利用NumPy函数获得均值
arr.sum() # 利用数组实例方法获得和
# 如mean和sum函数可以接受一个可选参数axis来计算在给定轴上的统计数据,从而生成低一个维度的数组:
arr.mean(axis = 1) # 沿着列计算均值,即相同行,不同列的元素用于计算得到一个均值
arr.sum(axis = 0) # 沿着行求和,即不同行,相同列的元素用于计算得到一个和
# 总结:axis设置为x,则沿着x进行操作,即对x轴下标不同,其他轴下标相同的元素做聚合操作
# 其他方法如cumsum和cumprod则不进行聚合,而是产生一个累加的数组
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
arr.cumsum() # 返回array([0, 1, 3, 6, 10, 15, 21, 28])
arr.cumsum(axis = 0) # 沿着行进行累加,对不同行,相同列的元素进行累计求和
arr.cumsum(axis = 1) # 沿着列进行累加,对相同行,不同列的元素进行累计求和
下表是基本数组统计方法。
布尔数组的方法
# 通常用sum函数来对布尔数组中的True值计数
arr = rng.standard_normal(100)
(arr > 0).sum() # 正值的个数
(arr <=0).sum() # 负值的个数
# 注意:表达式(arr > 0).sum()中"arr>0"外的括号是必须的
# 另外两个方法any和all对布尔数组尤其有用。any测试数组中是否存在一个或者多个值为True,all则检查数组中的值是否全为True
bools = np.array([False, False, True, False])
bools.any() # 返回True
bools.all() # 返回False
# 注意:这些方法也适用于非布尔数组,只不过在非布尔数组中,所有非零值都视为True
排序
# 与Python内置的list类型一元,NumPy数组也可以使用sort函数进行就地排序
arr = rng.standard_normal(6)
arr.sort()
# 可以通过将轴号传递给sort函数,实现对多维数组中的每个一维部分进行排序
arr = rng.standard_normal((5, 3)) # 从标准正态分布抽样,抽样结果组成5行3列的数组
arr.sort(axis=0) # 沿着行进行排序,即对不同行,相同列的元素进行排序
arr.sort(axis=1) # 沿着列进行排序,即对相同行,不同列的元素进行排序
# 顶级方法numpy.sort则返回一个已排序号的数组副本(类似于Python内置函数sorted),而不是在原地修改数组
arr2 = np.array([5, -10, 7, 1, 0, -3])
sorted_arr2 = np.sort(arr2)
# 有关NumPy排序方法和间接排序等更高级技术可以参阅附录A
唯一和其他集合逻辑
NumPy对一维数组有一些基本集合运算。
# 一个常用的方法是numpy.unique:它返回数组的已完成排序的唯一值
names = np.array(['Bob', 'Will', 'Joe', 'Bob', 'Will', 'Joe', 'Joe'])
np.unique(names)
ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
np.unique(ints) # 返回array([1, 2, 3, 4])
# 纯Python也通过sorted(set(names))实现,但numpy.unique速度更快,且返回一个数组而不是Python列表
# 另一个函数是numpy.in1d:它会测试一个数组中的值在另一个数组中的成员资格,并返回一个布尔数组
values = np.array([6, 0, 0, 3, 2, 5, 6])
np.in1d(valurs, [2, 3, 6]) # 返回布尔数组,如果第一个数组中的值在第二个数组中,则布尔数组对应位置的元素为True,否则为False
下表为NumPy中的数组集操作的列表
4.5带数组的文件输入和输出
NumPy能够以某些文本或二进制格式将数据保存和加载到磁盘和从磁盘加载数据。在本节中,我们只讨论NumPy的内置二进制格式,因为多数用户更喜欢pandas和其他用于加载文本或表格数据的工具(参阅第六章了解更多信息)。
# numpy.save和numpy.load是用于在磁盘上高效保存和加载数组数据的主力函数。默认情况下,数组以未压缩的原始二进制格式保存,文件扩展名为.npy:
arr = np.arange(10)
# 使用np.save函数,在相同目录下将arr数组保存到some_array.npy(文件路径未以.npy结尾,则自动附加扩展名.npy)
np.save('some_array', arr)
# 使用np.load函数,可以加载磁盘上的数组
np.load('some_array.npy')
# 通过使用numpy.savez函数和传递多个数组作为关键字参数,可以在未压缩的存档中保存多个数组:
np.savez('array_archive.npz', a=arr, b=arr)
# 加载.npz文件时,可以取回一个类似字典的对象,该对象可以延迟加载各个数组
arch = np.load('array_archive.npz')
arch['b'] # 返回array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 如果你的数据可以压缩,则你可以使用numpy.savez_compressed代替,这样得到的文件会进行压缩,大小将更小
np.savez_compressed('arrays_compressed.npz', a=arr, b=arr)
# 删除这些文件
!rm some_array.npy
!rm array_archive.npz
!rm arrays_compressed.npz
4.6线性代数
线性代数操作,如矩阵乘法,分解,行列式以及其他平方矩阵数学,是很多数组库函数的重要组成部分。
# 用*将两个多维数组相乘是逐元素乘积,而矩阵乘法要求要么使用dot函数,要么使用@运算符。dot函数既是numpy命名空间的用于矩阵乘法的一个数组方法也是一个函数
x = np.array([[1., 2., 3.], [4., 5., 6.]]) # 创建2*3的数组
y = np.array([[6., 23.], [-1, 7], [8, 9]]) # 创建3*2的数组
# 通过dot函数实现矩阵乘法
x.dot(y) # 也等价于写法np.dot(x, y)
# 通过@运算符实现矩阵乘法
x @ np.ones(3)
numpy.linalg模块有一系列矩阵分解和诸如逆和行列式的函数
from numpy.linalg import inv, qr
X = rng.standard_normal((5, 5))
mat = X.T @ X
inv(mat) # 求矩阵的逆
mat @ inv(mat)
下表是一些常用的线性代数函数的列表
4.7示例:Random Walks随机游走
考虑一个简单的随机游走,从0开始,步长为1和-1,两者概率相等。
# 使用内置random模块实现具有1000步的单个随机游走
import random
position = 0
walk = [position]
nsteps = 1000
for _ in range(nsteps):
step = 1 if random.randint(0, 1) else -1 # 随机生成数为1则前进1,否则后退1
position += step
walk.append(position)
plt.plot(walk[:100] # 画出前100个点的折线图
可以观察到,游走是随机步长的累积和,并且可以作为数组表达式被计算。因此,我们可以用numpy.random模块在一次性模拟1000枚硬币翻转,将它们设置为1和-1,并计算累积和:
# 利用numpy.random函数创建一个随机游走
nsteps = 1000
rng = np.random.default_rng(seed=12345)
draws = rng.interges(0, 2, size = nsteps) # 利用随机数生成器生成1000个随机数,值为0或者1
steps = np.where(draws == 0, 1, -1) # 将draws中的0转化为1,1转化为-1
walk = steps.cumsum() # 对steps数组进行累加
# 统计数据
walk.min()
walk.max()
(np.abs(walk) >= 10).argmax() # 随机游走达到距离原点10步需要的最短时间:先转化为布尔数组(距原点10步远记为True),再通过argmax返回第一个出现最大值(最大值为True)的位置
一次性模拟多个随机游走
通过对上面的代码进行微调,我们可以生成多个随机游走(如5000个)。
# 创建5000个随机游走
nwalks = 5000
nsteps = 1000
draws = rng.integers(0, 2, size=(nwalks, nsteps)) # 传递一个二元组给numpy.random函数,会生成一个二维抽样素组
steps = np.where(draws > 0, 1, -1)
walks = steps.cumsum(axis = 1) # 沿着列求累积和
# 统计数据
walks.min()
walks.max()
hits30 = (np.abs(walks) >= 30).any(axis=1) # 沿着列找是否存在距离原点大于等于30的值
hits30.sum() # 统计游走路程原点大于等于30的游走个数
crossing_times = (np.abs(walks[hits30] >= 30).argmax(axis=1)
crossing_times.mean() # 计算平均穿越时间