什么是 NumPy?根据其官方文档的介绍:
NumPy 是Python中科学计算的基础包。它是一个Python库,提供多维数组对象,各种派生对象(如掩码数组和矩阵),以及用于数组快速操作的各种API,有包括数学、逻辑、形状操作、排序、选择、输入输出、离散傅立叶变换、基本线性代数,基本统计运算和随机模拟等等。
NumPy 的核心是一个特殊的数组对象——ndarray 对象。当运算涉及到 ndarray 对象时,默认通过预编译的 C 代码对逐个元素操作,在运算大量数据时,运算速度极快。此外,我们还可以看到 NumPy 的语法更简单。
乍一看有些笼统。那么,就让我们一点一点,揭开 NumPy 的面纱。
2.1 ndarray: 一种多维数组对象
想使用 NumPy,首先让我们导入 NumPy 包:
import numpy as np
2.1.1 创建 ndarray 对象
NumPy 的所有运算几乎都是围绕数组( ndarray 对象)展开的。那么如何创建数组呢?
简单的,我们可以将一个序列转换为数组。
in : data1 = [6, 7.5, 8, 0, 1]
arr1 = np.array(data1) # np.array() 可以将一切序列型的对象转换为一个数组
arr1
out: array([6. , 7.5, 8. , 0. , 1. ])
in : data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
arr2 = np.array(data2) # 等长嵌套序列将被转换为多维数组
arr2
out: array([[1, 2, 3, 4],
[5, 6, 7, 8]])
也有其他的函数能帮助我们直接创建一些特殊数组。
in : np.zeros((2, 3)) # 创建指定规格的全 0 数组
out: array([[0., 0., 0.],
[0., 0., 0.]])
in : np.ones(4) # 创建指定规格的全 1 数组
out: array([1., 1., 1., 1.])
in : np.arange(10) # 类似 range()
out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
in : data = np.random.randn(2, 3) # 创建指定规格的随机值数组(基于标准正态分布)
data
out: array([[-0.22036948, -0.92073677, 0.38747356],
[ 0.48489615, -0.74062949, 0.5220284 ]])
每个数组都有两个基本属性:shape (表示各维度大小的元组),dtype (用于说明数组数据类型的对象)。
in : data.shape
out: (2, 3)
in : data.dtype
out: dtype('float64')
2.1.2 ndarray 的数据类型
NumPy 能自动为输入的序列设定一个合适的数据类型。
in : arr1 = np.array([1, 2, 3])
arr1.dtype
out: dtype('int32')
in : arr2 = np.array([1.2, 0.78, 5])
arr2.dtype
out: dtype('float64')
也可以自己手动设置数组的数据类型。
arr3 = np.array([1, 2, 3], dtype=np.float64) # 注意这里的类型名称是“np.float64”
如果你想对一个数组进行类型转换,可以使用 astype() 。但是注意,astype() 并非在原数组上进行修改,而是会建立一个新数组。
in : arr3.astype(np.int32)
out: array([1, 2, 3])
in : arr3.dtype
out: dtype('float64') # 可以看到,原数组 arr3 的数据类型并未被修改
2.1.3 相同大小数组的运算
大小相等的数组之间,任何算术运算都会将运算应用到元素级。
in : arr = np.array([[1., 2., 3.], [4., 5., 6.]])
arr
out: array([[1., 2., 3.],
[4., 5., 6.]])
in : arr * arr
out: array([[ 1., 4., 9.],
[16., 25., 36.]])
in : arr + arr
out: array([[ 2., 4., 6.],
[ 8., 10., 12.]])
in : arr ** 2 # 幂运算
out: array([[ 1., 4., 9.],
[16., 25., 36.]])
数组之间进行比较,会生成布尔值数组。
in : arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
arr2
out: array([[ 0., 4., 1.],
[ 7., 2., 12.]])
in : arr2 > arr
out: array([[False, True, False],
[ True, False, True]])
2.1.4 基本的索引和切片
我们先来看一维数组。首先建立一个一维数组:
in : arr = np.arange(10)
arr
out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
创建数组切片的方式与创建序列切片的方式相同。
in : arr[5:8]
out: array([5, 6, 7])
对切片赋值会被应用到每个元素之上。
in : arr[5:8] = 12 # 等式右边输入 1 个值,即可对切片包含的所有值赋值
arr
out: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
in : arr = 12 # 这样赋值的话,arr 就被赋值为一个整数了,并不能保持原来的数组结构
arr
out: 12
# 如果想在原来的数组结构上,将所有值赋值为一个整数,可以这样:
in : arr[:] = 12
arr
out: array([12, 12, 12, 12, 12, 12, 12, 12, 12, 12])
即使将切片保存为一个新的数组,对新数组的任何修改也会被应用到原数组上。这是因为 NumPy 为了处理大量数据,需要避免复制数组造成的占用运算性能问题。可以说,我们并没得到原数组切片的一个副本,而是得到了原数组切片的一个视图。
in : arr_slice = arr[5:8] # 创建一个数组切片的视图
arr_slice[0] = 100
arr
out: array([ 12, 12, 12, 12, 12, 100, 12, 12, 12, 12]) # 可以看到原数组也被改变了
以上特性是和 Python 基础的序列对象不同的。以 list 为例:
in : list1 = list(range(10))
list1
out: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
in : list1[5:8] = 12 # 普通的 python 列表则不能直接以这样的方式赋值给每个元素(NumPy 数组可以)
out: Traceback (most recent call last):
File "", line 1, in
TypeError: can only assign an iterable
in : list1[5:8] = [12, 12, 12] # 可以通过这样的方法赋值
list1
out: [0, 1, 2, 3, 4, 12, 12, 12, 8, 9]
in : list1_slice = list1[5:8]
list1_slice[0] = 100
list1
out: [0, 1, 2, 3, 4, 12, 12, 12, 8, 9] # 可以看到原列表没有被改变
如果想得到数组切片的副本(而不是视图),可以采用 copy() 。
in : arr_slice_copy = arr[5:8].copy() # 创建一个数组切片的副本
arr_slice_copy[0] = 123456
arr
out: array([ 12, 12, 12, 12, 12, 100, 12, 12, 12, 12]) # 可以看到原数组就没有被改变了
那么如何对二维数组进行切片呢?
in : arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d
out: array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
in : arr2d[0, 2]
arr2d[0][2] # 这两种索引方式是一样的效果
out: 3
in : arr2d[:2] # 对第0轴切片
out: array([[1, 2, 3],
[4, 5, 6]]) # 切片后,数组仍是个二维数组
in : arr2d[:2, :1] # 对第0轴、第1轴切片
out: array([[1],
[4]]) # 切片后,数组仍是个二维数组
# 注:可以看到,切片不改变数组的维度
2.1.5 布尔型索引
我们先建立两个数组用于举例:
in : names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
data = np.random.randn(7, 4)
names
data
out: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='
逻辑表达式可以用于生成布尔型数组,这个布尔型数组可以用于索引。
in : names == 'Bob' # 用逻辑表达式生成布尔型数组
out: array([ True, False, False, True, False, False, False])
in : data_bob = data[names == 'Bob'] # 布尔型数组用于索引
# 注意:布尔型数组的长度必须跟被索引的轴长度一致
data_bob
out: array([[ 0.05817461, 0.82783485, 0.46839737, 0.50975782],
[-0.64680744, -0.90829029, -0.74018895, 0.17760956]])
布尔型数组用于索引时,可以用 “~” 反转条件。
in : data[~(names == 'Bob')] # ~ 用于反转条件
out: array([[ 1.15212531, 1.4481847 , -0.4201631 , -0.30265817],
[ 1.05658084, -0.38813849, 0.16464375, 1.48137849],
[ 1.96177662, 0.747304 , 0.08973106, 1.37109694],
[-0.46767001, -0.50060317, 0.07396933, 0.8385746 ],
[-1.71710491, -0.58035244, 2.19566878, 1.33896025]])
创建布尔型索引时,Python 能识别的 not, and, or 是无效的,需要使用 ! & | 。
in : data[(names != 'Bob') & (names != 'Joe')]
out: array([[ 1.05658084, -0.38813849, 0.16464375, 1.48137849],
[ 1.96177662, 0.747304 , 0.08973106, 1.37109694]])
2.1.6 数组的结构重塑和转置
如果想把一个一维数组进行结构重塑,变成 3 × 5 的二维数组,该怎么做呢?
in : arr = np.arange(15).reshape((3, 5)) # reshape() 用于数组的结构重塑
arr
out: array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
如果想把上述数组转置,该怎么做呢?
in : arr.T
out: array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
2.2 通用函数(ufunc)
通用函数( ufunc )是一种对 ndarray 中的数据执行元素级运算的函数。
ufunc 需要至少一个数组作为参数。输入一个数组的 ufunc,被称为一元 ufunc。
in : arr = np. arrange(10)
arr
out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# 开平方
in : np.sqrt(arr)
out: array([0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131, 2.82842712, 3. ])
# 自然底数 e 的 x 次方
in : np.exp(arr)
out: 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])
而输入两个数组的 ufunc 被称为 二元 ufunc。
in : x = np.random.randn(8)
y = np.random.randn(8)
x
y
out: array([ 0.88741632, -0.47989197, 0.961642 , 1.43875204, -1.28796969,
-0.04834001, 0.27405319, -0.03876604])
array([-2.25585454, 0.9791547 , 2.72284215, -0.44911582, 0.39418769,
-0.73406601, -0.77432083, -3.21548859])
# 返回两个数组中,对应位置更大的值组成的数组(与 x, y 相同结构)
in : np.maximum(x, y)
out: array([ 0.88741632, 0.9791547 , 2.72284215, 1.43875204, 0.39418769,
-0.04834001, 0.27405319, -0.03876604])
也有能返回两个结果数组的函数。
in : arr = np.random.randn(7) * 5
arr
out: array([-7.01330576, -0.40563694, 5.65876183, -6.69774849, 0.42850616,
4.65646863, 4.64433757])
# 返回浮点数数组的小数和整数部分
in : remainder, whole_part = np.modf(arr)
remainder
whole_part
out: array([-0.01330576, -0.40563694, 0.65876183, -0.69774849, 0.42850616,
0.65646863, 0.64433757])
array([-7., -0., 5., -6., 0., 4., 4.]) # 虽然是整数部分,但数组类型仍是“float64”
通用函数都并非在原始数组上进行更改,而是构建了一个新的数组。如果想原地操作数组,可以明确指明 out 参数。
in : arr = np.random.randn(5)
arr
out: array([-0.97691849, 0.31726746, -0.75027946, 0.51031132, -0.54012687])
in : np.sqrt(arr)
arr
out: array([-0.97691849, 0.31726746, -0.75027946, 0.51031132, -0.54012687]) # 可以看到原数组没有被修改
in : np.sqrt(arr, arr)
out: array([ nan, 0.563265 , nan, 0.71436078, nan])
其他的通用函数罗列于此:
2.3 利用数组进行数据处理
NumPy 数组可以让我们在不编写循环的情况下,用简洁的数组表达式进行数学运算。这种用数组表达式代替循环的做法,通常被称为矢量化。
比如,我们现在想对一个网格型数据计算:
$$ \sqrt{(x^2 + y^2)} $$
首先,让我们创建一个网格型数据。
in : points1 = np.arange(-5, 0) # x轴
points2 = np.arange(0, 5) # y轴
points1
points2
out: array([-5, -4, -3, -2, -1])
array([0, 1, 2, 3, 4])
# np.meshgrid() 可以接受两个一维数组,并产生两个二维矩阵。
# 可以把上面建立的网格画出来,将x轴和y轴组成的网格中,每个 (x, y) 都写出来,就容易理解了。
# xs 是这个网格上 (x, y) 中 x 组成的二维矩阵;ys 是这个网格上 (x, y) 中 y 组成的二维矩阵;
in : xs, ys = np.meshgrid(points1, points2)
xs
ys
out: array([[-5, -4, -3, -2, -1],
[-5, -4, -3, -2, -1],
[-5, -4, -3, -2, -1],
[-5, -4, -3, -2, -1],
[-5, -4, -3, -2, -1]])
array([[0, 0, 0, 0, 0],
[1, 1, 1, 1, 1],
[2, 2, 2, 2, 2],
[3, 3, 3, 3, 3],
[4, 4, 4, 4, 4]])
# 现在,我们就可以来计算上面的公式了
in : z = np.sqrt(xs ** 2 + ys ** 2) # NumPy 让我们可以像计算浮点数一样,编写计算数组的代码
z
out: array([[5. , 4. , 3. , 2. , 1. ],
[5.09901951, 4.12310563, 3.16227766, 2.23606798, 1.41421356],
[5.38516481, 4.47213595, 3.60555128, 2.82842712, 2.23606798],
[5.83095189, 5. , 4.24264069, 3.60555128, 3.16227766],
[6.40312424, 5.65685425, 5. , 4.47213595, 4.12310563]])
2.3.1 将条件逻辑表述为数组运算
这一节介绍一下 np.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)]
但如果用 np.where() 编写,代码会简洁很多:
in : result = np.where(cond, xarr, yarr)
result
out: array([1.1, 2.2, 1.3, 1.4, 2.5])
np.where() 的第一个参数是逻辑型数组,第二、三个参数则无需是数组。该函数的逻辑是:根据第一个逻辑型数组参数进行判定,若是,把数据替换为参数2,;若不是,把数据替换为参数3。
比如,我们想把一个二维数组中,正数赋值为2,负数保持不变。
in : arr = np.random.randn(4,4)
arr
out: array([[-1.14810005, -1.26020793, -0.88391974, 0.92820162],
[-0.37743466, 0.72906491, -2.08884266, 1.02572131],
[-0.18377037, 0.92768921, 0.80025967, 1.63431864],
[ 2.05365743, -0.13690236, -0.84462678, 1.96340251]])
in : result = np.where(arr > 0, 2, arr)
out: array([[-1.14810005, -1.26020793, -0.88391974, 2. ],
[-0.37743466, 2. , -2.08884266, 2. ],
[-0.18377037, 2. , 2. , 2. ],
[ 2. , -0.13690236, -0.84462678, 2. ]])
2.3.2 数学和统计方法
NumPy 可以对整个数组进行数学统计,举例如下:
in : arr = np.random.randn(3,2)
arr
out: array([[ 0.39769436, -0.8714971 ],
[-1.98139244, -0.91648828],
[-0.61709608, 0.82630001]])
in : arr.mean() # 求均值
np.mean(arr) # 和上式是等价的
out: -0.527079921854901
in : arr.std() # 求标准差
out: 0.9201661620576673
in : arr.sum() # 求和
out: -3.1624795311294065
in : arr.mean(axis=0) # 计算每列的均值
out: array([-0.73359805, -0.32056179])
in : arr.sum(axis=1) # 计算每行的和
out: array([-0.47380273, -2.89788073, 0.20920393])
2.3.3 用于布尔型数组的方法
上一节的方法也可以应用到布尔型数组里,此时 True = 1,False = 0。
in : arr = np.random.randn(100)
(arr > 0).sum() # 可以利用 sum() 对布尔型数组中的 True 值计数
out: 52
对于布尔型数组而言,也有一些特殊的常用方法。
in : bools = np.array([False, False, True, False])
bools.any() # 数组中是否存在一个或多个 True
out: True
in : bools.all() # 数组中是否都是 True
out: False
2.3.4 排序
如何对数组中的值排序呢?
in : arr = np.random.randn(6)
arr
out: array([-0.30308892, -1.18380195, -1.06029921, -0.92172841, -0.25920488, -0.53782583])
# 将数组原地排序
in : arr.sort()
arr
out: array([-1.18380195, -1.06029921, -0.92172841, -0.53782583, -0.30308892, -0.25920488])
对于多维数组来说,指明 sort() 中的轴参数,可以按轴对数组进行排序。
in : arr = np.array([[1, 6, 2], [8, 2, 0], [5, 7, 9]])
arr
out: array([[1, 6, 2],
[8, 2, 0],
[5, 7, 9]])
in : arr.sort(1)
arr
out: array([[1, 2, 6],
[0, 2, 8],
[5, 7, 9]])
in : arr.sort(0)
arr
out: array([[0, 2, 6],
[1, 2, 8],
[5, 7, 9]])
2.3.5 唯一化和其他的集合逻辑
在数据分析中,常常需要从一个包含重复值的数组中,提取出唯一值。这时,我们可以使用 np.unique()。
in : names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
np.unique(names) # 返回不包含重复值、已排序的结果数组
out: array(['Bob', 'Joe', 'Will'], dtype='
有时,还需要判断一个数组中的值,是否包含在另一个数组中。
in : values = np.array([6, 0, 0, 3, 2, 5, 6])
np.in1d(values, [2, 3, 6]) # 返回一个布尔型结果数组
out: array([ True, False, False, True, True, False, True])
2.4 伪随机数生成
本节以上的部分,我们使用了很多次 np.random.randn() 函数来创建一个随机数构建的数组,它表示按照标准正态分布取随机值。NumPy 生成的随机数并非真正的随机数,而是根据一个随机种子通过算法计算得到的,被称为伪随机数。如果你愿意,也可以更改随机种子。
其他的函数可以用于生成不同的伪随机数数组:
2.5 示例:随机漫步
学习了很多零散的知识,接下来我们来看一个综合运用的例子。
假设我们的初始位置是 0,往前走一步记为 1,往后走一步记为 -1。我们开始随机漫步,即步长为 1 和 -1 出现的概率相等,每次我们都可以往前走一步,或者往后走一步,一共走 1000 步。
nsteps = 1000
draws = np.random.randint(0, 2, size=nsteps) # np.random.randint() 是从给定上下限的范围内随机选取整数
steps = np.where(draws > 0, 1, -1)
walk = steps.cumsum() # 生成漫步路径:累加,返回每一步的累加值构成的结果数组
接下来,让我们沿着漫步路径做一些统计工作。
往后最远走到哪里?
walk.min()
往前最远走到哪里?
walk.max()
第一次走到第 10 步远是什么时候?
for i, walk_len in enumerate(np.abs(walk) >= 10):
if walk_len == True:
print(i)
break
# 或者,用下面的方式更简洁:
(np.abs(walk) >= 10).argmax() # argmax() 返回第一个最大值的索引(True 就是布尔型数组中的最大值)
注:转载请注明出处。
本文属于《利用 Python 进行数据分析》读书笔记系列: