Python 数据处理:NumPy库的使用

NumPy简介

NumPy(Numerical Python的简称)是Python数值计算最重要的基础包。大多数提供科学计算的包都是用NumPy的数组作为构建基础。

NumPy的部分功能如下:

  • ndarray,一个具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组
  • 用于对整组数据进行快速运算的标准数学函数(无需编写循环)。
  • 用于读写磁盘数据的工具以及用于操作内存映射文件的工具。
  • 线性代数、随机数生成以及傅里叶变换功能。
  • 用于集成由C、C++、Fortran等语言编写的代码的API。

NumPy对于数值计算特别重要的原因之一,是因为它可以高效处理大数组的数据。这是因为:

  • NumPy是在一个连续的内存块中存储数据,独立于其他Python内置对象。NumPy的C语言编写的算法库可以操作内存,而不必进行类型检查或其它前期工作。比起Python的内置序列,NumPy数组使用的内存更少。
  • NumPy可以在整个数组上执行复杂的计算,而不需要Python的for循环

jupyter notebook中运行以下代码,可以比较NumPy数组和Python列表的数据运算效率:

1
2
3
4
5
6
7
# 考察一个包含一百万整数的数组,和一个等价的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
%time for _ in range(10): my_list2 = [x * 2 for x in my_list]

其中:

  • CPU time:衡量的是CPU用来执行程序的时间;
  • Wall time:从计算开始到计算结束等待的时间。

NumPy的ndarray:一种多维数组对象

NumPy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器。你可以利用这种数组对整块数据执行一些数学运算,其语法跟标量元素之间的运算一样。

1
2
3
4
5
import numpy as np
data = np.random.randn(2,3)
print(data)
print(data * 10)
print(data + data)

对于Python原生的列表,这些乘法和加法的规则有点不一样

1
2
3
pythonList = [1,2,3]
print(pythonList * 10)
print(pythonList + pythonList)

ndarray是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的(记录数组除外)。每个数组都有一个shape(一个表示各维度大小的元组)和一个dtype(一个用于说明数组数据类型的对象):

1
2
3
4
5
import numpy as np
data = np.random.randn(2,3)
print(data)
print(data.shape)
print(data.dtype)

创建ndarray

创建数组最简单的办法就是使用array函数。它接受一切序列型的对象(包括其他数组),然后产生一个新的含有传入数据的NumPy数组。以一个列表的转换为例:

1
2
3
4
5
6
7
import numpy as np
data1 = [1,2,3,5.0,6]
arr1 = np.array(data1)
print(arr1)
print(arr1.ndim)
print(arr1.shape)
print(arr1.dtype)

嵌套序列(比如由一组等长列表组成的列表)将会被转换为一个多维数组:

1
2
3
4
5
6
7
import numpy as np
data2 = [[1, 2, 3, 4], [5, 6.1, 7, 8]]
arr2 = np.array(data2)
print(arr2)
print(arr2.ndim)
print(arr2.shape)
print(arr2.dtype)

np.array之外,还有一些函数也可以新建数组。比如,zerosones分别可以创建指定长度或形状的全0或全1数组。empty可以创建一个没有任何具体值的数组。要用这些方法创建多维数组,只需传入一个表示形状的元组即可:

1
2
3
4
import numpy as np
print(np.zeros(10))
print(np.ones((3,6)))
print(np.empty((2,3,2)))

arange是Python内置函数range的数组版:

1
2
3
4
import numpy as np
dataArange = np.arange(15)
print(type(dataArange))
print(dataArange)

下表列出了一些数组创建函数。由于NumPy关注的是数值计算,因此,如果没有特别指定,数据类型基本都是float64(浮点数)。

函数描述
array将输入数据(列表、元组、数组或其它序列类型)转换为ndarray。要么推断出dtype,要么特别指定dtype。默认直接复制输入数据
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)

ndarray的数据类型

dtype(数据类型)是一个特殊的对象,它含有ndarray将一块内存解释为特定数据类型所需的信息:

1
2
3
4
5
6
7
import numpy as np
arr1 = np.array([1,2,3], dtype=np.float64)
arr2 = np.array([1,2,3], dtype=np.int32)
print(arr1.dtype)
print(arr1)
print(arr2.dtype)
print(arr2)

数值型dtype的命名方式相同:一个类型名(如 float 或 int ),后面跟一个用于表示各元素位长的数字。标准的双精度浮点值(即Python中的 float 对象)需要占用8字节(即64位)。因此,该类型在NumPy中就记作float64

下表列出了NumPy所支持的全部数据类型:

类型类型代码描述
int8、 uint8i1、 u1有符号和无符号的8位(1个字节)整型
int16、uint16i2、u2有符号和无符号的16位 (2个字节)整型
int32、uint32i4、u4有符号和无符号的32位 (4个字节)整型
int64、 uint64i8、u8有符号和无符号的64位(8个字节)整型
float16f2半精度浮点数
float32f4或f标准的单精度浮点数。与C的float兼容
float64f8或d标准的双精度浮点数。与C的double和Python的float对象兼容
float128f16或g扩展精度浮点数
complex64、complex128、 complex256c8、c16、c32分别用两个32位、64位或128位浮点数表示的复数
bool?存储True和False值的布尔类型
objectOPython对象类型
string_S固定长度的字符串类型(每个字符1个字节)。例如,要创建一个长度为10的字符串,应使用S10
unicode_U固定长度的unicode类型(字节数由平台决定)。跟字符串的定义方式一样(如U10)

