一、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) # 随机生成一个2x3的数组,符合标准正态分布
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之外,还有一些函数也可以新建数组。比如,zerosones分别可以创建指定长度或形状的全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)) # 认为np.empty会返回全0数组的想法是不安全的。很多情况下(如前所示),它返回的都是一些未初始化的垃圾值。
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
···
# arange
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(浮点数)。

表4-1 数组创建函数

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
...
# astype转换dtype
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
...
# astype转换dtype
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
···
# astype转换dtype
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
···
# dtype另外的属性
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])

# 结果, 注意这里因为重新运行了程序,所以data不一样了
-----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.]]

花式索引跟切片不一样,它总是将数据复制到新数组中。