一、Numpy基础知识
NumPy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器。可以利用这种数组对整块数据执行一些数学运算,语法跟标量元素之间的运算一样。
1.1 按元素简单运算
1 2 3 4 5 6 7 8 9 10
| import numpy as np
data = np.random.randn(2,3) print(type(data), '\n' ,data) print("-----元素相乘-----") data_mul = data * 10 print(type(data_mul), '\n' ,data_mul) print("-----元素相加-----") data_sum = data + data print(type(data_sum), '\n' ,data_sum)
|
以上例子中,所有的元素都乘以10,每个元素都与自身相加,其结果输出如下:
1 2 3 4 5 6 7 8 9 10 11
| <class 'numpy.ndarray'> [[ 0.22458451 1.50467481 -1.10270028] [-0.6519424 -1.78475736 1.62477908]] -----元素相乘----- <class 'numpy.ndarray'> [[ 2.24584506 15.04674812 -11.02700277] [ -6.51942398 -17.84757364 16.24779077]] -----元素相加----- <class 'numpy.ndarray'> [[ 0.44916901 3.00934962 -2.20540055] [-1.3038848 -3.56951473 3.24955815]]
|
ndarray是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的。每个数组都有一个shape
(一个表示各维度大小的元组)和一个dtype
(一个用于说明数组数据类型的对象):
1 2 3 4
| ··· print("-----shape和dtype-----") print(data.shape) print(data.dtype)
|
输出如下:
1 2 3
| -----shape和dtype----- (2, 3) float64
|
精通面向数组的编程和思维方式是成为Python科学计算牛人的一大关键步骤。
1.2 创建ndarray
使用array函数,将序列型的对象(包括其他数组)转换成一个Numpy数组。除非特别说明,np.array会尝试为新建的这个数组推断出一个较为合适的数据类型。
1 2 3 4 5 6 7 8 9
| import numpy as np data1 = [1, 2, 3.4, 7, 0] arr1 = np.array(data1) print(arr1) print(arr1.dtype)
[1. 2. 3.4 7. 0. ] float64
|
嵌套序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ··· data2 = [[1, 2, 3, 4], [5, 6, 7, 8]] arr2 = np.array(data2) print(arr2) print(arr2.ndim) print(arr2.shape) print(arr2.dtype)
[[1 2 3 4] [5 6 7 8]] 2 (2, 4) int64
|
除np.array
之外,还有一些函数也可以新建数组。比如,zeros和ones分别可以创建指定长度或形状的全0或全1数组。empty可以创建一个没有任何具体值的数组。要用这些方法创建多维数组,只需传入一个表示形状的元组即可:
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
| ···
print('----全零数组-----') data3 = np.zeros((18)) print(data3) print('-----全一数组-----') data4 = np.zeros((3,6)) print(data4) print('-----空数组-----') data5 = np.empty((2,3,3)) print(data5)
----全零数组----- [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. 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.]]]
|
arange
是Python内置函数range的数组版:
1 2 3 4 5 6 7 8 9
| ···
print('-----arange-----') data6 = np.arange(16) print(data6)
-----arange----- [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
表4-1列出了一些数组创建函数。由于NumPy关注的是数值计算,因此,如果没有特别指定,数据类型基本都是float64(浮点数)。
1.3 ndarray的数据类型
dtype(数据类型)是一个特殊的对象,它含有ndarray将一块内存解释为特定数据类型所需的信息:
1 2 3 4 5 6 7 8 9 10 11
| import numpy as np arr1 = np.array([1, 2, 3], dtype = np.float64) arr2 = np.array([1.3, 2.1, 3.8], dtype = np.int32) print(arr1, '\n' ,arr1.dtype) print(arr2, '\n' ,arr2.dtype)
[1. 2. 3.] float64 [1 2 3] int32
|
dtype是NumPy灵活交互其它系统的源泉之一。多数情况下,它们直接映射到相应的机器表示,这使得“读写磁盘上的二进制数据流”以及“集成低级语言代码(如C、Fortran)”等工作变得更加简单。数值型dtype的命名方式相同:一个类型名(如float或int),后面跟一个用于表示各元素位长的数字。标准的双精度浮点值(即Python中的float对象)需要占用8字节(即64位)。因此,该类型在NumPy中就记作float64。记不住这些NumPy的dtype也没关系,新手更是如此。通常只需要知道你所处理的数据的大致类型是浮点数、复数、整数、布尔值、字符串,还是普通的Python对象即可。当你需要控制数据在内存和磁盘中的存储方式时(尤其是对大数据集),那就得了解如何控制存储类型。
可以通过ndarray的astype方法明确地将一个数组从一个dtype转换成另一个dtype:
1 2 3 4 5 6 7 8 9 10
| ...
arr = np.array([1, 2, 3, 4, 5]) print(arr.dtype) float_arr = arr.astype(np.float64) print(float_arr.dtype)
int64 float64
|
如果将浮点数转换成整数,则小数部分将会被截取删除:
1 2 3 4 5 6 7 8 9 10 11 12
| ...
arr = np.array([1.1, 2.2, 3.3, 4.6, 5.7]) print(arr.dtype) int_arr = arr.astype(np.int64) print(int_arr) print(int_arr.dtype)
float64 [1 2 3 4 5] int64
|
如果某字符串数组表示的全是数字,也可以用astype将其转换为数值形式:
1 2 3 4 5 6 7 8 9 10
| ···
numeric_strings = np.array(['1.25', '-9.6', '42'], dtype = np.string_) print(numeric_strings) arr_string = numeric_strings.astype(np.float64) print(arr_string, arr_string.dtype)
[b'1.25' b'-9.6' b'42'] [ 1.25 -9.6 42. ] float64
|
注意:使用numpy.string_类型时,一定要小心,因为NumPy的字符串数据是大小固定的,发生截取时,不会发出警告。pandas提供了更多非数值数据的便利的处理方法。
如果转换过程因为某种原因而失败了(比如某个不能被转换为float64的字符串),就会引发一个ValueError。NumPy很聪明,它会将Python类型映射到等价的dtype上(所以原作者写的是float而不是np.float64)。
数组的dtype还有另一个属性:
1 2 3 4 5 6 7 8 9
| ···
int_array = np.arange(10) calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64) int_array = int_array.astype(calibers.dtype) print(int_array.dtype)
float64
|
注意:调用astype总会创建一个新的数组(一个数据的备份)所以它不是inplace操作,即使新的dtype与旧的dtype相同。
二、Numpy数组的运算
2.1 同尺寸数组算数运算
大小相等的数组之间的任何算术运算都会将运算应用到元素级:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import numpy as np
arr = np.array([[1., 2., 3.], [4., 5., 6.]]) print(arr)
print('-----乘法-----') mul_arr = arr * arr print(mul_arr) print('-----减法-----') sub_arr = arr - arr print(sub_arr)
[[1. 2. 3.] [4. 5. 6.]] -----乘法----- [[ 1. 4. 9.] [16. 25. 36.]] -----减法----- [[0. 0. 0.] [0. 0. 0.]]
|
2.2 数组与标量算数运算
数组与标量的算术运算会将标量值传播到各个元素:
1 2 3 4 5 6 7 8 9 10 11 12
| ···
print('-----标量与数组-----') scalar_arr = 1 / arr print(scalar_arr)
[[1. 2. 3.] [4. 5. 6.]] -----标量与数组----- [[1. 0.5 0.33333333] [0.25 0.2 0.16666667]]
|
大小相同的数组之间的比较会生成布尔值数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ···
print('-----大小相同的数组之间的比较------') arr2 = np.random.randn(2,3) bool_arr = arr2 > arr print(arr2) print(bool_arr)
[[1. 2. 3.] [4. 5. 6.]] -----大小相同的数组之间的比较------ [[-0.03972645 0.48887017 -0.12327814] [ 0.79288534 0.22763344 -1.8064193 ]] [[False False False] [False False False]]
|
不同大小的数组之间的运算叫做广播(broadcasting)
三、基本的索引和切片
3.1 一维数组索引
一维数组很简单。从表面上看,它们跟Python列表的功能差不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import numpy as np
arr = np.arange(10) print(f'arr的值为{arr}')
print(f'arr[5]的值为{arr[5]}')
print(f'arr[5:8]的值为{arr[5:8]}')
arr[5:8] = 12 print(f'赋值后arr的值为{arr}')
|
其结果为:
1 2 3 4
| arr的值为[0 1 2 3 4 5 6 7 8 9] arr[5]的值为5 arr[5:8]的值为[5 6 7] 赋值后arr的值为[ 0 1 2 3 4 12 12 12 8 9]
|
当你将一个标量值赋值给一个切片时(如arr[5:8]=12),该值会自动传播(也就说后面将会讲到的“广播”)到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上。
1 2 3 4 5 6 7 8
| ···
print('-----作用在源数组上-----') arr_slice = arr[5:8] print(f'arr_slice的值为{arr_slice}') arr_slice[1] = 12345 print(f'arr_slice的值为{arr_slice}') print(f'此时arr的值为{arr}')
|
其结果如下,切实的反映到源数组元素中:
1 2 3 4
| -----作用在源数组上----- arr_slice的值为[12 12 12] arr_slice的值为[ 12 12345 12] 此时arr的值为[ 0 1 2 3 4 12 12345 12 8 9]
|
如果你想要得到的是ndarray切片的一份副本而非视图,就需要明确地进行复制操作,例如arr[5:8].copy()
3.2 高维数组索引
对于高维度数组,能做的事情更多。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组。因此,可以对各个元素进行递归访问,但这样需要做的事情有点多。你可以传入一个以逗号隔开的索引列表来选取单个元素。也就是说,下面两种方式是等价的:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import numpy as np
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
|
多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的ndarray(它含有高一级维度上的所有数据)。因此,在2×2×3数组arr3d中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import numpy as np
arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]) print(f'arr3d的值为{arr3d}') print('\n') print(f'arr3d[0]的值为{arr3d[0]}')
arr3d的值为[[[ 1 2 3] [ 4 5 6]]
[[ 7 8 9] [10 11 12]]]
arr3d[0]的值为[[1 2 3] [4 5 6]]
|
arr3d[0]是一个2×3数组。标量值和数组都可以被赋值给arr3d[0]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ··· old_values = arr3d[0].copy() arr3d[0] = 42 print(f'arr3d的值为{arr3d}') print('\n') arr3d[0] = old_values print(f'arr3d的值为{arr3d}')
arr3d的值为[[[42 42 42] [42 42 42]]
[[ 7 8 9] [10 11 12]]]
arr3d的值为[[[ 1 2 3] [ 4 5 6]]
[[ 7 8 9] [10 11 12]]]
|
相似的,arr3d[1,0]可以访问索引以(1,0)开头的那些值(以一维数组的形式返回):
1 2 3 4
| ··· print(f'arr3d[1,0]的值为{arr3d[1,0]}') print(f'arr3d[1]的值为{arr3d[1]}') print(f'arr3d[0]的值为{arr3d[0]}')
|
其结果为:
1 2 3 4 5
| arr3d[1,0]的值为[7 8 9] arr3d[1]的值为[[ 7 8 9] [10 11 12]] arr3d[0]的值为[[1 2 3] [4 5 6]]
|
注意,在上面所有这些选取数组子集的例子中,返回的数组都是视图。
3.3 切片索引
ndarray的切片语法跟Python列表这样的一维对象差不多:
1 2 3 4 5 6 7
| import numpy as np arr = np.arange(10) print(f'arr[1:6]的值为{arr[1:6]}')
arr[1:6]的值为[1 2 3 4 5]
|
对于之前的二维数组arr2d,其切片方式稍显不同:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import numpy as np
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) print(f'arr2d的值为{arr2d}') print(f'arr2d[:2]的值为{arr2d[:2]}')
arr2d的值为[[1 2 3] [4 5 6] [7 8 9]] arr2d[:2]的值为[[1 2 3] [4 5 6]]
|
可以看出,它是沿着第0轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。表达式arr2d[:2]可以被认为是“选取arr2d的前两行”。
以一次传入多个切片,就像传入多个索引那样:
1 2 3 4 5 6
| ··· print(f'arr2d[:2, 1:]的值为{arr2d[:2, 1:]}')
arr2d[:2, 1:]的值为[[2 3] [5 6]]
|
像这样进行切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片。
1 2 3 4 5 6 7
| ··· print(f'arr2d[1, :2]的值为{arr2d[1, :2]}') print(f'arr2d[:2, 2]的值为{arr2d[:2, 2]}')
arr2d[1, :2]的值为[4 5] arr2d[:2, 2]的值为[3 6]
|
“只有冒号”表示选取整个轴,也可以像下面这样只对高维轴进行切片:
1 2 3 4 5 6 7
| ··· print(f'arr2d[:, :1]的值为{arr2d[:, :1]}')
arr2d[:, :1]的值为[[1] [4] [7]]
|
对切片表达式的赋值操作也会被扩散到整个选区:
1 2 3 4 5 6 7 8
| ··· arr2d[:2, 1] = 88 print(arr2d)
[[ 1 88 3] [ 4 88 6] [ 7 8 9]]
|
3.4 布尔型索引
假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据。假设每个名字都对应data数组中的一行,而我们想要选出对应于名字"Bob"的所有行。跟算术运算一样,数组的比较运算(如==)也是矢量化的。因此,对names和字符串"Bob"的比较运算将会产生一个布尔型数组,这个布尔型数组可用于数组索引:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import numpy as np names = np.array(['chen', 'zhang', 'li', 'xu', 'zhao', 'chen']) data = np.random.randn(6, 4)
print('-----names-----') print(names) print('-----data-----') print(data)
print('-----Bool_Numpy-----') bool_numpy = names == 'chen' print(bool_numpy) print('-----Bool索引-----') print(data[bool_numpy])
|
结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| -----names----- ['chen' 'zhang' 'li' 'xu' 'zhao' 'chen'] -----data----- [[ 0.54172558 -0.63760429 -1.52236895 0.47188601] [-0.6040155 -0.47223536 -0.76297061 -1.04636066] [-0.73840056 -2.39232494 -1.2425439 0.06129273] [-1.13550151 0.61874361 1.09254494 0.30680673] [ 0.04550459 0.3549206 0.83898802 -0.07078892] [-0.610355 -0.51592092 -0.82877266 1.15671715]] -----Bool_Numpy----- [ True False False False False True] -----Bool索引----- [[ 0.54172558 -0.63760429 -1.52236895 0.47188601] [-0.610355 -0.51592092 -0.82877266 1.15671715]]
|
如果布尔型数组的长度不对,布尔型选择就会出错,因此一定要小心。
可以将布尔型数组跟切片、整数(或整数序列,稍后将对此进行详细讲解)混合使用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| ··· print('-----Bool索引与切片等混用-----') print(data[bool_numpy, :2]) print('\n') print(data[bool_numpy, 1])
-----Bool索引与切片等混用----- [[ 1.73880662 -1.04527357] [ 0.96951486 -0.17282384]]
[-1.04527357 -0.17282384]
|
要选择除"Bob"以外的其他值,既可以使用不等于符号(!=),也可以通过~对条件进行否定:
1 2 3 4 5 6 7 8 9 10
| ··· print('-----反转Bool结果-----') print(data[~bool_numpy])
-----反转Bool结果----- [[ 0.93945161 -0.7544237 -0.34644503 -0.09743455] [-0.42706265 0.43095953 -0.3795392 0.69471133] [ 0.18386234 1.33093908 0.45020598 0.29030489] [ 1.3630922 -0.62001429 -0.64274597 -0.47705963]]
|
选取这三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可:
1 2 3 4 5 6 7 8 9 10 11 12
| ··· print('-----不一样的Bool结果-----') bool_numpy = (names == 'chen') | (names == 'zhang') print(bool_numpy) print(data[bool_numpy])
-----不一样的Bool结果----- [ True True False False False True] [[-1.66626687 1.05335126 1.33397161 0.1272513 ] [-0.16854486 0.80258699 1.53339818 -1.35417792] [-1.10472392 -0.25433755 0.70161534 0.46733374]]
|
通过布尔型索引选取数组中的数据,将总是创建数据的副本,即使返回一模一样的数组也是如此。Python关键字and和or在布尔型数组中无效。要使用&与|。
通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:
1 2 3 4 5 6 7 8 9 10 11 12 13
| ··· print('-----通过Bool设置数值的值-----') data[data < 0] = 0 print(data)
-----通过Bool设置数值的值----- [[0. 0.45907411 0.23298449 0. ] [0. 0. 0.92147962 2.43230256] [0. 0.65158384 0. 0.93109638] [1.12697997 0.22151994 0. 0. ] [0. 0.08199407 1.32571875 0. ] [0.12717105 0.67663375 0.49319767 0.21915886]]
|
通过一维布尔数组设置整行或列的值也很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13
| ··· print('-----通过Bool设置数值的值-----') data[names != 'chen'] = 7 print(data)
-----通过Bool设置数值的值----- [[-2.69925965 -0.90946446 -0.25263602 0.15827415] [ 7. 7. 7. 7. ] [ 7. 7. 7. 7. ] [ 7. 7. 7. 7. ] [ 7. 7. 7. 7. ] [-0.7369675 0.55405615 -0.88576397 -0.43520667]]
|
3.5 花式索引
花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引。下面有一个8x4数组:
1 2 3 4 5
| import numpy as np arr = np.empty((8, 4)) for i in range(8): arr[i] = i print(arr)
|
其结果为:
1 2 3 4 5 6 7 8
| [[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即可:
1 2 3 4 5 6 7 8
| ··· print(arr[[4,3,0,6]])
[[4. 4. 4. 4.] [3. 3. 3. 3.] [0. 0. 0. 0.] [6. 6. 6. 6.]]
|
使用负数索引将会从末尾开始选取行:
1 2 3 4 5 6 7 8
| ··· print(arr[[-1, -2, -3, -4]])
[[7. 7. 7. 7.] [6. 6. 6. 6.] [5. 5. 5. 5.] [4. 4. 4. 4.]]
|
一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:
1 2 3 4 5
| ··· print(arr[[1,5,7],[0,3,1]])
[1. 5. 7.]
|
最终选出的是元素(1,0)、(5,3)、(7,1)。无论数组是多少维的,花式索引总是一维的。
选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:
1 2 3 4 5 6 7 8
| ···· print(arr[[1,5,7,2]][:,[0, 3, 1, 2]])
[[1. 1. 1. 1.] [5. 5. 5. 5.] [7. 7. 7. 7.] [2. 2. 2. 2.]]
|
花式索引跟切片不一样,它总是将数据复制到新数组中。