可以通过ndarrayastype方法明确地将一个数组从一个dtype转换成另一个dtype

1
2
3
4
5
6
7
import numpy as np
arr = np.array([1,2,3,4,5])
print(arr.dtype)
print(arr)
float_arr = arr.astype(np.float64)
print(float_arr.dtype)
print(float_arr)

如果将浮点数转换成整数,则小数部分将会被截取删除:

1
2
3
4
import numpy as np
arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
print(arr)
print(arr.astype(np.int32))

如果某字符串数组表示的全是数字,也可以用astype将其转换为数值形式:

1
2
3
4
import numpy as np
numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)
print(numeric_strings)
print(numeric_strings.astype(np.float64))

NumPy数组的运算

数组很重要,因为它使你不用编写循环即可对数据执行批量运算。NumPy用户称其为矢量化(vectorization)。大小相等的数组之间的任何算术运算都会将运算应用到元素级

1
2
3
4
5
import numpy as np
arr = np.array([[1.,2,3],[4,5,6]])
print(arr)
print(arr * arr)
print(arr - arr)

数组与标量的算术运算会将标量值传播到各个元素:

1
2
3
4
5
import numpy as np
arr = np.array([[1.,2,3],[4,5,6]])
print(arr)
print(1 / arr)
print(arr ** 0.5)

大小相同的数组之间的比较会生成布尔值数组:

1
2
3
4
5
6
7
8
9
import numpy as np

arr = np.array([[1.,2,3],[4,5,6]])
print(arr)

arr2 = np.array([[0., 4,1],[7,2,12]])
print(arr2)

print(arr arr2)

基本的索引和切片

NumPy数组的索引是一个内容丰富的主题,因为选取数据子集或单个元素的方式有很多。一维数组很简单。从表面上看,它们跟Python列表的功能差不多:

1
2
3
4
5
6
7
8
9
10
import numpy as np
arr = np.arange(10) * 2
print(arr)
print(arr[5])

arr_tmp = arr[5:8]
print(arr_tmp)

arr_tmp[0] = 100
print(arr_tmp)

跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上

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
import numpy as np
arr = np.arange(10) * 2
print(arr)

# 先创建一个arr的切片:
arr_slice = arr[5:8]
print(arr_slice)

# 现在,当修改arr_slice中的值,变动也会体现在原始数组arr中:
arr_slice[1] = 108
print(arr)

# 切片[ : ]会给数组中的所有值赋值:
arr_slice[:] = 64
print(arr)

# 再来试试List
lst = list(range(10))
print(lst)

lst_slice = lst[5:8]
print(lst_slice)

lst_slice[1] = 12345
print(lst_slice)
print(lst)

如果你想要得到的是ndarray切片的一份副本而非视图,就需要明确地进行复制操作,例如:

1
2
3
4
5
6
7
8
9
10
import numpy as np
arr = np.arange(10) * 2
print(arr)

arr_slice = arr[5:8].copy()
print(arr_slice)

arr_slice[1] = 108
print(arr_slice)
print(arr)

对于高维度数组,能做的事情更多。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组:

1
2
3
import numpy as np
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(arr2d[2])

因此,可以对各个元素进行递归访问,但这样需要做的事情有点多。你可以传入一个以逗号隔开的索引列表来选取单个元素。也就是说,下面两种方式是等价的:

1
2
3
4
5
import numpy as np
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])

print(arr2d[0][2])
print(arr2d[0,2])

二维数组的索引方式,轴0作为行,轴1作为列:
二维数组

在多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的ndarray(它含有高一级维度上的所有数据)。因此,在2×2×3数组arr3d中:

1
2
3
4
5
import numpy as np
arr3d = np.array([[[1, 2, 3], [4, 5, 6]],
[[7, 8, 9], [10, 11, 12]]])
print(arr3d)
print(arr3d[0,1])

标量值和数组都可以被赋值给arr3d[0]

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
arr3d = np.array([[[1, 2, 3], [4, 5, 6]],
[[7, 8, 9], [10, 11, 12]]])

old_values = arr3d[0].copy()

arr3d[0] = 42
print(arr3d)

print('\n' + '-'*20 + '\n')

arr3d[0] = old_values
print(arr3d)

切片索引

ndarray的切片语法跟Python列表这样的一维对象差不多:

1
2
3
4
import numpy as np
arr = np.arange(10) * 2
print(arr)
print(arr[1:6])

对于之前的二维数组arr2d,其切片方式稍显不同,它是沿着第0轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。表达式arr2d[:2]可以被认为是“选取arr2d的前两行”:

1
2
3
4
5
import numpy as np
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(arr2d)
print("-"*20)
print(arr2d[:2])

可以一次传入多个切片,就像传入多个索引那样:

1
2
3
4
5
import numpy as np
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(arr2d)
print("-"*20)
print(arr2d[1:, 1:])

像这样进行切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片。

例如,可以选取第二行的前两列:

