读书笔记:利用Python进行数据分析【第四章:NumPy基础-array数组】
不要误会,并不是作者不识数,1后面直接接了个4。
主要是这本书的第二章其实是一些范例,而且都是从很高的层次来提起兴趣的,可能后面全学完了回来仔细研读更好一些。而第三章主要讲Ipython,不过这个版本的Enthought Canopy已经有自己的IDE了。
总之,接下来就直接第四章了。
Numpy是高性能科学计算和数据分析的基础包,其部分功能如下:
- ndarray,一个具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组。
- 用于对整组数据进行快速运算的标准数学函数。
- 用于读写磁盘数据的工具以及用于操作内存映射文件的工具。
- 线性代数、随机数生成以及傅里叶变换。
- 用于集成由C、C++、Fortran等语言编写的代码的工具。
依照标准的NumPy约定,加载Numpy时总是使用import numpy as np。
虽然使用from numpy import *也是可行的,但是书中不建议这么做。
NumPy的ndarray:一种多维数组对象
NumPy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器。利用这种数组,可以对整块数据执行一些数学运算,而其语法跟标量元素之间的运算是一样的。需要注意的是,下文中提到的NumPy数组、ndarray、数组等,基本上指的都是ndarray对象。
创建ndarray:
创建数组最简单的办法就是使用array函数,该函数接受一切序列型的对象(包括其他数组),然后产生一个新的含有传入数据的NumPy数组,以一个列表转换为例:
#单列表转换:
import numpy as np
data1 = [6,7.5,8,0,1] #list
arr1 = np.array(data1) #把list转换为ndarray对象
这样就把一个列表转换为数组:
[ 60. 75. 80. 0. 10.]
而嵌套序列将会被转换为一个多维数组:
#嵌套序列:
data2 = [[1,2,3,4],[5,6,7,8]]
arr2 = np.array(data2)
本例中的嵌套序列被转换为一个二维数组:
[[1 2 3 4]
[5 6 7 8]]
除非显式说明,np.array会尝试为新建的这个数据推断出一个较为合适的数据类型,数据类型会保存在一个特殊的dtype对象中,比如说,在上面的两个例子中,打印两个数组的dtype对象
print arr1.dtype
print arr2.dtype
结果如下:
float64
int32
可以看到两个数组分别被归类为float64和int32。
除了np.array之外,还有一些函数也可以新建数组,比如,zeros和ones分别可以创建指定长度或形状的全0或全1数组,而empty可以创建一个没有任何具体值的数组,使用这些方法创建多维数组的时候,需要传入一个表示形状的元组:
print np.zeros(10) ##长度为10的1维全0数组
print np.zeros((3,6)) ##3维的长度为6的全0数组
print np.empty((2,3,2)) ##以此类推,维度可以无限扩大
获得结果为:
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[[ 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0.]]
[[[ 8.11256107e-316 2.19014637e-316]
[ 8.11250850e-316 8.11253617e-316]
[ 8.11257213e-316 8.11257490e-316]]
[[ 8.11257767e-316 8.13140394e-316]
[ 8.13140671e-316 8.13140947e-316]
[ 8.13141224e-316 7.03232764e-316]]]
必须要注意的是,认为np.empty会返回全0数组的想法是不安全的,很多情况下它返回的是一些没有初始化的垃圾值,上述代码已经呈现出这种情况。
而np.arrange则是Python内置函数range的数组版:
print np.arange(15) ##取包括0-14的数组
结果如下:
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14]
由于NumPy基本上关注的都是数值计算,因此,如果没有特别指定,数据类型基本都是float64。
书中未详述但是提及了的其他数组创建函数,还有eyes、identity和ones_like、zeros_like、empty_like。
后三个都是以现有数组作为参数,返回与该数组形状相同的全1、全0或空数组:
print np.ones_like(arr1)
print np.zeros_like(arr2)
print np.empty_like(arr2[1])
结果如下:
[ 1. 1. 1. 1. 1.]
[[0 0 0 0]
[0 0 0 0]]
[16843009 16843009 16843009 16843009]
而eyes和identity则是创建一个正方形的N*N单位矩阵:
print np.eye(5)
print np.identity(3)
结果如下:
[[ 1. 0. 0. 0. 0.]
[ 0. 1. 0. 0. 0.]
[ 0. 0. 1. 0. 0.]
[ 0. 0. 0. 1. 0.]
[ 0. 0. 0. 0. 1.]]
[[ 1. 0. 0.]
[ 0. 1. 0.]
[ 0. 0. 1.]]
关于eye和identity的区别可以看这里:http://blog.csdn.net/lanchunhui/article/details/52928517
ndarray的数据类型
dtype(数据类型)是数组的一个特殊对象,它含有ndarray将一块内存解释为特定数据类型时所需要的信息。前面提到array会给数组默认分配数据类型,不过也可以自行设定:
arr1 = np.array([1,2,3],dtype=np.float64)
arr2 = np.array([1,2,3],dtype=np.int32)
print arr1,arr1.dtype
print arr2,arr2.dtype
结果则为:
[ 1. 2. 3.] float64
[1 2 3] int32
dtype支持的全部数据类型如下:
不管是我还是书中都不建议强行记住这些玩意,通常来说,知道你要处理的数据大致属于什么类别就可以了。但是,当你需要控制数据在内存和磁盘中的存储方式,就必须要了解如何控制存储类型。
可以通过ndarray的astype方法显式地转换数组的dtype:
arr = np.array([1,2,3,4,5])
float_arr = arr.astype(np.float64) ##把arr的数据类别转换为float64
print arr.dtype,float_arr.dtype
print float_arr
结果如下:
int32 float64
[ 1. 2. 3. 4. 5.]
如果用astype把浮点数转换为整数,那么,小数点后的部分会被截断。
arr=np.array([3.7,-1.2,-2.6,0.5,12.9,10.1])
print arr.astype(np.int64)
结果如下:
[ 3 -1 -2 0 12 10]
对于全部由数字组成的字符串数组,也可以用astype转换为数值形式:
numeric_string = np.array(["1.25","-9.6","42"],dtype=np.string_) #下划线表示长度不固定
print numeric_string.astype(float)
结果如下:
[ 1.25 -9.6 42. ]
astype的后面不仅可以接np.float64等数据类型,也可以接其他数组的dtype。
int_array = np.arange(10) #一个0-9的数组,自动判别类型为int
calibers = np.array([.22,.270,.357,.380,.44,.50],dtype=np.float64)
print int_array.astype(calibers.dtype) ##把该数组的类型转化为与int_array数组相同的类型。
结果如下:
[ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
需要注意的是,调用astype的话,无论如何都会创造出一个新的数组,哪怕新的dtype和旧的相同也是一样的。另外,浮点数只能表示近似的分数值,在复杂的计算中,因为可能会积累一些浮点错误,因此比较操作只能在一定的小数位以内有效。
数组和标量之间的运算
数组最大的作用之一,就是在不需要编写循环的情况下,对数据执行批量运算,这通常就叫做矢量化(vectorization)。大小相等的数组之间的任何运算,则会把运算应用到元素级。
arr = np.array([[1.,2.,3.],
[4.,5.,6.]])
print arr
print arr*arr ##数组之间的运算,反映到各个对应元素之间
print arr-arr
结果如下:
[[ 1. 2. 3.]
[ 4. 5. 6.]]
[[ 1. 4. 9.]
[ 16. 25. 36.]]
[[ 0. 0. 0.]
[ 0. 0. 0.]]
可以看到,相同形状数组之间的直接运算,相当于各个对应位置的元素之间进行运算。
数组和标量之间的运算,则是把标量值的运算传播到各个元素:
print 1/arr
print arr**0.5 ##0.5次方,即开方
结果如下:
[[ 1. 0.5 0.33333333]
[ 0.25 0.2 0.16666667]]
[[ 1. 1.41421356 1.73205081]
[ 2. 2.23606798 2.44948974]]
而不同大小的数组之间的运算则被称为广播,这会在书中以后的内容中介绍。
虽然这么说,大概我会在这本书学完之前就提前学习这部分的吧,如果的确是这样的话,日后会进行补充。
基本的索引和切片
NumPy数组的索引是一个内容丰富的主题,因为选取数据子集或单个元素的方式有很多。一维数组很简单,从表面上看,它们跟Python列表的功能差不多:
arr = np.arange(10) ##0-9的数组
print arr
print arr[5] ##取第6个值
print arr[5:8] ##取第6到8个元素切片,注意含头不含尾
arr[5:8] = 12 #把第6到8的值改为12
print arr
结果如下:
[0 1 2 3 4 5 6 7 8 9]
5
[5 6 7]
[ 0 1 2 3 4 12 12 12 8 9]
把一个标量值赋予一个切片(如arr[5:8]=12)时,该值会自动传播到整个选区,数组切片与列表最大的不同是,数组切片是原始数组的使徒,这意味着数据是不会被复制的,视图上的任何修改,都会直接反映到原数组:
arr_slice = arr[5:8] #切片5-7
print arr_slice
arr_slice[1] = 12345 #对这个切片的赋值
print arr_slice
print arr
结果如下:
[12 12 12]
[ 12 12345 12]
[ 0 1 2 3 4 12 12345 12 8 9]
可以看到,对切片arr_slice的修改,最终返回了原始数组arr。
这是必须要注意的内容,和其他所有语言都不同,这是因为NumPy的设计目的是为了处理大数据,如果在这类切片操作中不断将数据复制来复制去,会造成许多问题。
如果要得到ndarray数组切片的一份副本而不仅仅是视图而已,需要显式地进行复制操作,例如arr[5:8].copy()
对于高维度的数组,能做的事情更多一些,在一个二维数组中,各索引位置上的元素不再是标量,而是一维数组。因此,可以对各个元素进行递归访问,存在两种等价的表示方式:
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print arr2d[2]
print arr2d[0][2] #递归地访问元素
print arr2d[0,2] #和上一行的表示方式等价
结果:
[7 8 9]
3
3
二维数组的索引方式,可以用下图说明:
而在多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的array(它含有高一级维度的array在某维度上的所有数据)。因此,在2*2*3数组arr3d中:
arr3d = np.array([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]) ##一个2*2*3的数组
print arr3d
print arr3d[0] #这是一个2*3的数组
结果如下:
[[[ 1 2 3]
[ 4 5 6]]
[[ 7 8 9]
[10 11 12]]]
[[1 2 3]
[4 5 6]]
标量值和数组都可以被赋值给arrar3d[0]:
old_values = arr3d[0].copy() #复制数组的正确姿势
arr3d[0] = 42 #把arr3d[0]赋值为标量
print arr3d
arr3d[0] = old_values #把arr3d[0]赋值为数组
print arr3d
结果如下:
[[[42 42 42]
[42 42 42]]
[[ 7 8 9]
[10 11 12]]]
[[[ 1 2 3]
[ 4 5 6]]
[[ 7 8 9]
[10 11 12]]]
切片索引
ndarray的切片语法和Python列表的一维对象切片是类似的,而对于高维数组,花样会多一些:
print arr2d
print arr2d[:2] ##从0轴开始切片,到2之前(不包括2)
print arr2d[:2,1:] ##对两个维度同时切片,选出在两个维度中都满足条件的元素。
print arr2d[1,:2]
print arr2d[2,:1]
print arr2d[:,:1] ##“只有冒号”,表示选取整个轴。
结果如下:
[[1 2 3]
[4 5 6]
[7 8 9]]
[[1 2 3]
[4 5 6]]
[[2 3]
[5 6]]
[4 5]
[7]
[[1]
[4]
[7]]
对切片表达式的赋值操作,也同样会被扩散到整个选区。
布尔型索引
可以看这样的一个例子:
names = np.array(["Bob","Joe","Will","Bob","Will","Joe","Joe"])
data = np.random.randn(7,4) ##numpy.random中的randn函数,随机生成一些正态分布的随机数据,维数为(7,4)
print names
print "\n"
print data
数据形式是这样的:
['Bob' 'Joe' 'Will' 'Bob' 'Will' 'Joe' 'Joe']
[[-0.51840308 -1.17475378 1.80119483 0.82550115]
[ 0.07116089 1.88526883 -0.18622611 0.20386887]
[-0.67011801 -0.34192716 0.52408749 0.80184717]
[ 1.17596727 0.27079258 -0.63718112 0.24706236]
[-0.57339128 -1.35908479 1.73181921 0.21168621]
[ 0.70643601 -0.61099849 -0.73577367 -0.13069403]
[ 0.03956691 -1.38386475 1.66659704 1.9505884 ]]
假设每个名字都对应data数组中的一行,而我们想要选出对应于名字“Bob”的所有行,跟算术运算一样,数组的比较运算(如==)也是矢量化的,因此,对names和字符串“Bob”的比较运算,将会产生一个布尔型数组,这个布尔型数组可以用于数组索引:
print names=="Bob"
print data[names=="Bob"]
结果为:
[ True False False True False False False]
[[-0.12145952 -0.54750608 -0.2461483 -0.37720365]
[-0.4986304 0.78934264 0.68174587 -0.49274356]]
此外,布尔型数组索引也可以和切片、整数混合使用:
print data[names=="Bob",2:]
print data[names=="Bob",3]
结果如下:
[[ 0.4444331 0.54836831]
[ 1.0157106 -0.4496924 ]]
[ 0.54836831 -0.4496924 ]
也可以用不等号!=、负号-等布尔运算符对条件进行否定,同样的,&、|等布尔算术运算符皆可运作。
但是,and、or等Python关键字,在布尔型数组中是无效的。
通过布尔数组,可以轻易地设定整行整列的值,只需要对符合布尔条件的检索赋予标量值即可。
花式索引
花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引,下例假设我们拥有一个8*4的数组:
arr= np.empty((8,4))
for i in range(8):
arr[i]=i ##使用循环逐行对整行赋值
print arr
可以看到数组形式如下:
[[ 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:
print arr[[4,3,0,6]] ##[4,3,0,6]是一个用于指定顺序的list
结果:
[[ 4. 4. 4. 4.]
[ 3. 3. 3. 3.]
[ 0. 0. 0. 0.]
[ 6. 6. 6. 6.]]
而使用负数索引,则可以从末尾开始选取:
print arr[[-3,-5,-7]]
结果:
[[ 5. 5. 5. 5.]
[ 3. 3. 3. 3.]
[ 1. 1. 1. 1.]]
一次传入多个数组会有一点特别,它返回的是一个一维数组,其中的元素对应各个索引元组:
arr = np.arange(32).reshape((8,4)) #0:31的range数组,reshape规定维数
print arr
print arr[[1,5,7,2],[0,3,1,2]]
结果如下:
[[ 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]]
[ 4 23 29 10]
可以看到,第一维的整数列表提取出指定的元素行,第二维的整数列表则分别指定这些行的元素位置,最终获得的是每行中指定位置的元素,而不是对通过第一维选出的各行继续排列组合。
如果想要得到原数组分别根据两个整数列表对两个维度进行花式索引的结果,有两种方案:
print arr[[1,5,7,2]][:,[0,3,1,2]]
print arr[np.ix_([1,5,7,2],[0,3,1,2])]
结果如下:
[[ 4 7 5 6]
[20 23 21 22]
[28 31 29 30]
[ 8 11 9 10]]
[[ 4 7 5 6]
[20 23 21 22]
[28 31 29 30]
[ 8 11 9 10]]
数轴转置与轴对换:
转置(transpose)是重塑的一种特殊形式,它返回的是源数据的视图(不会进行任何复制操作)。
对于ndarray数组,它不仅有transpose方法,还有一个特殊的T属性:
arr= np.arange(15).reshape((3,5))
print arr
print arr.T #转置
结果如下:
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]]
[[ 0 5 10]
[ 1 6 11]
[ 2 7 12]
[ 3 8 13]
[ 4 9 14]]
在进行矩阵计算时,经常需要用到该操作。
对于高维数组,transpose则需要得到一个由轴编号组成的元组,才能对这些轴进行转置:
arr = np.arange(16).reshape((2,2,4))
print arr
print arr.transpose((1,0,2)) ##等于一维和二维转换,第三维度不受影响
结果:
[[[ 0 1 2 3]
[ 4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]]
[[[ 0 1 2 3]
[ 8 9 10 11]]
[[ 4 5 6 7]
[12 13 14 15]]]
简单的转置使用.T就可以了,它其实就是进行轴对换而已。
ndarray还有一个swapaxes方法,需要接受一对轴编号:
print arr.swapaxes(0,1)
print arr.swapaxes(1,2)
结果:
[[[ 0 1 2 3]
[ 8 9 10 11]]
[[ 4 5 6 7]
[12 13 14 15]]]
[[[ 0 4]
[ 1 5]
[ 2 6]
[ 3 7]]
[[ 8 12]
[ 9 13]
[10 14]
[11 15]]]
通用函数:快速的元素级数组函数
通用函数(即ufunc)是一种对ndarray中的数据执行元素级运算的函数,可以把它看成简单函数(接受一个或多个标量值,并产生一个或多个标量值)的矢量包装器。
许多ufunc都是简单的元素级变体,如sqrt和exp:
arr = np.arange(10)
print arr
print np.sqrt(arr)
print np.exp(arr)
结果如下:
[0 1 2 3 4 5 6 7 8 9]
[ 0. 1. 1.41421356 1.73205081 2. 2.23606798
2.44948974 2.64575131 2.82842712 3. ]
[ 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,直接对每个单个元素作用。另外一些ufunc如odd或maximum,接受2个数组并返回一个结果数组,因此也叫二元(binary)ufunc:
x = np.random.randn(8)
y = np.random.randn(8)
print x
print y
print np.maximum(x,y) #两对应元素中取最大值
print np.add(x,y) #两对应元素相加
结果如下:
[-0.39285177 -0.82661732 -2.04100761 1.46466741 0.54787724 -1.64333526
-1.6435731 0.69702387]
[-2.85969706 0.97906267 0.40894504 -0.23225467 1.03203881 -1.07558835
0.95851455 -0.42433272]
[-0.39285177 0.97906267 0.40894504 1.46466741 1.03203881 -1.07558835
0.95851455 0.69702387]
[-3.25254883 0.15244536 -1.63206257 1.23241274 1.57991605 -2.71892361
-0.68505855 0.27269114]
另外,虽然并不常见,但的确有一些ufunc可以返回多个数组,modf就是一个例子,它是Python内置函数divmod的矢量化版本,用于切分浮点数数组的小树和整数部分:
arr = np.random.randn(7)*5
print arr
print np.modf(arr)
结果如下:
[-1.06370557 2.70985254 -4.04098971 -6.44241676 -6.44296255 2.49964631
2.5296449 ]
(array([-0.06370557, 0.70985254, -0.04098971, -0.44241676, -0.44296255,
0.49964631, 0.5296449 ]), array([-1., 2., -4., -6., -6., 2., 2.]))
一些常用的一元和二元ufunc:
惯例,没有人会想要把所有这些都熟记于心,需要的时候查询即可。
利用数组进行数据处理:
Numpy数组使你可以将许多种数据处理任务表述为简洁的数组表达式(否则需要编写循环),这是数组最重要的功能之一。用数组表达式代替循环的做法,通常被称为矢量化,一般来说,矢量化数组的运算,比等价的纯Python方式快上一两个数量级甚至更多,尤其是各种数值计算。
假设我们想要在一组值(网络型)上计算函数sqrt(x^2+y^2),则np.meshgrid函数接受两个一维数组,并产生两个二维矩阵(对应两个数组中的所有(x,y)对):
points = np.arange(-5,5,0.01) #从-5到5的区间中每隔0.01取一个点,共1000个。
xs,ys = np.meshgrid(points,points) ##这个meshgrid函数接受两个一维数组,生成两个二维数组,每个二维数组都是把原本的一维数组A广播到另一个一维数组B的行(列)数
##如:A是长度为10的一维数组,B是长度为9的一维数组,则数组1:以A为行向量广播9行;数组B:以B为列向量广播10列
##总之生成两个形状相同,分别对两个向量从不同维度广播的二维矩阵
print xs
print ys
结果如下:
[[-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]]
[[-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]]
现在,只要对两个数组当成两个浮点数编写表达式运算就可以了:
import matplotlib.pyplot as plt
z = np.sqrt(xs**2+ys**2)
print z
plt.imshow(z,cmap=plt.cm.gray)
plt.colorbar()
plt.show()
可以看到获得了一个对角矩阵:
[[ 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]]
并画出函数值的图形化结果:
将条件逻辑表述为数组运算:
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)] ##zip函数:接受一系列对象作为参数,对每个参数中相同位置的对应值打包成一个个tuple,然后返回由这些tuple组成的列表
##可以理解为对所有的list进行转置,然后取行
print result
结果如下:
[1.1000000000000001, 2.2000000000000002, 1.3, 1.3999999999999999, 2.5]
这有几个问题:第一,它对大数组的处理速度不是很快(因为所有工作都是纯Python完成的);第二,无法适用于多维数组。而如果使用np.where,则可以把该功能写的非常简洁:
result = np.where (cond,xarr,yarr)
print result
结果:
[ 1.1 2.2 1.3 1.4 2.5]
np.where的第二个和第三个参数不必是数组,它们都可以是标量值,在数据分析工作中,where通常用于根据另一个数组而产生一个新的数组,假设有一个由随机数据组成的矩阵,你希望把所有正值替换为2,将所有负值替换为-2,若利用np.where,则会非常简单:
arr = np.random.randn(4,4)
print arr
print np.where(arr>0,2,-2)
print np.where(arr>0,2,arr)
结果如下:
[[-1.50788361 -1.56723442 -0.65437385 -0.11532383]
[-0.98044173 -0.37096436 -0.2883842 1.37215698]
[ 0.58855527 1.94801601 0.78428079 -1.77236309]
[ 0.95338908 -0.32489175 -0.91314603 0.02633972]]
[[-2 -2 -2 -2]
[-2 -2 -2 2]
[ 2 2 2 -2]
[ 2 -2 -2 2]]
[[ 2. -1.48279979 -0.05612731 2. ]
[ 2. -0.25539836 -0.36361196 -0.38392533]
[ 2. -0.14320375 2. -1.22339769]
[-1.3330137 2. -1.06812427 2. ]]
传递给where的数组大小可以不相等,甚至也可以是标量值。
通过嵌套等手段,np.where也能够表述很多复杂的逻辑。
数学和统计方法
可以通过数组上的一组数学函数,对整个数组或某个轴向的数据进行统计计算。sum、mean以及标准差std等聚合计算,既可以当做数组的实例方法调用,也可以当做*NumPy函数使用:
arr = np.random.randn(5,4)
print arr
print arr.mean()
print np.mean(arr)
print arr.sum()
print np.sum(arr)
#mean、sum这类的聚合函数,可以接受一个axis参数(用于计算该轴向的统计值),最终结果是一个少一维的数组:
print arr.mean(axis=1)
print arr.sum(0)
结果如下:
[[-0.92842549 -1.90215005 -0.79182489 1.56137107]
[ 0.76127099 -0.33480591 0.75191817 -0.9859128 ]
[-1.04268781 -0.0788721 0.03262631 -0.22192648]
[ 0.16301499 0.12161314 0.67277378 -0.33700148]
[ 0.52364531 -0.25818886 -0.95605535 -1.48211624]]
-0.236586685218
-0.236586685218
-4.73173370435
-4.73173370435
[-0.51525734 0.04811761 -0.32771502 0.15510011 -0.54317878]
[-0.52318202 -2.45240378 -0.29056198 -1.46558593]
而其他如cumsum(包括本元素与之前所有元素的累加)和cumprod(累乘)之类的方法则不聚合,而是产生一个由中间结果组成的数组:
arr = np.array([[0,1,2],[3,4,5],[6,7,8]])
print arr
print arr.cumsum(0)
print arr.cumprod(1)
结果如下:
[[0 1 2]
[3 4 5]
[6 7 8]]
[[ 0 1 2]
[ 3 5 7]
[ 9 12 15]]
[[ 0 0 0]
[ 3 12 60]
[ 6 42 336]]
全部的基本数组统计方法如下表:
用于布尔型数组的方法
在上面这些方法中,布尔值会被强制转换为1(True)和0(False),因此,sum经常被用来对布尔型数组中的True值计数:
arr = np.random.randn(100)
print arr
print (arr>0).sum()
结果如下:
[ 0.17620485 0.83476677 -1.01596503 -0.76794435 -0.92072534 0.35038885
0.11472891 -0.14437387 1.53018089 -0.63833615 -1.05527102 -0.34796197
-0.03979406 -0.77135861 -0.56190744 -0.37443598 0.54111047 -0.67228931
-1.08187707 -0.43295852 1.13126067 -0.67037054 0.42477886 0.83838601
0.01835025 0.39125945 1.03106054 -0.39493467 1.25272682 0.35701184
0.99063356 -0.45274263 0.56568443 0.68314395 0.39536539 0.26602475
1.52088205 0.46901377 1.42446694 0.67406639 0.68537452 0.69931914
0.36420238 0.85081951 -1.21170863 -1.58366186 0.10715053 0.65385801
-0.28607256 -0.14301151 0.78419336 -0.45794042 -0.53238137 -0.46146282
1.64663511 -0.28806133 -1.11590803 0.36902296 -1.28171611 0.49208342
-0.40975246 -0.02708362 -1.97090965 0.31931754 -1.21905815 0.35487344
-0.58337291 -1.47005258 -0.01794697 -0.53156516 -0.62684632 1.86533889
1.06882899 -1.29904495 -0.11871839 1.81454613 -0.45450993 -0.13192184
-0.85205348 -0.85522946 0.84403619 -0.12598852 -0.10881044 -0.57198149
-0.46131897 -0.24928557 -1.45367189 -0.2497887 -0.2964098 0.32855852
-2.15147808 -1.6314315 -0.28318682 0.15660364 -0.26054962 -1.23190775
-0.63429707 -1.63493755 0.59958001 -0.07723245]
42
另外还有两个方法any和all,它们对布尔型数组非常有用。any用于测试数组中是否存在True,all则检查数组中是否所有值都是True:
bools = np.array([False,False,True,False])
print bools.any()
print bools.all()
结果:
True
False
这两个方法也能用于非布尔型的数组:所有非0元素被视为True。
排序
和Python内置的列表类型一样,NumPy数组也可以通过sort方法就地排序:
##一维数组的场合:
arr = np.random.randn(8)
print arr
arr.sort()
print arr
结果如下:
[-3.03875032 0.56032173 -0.14472072 0.01346827 1.60327126 0.92531084
0.32799482 1.224844 ]
[-3.03875032 -0.14472072 0.01346827 0.32799482 0.56032173 0.92531084
1.224844 1.60327126]
对于多维数组,可以在任何一个轴向上进行排序,只需要将轴编号传给sort:
arr = np.random.randn(5,3)
print arr
arr.sort(1)#在行方向进行排列
print arr
结果如下:
[[-0.04372987 1.63452795 0.80646153]
[ 1.23282673 0.16795756 0.78570604]
[ 0.56865793 -1.45892085 0.27496028]
[-1.75007769 -0.97845892 -0.69002895]
[-1.8136052 0.36670539 1.23040479]]
[[-0.04372987 0.80646153 1.63452795]
[ 0.16795756 0.78570604 1.23282673]
[-1.45892085 0.27496028 0.56865793]
[-1.75007769 -0.97845892 -0.69002895]
[-1.8136052 0.36670539 1.23040479]]
*方法np.sort返回的是数组的已排序副本,而就地拍需则会修改数组本身,计算数组分位数最简单的方法就是对其进行排序,然后选取特定位置的值。
唯一化以及其他的集合逻辑
NumPy提供了一些针对一维ndarray的基本集合运算,最常用的可能就要数np,unique了,它用于找出数组中的唯一值,并返回已经排序的结果:
names = np.array(["Bob","Joe","Will","Bob","Will","Joe","Joe"])
print np.unique(names) ##会自动按首字母顺序排列
ints = np.array([3,3,3,2,2,1,1,4,4])
print np.unique(ints)
结果如下:
['Bob' 'Joe' 'Will']
[1 2 3 4]
而另一个函数np.in1d用于测试一个数组中的值在另一个数组中的成员资格,返回一个布尔型数组:
values = np.array([6,0,0,3,2,5,6])
print np.in1d(values,[2,3,6])##返回第一个数组在第二个数组中的成员资格
结果如下:
[ True False False True True False True]
其他的集合函数参考:
用于数组的文件输入输出
NumPy能够读写磁盘上的文本数据或者二进制数据。
将数据以二进制的格式保存到磁盘
np.save和np.load是读写磁盘数组数据的两个主要函数,默认情况下,数组是以未压缩的原始二进制格式保存在扩展名为.npy的文件中的。
arr = np.arange(10)
print arr
np.save("some_array",arr) #文件保存,如果文件路径末尾不加扩展名.npy,则该扩展名会在保存时自动加上。
print np.load("some_array.npy")
结果如下:
[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 5 6 7 8 9]
通过np.savez则可以把多个数组保存到一个压缩文件中,将数组以关键字参数的形式传入即可:
np.savez('array_archieve.npz',a=arr,b=arr) ##把两个数组保存到一个压缩文件里
arch = np.load("array_archieve.npz") ##加载.npz文件时,你会得到一个类似字典的对象,该对象会对各个数组进行延迟加载
print arch['a'],arch['b']
结果如下:
[0 1 2 3 4 5 6 7 8 9] [0 1 2 3 4 5 6 7 8 9]
线性代数
线性代数是任何数组库的重要组成部分,由于NumPy中,通过*对两个二维数组相乘得到的是一个元素级的积,而非其他语言中的点积,因此,NumPy提供了一个用于矩阵乘法的dot函数:
x = np.array([[1.,2.,3.],[4.,5.,6.]])
y = np.array([[6.,23.],[-1,7],[8,9]])
print x
print y
print x.dot(y)
结果如下:
[[ 1. 2. 3.]
[ 4. 5. 6.]]
[[ 6. 23.]
[ -1. 7.]
[ 8. 9.]]
[[ 28. 64.]
[ 67. 181.]]
numpy.linalg中有一组标准的线性代数运算函数,一些常用的运算函数如下:
随机数生成
numpy.random模块对Python内置的random进行了补充,增加了一些用于高效生成多种概率分布的样本值的函数,如上文中多次使用的np.random.randn,是生成一组正态分布的样本值。由于Python内置的random模块一次只能生成一个样本值,因此,在需要产生大量样本值的时候,numpy.random快了不止一个数量级。
下表是numpy.random中的部分函数:
范例:随机漫步
我们通过模拟随机漫步,来说明如何运用数组运算。
先来看一个简单地随机漫步的例子:从0开始,步长1和-1出现的概率相等,我们通过内置的random模块,以纯Python的方式实现1000步的随机漫步:
import random
position = 0 #位置初始化为0
walk = [position] #漫步表示为一个由多个position组成的list,同样此时留在初始值
steps = 1000 #步数1000
for i in xrange(steps): ##xrange与range在用法上完全相同,所不同的是range生成一个列表,xrange则是获得一个生成器。
step = 1 if random.randint(0,1) else -1 ##随机从0,1中生成一个数,1则+1,0则-1
position +=step
walk.append(position)
可以看到,最终位置这其实就是随机漫步中各步的累计和,可以用一个数组运算来实现,这里,用np.random模块一次性随机产生1000个“掷硬币”结果,将其分别设置为1或-1,然后计算累计和:
nsteps = 1000
draws = np.random.randint(0,2,size=nsteps) ##randint:从给定的上下限范围内随机选取整数,惯例上限不包含在内,因此下限0上限2的意思就是只有0和1,生成1000个随机数。
steps = np.where(draws>0,1,-1) #draws>0则1否则-1
walk = steps.cumsum() #累加,这是一维数组所以不需要标注维度
有了这些数据之后,我们就可以做一些统计工作了,比如求取最大值和最小值:
print walk.min()
print walk.max()
结果如下:
-46
16
现在来看一个复杂点的统计任务——首次穿越时间,即随机漫步过程中第一次到达某个特定值的时间。
假设我们想要知道本次随机漫步需要多久才能距离初始0点至少10步远(任一方向均可),np.abs(walk)>=10可以得到一个布尔型数组,它表示的是距离是否达到或超过10,而我们想知道的是第一个10或-10的索引,可以用argmax解决这个问题,它返回的是该布尔值数组第一个最大值的索引(在布尔数组中,True即1就是最大值):
print (np.abs(walk)>=10).argmax() ##argmax,返回第一个出现的最大值的位置
结果如下:
97
需要注意的是,argmax并不高效,因为它无论如何都会对数组完全扫描。
一次模拟多个随机漫步
如果希望模拟多个随机漫步过程(比如5000个),只需要对上述代码做以第按点修改即可生成所有的随机漫步过程。只要给numpy.random的函数传入一个二元元组就可以产生一个二维数组,然后我们就可以一次性计算5000个随机漫步过程(一行一个)的累计和了:
#####多个随机漫步:
nwalks = 5000
nsteps = 1000
draws = np.random.randint(0,2,size=(nwalks,nsteps)) ##size变成了二维的,会生成一组5000*1000的二维数组
steps = np.where(draws>0,1,-1)
walks = steps.cumsum(1)
print walks
print walks.max()
print walks.min()
结果如下:
[[ -1 -2 -3 ..., -4 -5 -6]
[ 1 2 1 ..., -36 -37 -38]
[ 1 2 1 ..., 36 35 34]
...,
[ 1 0 1 ..., 64 65 64]
[ 1 0 1 ..., -36 -37 -38]
[ 1 2 1 ..., 30 31 30]]
129
-116
接下来,计算到达30或-30(即离开了30的距离)的最小穿越时间,因为并不是5000个过程都到达了30或-30,因此,可以用any的方法来进行检查:
hits30 = (np.abs(walks)>=30).any(1)
print hits30 #是否到达30
print hits30.sum() #到达30的数量
结果如下:
[ True True True ..., True True True]
3429
然后,选出那些穿越了绝对值30的随机漫步行,并调用argmax在轴1上获取穿越时间:
crossing_times = (np.abs(walks[hits30])>=30).argmax(1)
print crossing_times
结果如下:
[361 807 671 ..., 587 405 831]