NumPy(Numerical Python的简称)是Python数值计算最重要的基础包。大多数提供科学计算的包都是用NumPy的数组作为构建基础。
NumPy的部分功能如下:
由于NumPy提供了一个简单易用的C API,因此很容易将数据传递给由低级语言编写的外部库,外部库也能以NumPy数组的形式将数据返回给Python。这个功能使Python成为一种包装C/C++/Fortran历史代码库的选择,并使被包装库拥有一个动态的、易用的接口。
NumPy本身并没有提供多么高级的数据分析功能,理解NumPy数组以及面向数组的计算将有助于你更加高效地使用诸如pandas之类的工具。因为NumPy是一个很大的题目,我会在附录A中介绍更多NumPy高级功能,比如广播。
对于大部分数据分析应用而言,我最关注的功能主要集中在:
虽然NumPy提供了通用的数值数据处理的计算基础,但大多数读者可能还是想将pandas作为统计和分析工作的基础,尤其是处理表格数据时。pandas还提供了一些NumPy所没有的领域特定的功能,如时间序列处理等。
笔记:Python的面向数组计算可以追溯到1995年,Jim Hugunin创建了Numeric库。接下来的10年,许多科学编程社区纷纷开始使用Python的数组编程,但是进入21世纪,库的生态系统变得碎片化了。2005年,Travis Oliphant从Numeric和Numarray项目整了出了NumPy项目,进而所有社区都集合到了这个框架下。
NumPy之于数值计算特别重要的原因之一,是因为它可以高效处理大数组的数据。这是因为:
NumPy是在一个连续的内存块中存储数据,独立于其他Python内置对象。NumPy的C语言编写的算法库可以操作内存,而不必进行类型检查或其它前期工作。比起Python的内置序列,NumPy数组使用的内存更少。
NumPy可以在整个数组上执行复杂的计算,而不需要Python的for循环。
要搞明白具体的性能差距,考察一个包含一百万整数的数组,和一个等价的Python列表:
import numpy as np
my_arr = np.arange(1000000)
my_list = list(range(1000000))
各个序列分别乘以2:
%time for _ in range(10): my_arr2 = my_arr * 2
CPU times: user 16.5 ms, sys: 16.8 ms, total: 33.3 ms
Wall time: 38.8 ms
%time for _ in range(10): my_list2 = [x * 2 for x in my_list]
CPU times: user 594 ms, sys: 214 ms, total: 808 ms
Wall time: 842 ms
基于NumPy的算法要比纯Python快10到100倍(甚至更快),并且使用的内存更少
NumPy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器。你可以利用这种数组对整块数据执行一些数学运算,其语法跟标量元素之间的运算一样。
要明白Python是如何利用与标量值类似的语法进行批次计算,我先引入NumPy,然后生成一个包含随机数据的小数组:
import numpy as np
# Generate some random data
data = np.random.randn(2, 3)
data
array([[ 0.55792899, -0.17399359, -0.23526153],
[-1.29798384, 0.7801505 , -1.00273266]])
然后进行数学运算:
data * 10
array([[ 5.57928991, -1.73993589, -2.35261533],
[-12.97983838, 7.80150497, -10.02732664]])
data + data
array([[ 1.11585798, -0.34798718, -0.47052307],
[-2.59596768, 1.56030099, -2.00546533]])
第一个例子中,所有的元素都乘以10。第二个例子中,每个元素都与自身相加。
笔记:在本章及全书中,我会使用标准的NumPy惯用法import numpy as np。你当然也可以在代码中使用from numpy import *,但不建议这么做。numpy的命名空间很大,包含许多函数,其中一些的名字与Python的内置函数重名(比如min和max)。
ndarray是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的。每个数组都有一个shape(一个表示各维度大小的元组)和一个dtype(一个用于说明数组数据类型的对象):
data.shape
(2, 3)
data.dtype
dtype('float64')
本章将会介绍NumPy数组的基本用法,这对于本书后面各章的理解基本够用。虽然大多数数据分析工作不需要深入理解NumPy,但是精通面向数组的编程和思维方式是成为Python科学计算牛人的一大关键步骤。
笔记:当你在本书中看到“数组”、“NumPy数组”、"ndarray"时,基本上都指的是同一样东西,即ndarray对象。
创建数组最简单的办法就是使用array函数。它接受一切序列型的对象(包括其他数组),然后产生一个新的含有传入数据的NumPy数组。以一个列表的转换为例:
data1 = [6, 7.5, 8, 0, 1]
arr1 = np.array(data1)
arr1
array([6. , 7.5, 8. , 0. , 1. ])
嵌套序列(比如由一组等长列表组成的列表)将会被转换为一个多维数组:
data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
arr2 = np.array(data2)
arr2
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
因为data2是列表的列表,NumPy数组arr2的两个维度的shape是从data2引入的。可以用属性ndim和shape验证:
arr2.ndim
2
arr2.shape
(2, 4)
除非特别说明(稍后将会详细介绍),np.array会尝试为新建的这个数组推断出一个较为合适的数据类型。数据类型保存在一个特殊的dtype对象中。比如说,在上面的两个例子中,我们有:
arr1.dtype
dtype('float64')
arr2.dtype
dtype('int64')
除np.array之外,还有一些函数也可以新建数组。比如,zeros和ones分别可以创建指定长度或形状的全0或全1数组。empty可以创建一个没有任何具体值的数组。要用这些方法创建多维数组,只需传入一个表示形状的元组即可:
np.zeros(10)
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
np.zeros((3, 6))
array([[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]])
np.empty((2, 3, 2))
array([[[2.35541533e-312, 2.05833592e-312],
[2.22809558e-312, 2.56761491e-312],
[2.48273508e-312, 2.05833592e-312]],
[[2.05833592e-312, 2.29175545e-312],
[2.07955588e-312, 2.14321575e-312],
[5.18589980e+170, 4.79243676e-322]]])
注意:认为np.empty会返回全0数组的想法是不安全的。很多情况下(如前所示),它返回的都是一些未初始化的垃圾值。
arange是Python内置函数range的数组版:
np.arange(15)
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
列出了一些数组创建函数。由于NumPy关注的是数值计算,因此,如果没有特别指定,数据类型基本都是float64(浮点数)。
函数 | 说明 |
---|---|
array | 将输入数据(列表,元组,数组或其它序列类型)转换为ndarray |
asarray | 将输入转换为ndarray,如果输入本身就是一个ndarray就不进行复制 |
arange | 类似于内置的range,但返回的是一个ndarray而不是列表 |
ones, ones_like | 根据指定的形状和dtype创建一个全1数组。one_like以另一个数组为参数,并根据其形状和dtype创建一个全1数组 |
zeros, zeros_like | 类似于 ones, ones_like |
empty, empty_like | 创建新数组 |
full, full_like | 用fill value 中的所有值,根据指定的形状和dtype创建一个数组。full_like使用另一个数组,用相同的形状和dtype创建 |
eye, identity | 创建一个正方的N*N单位矩阵(对角线为1,其余为0) |
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')
dtype是NumPy灵活交互其它系统的源泉之一。多数情况下,它们直接映射到相应的机器表示,这使得“读写磁盘上的二进制数据流”以及“集成低级语言代码(如C、Fortran)”等工作变得更加简单。数值型dtype的命名方式相同:一个类型名(如float或int),后面跟一个用于表示各元素位长的数字。标准的双精度浮点值(即Python中的float对象)需要占用8字节(即64位)。因此,该类型在NumPy中就记作float64。表4-2列出了NumPy所支持的全部数据类型。
笔记:记不住这些NumPy的dtype也没关系,新手更是如此。通常只需要知道你所处理的数据的大致类型是浮点数、复数、整数、布尔值、字符串,还是普通的Python对象即可。当你需要控制数据在内存和磁盘中的存储方式时(尤其是对大数据集),那就得了解如何控制存储类型。
类型 | 类型代码 | 说明 |
---|---|---|
int8, uint8 | i1, u1 | 有符号和无符号的8位(1个字节),整型 |
int16, uint16 | i2, u2 | 有符号和无符号的16位(2个字节),整型 |
int32, uint32 | i4, u4 | 有符号和无符号的32位(4个字节),整型 |
int64, uint64 | i8, u8 | 有符号和无符号的64位(8个字节),整型 |
float16 | f2 | 半精度浮点数 |
float32 | f4或f | 标准的单精度浮点数,与C的float兼容 |
float64 | f8或d | 标准的双精度浮点数,与C的double和python的float对象兼容 |
float128 | f16或g | 扩展精度浮点数 |
complex64,complex128 | c8,c16 | - |
complex256 | c32 | - |
bool | ? | 布尔 |
object | O | Python对象类型 |
string_ | S | 固定长度的字符串类型,每个字符一个字节(S10,长度为10 的字符串) |
unicode_ | U | 固定长度的unicode类型,U10,长度为10) |
你可以通过ndarray的astype方法明确地将一个数组从一个dtype转换成另一个dtype:
arr = np.array([1, 2, 3, 4, 5])
arr.dtype
dtype('int64')
float_arr = arr.astype(np.float64)
float_arr.dtype
dtype('float64')
在本例中,整数被转换成了浮点数。如果将浮点数转换成整数,则小数部分将会被截取删除:
arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
arr
array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
arr.astype(np.int32)
array([ 3, -1, -2, 0, 12, 10], dtype=int32)
如果某字符串数组表示的全是数字,也可以用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。这里,我比较懒,写的是float而不是np.float64;NumPy很聪明,它会将Python类型映射到等价的dtype上。
数组的dtype还有另一个属性:
int_array = np.arange(10)
calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
int_array.astype(calibers.dtype)
array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
你还可以用简洁的类型代码来表示dtype:
empty_uint32 = np.empty(8, dtype='u4')
empty_uint32
array([ 0, 1075314688, 0, 1075707904, 0,
1075838976, 0, 1072693248], dtype=uint32)
笔记:调用astype总会创建一个新的数组(一个数据的备份),即使新的dtype与旧的dtype相同。
数组很重要,因为它使你不用编写循环即可对数据执行批量运算。NumPy用户称其为矢量化(vectorization)。大小相等的数组之间的任何算术运算都会将运算应用到元素级:
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
array([[1. , 0.5 , 0.33333333],
[0.25 , 0.2 , 0.16666667]])
arr ** 0.5
array([[1. , 1.41421356, 1.73205081],
[2. , 2.23606798, 2.44948974]])
大小相同的数组之间的比较会生成布尔值数组:
arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
arr2
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
arr2 > arr
array([[False, True, False],
[ True, False, True]])
不同大小的数组之间的运算叫做广播(broadcasting),将在附录A中对其进行详细讨论。本书的内容不需要对广播机制有多深的理解。
NumPy数组的索引是一个内容丰富的主题,因为选取数据子集或单个元素的方式有很多。一维数组很简单。从表面上看,它们跟Python列表的功能差不多:
arr = np.arange(10)
arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arr[5]
5
arr[5:8]
array([5, 6, 7])
arr[5:8] = 12
arr
array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
如上所示,当你将一个标量值赋值给一个切片时(如arr[5:8]=12),该值会自动传播(也就说后面将会讲到的“广播”)到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上。
作为例子,先创建一个arr的切片:
arr_slice = arr[5:8]
arr_slice
array([12, 12, 12])
现在,当我修改arr_slice中的值,变动也会体现在原始数组arr中:
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的设计目的是处理大数据,所以你可以想象一下,假如NumPy坚持要将数据复制来复制去的话会产生何等的性能和内存问题。
注意:如果你想要得到的是ndarray切片的一份副本而非视图,就需要明确地进行复制操作,例如arr[5:8].copy()。
对于高维度数组,能做的事情更多。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组:
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d[2]
array([7, 8, 9])
因此,可以对各个元素进行递归访问,但这样需要做的事情有点多。你可以传入一个以逗号隔开的索引列表来选取单个元素。也就是说,下面两种方式是等价的:
arr2d[0][2]
3
arr2d[0, 2]
3
在多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的ndarray(它含有高一级维度上的所有数据)。因此,在2×2×3数组arr3d中:
arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
arr3d
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d[0]是一个2×3数组:
arr3d[0]
array([[1, 2, 3],
[4, 5, 6]])
标量值和数组都可以被赋值给arr3d[0]:
old_values = arr3d[0].copy()
arr3d[0] = 42
arr3d
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d[0] = old_values
arr3d
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
相似的,arr3d[1,0]可以访问索引以(1,0)开头的那些值(以一维数组的形式返回):
arr3d[1, 0]
array([7, 8, 9])
虽然是用两步进行索引的,表达式是相同的:
x = arr3d[1]
x
array([[ 7, 8, 9],
[10, 11, 12]])
x[0]
array([7, 8, 9])
注意,在上面所有这些选取数组子集的例子中,返回的数组都是视图。
切片索引
arr
array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
arr[1:6]
array([ 1, 2, 3, 4, 64])
对于之前的二维数组arr2d,其切片方式稍显不同:
arr2d
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
arr2d[:2]
array([[1, 2, 3],
[4, 5, 6]])
可以看出,它是沿着第0轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。表达式arr2d[:2]可以被认为是“选取arr2d的前两行”。
你可以一次传入多个切片,就像传入多个索引那样:
arr2d[:2, 1:]
array([[2, 3],
[5, 6]])
像这样进行切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片。
例如,我可以选取第二行的前两列:
arr2d[1, :2]
array([4, 5])
相似的,还可以选择第三列的前两行:
arr2d[:2, 2]
array([3, 6])
下图对此进行了说明。注意,“只有冒号”表示选取整个轴,因此你可以像下面这样只对高维轴进行切片:
arr2d[:, :1]
array([[1],
[4],
[7]])
自然,对切片表达式的赋值操作也会被扩散到整个选区:
arr2d[:2, 1:] = 0
arr2d
array([[1, 0, 0],
[4, 0, 0],
[7, 8, 9]])
来看这样一个例子,假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据:
names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
names
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='
data = np.random.randn(7, 4)
data
array([[ 1.08819495, -1.35977196, -2.12340176, 0.39334673],
[ 2.80377332, -0.44458984, -1.60059384, 0.80966112],
[-0.45957415, -0.61014123, 1.20336836, 0.4713561 ],
[-0.57854339, -1.6344551 , 1.44365184, -1.55334838],
[-1.03240667, -1.24750262, -0.29724331, 1.97207636],
[-1.42829023, 1.22492029, 0.76296031, -0.54694285],
[ 0.59248645, -0.33183171, 0.03729959, -0.8366643 ]])
假设每个名字都对应data数组中的一行,而我们想要选出对应于名字"Bob"的所有行。跟算术运算一样,数组的比较运算(如==)也是矢量化的。因此,对names和字符串"Bob"的比较运算将会产生一个布尔型数组:
names == 'Bob'
array([ True, False, False, True, False, False, False])
这个布尔型数组可用于数组索引:
data[names == 'Bob']
array([[ 0.73365649, -0.87923435, -0.53347382, -0.04034894],
[ 1.81553058, 0.56496127, -1.40861591, -1.41105561]])
注意:如果布尔型数组的长度不对,布尔型选择就会出错,因此一定要小心。
下面的例子,我选取了names == 'Bob’的行,并索引了列:
data[names == 'Bob', 2:]
array([[-0.53347382, -0.04034894],
[-1.40861591, -1.41105561]])
data[names == 'Bob', 3]
array([-0.04034894, -1.41105561])
要选择除"Bob"以外的其他值,既可以使用不等于符号(!=),也可以通过~对条件进行否定
names != 'Bob'
array([False, True, True, False, True, True, True])
data[~(names == 'Bob')]
array([[-1.38529829, -1.01480157, 0.97968513, -1.03019607],
[ 0.67961435, 1.0160977 , -0.3109035 , 0.47552943],
[-0.1740475 , -0.55824642, 0.63353386, 1.44389722],
[ 1.67458605, -0.27808826, -0.35267399, 0.43909726],
[ 1.35605024, 0.79838175, 0.35247327, -1.13700882]])
~操作符用来反转条件很好用:
cond = names == 'Bob'
data[~cond]
array([[ 2.80377332, -0.44458984, -1.60059384, 0.80966112],
[-0.45957415, -0.61014123, 1.20336836, 0.4713561 ],
[-1.03240667, -1.24750262, -0.29724331, 1.97207636],
[-1.42829023, 1.22492029, 0.76296031, -0.54694285],
[ 0.59248645, -0.33183171, 0.03729959, -0.8366643 ]])
选取这三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可:
mask = (names == 'Bob') | (names == 'Will')
mask
array([ True, False, True, True, True, False, False])
data[mask]
array([[ 1.08819495, -1.35977196, -2.12340176, 0.39334673],
[-0.45957415, -0.61014123, 1.20336836, 0.4713561 ],
[-0.57854339, -1.6344551 , 1.44365184, -1.55334838],
[-1.03240667, -1.24750262, -0.29724331, 1.97207636]])
通过布尔型索引选取数组中的数据,将总是创建数据的副本,即使返回一模一样的数组也是如此。
注意:Python关键字and和or在布尔型数组中无效。要使用&与|。
通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:
data[data < 0] = 0
data
array([[1.08819495, 0. , 0. , 0.39334673],
[2.80377332, 0. , 0. , 0.80966112],
[0. , 0. , 1.20336836, 0.4713561 ],
[0. , 0. , 1.44365184, 0. ],
[0. , 0. , 0. , 1.97207636],
[0. , 1.22492029, 0.76296031, 0. ],
[0.59248645, 0. , 0.03729959, 0. ]])
通过一维布尔数组设置整行或列的值也很简单:
data[names != 'Joe'] = 7
data
array([[7. , 7. , 7. , 7. ],
[2.80377332, 0. , 0. , 0.80966112],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[0. , 1.22492029, 0.76296031, 0. ],
[0.59248645, 0. , 0.03729959, 0. ]])
后面会看到,这类二维数据的操作也可以用pandas方便的来做。
花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引。假设我们有一个8×4数组:
arr = np.empty((8, 4))
for i in range(8):
arr[i] = i
arr
array([[0., 0., 0., 0.],
[1., 1., 1., 1.],
[2., 2., 2., 2.],
[3., 3., 3., 3.],
[4., 4., 4., 4.],
[5., 5., 5., 5.],
[6., 6., 6., 6.],
[7., 7., 7., 7.]])
为了以特定顺序选取行子集,只需传入一个用于指定顺序的整数列表或ndarray即可:
arr[[4, 3, 0, 6]]
array([[4., 4., 4., 4.],
[3., 3., 3., 3.],
[0., 0., 0., 0.],
[6., 6., 6., 6.]])
这段代码确实达到我们的要求了!使用负数索引将会从末尾开始选取行:
arr[[-3, -5, -7]]
array([[5., 5., 5., 5.],
[3., 3., 3., 3.],
[1., 1., 1., 1.]])
一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:
arr = np.arange(32).reshape((8, 4))
arr
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23],
[24, 25, 26, 27],
[28, 29, 30, 31]])
arr[[1, 5, 7, 2], [0, 3, 1, 2]]
array([ 4, 23, 29, 10])
附录A中会详细介绍reshape方法。
最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。无论数组是多少维的,花式索引总是一维的。
这个花式索引的行为可能会跟某些用户的预期不一样(包括我在内),选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:
arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
记住,花式索引跟切片不一样,它总是将数据复制到新数组中。
转置是重塑的一种特殊形式,它返回的是源数据的视图(不会进行任何复制操作)。数组不仅有transpose方法,还有一个特殊的T属性:
arr = np.arange(15).reshape((3, 5))
arr
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
arr.T
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
在进行矩阵计算时,经常需要用到该操作,比如利用np.dot计算矩阵内积:
arr = np.random.randn(6, 3)
arr
array([[ 0.36917138, 1.90387131, -0.88579746],
[-0.41897263, 0.07419862, -0.03292 ],
[ 0.51693788, 0.2040625 , -1.62235935],
[ 0.07788554, 0.2651492 , -1.81757859],
[ 1.2560305 , -0.72840939, -0.56874403],
[ 0.57864791, -0.29702894, -0.70705015]])
arr.T
array([[ 0.36917138, -0.41897263, 0.51693788, 0.07788554, 1.2560305 ,
0.57864791],
[ 1.90387131, 0.07419862, 0.2040625 , 0.2651492 , -0.72840939,
-0.29702894],
[-0.88579746, -0.03292 , -1.62235935, -1.81757859, -0.56874403,
-0.70705015]])
np.dot(arr.T, arr)
array([[ 2.49756252, -0.28887305, -2.41693351],
[-0.28887305, 4.36098345, -1.87758635],
[-2.41693351, -1.87758635, 7.54475232]])
sum([i**2 for i in arr.T[0]])
2.497562517200302
对于高维数组,transpose需要得到一个由轴编号组成的元组才能对这些轴进行转置(比较费脑子)
arr = np.arange(16).reshape((2, 2, 4))
arr
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
形状 | 索引 |
---|---|
2 | 0 |
2 | 1 |
4 | 2 |
这里,第一个轴被换成了第二个,第二个轴被换成了第一个,最后一个轴不变。
arr.transpose((1, 0, 2))
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
比如,数值0开始的索引是[0,0,0],变换后变成[0,0,0];数值1开始的索引是[0,0,1],变换后还是[0,0,1]
数值4开始的索引是[0,1,0],变换后变成[1,0,0];数值8开始的索引是[1,0,0],变换后还是[0,1,0]
这也说明了,transpose依赖于shape。
简单的转置可以使用.T,它其实就是进行轴对换而已。ndarray还有一个swapaxes方法,它需要接受一对轴编号:
arr
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
arr.swapaxes(1, 2)
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])
swapaxes也是返回源数据的视图(不会进行任何复制操作)。
通用函数(即ufunc)是一种对ndarray中的数据执行元素级运算的函数。你可以将其看做简单函数(接受一个或多个标量值,并产生一个或多个标量值)的矢量化包装器。
许多ufunc都是简单的元素级变体,如sqrt和exp:
arr = np.arange(10)
arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.sqrt(arr)
array([0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131, 2.82842712, 3. ])
np.exp(arr)
array([1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
5.45981500e+01, 1.48413159e+02, 4.03428793e+02, 1.09663316e+03,
2.98095799e+03, 8.10308393e+03])
这些都是一元(unary)ufunc。另外一些(如add或maximum)接受2个数组(因此也叫二元(binary)ufunc),并返回一个结果数组:
x = np.random.randn(8)
y = np.random.randn(8)
np.maximum(x, y)
array([0.50689777, 1.02129695, 0.75780391, 0.93518835, 1.27140324,
0.04950896, 0.16249104, 0.8587124 ])
这里,numpy.maximum计算了x和y中元素级别最大的元素。
虽然并不常见,但有些ufunc的确可以返回多个数组。modf就是一个例子,它是Python内置函数divmod的矢量化版本,它会返回浮点数数组的小数和整数部分:
arr = np.random.randn(7) * 5
arr
array([ 4.58467619, 1.84345193, -4.25195281, -7.25603847, 11.36832203,
-3.66425455, 3.44991866])
remainder, whole_part = np.modf(arr)
whole_part
array([ 4., 1., -4., -7., 11., -3., 3.])
Ufuncs可以接受一个out可选参数,这样就能在数组原地进行操作:
arr
array([ 4.58467619, 1.84345193, -4.25195281, -7.25603847, 11.36832203,
-3.66425455, 3.44991866])
np.sqrt(arr)
/root/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in sqrt
"""Entry point for launching an IPython kernel.
array([2.1411857 , 1.3577378 , nan, nan, 3.37169424,
nan, 1.85739567])
np.sqrt(arr, arr)
/root/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in sqrt
"""Entry point for launching an IPython kernel.
array([2.1411857 , 1.3577378 , nan, nan, 3.37169424,
nan, 1.85739567])
arr
array([2.1411857 , 1.3577378 , nan, nan, 3.37169424,
nan, 1.85739567])
NumPy数组使你可以将许多种数据处理任务表述为简洁的数组表达式(否则需要编写循环)。用数组表达式代替循环的做法,通常被称为矢量化。一般来说,矢量化数组运算要比等价的纯Python方式快上一两个数量级(甚至更多),尤其是各种数值计算。在后面内容中(见附录A)我将介绍广播,这是一种针对矢量化计算的强大手段。
作为简单的例子,假设我们想要在一组值(网格型)上计算函数sqrt(x2+y2)。np.meshgrid函数接受两个一维数组,并产生两个二维矩阵(对应于两个数组中所有的(x,y)对):
points = np.arange(-5, 5, 0.01) # 1000 equally spaced points
xs, ys = np.meshgrid(points, points)
ys
array([[-5. , -5. , -5. , ..., -5. , -5. , -5. ],
[-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
[-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
...,
[ 4.97, 4.97, 4.97, ..., 4.97, 4.97, 4.97],
[ 4.98, 4.98, 4.98, ..., 4.98, 4.98, 4.98],
[ 4.99, 4.99, 4.99, ..., 4.99, 4.99, 4.99]])
xs
array([[-5. , -4.99, -4.98, ..., 4.97, 4.98, 4.99],
[-5. , -4.99, -4.98, ..., 4.97, 4.98, 4.99],
[-5. , -4.99, -4.98, ..., 4.97, 4.98, 4.99],
...,
[-5. , -4.99, -4.98, ..., 4.97, 4.98, 4.99],
[-5. , -4.99, -4.98, ..., 4.97, 4.98, 4.99],
[-5. , -4.99, -4.98, ..., 4.97, 4.98, 4.99]])
现在,对该函数的求值运算就好办了,把这两个数组当做两个浮点数那样编写表达式即可:
z = np.sqrt(xs ** 2 + ys ** 2)
z
array([[7.07106781, 7.06400028, 7.05693985, ..., 7.04988652, 7.05693985,
7.06400028],
[7.06400028, 7.05692568, 7.04985815, ..., 7.04279774, 7.04985815,
7.05692568],
[7.05693985, 7.04985815, 7.04278354, ..., 7.03571603, 7.04278354,
7.04985815],
...,
[7.04988652, 7.04279774, 7.03571603, ..., 7.0286414 , 7.03571603,
7.04279774],
[7.05693985, 7.04985815, 7.04278354, ..., 7.03571603, 7.04278354,
7.04985815],
[7.06400028, 7.05692568, 7.04985815, ..., 7.04279774, 7.04985815,
7.05692568]])
作为第9章的先导,我用matplotlib创建了这个二维数组的可视化:
import matplotlib.pyplot as plt
plt.imshow(z, cmap=plt.cm.gray); plt.colorbar()
plt.title("Image plot of $\sqrt{x^2 + y^2}$ for a grid of values")
Text(0.5, 1.0, 'Image plot of $\\sqrt{x^2 + y^2}$ for a grid of values')
numpy.where函数是三元表达式x if condition else y的矢量化版本。假设我们有一个布尔数组和两个值数组:
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])
假设我们想要根据cond中的值选取xarr和yarr的值:当cond中的值为True时,选取xarr的值,否则从yarr中选取。列表推导式的写法应该如下所示:
result = [(x if c else y) for x, y, c in zip(xarr, yarr, cond)]
result
[1.1, 2.2, 1.3, 1.4, 2.5]
这有几个问题。第一,它对大数组的处理速度不是很快(因为所有工作都是由纯Python完成的)。第二,无法用于多维数组。若使用np.where,则可以将该功能写得非常简洁:
result = np.where(cond, xarr, yarr)
result
array([1.1, 2.2, 1.3, 1.4, 2.5])
np.where的第二个和第三个参数不必是数组,它们都可以是标量值。在数据分析工作中,where通常用于根据另一个数组而产生一个新的数组。假设有一个由随机数据组成的矩阵,你希望将所有正值替换为2,将所有负值替换为-2。若利用np.where,则会非常简单
arr = np.random.randn(4, 4)
arr
array([[-0.45529504, 0.32161894, 0.45185456, -0.69029542],
[-0.32492526, 0.62841903, -0.13291365, -0.9053368 ],
[-1.23221209, 0.04308516, -0.46039459, 0.97604205],
[ 0.63979791, 0.81190225, -0.49690241, 1.22641577]])
arr > 0
array([[False, True, True, False],
[False, True, False, False],
[False, True, False, True],
[ True, True, False, True]])
np.where(arr > 0, 2, -2)
array([[-2, 2, 2, -2],
[-2, 2, -2, -2],
[-2, 2, -2, 2],
[ 2, 2, -2, 2]])
使用np.where,可以将标量和数组结合起来。例如,我可用常数2替换arr中所有正的值:
np.where(arr > 0, 2, arr) # set only positive values to 2
array([[-0.45529504, 2. , 2. , -0.69029542],
[-0.32492526, 2. , -0.13291365, -0.9053368 ],
[-1.23221209, 2. , -0.46039459, 2. ],
[ 2. , 2. , -0.49690241, 2. ]])
传递给where的数组大小可以不相等,甚至可以是标量值
可以通过数组上的一组数学函数对整个数组或某个轴向的数据进行统计计算。sum、mean以及标准差std等聚合计算(aggregation,通常叫做约简(reduction))既可以当做数组的实例方法调用,也可以当做顶级NumPy函数使用。
这里,我生成了一些正态分布随机数据,然后做了聚类统计:
arr = np.random.randn(5, 4)
arr
array([[ 0.51020063, 1.06885933, 0.12436029, 0.24164831],
[ 0.39618325, -0.80346153, 1.37067235, 0.07386001],
[-1.06963287, 1.02782789, 1.87949971, 0.16006538],
[ 0.70186879, -0.48088616, -0.09605486, -1.33213067],
[ 0.03045268, -1.12776699, 0.78349108, -1.84200758]])
arr.mean()
0.08085245226505797
np.mean(arr)
0.08085245226505797
arr.sum()
1.6170490453011594
mean和sum这类的函数可以接受一个axis选项参数,用于计算该轴向上的统计值,最终结果是一个少一维的数组:
arr.mean(axis=1)
array([ 0.48626714, 0.25931352, 0.49944003, -0.30180073, -0.5389577 ])
arr.sum(axis=0)
array([ 0.56907248, -0.31542746, 4.06196858, -2.69856455])
这里,arr.mean(1)是“计算行的平均值”,arr.sum(0)是“计算每列的和”。
其他如cumsum和cumprod之类的方法则不聚合,而是产生一个由中间结果组成的数组:
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
arr.cumsum()
array([ 0, 1, 3, 6, 10, 15, 21, 28])
在多维数组中,累加函数(如cumsum)返回的是同样大小的数组,但是会根据每个低维的切片沿着标记轴计算部分聚类:
arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
arr
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
arr.cumsum(axis=0)
array([[ 0, 1, 2],
[ 3, 5, 7],
[ 9, 12, 15]])
arr.cumprod(axis=1)
array([[ 0, 0, 0],
[ 3, 12, 60],
[ 6, 42, 336]])
基本数组统计方法:
方法 | 说明 |
---|---|
sum | 对数组中全部或某轴的元素求和。 |
mean | 算术平均数 |
std,var | 分别为标准差和方差,自由度可调 |
min,max | 最大值,最小值 |
argmin,argmax | 分别为最大和最小元素的索引 |
cumsum | 所有元素的累计和 |
cumprod | 所有元素的累计积 |
在上面这些方法中,布尔值会被强制转换为1(True)和0(False)。因此,sum经常被用来对布尔型数组中的True值计数:
arr = np.random.randn(100)
(arr > 0).sum() # Number of positive values
53
另外还有两个方法any和all,它们对布尔型数组非常有用。any用于测试数组中是否存在一个或多个True,而all则检查数组中所有值是否都是True:
bools = np.array([False, False, True, False])
bools.any()
True
bools.all()
False
这两个方法也能用于非布尔型数组,所有非0元素将会被当做True。
跟Python内置的列表类型一样,NumPy数组也可以通过sort方法就地排序:
arr = np.random.randn(6)
arr
array([ 1.25650403, 0.10201001, -1.8935299 , -1.58425067, 1.06850301,
1.24895649])
arr.sort()
arr
array([-1.8935299 , -1.58425067, 0.10201001, 1.06850301, 1.24895649,
1.25650403])
多维数组可以在任何一个轴向上进行排序,只需将轴编号传给sort即可:
arr = np.random.randn(5, 3)
arr
array([[ 0.37991608, 0.40415977, -0.7699296 ],
[-1.38545197, 0.13529259, -0.55206188],
[-0.92502533, 0.67237106, 0.53070269],
[ 1.4652058 , -0.66002003, 1.06003712],
[ 1.25839325, -0.1037674 , 1.20030804]])
arr.sort(1)
arr
array([[-0.7699296 , 0.37991608, 0.40415977],
[-1.38545197, -0.55206188, 0.13529259],
[-0.92502533, 0.53070269, 0.67237106],
[-0.66002003, 1.06003712, 1.4652058 ],
[-0.1037674 , 1.20030804, 1.25839325]])
顶级方法np.sort返回的是数组的已排序副本,而就地排序则会修改数组本身。计算数组分位数最简单的办法是对其进行排序,然后选取特定位置的值:
large_arr = np.random.randn(1000)
large_arr.sort()
large_arr[int(0.05 * len(large_arr))] # 5% quantile
-1.577574405810572
更多关于NumPy排序方法以及诸如间接排序之类的高级技术,请参阅附录A。在pandas中还可以找到一些其他跟排序有关的数据操作(比如根据一列或多列对表格型数据进行排序)。
NumPy提供了一些针对一维ndarray的基本集合运算。最常用的可能要数np.unique了,它用于找出数组中的唯一值并返回已排序的结果:
names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
np.unique(names)
array(['Bob', 'Joe', 'Will'], dtype='
ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
np.unique(ints)
array([1, 2, 3, 4])
拿跟np.unique等价的纯Python代码来对比一下:
sorted(set(names))
['Bob', 'Joe', 'Will']
另一个函数np.in1d用于测试一个数组中的值在另一个数组中的成员资格,返回一个布尔型数组:
values = np.array([6, 0, 0, 3, 2, 5, 6])
np.in1d(values, [2, 3, 6])
array([ True, False, False, True, True, False, True])
基本数组统计方法:
方法 | 说明 |
---|---|
unique(x) | 计算x中的唯一元素,并返回有序结果 |
intersect1d(x,y) | 计算x和y中的公共元素,并返回有序结果 |
union1d(x,y) | 计算x和y的并集,并返回有序结果 |
in1d(x,y) | 得到一个表示“x的元素是否包含于y”的布尔行数组 |
setdiff1d(x,y) | 集合的差,即元素在x中且不在y中 |
setor1d(x,y) | 集合的对称差,即存在于一个数组中但不同时存在于两个数组中的元素 |
NumPy能够读写磁盘上的文本数据或二进制数据。这一小节只讨论NumPy的内置二进制格式,因为更多的用户会使用pandas或其它工具加载文本或表格数据(见第6章)。
np.save和np.load是读写磁盘数组数据的两个主要函数。默认情况下,数组是以未压缩的原始二进制格式保存在扩展名为.npy的文件中的:
arr = np.arange(10)
np.save('some_array', arr)
如果文件路径末尾没有扩展名.npy,则该扩展名会被自动加上。然后就可以通过np.load读取磁盘上的数组:
np.load('some_array.npy')
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
通过np.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)
线性代数(如矩阵乘法、矩阵分解、行列式以及其他方阵数学等)是任何数组库的重要组成部分。不像某些语言(如MATLAB),通过*对两个二维数组相乘得到的是一个元素级的积,而不是一个矩阵点积。因此,NumPy提供了一个用于矩阵乘法的dot函数(既是一个数组方法也是numpy命名空间中的一个函数):
x = np.array([[1., 2., 3.], [4., 5., 6.]])
y = np.array([[6., 23.], [-1, 7], [8, 9]])
x.dot(y)
array([[ 28., 64.],
[ 67., 181.]])
x.dot(y)等价于np.dot(x, y):
np.dot(x, y)
array([[ 28., 64.],
[ 67., 181.]])
一个二维数组跟一个大小合适的一维数组的矩阵点积运算之后将会得到一个一维数组:
np.dot(x, np.ones(3))
array([ 6., 15.])
@符(类似Python 3.5)也可以用作中缀运算符,进行矩阵乘法:
x @ np.ones(3)
array([ 6., 15.])
numpy.linalg中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的东西。它们跟MATLAB和R等语言所使用的是相同的行业标准线性代数库,如BLAS、LAPACK、Intel MKL(Math Kernel Library,可能有,取决于你的NumPy版本)等:
from numpy.linalg import inv, qr
X = np.random.randn(5, 5)
mat = X.T.dot(X)
inv(mat)
array([[ 2.20580238, 1.63229496, -2.5555793 , 2.29985352, -1.45554023],
[ 1.63229496, 1.59599248, -1.98659848, 1.73905002, -0.96644212],
[-2.5555793 , -1.98659848, 3.31978653, -2.88662032, 1.77588852],
[ 2.29985352, 1.73905002, -2.88662032, 2.93992058, -1.59344898],
[-1.45554023, -0.96644212, 1.77588852, -1.59344898, 1.28647826]])
mat.dot(inv(mat))
array([[ 1.00000000e+00, -1.27548444e-15, 1.56822492e-15,
-3.68777188e-16, 6.83513374e-16],
[-1.03609014e-15, 1.00000000e+00, 5.80507380e-16,
-5.33846887e-16, -4.28976346e-16],
[ 1.31947780e-16, 9.29828890e-16, 1.00000000e+00,
2.21294659e-16, -7.08895302e-16],
[-8.64927559e-17, 7.77461504e-18, -9.98282625e-16,
1.00000000e+00, -3.69115289e-16],
[ 2.90609939e-16, -8.51501308e-16, 1.31781280e-15,
1.85886425e-16, 1.00000000e+00]])
q, r = qr(mat)
r
array([[-6.97794775, 2.81611288, -1.59440506, 0.61900361, -3.04039084],
[ 0. , -3.28930144, -4.0270272 , -0.70389031, 2.36435378],
[ 0. , 0. , -3.03733772, -2.41659764, 1.45496401],
[ 0. , 0. , 0. , -1.57589292, -2.55905093],
[ 0. , 0. , 0. , 0. , 0.31007948]])
列出了一些最常用的线性代数函数。
函数 | 说明 |
---|---|
diag | 以一维数组的形式返回方阵的对角线(或非对角线)元素,或将一维数组转换为方阵(非对角线元素为0) |
dot | 矩阵乘法 |
trace | 计算对角线元素的和 |
det | 计算矩阵行列式 |
eig | 计算方阵的本征值和本征向量 |
inv | 计算方阵的逆 |
pinv | 计算矩阵的Moore-Penrose |
qr | 计算QR分解 |
svd | 计算奇异值分解(SVD) |
solve | 解线性方程组Ax = b,其中A为一个方阵 |
lstsq | 计算Ax = b 的最小二乘解 |
numpy.random模块对Python内置的random进行了补充,增加了一些用于高效生成多种概率分布的样本值的函数。例如,你可以用normal来得到一个标准正态分布的4×4样本数组:
samples = np.random.normal(size=(4, 4))
samples
array([[ 0.36475649, -2.45336442, -2.61352248, -0.08786457],
[-0.11431696, -1.44986625, 1.96147565, 1.09025397],
[ 0.02984214, -0.58975211, 0.2066668 , 0.3834713 ],
[-1.40952008, -0.82907783, 0.97871176, 0.63354823]])
而Python内置的random模块则只能一次生成一个样本值。从下面的测试结果中可以看出,如果需要产生大量样本值,numpy.random快了不止一个数量级:
from random import normalvariate
N = 1000000
%timeit samples = [normalvariate(0, 1) for _ in range(N)]
829 ms ± 7.03 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit np.random.normal(size=N)
49.6 ms ± 1.88 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
我们说这些都是伪随机数,是因为它们都是通过算法基于随机数生成器种子,在确定性的条件下生成的。你可以用NumPy的np.random.seed更改随机数生成种子:
np.random.seed(1234)
numpy.random的数据生成函数使用了全局的随机种子。要避免全局状态,你可以使用numpy.random.RandomState,创建一个与其它隔离的随机数生成器:
rng = np.random.RandomState(1234)
rng.randn(10)
array([ 0.47143516, -1.19097569, 1.43270697, -0.3126519 , -0.72058873,
0.88716294, 0.85958841, -0.6365235 , 0.01569637, -2.24268495])
函数 | 说明 |
---|---|
seed | 确定随机数生成器的种子 |
permutation | 返回一个序列的随机排列会返回一个随机排列的范围 |
shuffle | 对一个序列就地随机排列 |
rand | 产生均匀分布的样本值 |
randint | 从给定的上下限范围内随机选取整数 |
randn | 产生正态分布(平均值为0,标准差为1)的样本值,类似于MATLAB接口 |
binomial | 产生二项分布的样本值 |
normal | 产生正态(高斯)分布的样本值 |
beta | 产生Beta分布的样本值 |
chisquare | 产生卡方分布的样本值 |
gamma | 产生Gamma分布的样本值 |
uniform | 产生在[0,1]中均匀分布的样本值 |
我们通过模拟随机漫步来说明如何运用数组运算。先来看一个简单的随机漫步的例子:从0开始,步长1和-1出现的概率相等。
下面是一个通过内置的random模块以纯Python的方式实现1000步的随机漫步
import random
position = 0
walk = [position]
steps = 1000
for i in range(steps):
step = 1 if random.randint(0,1) else -1
position += step
walk.append(position)
walk[:10]
[0, -1, -2, -3, -4, -3, -2, -1, -2, -3]
plt.plot(walk[:100])
[]
不难看出,这其实就是随机漫步中各步的累计和,可以用一个数组运算来实现。因此,我用np.random模块一次性随机产生1000个“掷硬币”结果(即两个数中任选一个),将其分别设置为1或-1,然后计算累计和:
nsteps = 1000
draws = np.random.randint(0, 2, size=nsteps)
steps = np.where(draws > 0, 1, -1)
walk = steps.cumsum()
有了这些数据之后,我们就可以沿着漫步路径做一些统计工作了,比如求取最大值和最小值:
walk.min()
-9
walk.max()
60
现在来看一个复杂点的统计任务——首次穿越时间,即随机漫步过程中第一次到达某个特定值的时间。假设我们想要知道本次随机漫步需要多久才能距离初始0点至少10步远(任一方向均可)。np.abs(walk)>=10可以得到一个布尔型数组,它表示的是距离是否达到或超过10,而我们想要知道的是第一个10或-10的索引。可以用argmax来解决这个问题,它返回的是该布尔型数组第一个最大值的索引(True就是最大值):
(np.abs(walk) >= 10).argmax()
297
注意,这里使用argmax并不是很高效,因为它无论如何都会对数组进行完全扫描。在本例中,只要发现了一个True,那我们就知道它是个最大值了。
如果你希望模拟多个随机漫步过程(比如5000个),只需对上面的代码做一点点修改即可生成所有的随机漫步过程。只要给numpy.random的函数传入一个二元元组就可以产生一个二维数组,然后我们就可以一次性计算5000个随机漫步过程(一行一个)的累计和了:
nwalks = 5000
nsteps = 1000
draws = np.random.randint(0, 2, size=(nwalks, nsteps)) # 0 or 1
steps = np.where(draws > 0, 1, -1)
walks = steps.cumsum(1)
walks
array([[ -1, 0, 1, ..., -6, -5, -4],
[ -1, -2, -1, ..., -18, -19, -18],
[ 1, 0, -1, ..., 24, 23, 22],
...,
[ -1, 0, -1, ..., -2, -3, -4],
[ -1, 0, -1, ..., 30, 29, 30],
[ 1, 0, -1, ..., 18, 19, 20]])
现在,我们来计算所有随机漫步过程的最大值和最小值:
walks.max()
137
walks.min()
-137
得到这些数据之后,我们来计算30或-30的最小穿越时间。这里稍微复杂些,因为不是5000个过程都到达了30。我们可以用any方法来对此进行检查:
hits30 = (np.abs(walks) >= 30).any(1)
hits30
array([False, True, True, ..., False, True, True])
hits30.sum() # Number that hit 30 or -30
3431
然后我们利用这个布尔型数组选出那些穿越了30(绝对值)的随机漫步(行),并调用argmax在轴1上获取穿越时间:
crossing_times = (np.abs(walks[hits30]) >= 30).argmax(1)
crossing_times.mean()
506.2532789274264
请尝试用其他分布方式得到漫步数据。只需使用不同的随机数生成函数即可,如normal用于生成指定均值和标准差的正态分布数据:
steps = np.random.normal(loc=0, scale=0.25,
size=(nwalks, nsteps))
虽然本书剩下的章节大部分是用pandas规整数据,我们还是会用到相似的基于数组的计算。在附录A中,我们会深入挖掘NumPy的特点,进一步学习数组的技巧。