1
2
3
4
5
import numpy as np
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(arr2d)
print("-"*20)
print(arr2d[1, :2])

相似的,还可以选择第三列的前两行:

1
2
3
4
5
import numpy as np
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(arr2d)
print("-"*20)
print(arr2d[:2, 2])

“只有冒号”表示选取整个轴,因此你可以像下面这样只对高维轴进行切片:

1
2
3
4
5
import numpy as np
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(arr2d)
print("-"*20)
print(arr2d[:,:1])

对切片表达式的赋值操作也会被扩散到整个选区:

1
2
3
4
5
6
import numpy as np
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(arr2d)
print("-"*20)
arr2d[:2, 1:] = 0
print(arr2d)

索引与切片


布尔型索引

来看这样一个例子,假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据:

1
2
3
4
5
6
import numpy as np
names = np.array(['Bob', 'Joe', 'Will', 'Bob',
'Will', 'Joe', 'Joe'])
data = np.random.randn(7,4)
print(names)
print(data)

假设每个名字都对应data数组中的一行,而我们想要选出对应于名字"Bob"的所有行。跟算术运算一样,数组的比较运算(如==)也是矢量化的。因此,对names和字符串"Bob"的比较运算将会产生一个布尔型数组:

1
2
3
4
import numpy as np
names = np.array(['Bob', 'Joe', 'Will', 'Bob',
'Will', 'Joe', 'Joe'])
print(names == 'Bob')

这个布尔型数组可用于数组索引:

1
2
3
4
5
6
import numpy as np
names = np.array(['Bob', 'Joe', 'Will', 'Bob',
'Will', 'Joe', 'Joe'])
data = np.random.randn(7,4)

print(data[names == 'Bob'])

布尔型数组的长度必须跟被索引的轴长度一致。此外,还可以将布尔型数组跟切片、整数混合使用。

下面的例子,选取了names == 'Bob'的行,并索引了列:

1
2
3
4
5
6
7
import numpy as np
names = np.array(['Bob', 'Joe', 'Will', 'Bob',
'Will', 'Joe', 'Joe'])
data = np.random.randn(7,4)

print(data[names == 'Bob', 2:])
print(data[names == 'Bob', 3])

要选择除"Bob"以外的其他值,既可以使用不等于符号(!=),也可以通过~对条件进行否定:

1
2
3
4
5
6
7
import numpy as np
names = np.array(['Bob', 'Joe', 'Will', 'Bob',
'Will', 'Joe', 'Joe'])
data = np.random.randn(7,4)

print(names != 'Bob')
print(data[~(names == 'Bob')])

选取这三个名字中的两个需要组合应用多个布尔条件,使用&(与)、|(或)之类的布尔算术运算符即可:

1
2
3
4
5
6
7
8
import numpy as np
names = np.array(['Bob', 'Joe', 'Will', 'Bob',
'Will', 'Joe', 'Joe'])
data = np.random.randn(7,4)

mask = (names == 'Bob') | (names == 'Will')
print(mask)
print(data[mask])

通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
names = np.array(['Bob', 'Joe', 'Will', 'Bob',
'Will', 'Joe', 'Joe'])
data = np.random.randn(7,4)

print(data)

print("-"*20)

data[data < 0] = 0
print(data)

通过一维布尔数组设置整行或列的值也很简单:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
names = np.array(['Bob', 'Joe', 'Will', 'Bob',
'Will', 'Joe', 'Joe'])
data = np.random.randn(7,4)

print(data)

print("-"*20)

data[names != 'Joe'] = 7
print(data)

花式索引

花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引。假设我们有一个8×4数组:

1
2
3
4
5
6
7
8
import numpy as np
arr = np.empty((8, 4))
for i in range(8):
arr[i] = i
print(arr)

arr[:2] = arr[[1,0]]
print(arr)

为了以特定顺序选取行子集,只需传入一个用于指定顺序的整数列表或ndarray即可:

1
2
3
4
5
6
7
8
9
import numpy as np
arr = np.empty((8, 4))
for i in range(8):
arr[i] = i
print(arr)

print("-"*20)

print(arr[[4, 3, 0, 6]])

使用负数索引将会从末尾开始选取行:

1
2
3
4
5
6
7
8
9
import numpy as np
arr = np.empty((8, 4))
for i in range(8):
arr[i] = i
print(arr)

print("-"*20)

print(arr[[-3, -5, -7]])

一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:

1
2
3
4
5
6
7
import numpy as np
arr = np.arange(32).reshape(8,4)
print(arr)

print("-"*20)

print(arr[[1,5,7,2],[0,3,1,2]])

最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。无论数组是多少维的,花式索引总是一维的。

这个花式索引的行为可能会跟某些用户的预期不一样,选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:

1
2
3
4
5
6
7
import numpy as np
arr = np.arange(32).reshape(8,4)
print(arr)

print("-"*20)

print(arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]])

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
arr = np.arange(32).reshape(8,4)
print(arr)

print("-"*20)

arrNew = arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
print(arrNew)

print("-"*20)

arrNew[1] = -100
print(arrNew)

print("-"*20)

print(arr)

数组转置和轴对换

转置是重塑的一种特殊形式,它返回的是源数据的视图(不会进行任何复制操作)。数组不仅有transpose方法,还有一个特殊的T属性:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
arr = np.arange(15).reshape((3,5))
print(arr)

print("-"*20)

print(arr.T)

print("-"*20)

print(arr.transpose((1,0)))

在进行矩阵计算时,经常需要用到该操作,比如利用np.dot计算矩阵内积:

1
2
3
4
5
6
7
import numpy as np
arr = np.random.randn(6,3)
print(arr)

print("-"*20)

print(np.dot(arr.T, arr))

简单的转置可以使用.T,它其实就是进行轴对换而已。ndarray还有一个swapaxes方法,它需要接受一对轴编号:

1
2
3
4
5
6
7
8
import numpy as np
arr = np.arange(16).reshape((2,2,4))
print(arr)

print("-"*20)

arr = arr.swapaxes(1,2)
print(arr)

通用函数:快速的元素级数组函数

通用函数(即ufunc)是一种对ndarray中的数据执行元素级运算的函数。你可以将其看做简单函数(接受一个或多个标量值,并产生一个或多个标量值)的矢量化包装器。

许多ufunc都是简单的元素级变体,如sqrtexp

1
2
3
4
5
import numpy as np
arr = np.arange(10)
print(arr)
print(np.sqrt(arr))
print(np.exp(arr))

这些都是一元(unary)ufunc。另外一些(如addmaximum)接受2个数组(因此也叫二元(binary)ufunc),并返回一个结果数组:

1
2
3
4
5
6
7
8
import numpy as np
x = np.random.randn(8)
y = np.random.randn(8)
print(x)
print("-"*20)
print(y)
print("-"*20)
print(np.maximum(x, y))

有些ufunc可以返回多个数组。modf就是一个例子,它是Python内置函数divmod的矢量化版本,它会返回浮点数数组的小数和整数部分:

1
2
3
4
5
6
7
8
import numpy as np
arr = np.random.randn(7) * 5
print(arr)
print('-' * 50)
remainder, whole_part = np.modf(arr)
print(remainder)
print('-' * 50)
print(whole_part)

ufuncs可以接受一个out可选参数,这样就能在数组原地进行操作:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
arr = np.arange(10)
print(arr)
print('-' * 50)
print(np.sqrt(arr))
print('-' * 50)
print(arr)
print('-' * 50)
arr = arr.astype(np.float64)
np.sqrt(arr,arr)
print(arr)

下表列出了一些一元和二元ufunc

一元ufunc描述
abs、 fabs计算整数、浮点数或复数的绝对值。对于非复数值,可以使用更快的fabs
sqrt计算各元素的平方根。相当于arr** 0.5
square计算各元素的平方。相当于arr**2
exp计算各元素的指数ex
log、log10、log2、log1p分别为自然对数(底数为e)、底数为10的log、底数为2的log、log(1 +x)
sign计算各元素的正负号:1(正数)、0(零)、-1(负数)
ceil计算各元素的ceiling值,即大于等于该值的最小整数
floor计算各元素的floor值,即小于等于该值的最大整数
arccos、arccosh、arcsin、arcsinh、arctan、arctanh反三角函数
logical_not计算各元素not x的真值。相当于-arr
二元ufunc描述
add将数组中对应的元素相加
subtract从第一个数组中减去第二个数组中的元素
multiply数组元素相乘
divide、floor_divide除法或向下圆整除法(丢弃余数)
power对第一个数组中的元素A,根据第二个数组中的相应元素B,计算AB
maximum、fmax元素级的最大值计算。fmax将忽略NaN
minimum、fmin元素级的最小值计算。fmin将忽略NaN
mod元素级的求模计算(除法的余数)
copysign将第二个数组中的值的符号复制给第一个数组中的值

指定输出

在进行大量运算时, 指定一个用于存放运算结果的数组时非常有用的。不同于创建临时数组,可以用这个特性将计算结果直接写入到期望的存储位置,这样在进行较大数据运算时,可以有效节约内存。所有的通用函数都可以通过out参数来指定计算结果的存放位置:

1
2
3
4
5
import numpy as np
x = np.arange(5)
y = np.empty(5)
np.multiply(x, 10, out = y)
print(y)

这个特性也可以被用作数组视图,例如可以将计算结果写入指定数组的每隔一个元素的位置:

1
2
3
4
5
import numpy as np
x = np.arange(5)
y = np.zeros(10)
np.power(2, x, out = y[::2])
print(y)

聚合

可以使用任何通用函数的reduce方法,该方法会对给定的元素和操作重复执行,直至得到单个的结果:

1
2
3
4
import numpy as np
x = np.arange(1, 6)
print(np.add.reduce(x))
print(np.multiply.reduce(x))

类似的还有accumulate,可以存储每次计算的中间结果:

1
2
3
4
import numpy as np
x = np.arange(1, 6)
print(np.add.accumulate(x))
print(np.multiply.accumulate(x))

外积

任何通用函数都可以用outer方法获得两个不同输入数组所有元素对的函数运算结果。

1
2
3
import numpy as np
x = np.arange(1,10)
print(np.multiply.outer(x, x))

利用数组进行数据处理

假设我们想要在一组值(网格型)上计算函数<font。 color=bluesqrt(x2+y2)</font。np.meshgrid函数接受两个一维数组,并产生两个二维矩阵(对应于两个数组中所有的(x,y)对):

1
2
3
4
5
6
7
8
import numpy as np
points = np.arange(5)
print(points)

xs, ys = np.meshgrid(points, points)
print(xs)
print("-" * 20)
print(ys)

现在,对该函数的求值运算就好办了,把这两个数组当做两个浮点数那样编写表达式即可:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
points = np.arange(5)
print(points)

xs, ys = np.meshgrid(points, points)
print(xs)
print("-" * 20)
print(ys)

z = np.sqrt(xs ** 2 + ys **2)
print(z)

将条件逻辑表述为数组运算

numpy.where函数是三元表达式x if condition else y的矢量化版本。假设我们有一个布尔数组和两个值数组:

1
2
3
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中的值选取xarryarr的值:当cond中的值为True时,选取xarr的值,否则从yarr中选取。列表推导式的写法应该如下所示:

1
result = [(x if c else y) for x, y, c in zip(xarr, yarr, cond)]

若使用np.where,则可以将该功能写得非常简洁:

1
result = np.where(cond, xarr, yarr)

在数据分析工作中,where通常用于根据另一个数组而产生一个新的数组。假设有一个由随机数据组成的矩阵,你希望将所有正值替换为2,将所有负值替换为-2。若利用np.where,则会非常简单:

1
2
3
4
5
import numpy as np
arr = np.random.randn(4,4)
print(arr)
print("-"*20)
print(np.where(arr0, 2, -2))

使用np.where,可以将标量和数组结合起来。例如,可用常数2替换arr中所有正的值:

1
2
3
4
5
import numpy as np
arr = np.random.randn(4,4)
print(arr)
print("-"*20)
print(np.where(arr 0, 2, arr))

去除异常值,例如,在成绩列表中,将所有小于0的成绩改为0,所有大于100的成绩改为100:

1
2
3
4
5
6
import numpy as np
scoreArr = np.array([88, 90, 58, -12, 91, 108])
scoreArr = np.where(scoreArr 100, 100, scoreArr)
print(scoreArr)
scoreArr = np.where(scoreArr < 0, 0, scoreArr)
print(scoreArr)

数学和统计方法

可以通过数组上的一组数学函数对整个数组或某个轴向的数据进行统计计算。summean以及标准差std等聚合计算(aggregation,通常叫做约简(reduction))既可以当做数组的实例方法调用,也可以当做顶级NumPy函数使用。

1
2
3
4
5
6
7
import numpy as np
arr = np.random.randn(5,4)
print(arr)
print(arr.mean())
print(np.mean(arr))
print(arr.sum())
print(np.sum(arr))

meansum这类的函数可以接受一个axis选项参数,用于计算该轴向上的统计值,最终结果是一个少一维的数组:

1
2
3
4
5
import numpy as np
arr = np.random.randn(5,4)
print(arr)
print(arr.mean(axis=1))
print(arr.sum(axis=0))

这里,arr.mean(1)是“计算行的平均值”,arr.sum(0)是“计算每列的和”。

axis关键字指定的时数组将会被折叠的维度,而不是将要返回的维度。因此,指定axis=0意味着第0个轴将要被折叠,对于二维数组来说,这意味着每一列的值都将被聚合。

其他如cumsumcumprod之类的方法则不聚合,而是产生一个由中间结果组成的数组:

1
2
3
import numpy as np
arr = np.array([0,1,2,3,4,5,6,7])
print(arr.cumsum())

在多维数组中,累加函数(如cumsum)返回的是同样大小的数组,但是会根据每个低维的切片沿着标记轴计算部分聚类:

1
2
3
4
5
import numpy as np
arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
print(arr)
print(arr.cumsum(axis=0))
print(arr.cumprod(axis=1))

下表列出了全部的基本数组统计方法:

方法描述
sum对数组中全部或某轴向的元素求和。零长度的数组的sum为0
mean算术平均数。零长度的数组的mean为NaN
std、 var分别为标准差和方差,自由度可调(默认为n)
min、 max最大值和最小值
argmin、argmax分别为最大和最小元素的索引
cumsum所有元素的累计和
cumprod所有元素的累计积

用于布尔型数组的方法

在上面这些方法中,布尔值会被强制转换为1(True)和0(False)。因此,sum经常被用来对布尔型数组中的True值计数:

1
2
3
4
import numpy as np
arr = np.random.randn(10)
print(arr)
print((arr 0).sum())

另外还有两个方法anyall,它们对布尔型数组非常有用。any用于测试数组中是否存在一个或多个True,而all则检查数组中所有值是否都是True

1
2
3
4
import numpy as np
bools = np.array([False, False, True, False])
print(bools.any())
print(bools.all())

排序

跟Python内置的列表类型一样,NumPy数组也可以通过sort方法就地排序

1
2
3
4
5
import numpy as np
arr = np.random.randn(5)
print(arr)
arr.sort()
print(arr)

多维数组可以在任何一个轴向上进行排序,只需将轴编号传给sort即可:

1
2
3
4
5
import numpy as np
arr = np.random.randn(5, 3)
print(arr)
arr.sort(1)
print(arr)

顶级方法np.sort返回的是数组的已排序副本,而就地排序则会修改数组本身。计算数组分位数最简单的办法是对其进行排序,然后选取特定位置的值:

1
2
3
4
import numpy as np
large_arr = np.random.randn(1000)
large_arr.sort()
print(large_arr[int(0.05 * len(large_arr))])

唯一化以及其它的集合逻辑

NumPy提供了一些针对一维ndarray的基本集合运算。最常用的可能要数np.unique了,它用于找出数组中的唯一值并返回已排序的结果:

1
2
3
4
5
6
7
import numpy as np
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))

np.in1d用于测试一个数组中的值在另一个数组中的成员资格,返回一个布尔型数组:

1
2
3
import numpy as np
values = np.array([6, 0, 0, 3, 2, 5, 6])
print(np.in1d(values, [2,3,6]))

下表列出了NumPy中的集合函数:

方法描述
unique(x)计算x中的唯一元素,并返回有序结果
intersect1d(x, y)计算x和y中的公共元素,并返回有序结果
union1d(x, y)计算x和y的并集,并返回有序结果
in1d(x, y)得到一个表示“x的元素是否包含于y”的布尔型数组
setdiff1d(x, y)集合的差,即元素在x中且不在y中
setxor1d(x, y)集合的对称差,即存在于一个数组中但不同时存在于两个数组中的元素

用于数组的文件输入输出

NumPy能够读写磁盘上的文本数据或二进制数据。这一小节只讨论NumPy的内置二进制格式,因为更多的用户会使用pandas或其它工具加载文本或表格数据。

np.savenp.load是读写磁盘数组数据的两个主要函数。默认情况下,数组是以未压缩的原始二进制格式保存在扩展名为.npy的文件中的:

1
2
3
4
import numpy as np
arr = np.arange(10)
np.save('some_array', arr)
print(np.load('some_array.npy'))

通过np.savez可以将多个数组保存到一个未压缩文件中,将数组以关键字参数的形式传入即可:

1
2
3
4
5
import numpy as np
arr = np.arange(10)
np.savez('array_archive.npz', a=arr, b=arr * 2)
arch = np.load('array_archive.npz')
print(arch['b'])

如果要将数据压缩,可以使用numpy.savez_compressed

1
np.savez_compressed('arrays_compressed.npz', a=arr, b=arr * 2)

线性代数

线性代数(如矩阵乘法、矩阵分解、行列式以及其他方阵数学等)是任何数组库的重要组成部分。NumPy提供了一个用于矩阵乘法的dot函数(既是一个数组方法也是numpy命名空间中的一个函数):

1
2
3
4
5
6
7
8
9
import numpy as np
x = np.array([[1., 2., 3.], [4., 5., 6.]])
y = np.array([[6., 23.], [-1, 7], [8, 9]])
print(x)
print(y)
print("-"*20)
print(x.dot(y))
# x.dot(y)等价于np.dot(x, y):
print(np.dot(x, y))

一个二维数组跟一个大小合适的一维数组的矩阵点积运算之后将会得到一个一维数组:

1
2
3
4
import numpy as np
x = np.array([[1., 2., 3.], [4., 5., 6.]])
print(np.dot(x, np.ones(3)))
print(print(np.ones(3).shape))

@符也可以用作中缀运算符,进行矩阵乘法:

1
2
3
import numpy as np
x = np.array([[1., 2., 3.], [4., 5., 6.]])
print(x @ np.ones(3))

numpy.linalg中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的东西。它们跟MATLAB和R等语言所使用的是相同的行业标准线性代数库,如BLAS、LAPACK、Intel MKL(Math Kernel Library,可能有,取决于你的NumPy版本)等:

1
2
3
4
5
6
7
8
import numpy as np
from numpy.linalg import inv, qr
X = np.random.randn(5, 5)
mat = X.T.dot(X)
print(inv(mat))
print(mat.dot(inv(mat)))
q, r = qr(mat)
print(r)

下表列出了一些最常用的线性代数函数:

函数描述
diag以一维数组的形式返回方阵的对角线(或非对角线)元素,或将一维数组转换为方阵(非对角线元素为O)
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样本数组:

1
2
3
import numpy as np
samples = np.random.normal(size=(4,4))
print(samples)

Python内置的random模块则只能一次生成一个样本值。从下面的测试结果中可以看出,如果需要产生大量样本值,numpy.random快了不止一个数量级:

1
2
3
4
5
import numpy as np
from random import normalvariate
N = 1000000
%timeit samples = [normalvariate(0, 1) for _ in range(N)]
%timeit np.random.normal(size=N)

我们说这些都是伪随机数,是因为它们都是通过算法基于随机数生成器种子,在确定性的条件下生成的。你可以用NumPy的np.random.seed更改随机数生成种子:

1
2
3
4
5
6
7
import numpy as np
np.random.seed(1234)
print(np.random.randn(10))
np.random.seed(1234)
print(np.random.randn(10))
print(np.random.randn(10))
print(np.random.randn(10))

numpy.random的数据生成函数使用了全局的随机种子。要避免全局状态,你可以使用numpy.random.RandomState,创建一个与其它隔离的随机数生成器:

1
2
3
import numpy as np
rng = np.random.RandomState(1234)
print(rng.randn(10))

下表列出了numpy.random中的部分函数:

函数描述
seed确定随机数生成器的种子
permutation返回一个序列的随机排列或返回一个随机排列的范围
shuffle对一个序列就地随机排列
rand产生均匀分布的样本值
randint从给定的上下限范围内随机选取整数
randn产生正态分布(平均值为0,标准差为1)的样本值,类似于MATLAB接口
binomial产生二项分布的样本值
normal产生正态(高斯)分布的样本值
beta产生Beta分布的样本值
chisquare产生卡方分布的样本值
gamma产生Gamma分布的样本值
uniform产生在[0,1)中均匀分布的样本值

高级数组操作

除花式索引、切片、布尔条件取子集等操作之外,数组的操作方式还有很多。虽然pandas中的高级函数可以处理数据分析工作中的许多重型任务,但有时你还是需要编写一些在现有库中找不到的数据算法。

数组重塑

多数情况下,你可以无需复制任何数据,就将数组从一个形状转换为另一个形状。只需向数组的实例方法reshape传入一个表示新形状的元组即可实现该目的。例如,假设有一个一维数组,我们希望将其重新排列为一个矩阵(结果见图):

1
2
3
4
5
import numpy as np
arr = np.arange(12)
print(arr)
print(arr.reshape((4,3), order='C'))
print(arr.reshape((4,3), order='F'))

数组重塑
多维数组也能被重塑:

1
2
3
import numpy as np
arr = np.arange(12)
print(arr.reshape((4, 3)).reshape((3, 4)))

作为参数的形状的其中一维可以是-1,它表示该维度的大小由数据本身推断而来:

1
2
3
import numpy as np
arr = np.arange(15)
print(arr.reshape((5,-1)))

reshape将一维数组转换为多维数组的运算过程相反的运算通常称为扁平化(flattening)或散开(raveling),对应的函数为flattenravel

1
2
3
4
5
import numpy as np
arr = np.arange(15).reshape((5, 3))
print(arr)
print(arr.flatten())
print(arr.ravel())

如果结果中的值与原始数组相同,ravel不会产生源数据的副本。flatten方法的行为类似于ravel,只不过它总是返回数据的副本:

1
2
3
import numpy as np
arr = np.arange(15).reshape((5, 3))
print(arr.flatten())

C和Fortran顺序

NumPy允许你更为灵活地控制数据在内存中的布局。默认情况下,NumPy数组是按行优先顺序创建的。在空间方面,这就意味着,对于一个二维数组,每行中的数据项是被存放在相邻内存位置上的。另一种顺序是列优先顺序,它意味着每列中的数据项是被存放在相邻内存位置上的。

reshapereval这样的函数,都可以接受一个表示数组数据存放顺序的order参数。一般可以是’C’或’F’(还有’A’和’K’等不常用的选项,具体请参考NumPy的文档)。上一小节的图片对此进行了说明。


数组的合并和拆分

numpy.concatenate可以按指定轴将一个由数组组成的序列(如元组、列表等)连接到一起:

1
2
3
4
5
import numpy as np
arr1 = np.array([[1, 2, 3], [4, 5, 6]])
arr2 = np.array([[7, 8, 9], [10, 11, 12]])
print(np.concatenate([arr1, arr2], axis=0))
print(np.concatenate([arr1, arr2], axis=1))

对于常见的连接操作,NumPy提供了一些比较方便的方法(如vstackhstack)。因此,上面的运算还可以表达为:

1
2
3
4
5
import numpy as np
arr1 = np.array([[1, 2, 3], [4, 5, 6]])
arr2 = np.array([[7, 8, 9], [10, 11, 12]])
print(np.vstack((arr1, arr2)))
print(np.hstack((arr1, arr2)))

与此相反,split用于将一个数组沿指定轴拆分为多个数组,传入到np.split的值指示在哪个索引处分割数组:

1
2
3
4
5
6
7
import numpy as np
arr = np.random.randn(5,2)
print(arr)
first, second, third = np.split(arr, [1,4])
print(first)
print(second)
print(third)

下表列出了所有关于数组连接和拆分的函数,其中有些是专门为了方便常见的连接运算而提供的:

函数描述
concatenate最一般化的连接,沿一条轴连接一组数组
vstack、row_stack以面向行的方式对数组进行堆叠(沿轴o)
hstack以面向列的方式对数组进行堆叠(沿轴1)
column_stack类似于hstack,但是会先将一维数组转换为二维列向量
dstack以面向“深度”的方式对数组进行堆叠((沿轴2)
split沿指定轴在指定的位置拆分数组
hsplit、 vsplit、dsplitsplit的便捷化函数,分别沿轴o、轴1、轴2进行拆分

元素的重复操作:tile和repeat

对数组进行重复以产生更大数组的工具主要是repeattile这两个函数。repeat会将数组中的各个元素重复一定次数,从而产生一个更大的数组:

1
2
3
4
import numpy as np
arr = np.arange(3)
print(arr)
print(arr.repeat(3))

默认情况下,如果传入的是一个整数,则各元素就都会重复那么多次。如果传入的是一组整数,则各元素就可以重复不同的次数:

1
2
3
4
import numpy as np
arr = np.arange(3)
print(arr)
print(arr.repeat([2,3,4]))

对于多维数组,还可以让它们的元素沿指定轴重复:

1
2
3
4
5
import numpy as np
arr = np.random.randn(2,2)
print(arr)
print(arr.repeat(2, axis=0))
print(arr.repeat(2, axis=1))

同样,在对多维进行重复时,也可以传入一组整数,这样就会使各切片重复不同的次数:

1
2
3
4
5
import numpy as np
arr = np.random.randn(2,2)
print(arr)
print(arr.repeat([2,3], axis=0))
print(arr.repeat([2,3], axis=1))

tile的功能是沿指定轴向堆叠数组的副本。你可以形象地将其想象成“铺瓷砖”:

1
2
3
4
import numpy as np
arr = np.random.randn(2,2)
print(arr)
print(np.tile(arr, 2))

第二个参数是瓷砖的数量。对于标量,瓷砖是水平铺设的,而不是垂直铺设。它可以是一个表示“铺设”布局的元组:

1
2
3
4
5
import numpy as np
arr = np.random.randn(2,2)
print(arr)
print(np.tile(arr, (2,1)))
print(np.tile(arr, (3,2)))

广播

广播(broadcasting)指的是不同形状的数组之间的算术运算的执行方式。它是一种非常强大的功能,但也容易令人误解,即使是经验丰富的老手也是如此。将标量值跟数组合并时就会发生最简单的广播:

1
2
3
4
import numpy as np
arr = np.arange(5)
print(arr)
print(arr * 4)

看一个例子,我们可以通过减去列平均值的方式对数组的每一列进行距平化处理。这个问题解决起来非常简单:

1
2
3
4
5
6
7
8
9
import numpy as np
arr = np.arange(12).reshape(4,3)
print(arr)
print(arr.mean(0))

demeaned = arr - arr.mean(0)
print(demeaned)

print(demeaned.mean(0))

下图形象地展示了该过程。用广播的方式对行进行距平化处理会稍微麻烦一些。幸运的是,只要遵循一定的规则,低维度的值是可以被广播到数组的任意维度的(比如对二维数组各列减去行平均值)。

广播
广播的原则

广播规则

  1. 如果两个数组的维度数不相同,那么小维度数组的形状将会在最左边补1。
  2. 如果两个数组的形状在任何一个维度上都不匹配,那么数组的形状会沿着维度为1的维度扩展以匹配另外一个数组的形状。
  3. 如果两个数组的形状在任何一个维度上都不匹配并且每一偶任何一个维度等于1,那么会引发异常。
1
2
3
4
5
import numpy as np
M = np.ones((2,3))
a = np.arange(3)
print(M.shape)
print(a.shape)

根据规则1,数组a的维度数更小,所以在其左边补1:

  • M.shape - (2, 3)
  • a.shape - (1, 3)

根据规则2,第一个维度不匹配,因此扩展这个维度以匹配数组:

  • M.shape - (2, 3)
  • a.shape - (2, 3)

这样两个数组的形状匹配了,他们最终的形状都是 (2, 3):

1
2
3
4
import numpy as np
M = np.ones((2,3))
a = np.arange(3)
print(M + a)

对于三维的情况,在三维中的任何一维上广播其实也就是将数据重塑为兼容的形状而已。下图说明了要在三维数组各维度上广播的形状需求。

三维广播
于是就有了一个非常普遍的问题(尤其是在通用算法中),即专门为了广播而添加一个长度为1的新轴。虽然reshape是一个办法,但插入轴需要构造一个表示新形状的元组。这是一个很无聊的过程。因此,NumPy数组提供了一种通过索引机制插入轴的特殊语法。下面这段代码通过特殊的np.newaxis属性以及“全”切片来插入新轴:

1
2
3
4
5
6
7
import numpy as np
arr = np.zeros((4, 4))
print(arr)
print("\n----------------\n")
arr_3d = arr[:, np.newaxis, :]
print(arr_3d)
print(arr_3d.shape)
1
2
3
4
5
import numpy as np
arr_1d = np.random.normal(size=3)
print(arr_1d[:, np.newaxis])
print(arr_1d[np.newaxis, :])
print(arr_1d.reshape((1,-1)))

因此,如果我们有一个三维数组,并希望对轴2进行距平化,那么只需要编写下面这样的代码就可以了:

1
2
3
4
5
6
7
import numpy as np
arr = np.random.randn(3, 4, 5)
depth_means = arr.mean(2)
print(depth_means)
print(depth_means.shape)
demeaned = arr - depth_means[:, :, np.newaxis]
print(demeaned.mean(2))

通过广播设置数组的值

算术运算所遵循的广播原则同样也适用于通过索引机制设置数组值的操作。对于最简单的情况,我们可以这样做:

1
2
3
4
import numpy as np
arr = np.zeros((4, 3))
arr[:] = 5
print(arr)

但是,假设我们想要用一个一维数组来设置目标数组的各列,只要保证形状兼容就可以了:

1
2
3
4
5
6
7
8
9
import numpy as np
arr = np.zeros((4, 3))
col = np.array([1.28, -0.42, 0.44, 1.6])
print(col.shape)
arr[:] = col[:, np.newaxis]
print(arr)
arr[:2] = [[-1.27],[0.509]]
print(arr)
print(arr.flags)