在数据分析、科学计算、机器学习等领域,NumPy 是一个不可或缺的 Python 库。它为 Python 提供了高效的多维数组对象以及大量的数学函数,极大地简化了数值计算的过程,提升了计算效率。本文将深入探讨 NumPy 的核心特性、基本操作以及实际应用场景,帮助你快速掌握这个强大的工具。
目录
一、NumPy 简介
NumPy,全称为 Numerical Python,是一个开源的 Python 科学计算库。它最初由 Travis Oliphant 在 2005 年创建,旨在解决 Python 在数值计算方面的效率问题。Python 本身虽然具有强大的可读性和易用性,但在处理大规模数值数据时,其内置的数据结构(如列表)效率较低。NumPy 的出现,弥补了这一缺陷,通过引入高效的多维数组对象 ndarray(N-dimensional array),使得 Python 在数值计算领域具备了与其他专业数学软件(如 MATLAB)相媲美的能力。
NumPy 的核心优势在于它的高效性和便捷性。高效性主要体现在以下几个方面:
-
内存布局优化 :ndarray 对象在内存中以连续的块存储数据,这种连续的内存布局使得数据在内存中的访问更加高效,缓存命中率更高,从而大大提升了计算速度。
-
向量化操作 :NumPy 支持向量化操作,即对整个数组进行操作,而不是像 Python 列表那样需要通过循环逐个元素操作。向量化操作可以充分利用现代 CPU 的指令集和并行计算能力,大大减少循环次数,提高计算效率。
-
预编译的 C 代码 :NumPy 的底层核心算法是用 C 语言编写的,这使得 NumPy 在执行数值计算时具有接近 C 语言的速度,远超纯 Python 实现的代码。
便捷性则体现在 NumPy 提供了丰富的数组操作接口和大量的数学函数,使得数据处理和计算变得简单直观。例如,它可以轻松地进行数组的创建、索引、切片、形状变换、转置等操作,还提供了诸如求和、求积、统计分析、线性代数运算等数学函数,几乎涵盖了数值计算的各个方面。
二、NumPy 的安装与导入
要使用 NumPy,首先需要确保它已经安装在你的 Python 环境中。可以通过以下几种常见的安装方式进行安装:
-
使用 pip 安装 :在命令行中输入 “pip install numpy”,pip 会自动下载并安装最新的 NumPy 版本及其依赖项。
-
使用 conda 安装 :如果你使用 Anaconda 发行版,可以在命令行中输入 “conda install numpy”,conda 会从其默认的仓库中安装适合你环境的 NumPy 版本。
安装完成后,在 Python 脚本或交互式环境中,通过以下代码导入 NumPy:
import numpy as np
通常我们会使用 “np” 作为 NumPy 的别名,这样在后续的代码中可以更简洁地调用 NumPy 的函数和类。
三、NumPy 的核心:ndarray 对象
ndarray 是 NumPy 的核心数据结构,它是一个多维数组对象,用于存储同类型元素的集合。数组中的元素可以通过索引访问,索引从 0 开始。ndarray 对象具有以下几个重要的属性:
-
ndim :表示数组的维度数,即数组的轴数。例如,一个一维数组的 ndim 为 1,一个二维数组的 ndim 为 2。
-
shape :表示数组在每个维度上的大小,即每个轴上的元素数量。例如,一个形状为(3,4)的二维数组,表示它有 3 行 4 列。
-
dtype :表示数组中元素的数据类型,如 int32、float64、bool 等。NumPy 提供了多种数据类型,可以满足不同的计算需求。
-
size :表示数组中元素的总个数,等于 shape 各维度大小的乘积。
-
itemsize :表示数组中每个元素所占的字节数,它与数据类型相关。例如,dtype 为 int32 的数组,itemsize 为 4 字节。
-
data :表示数组在内存中的实际数据缓冲区的起始地址,这是一个指向内存中实际数据的缓冲区对象。
创建 ndarray 对象有多种方法,常见的如下:
-
从 Python 列表创建 :可以使用 numpy.array() 函数将 Python 列表转换为 ndarray。例如:
import numpy as np
# 创建一维数组
list_1d = [1, 2, 3, 4, 5]
array_1d = np.array(list_1d)
print(array_1d)
print("维度数:", array_1d.ndim)
print("形状:", array_1d.shape)
# 创建二维数组
list_2d = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
array_2d = np.array(list_2d)
print(array_2d)
print("维度数:", array_2d.ndim)
print("形状:", array_2d.shape)
运行结果:
[1 2 3 4 5]
维度数:1
形状:(5,)
[[1 2 3]
[4 5 6]
[7 8 9]]
维度数:2
形状:(3, 3)
-
使用 NumPy 的内置函数创建 :NumPy 提供了一系列的函数用于创建特定形状和内容的数组,例如:
-
zeros(shape) :创建一个指定形状的数组,数组元素全部为 0。例如:
-
zeros_array = np.zeros((3, 4))
print(zeros_array)
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
* **ones(shape)** :创建一个指定形状的数组,数组元素全部为 1。例如:
ones_array = np.ones((2, 3))
print(ones_array)
[[1. 1. 1.]
[1. 1. 1.]]
* **full(shape, fill_value)** :创建一个指定形状的数组,并用指定的值填充。例如:
full_array = np.full((2, 2), 7)
print(full_array)
[[7 7]
[7 7]]
* **arange(start, stop, step)** :生成一个从 start 开始,到 stop 结束(不包括 stop),步长为 step 的一维数组。例如:
arange_array = np.arange(0, 10, 2)
print(arange_array)
输出结果:
[0 2 4 6 8]
* **linspace(start, stop, num)** :生成一个从 start 到 stop(包括 start 和 stop)的 num 个等间距的数组。例如:
linspace_array = np.linspace(0, 1, 5)
print(linspace_array)
输出结果:
[0. 0.25 0.5 0.75 1. ]
* **random.random(size)** :生成一个指定大小的数组,数组元素为 [0, 1) 区间的随机浮点数。例如:
random_array = np.random.random((2, 3))
print(random_array)
[[0.12345678 0.98765432 0.45678901]
[0.23456789 0.67890123 0.89012345]]
* **eye(N)** :创建一个 N×N 的二维数组,主对角线元素为 1,其余元素为 0,即单位矩阵。例如:
eye_array = np.eye(3)
print(eye_array)
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
* **empty(shape)** :创建一个指定形状的数组,数组元素未经初始化,其内容是随机的,取决于内存中的初始状态。例如:
empty_array = np.empty((2, 2))
print(empty_array)
[[0.61234568 0.23456789]
[0.34567891 0.45678901]]
需要注意的是,empty() 函数创建的数组不会进行初始化操作,因此元素的值是不可预测的。通常只有在我们知道后续会立即对数组中的所有元素赋值时,才会使用 empty() 函数来创建数组,以节省初始化的时间和内存开销。
四、NumPy 数组的基本操作
掌握 NumPy 数组的基本操作是高效使用 NumPy 进行数值计算的基础,以下将详细介绍数组的索引、切片、形状操作、转置、拼接与分割等常用操作。
(一)数组的索引与切片
与 Python 列表类似,NumPy 数组也支持索引和切片操作,但由于 NumPy 数组是多维的,索引和切片的语法在高维数组中有所不同。
-
一维数组的索引与切片 :
import numpy as np
array_1d = np.array([10, 20, 30, 40, 50])
# 索引访问元素
print(array_1d[0]) # 输出 10
print(array_1d[2]) # 输出 30
# 切片操作
print(array_1d[1:4]) # 输出 [20 30 40]
print(array_1d[:3]) # 输出 [10 20 30]
print(array_1d[3:]) # 输出 [40 50]
-
二维数组的索引与切片 :
import numpy as np
array_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# 索引访问元素
print(array_2d[0, 1]) # 输出 2,表示第一行第二列的元素
print(array_2d[2, 2]) # 输出 9,表示第三行第三列的元素
# 切片操作
# 提取子数组:从第 0 行到第 1 行(不包括第 2 行),第 0 列到第 2 列(不包括第 3 列)
sub_array = array_2d[0:2, 0:3]
print(sub_array)
[[1 2 3]
[4 5 6]]
也可以对某一行或某一列进行切片操作:
# 提取第一行的切片
row_slice = array_2d[0, 1:3]
print(row_slice) # 输出 [2 3]
# 提取第二列的切片
column_slice = array_2d[1:3, 1]
print(column_slice) # 输出 [5 8]
-
多维数组的索引与切片 :对于更高维度的数组,索引和切片的操作遵循类似的规则,只需要在方括号中依次指定每个轴的索引或切片范围即可。例如,对于一个三维数组:
import numpy as np
array_3d = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
# 索引访问元素:第一个三维块,第二行,第一列
print(array_3d[0, 1, 0]) # 输出 3
# 切片操作:提取第一个三维块的所有行和列
sub_array_3d = array_3d[0, :, :]
print(sub_array_3d)
[[1 2]
[3 4]]
(二)数组的形状操作
NumPy 提供了一系列的方法来改变数组的形状,而不改变其数据内容。常见的形状操作包括:
-
reshape(shape) :返回一个新数组,其形状由 shape 参数指定,原数组的形状不变。例如:
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
print("原数组形状:", array.shape)
reshaped_array = array.reshape(3, 2)
print("重塑后的数组形状:", reshaped_array.shape)
print(reshaped_array)
原数组形状: (2, 3)
重塑后的数组形状: (3, 2)
[[1 2]
[3 4]
[5 6]]
需要注意的是,新的形状中元素的总个数必须与原数组相同,否则会报错。例如,上面的例子中,原数组有 6 个元素,重塑为 3×2 的形状,元素总数仍为 6,符合要求。
-
resize(shape) :与 reshape 不同,resize 方法会直接修改原数组的形状,如果新的形状中元素个数与原数组不同,会截断或填充数组。例如:
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
print("原数组:\n", array)
array.resize(3, 2)
print("调整大小后的数组:\n", array)
原数组:
[[1 2 3]
[4 5 6]]
调整大小后的数组:
[[1 2]
[3 4]
[5 6]]
在这个例子中,原数组有 6 个元素,调整为 3×2 的形状后,元素仍然全部保留。如果调整为更小的形状,例如 2×2,则数组会被截断为前 4 个元素;如果调整为更大的形状,例如 3×3,则数组会被填充 0(如果是浮点数数组则填充 0.0)。
-
ravel() :将多维数组展平为一维数组,返回的是原数组的一个视图(即数据共享),对视图的修改会影响原数组。例如:
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
flattened_array = array.ravel()
print(flattened_array) # 输出 [1 2 3 4 5 6]
# 修改视图中的元素
flattened_array[0] = 10
print(array) # 输出 [[10 2 3] [4 5 6]]
可以看到,修改展平后的数组的第一个元素,原二维数组的第一个元素也随之改变。
-
flatten() :同样将多维数组展平为一维数组,但它返回的是原数组的一个副本(即数据独立),对副本的修改不会影响原数组。例如:
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
flattened_array = array.flatten()
print(flattened_array) # 输出 [1 2 3 4 5 6]
# 修改副本中的元素
flattened_array[0] = 10
print(array) # 输出 [[1 2 3] [4 5 6]]
在这里,修改副本的第一个元素,原数组保持不变。
(三)数组的转置
数组的转置是指将数组的行和列互换,对于二维数组来说,转置后的数组的形状是原数组形状的行数和列数互换。NumPy 提供了 transpose() 方法和 T 属性来实现数组的转置。
-
transpose() 方法 :
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
transposed_array = array.transpose()
print(transposed_array)
[[1 4]
[2 5]
[3 6]]
transpose() 方法也可以接受一个参数 axes,用于指定轴的排列顺序。例如,对于一个三维数组,axes 参数可以用来指定新的轴的顺序。例如:
import numpy as np
array_3d = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
# 原数组的形状为 (2, 2, 2)
# 转置轴的顺序为 (1, 0, 2)
transposed_3d_array = array_3d.transpose((1, 0, 2))
print("原数组形状:", array_3d.shape)
print("转置后的数组形状:", transposed_3d_array.shape)
原数组形状: (2, 2, 2)
转置后的数组形状: (2, 2, 2)
在这个例子中,通过 transpose((1, 0, 2)),将原数组的轴 1 和轴 0 互换位置,轴 2 保持不变。
-
T 属性 :对于二维数组,可以直接使用 T 属性来获取其转置数组,这是一个更简洁的方式。例如:
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
print(array.T)
输出结果与使用 transpose() 方法相同:
复制
[[1 4]
[2 5]
[3 6]]
(四)数组的拼接与分割
在实际的数据处理过程中,常常需要将多个数组拼接成一个更大的数组,或者将一个大的数组分割成多个较小的数组。NumPy 提供了多种方法来实现这些操作。
-
拼接数组 :
-
numpy.concatenate((a1, a2, ...), axis=0) :沿着指定的轴将多个数组拼接在一起。例如:
-
import numpy as np
array1 = np.array([[1, 2], [3, 4]])
array2 = np.array([[5, 6], [7, 8]])
# 沿行(轴 0)拼接
concatenated_array_row = np.concatenate((array1, array2), axis=0)
print(concatenated_array_row)
[[1 2]
[3 4]
[5 6]
[7 8]]
# 沿列(轴 1)拼接
concatenated_array_col = np.concatenate((array1, array2), axis=1)
print(concatenated_array_col)
[[1 2 5 6]
[3 4 7 8]]
* **numpy.vstack((a1, a2, ...))** :垂直堆叠数组,相当于沿行(轴 0)拼接,用于将数组堆叠成垂直的二维数组。例如:
import numpy as np
array1 = np.array([1, 2, 3])
array2 = np.array([4, 5, 6])
stacked_array = np.vstack((array1, array2))
print(stacked_array)
[[1 2 3]
[4 5 6]]
* **numpy.hstack((a1, a2, ...))** :水平堆叠数组,相当于沿列(轴 1)拼接,用于将数组堆叠成水平的二维数组。例如:
import numpy as np
array1 = np.array([[1], [2], [3]])
array2 = np.array([[4], [5], [6]])
stacked_array = np.hstack((array1, array2))
print(stacked_array)
[[1 4]
[2 5]
[3 6]]
-
分割数组 :
-
numpy.split(ary, indices_or_sections, axis=0) :将数组沿着指定的轴分割成多个子数组。indices_or_sections 可以是一个整数,表示将数组均分为多少份;也可以是一个数组,表示分割的位置索引。例如:
-
import numpy as np
array = np.array([1, 2, 3, 4, 5, 6])
# 将数组均分为 3 份
split_arrays = np.split(array, 3)
print(split_arrays)
输出结果:
[array([1, 2]), array([3, 4]), array([5, 6])]
# 在索引 2 和 4 的位置分割数组
split_arrays_custom = np.split(array, [2, 4])
print(split_arrays_custom)
输出结果:
[array([1, 2]), array([3, 4]), array([5, 6])]
* **numpy.vsplit(ary, indices_or_sections)** :垂直分割数组,相当于沿行(轴 0)分割二维数组。例如:
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
# 将数组垂直均分为 2 份
split_arrays = np.vsplit(array, 2)
print(split_arrays)
[array([[1, 2, 3],
[4, 5, 6]]), array([[ 7, 8, 9],
[10, 11, 12]])]
* **numpy.hsplit(ary, indices_or_sections)** :水平分割数组,相当于沿列(轴 1)分割二维数组。例如:
import numpy as np
array = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
# 将数组水平均分为 2 份
split_arrays = np.hsplit(array, 2)
print(split_arrays)
输出结果:
复制
[array([[1, 2],
[5, 6]]), array([[3, 4],
[7, 8]])]
五、NumPy 数组的运算
NumPy 的强大之处在于其高效的数组运算能力,包括数组的基本运算、广播机制以及丰富的数学函数应用。
(一)数组的基本运算
NumPy 数组支持各种基本的数学运算,如加、减、乘、除等,这些运算通常是按元素进行的。例如:
import numpy as np
array1 = np.array([1, 2, 3, 4])
array2 = np.array([5, 6, 7, 8])
# 加法运算
add_result = array1 + array2
print(add_result) # 输出 [ 6 8 10 12]
# 减法运算
sub_result = array1 - array2
print(sub_result) # 输出 [-4 -4 -4 -4]
# 乘法运算
mul_result = array1 * array2
print(mul_result) # 输出 [ 5 12 21 32]
# 除法运算
div_result = array1 / array2
print(div_result) # 输出 [0.2 0.33333333 0.42857143 0.5 ]
对于二维数组,基本运算同样按元素进行,且要求两个数组的形状相同。例如:
import numpy as np
array1 = np.array([[1, 2], [3, 4]])
array2 = np.array([[5, 6], [7, 8]])
# 加法运算
add_result = array1 + array2
print(add_result)
# 输出 [[ 6 8]
# [10 12]]
# 乘法运算
mul_result = array1 * array2
print(mul_result)
# 输出 [[ 5 12]
# [21 32]]
此外,NumPy 还支持数组的标量运算,即数组与一个单独的数值进行运算。例如:
import numpy as np
array = np.array([1, 2, 3, 4])
# 加上标量 2
add_scalar_result = array + 2
print(add_scalar_result) # 输出 [3 4 5 6]
# 乘以标量 3
mul_scalar_result = array * 3
print(mul_scalar_result) # 输出 [ 3 6 9 12]
标量运算同样适用于二维或多维数组。
(二)广播机制
广播(Broadcasting)是 NumPy 中一个强大的机制,它允许在形状不同的数组之间进行运算,而无需显式地复制数组以匹配形状。广播规则如下:
-
如果两个数组的维度数不同,形状较小的数组会在前面补 1,直到维度数与较大的数组相同。例如,一个形状为 (3,) 的一维数组和一个形状为 (2, 3) 的二维数组进行运算时,一维数组的形状会被视为 (1, 3),然后在第一个轴上扩展为 2,以匹配二维数组的形状。
-
对于每个维度,如果两个数组在该维度的大小相等,或者其中一个数组在该维度的大小为 1,则认为它们是兼容的。如果两个数组在该维度的大小都大于 1 且不相等,则会引发错误。
-
广播后的数组形状是两个输入数组在每个维度上大小的最大值。
import numpy as np
array1 = np.array([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3)
array2 = np.array([10, 20, 30]) # 形状 (3,)
# 广播机制下进行加法运算
result = array1 + array2
print(result)
[[11 22 33]
[44 55 66]]
在这里,array2 的形状为 (3,),在广播过程中,它会被视为形状为 (1, 3) 的数组,然后在第一个轴上扩展为 2,与 array1 的形状 (2, 3) 匹配,从而可以进行逐元素的加法运算。
广播机制极大地简化了数组运算的代码,提高了代码的可读性和效率。例如,如果我们想要将一个二维数组的每一列都加上不同的值,可以利用广播机制轻松实现,而无需对数组进行显式的转置或其他复杂操作。
(三)数学函数应用
NumPy 提供了大量的数学函数,用于执行各种数学运算,如三角函数、指数函数、对数函数、统计函数等。这些函数都可以直接应用于数组,并返回一个新的数组作为结果。
-
三角函数 :
import numpy as np
array = np.array([0, np.pi/2, np.pi])
# 正弦函数
sin_result = np.sin(array)
print(sin_result) # 输出 [0.00000000e+00 1.00000000e+00 1.22464680e-16]
# 余弦函数
cos_result = np.cos(array)
print(cos_result) # 输出 [ 1.00000000e+00 6.12323400e-17 -1.00000000e+00]
# 正切函数
tan_result = np.tan(array)
print(tan_result) # 输出 [0.00000000e+00 1.63312394e+16 -1.22464680e-16]
-
指数与对数函数 :
import numpy as np
array = np.array([1, 2, 3, 4])
# 指数函数
exp_result = np.exp(array)
print(exp_result) # 输出 [ 2.71828183 7.3890561 20.08553692 54.59815003]
# 自然对数函数
ln_result = np.log(array)
print(ln_result) # 输出 [0. 0.69314718 1.09861229 1.38629436]
# 以 10 为底的对数函数
log10_result = np.log10(array)
print(log10_result) # 输出 [0. 0.30103 0.47712125 0.60205999]
-
统计函数 :
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
# 求和
sum_result = np.sum(array)
print(sum_result) # 输出 21
# 求平均值
mean_result = np.mean(array)
print(mean_result) # 输出 3.5
# 求标准差
std_result = np.std(array)
print(std_result) # 输出约 1.70782513
# 求最大值
max_result = np.max(array)
print(max_result) # 输出 6
# 求最小值
min_result = np.min(array)
print(min_result) # 输出 1
# 求中位数
median_result = np.median(array)
print(median_result) # 输出 3.5
这些数学函数不仅可以应用于整个数组,还可以沿着指定的轴进行运算。例如,计算二维数组每行或每列的和:
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
# 沿行(轴 1)求和
sum_along_row = np.sum(array, axis=1)
print(sum_along_row) # 输出 [6 15]
# 沿列(轴 0)求和
sum_along_col = np.sum(array, axis=0)
print(sum_along_col) # 输出 [5 7 9]
通过指定 axis 参数,可以方便地对数组的不同维度进行统计分析,这对于处理多维数据非常有用。
六、NumPy 的高级特性
除了上述的基本功能,NumPy 还具有一些高级特性,如通用函数(ufunc)、数组的迭代器、随机数生成等,这些特性为 NumPy 提供了更强大的功能和灵活性。
(一)通用函数(ufunc)
通用函数(ufunc)是 NumPy 提供的一组在数组上进行元素级操作的函数,它们是 NumPy 的核心功能之一。ufunc 函数具有以下特点:
-
高效性 :ufunc 函数是用 C 语言实现的,因此它们的执行速度非常快,远超纯 Python 实现的循环操作。
-
广播支持 :ufunc 函数内置了对广播机制的支持,可以自动处理形状不同的输入数组。
-
矢量化操作 :ufunc 函数可以直接对数组进行操作,而无需编写显式的循环,使得代码更加简洁易读。
常见的 ufunc 函数包括前面提到的各种数学函数(如加、减、乘、除、三角函数、指数函数等),以及一些专门的函数,如:
-
np.add(x, y) :计算两个数组的加法。
-
np.subtract(x, y) :计算两个数组的减法。
-
np.multiply(x, y) :计算两个数组的乘法。
-
np.divide(x, y) :计算两个数组的除法。
-
np.power(x, y) :计算 x 的 y 次幂。
-
np.sqrt(x) :计算数组元素的平方根。
-
np.exp(x) :计算数组元素的指数。
-
np.log(x) :计算数组元素的自然对数。
使用 ufunc 函数的示例:
import numpy as np
array1 = np.array([1, 2, 3, 4])
array2 = np.array([5, 6, 7, 8])
# 使用 np.add 通用函数进行加法运算
add_result = np.add(array1, array2)
print(add_result) # 输出 [ 6 8 10 12]
# 使用 np.power 通用函数计算平方
square_result = np.power(array1, 2)
print(square_result) # 输出 [1 4 9 16]
# 使用 np.sqrt 通用函数计算平方根
sqrt_result = np.sqrt(array1)
print(sqrt_result) # 输出 [1. 1.41421356 1.73205081 2. ]
ufunc 函数还可以通过设置参数来控制运算的行为,例如:
-
out 参数 :指定输出结果存储的数组,这可以避免创建新的数组,从而节省内存和提高效率。例如:
import numpy as np
array1 = np.array([1, 2, 3, 4])
array2 = np.array([5, 6, 7, 8])
result_array = np.empty(4)
np.add(array1, array2, out=result_array)
print(result_array) # 输出 [ 6. 8. 10. 12.]
-
where 参数 :指定一个布尔数组,只有在 where 参数为 True 的位置才执行运算,否则输出结果中的对应位置保持原值或为 0。例如:
import numpy as np
array1 = np.array([1, 2, 3, 4])
array2 = np.array([5, 6, 7, 8])
condition = np.array([True, False, True, False])
add_result = np.add(array1, array2, where=condition)
print(add_result) # 输出 [ 6 2 10 4]
在这个例子中,只有在条件数组 condition 为 True 的位置(即索引 0 和 2),才执行加法运算,其他位置的结果保持 array1 中的原始值。
(二)数组的迭代器
NumPy 提供了专门的迭代器对象 numpy.nditer,用于高效地遍历数组中的元素。nditer 可以自动处理多维数组的迭代逻辑,使得我们可以更方便地对数组元素进行操作。
例如,使用 nditer 遍历一个二维数组:
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
for element in np.nditer(array):
print(element, end=' ')
输出结果:
1 2 3 4 5 6
nditer 默认以 C 风格的顺序(行优先)遍历数组元素。如果需要以 Fortran 风格的顺序(列优先)遍历,可以通过设置 order 参数为 'F':
import numpy as np
array = np.array([[1, 2, 3], [4, 5, 6]])
for element in np.nditer(array, order='F'):
print(element, end=' ')
输出结果:
1 4 2 5 3 6
此外,nditer 还支持对数组元素的读写操作。通过设置 op_flags 参数,可以指定数组的访问模式。例如,将数组中的每个元素乘以 2:
import numpy as np
array = np.array([1, 2, 3, 4])
for element in np.nditer(array, op_flags=['readwrite']):
element[...] = element * 2
print(array) # 输出 [2 4 6 8]
在这里,op_flags 设置为 ['readwrite'],表示允许对数组元素进行读写操作,然后通过 element[...] = element * 2 来修改数组中的元素值。
(三)随机数生成
NumPy 提供了 numpy.random 模块,用于生成各种类型的随机数。这些随机数在模拟、统计采样、机器学习等领域有着广泛的应用。
-
生成指定范围内的随机整数 :
import numpy as np
# 生成一个 1 到 10 之间的随机整数(不包括 10)
random_int = np.random.randint(1, 10)
print(random_int)
# 生成一个形状为 (3, 4) 的随机整数数组,元素范围在 0 到 100 之间
random_int_array = np.random.randint(0, 100, size=(3, 4))
print(random_int_array)
-
生成 [0, 1) 区间的随机浮点数 :
import numpy as np
# 生成一个 [0, 1) 区间的随机浮点数
random_float = np.random.random()
print(random_float)
# 生成一个形状为 (2, 3) 的随机浮点数数组
random_float_array = np.random.random(size=(2, 3))
print(random_float_array)
-
从标准正态分布中生成随机数 :
import numpy as np
# 生成一个标准正态分布的随机数
random_normal = np.random.randn()
print(random_normal)
# 生成一个形状为 (2, 2) 的标准正态分布随机数数组
random_normal_array = np.random.randn(2, 2)
print(random_normal_array)
-
生成均匀分布的随机数 :
import numpy as np
# 生成一个在 [0, 1) 区间均匀分布的随机数
random_uniform = np.random.uniform(0, 1)
print(random_uniform)
# 生成一个形状为 (3,) 的在 [5, 10) 区间均匀分布的随机数数组
random_uniform_array = np.random.uniform(5, 10, size=3)
print(random_uniform_array)
-
随机数种子 :为了能够重复产生相同的随机数序列,可以使用 numpy.random.seed() 函数设置随机数种子。例如:
import numpy as np
np.random.seed(42)
random_numbers1 = np.random.rand(3)
print(random_numbers1)
np.random.seed(42)
random_numbers2 = np.random.rand(3)
print(random_numbers2)
[0.37454012 0.95071431 0.73199394]
[0.37454012 0.95071431 0.73199394]
可以看到,设置相同的种子后,两次生成的随机数序列完全相同。这对于调试代码、复现实验结果等场景非常有用。
七、NumPy 在实际应用中的案例
为了更好地理解 NumPy 的实际应用价值,以下列举几个常见的应用场景及代码示例。
(一)数据分析中的数据预处理
在数据分析过程中,通常需要对原始数据进行预处理,如缺失值处理、数据标准化等。NumPy 提供了方便的工具来完成这些任务。
-
处理缺失值 :
假设我们有一个包含缺失值的数据数组,缺失值用 NaN(Not a Number)表示。
import numpy as np
data = np.array([10.2, 12.5, np.nan, 14.8, np.nan, 17.3])
# 将缺失值替换为数组的均值
mean_value = np.nanmean(data)
cleaned_data = np.where(np.isnan(data), mean_value, data)
print(cleaned_data)
输出结果:
[10.2 12.5 15. 14.8 15. 17.3]
在这里,我们使用 np.nanmean() 函数计算数组中非 NaN 元素的均值,然后使用 np.where() 函数将 NaN 值替换为均值。
-
数据标准化 :
数据标准化是将数据按比例缩放,使其落入一个小范围(如 0-1 或标准正态分布)的过程。以下是一个将数据标准化到 0-1 范围的示例:
import numpy as np
data = np.array([5.1, 7.8, 3.2, 9.5, 1.0])
# 计算数据的最大值和最小值
max_val = np.max(data)
min_val = np.min(data)
# 数据标准化到 [0, 1] 范围
scaled_data = (data - min_val) / (max_val - min_val)
print(scaled_data)
输出结果:
[0.4375 0.7625 0.25 1. 0. ]
通过简单的数学变换,我们将原始数据映射到了 0-1 范围内,便于后续的分析和建模。
(二)科学计算中的矩阵运算
在科学计算领域,矩阵运算是一个常见的任务。NumPy 提供了强大的矩阵运算功能,使得复杂的矩阵计算变得轻松简单。
-
矩阵乘法 :
import numpy as np
matrix_a = np.array([[1, 2], [3, 4]])
matrix_b = np.array([[5, 6], [7, 8]])
# 使用 np.dot() 函数计算矩阵乘法
matrix_product = np.dot(matrix_a, matrix_b)
print(matrix_product)
[[19 22]
[43 50]]
np.dot() 函数用于计算两个数组的点积,对于二维数组来说,就是矩阵乘法。
-
求解线性方程组 :
假设我们有一个线性方程组:
复制
3x + y = 9
x + 2y = 8
可以使用 NumPy 的 np.linalg.solve() 函数来求解这个方程组:
import numpy as np
# 系数矩阵
A = np.array([[3, 1], [1, 2]])
# 常数项数组
B = np.array([9, 8])
# 求解线性方程组
solution = np.linalg.solve(A, B)
print(solution)
输出结果:
[2. 3.]
这表示方程组的解为 x=2,y=3。np.linalg.solve() 函数专门用于求解线性矩阵方程,它是 NumPy 线性代数模块(numpy.linalg)中的一个功能强大的工具。
(三)机器学习中的特征工程
在机器学习项目中,特征工程是至关重要的一环,NumPy 可以帮助我们高效地进行特征处理。
-
特征缩放 :
特征缩放是将不同尺度的特征数据转换到同一尺度范围,以提高机器学习算法的性能。以下是一个使用 NumPy 进行特征缩放的示例:
import numpy as np
# 假设我们有两个特征:房屋面积(平方米)和房间数量
features = np.array([[150, 3], [200, 4], [180, 2], [220, 5]])
# 分别计算每个特征的均值和标准差
mean_area = np.mean(features[:, 0])
std_area = np.std(features[:, 0])
mean_rooms = np.mean(features[:, 1])
std_rooms = np.std(features[:, 1])
# 对特征进行标准化(Z-score 标准化)
scaled_features = np.zeros_like(features)
scaled_features[:, 0] = (features[:, 0] - mean_area) / std_area
scaled_features[:, 1] = (features[:, 1] - mean_rooms) / std_rooms
print(scaled_features)
[[ -0.59830287 -0.47140452]
[ 1.09660574 1.32453834]
[ -0.29915143 -1.32453834]
[ 0.20084856 1.47140452]]
通过标准化处理,我们将房屋面积和房间数量这两个特征转换到了以均值为 0、标准差为 1 的标准正态分布范围内,这样可以使机器学习算法(如神经网络、支持向量机等)在训练过程中更快地收敛,提高模型的性能。
-
特征选择 :
特征选择是从原始特征中选择出对模型最有用的特征子集,以减少特征维度、提高模型的泛化能力。以下是一个简单的特征选择示例,根据特征的方差来选择方差较大的特征:
import numpy as np
# 假设我们有三个特征
features = np.array([[2.5, 3.2, 0.8], [3.1, 4.5, 1.2], [1.8, 2.9, 0.5], [4.2, 5.1, 1.0]])
# 计算每个特征的方差
variances = np.var(features, axis=0)
print("特征方差:", variances)
# 选择方差大于 0.5 的特征
selected_features = features[:, variances > 0.5]
print("选择的特征:\n", selected_features)
特征方差: [1.09166667 1.32244444 0.08666667]
选择的特征:
[[2.5 3.2]
[3.1 4.5]
[1.8 2.9]
[4.2 5.1]]
在这里,我们计算了每个特征的方差,然后选择方差大于 0.5 的特征(即前两个特征),舍弃了方差较小的第三个特征,从而简化了特征空间,为后续的模型训练提供了更有意义的特征输入。
八、NumPy 的性能优化技巧
为了充分发挥 NumPy 的性能优势,在使用过程中掌握一些性能优化技巧是非常重要的。
-
避免显式循环 :尽可能使用 NumPy 的内置函数和向量化操作,而不是使用 Python 的 for 循环或 while 循环对数组元素进行逐个操作。内置函数和向量化操作利用了 NumPy 的底层优化机制,能够更高效地利用 CPU 资源和内存带宽。例如,计算两个数组的点积时,使用 np.dot() 函数而不是手动编写循环计算:
import numpy as np
array1 = np.random.rand(1000)
array2 = np.random.rand(1000)
# 使用 np.dot() 函数计算点积(推荐)
dot_product = np.dot(array1, array2)
# 使用显式循环计算点积(不推荐)
dot_product_loop = 0.0
for i in range(len(array1)):
dot_product_loop += array1[i] * array2[i]
print(dot_product == dot_product_loop) # 在数值上应该相等(忽略浮点精度误差)
通常情况下,使用 np.dot() 函数的计算速度会比显式循环快很多,尤其是当数组较大时。
-
预分配数组内存 :如果需要创建一个较大的数组,并且知道其最终的形状和大小,可以预先分配内存空间,而不是在循环中动态扩展数组。动态扩展数组会导致频繁的内存复制和重新分配,这会显著降低程序的性能。例如,预先分配一个数组来存储循环计算的结果:
import numpy as np
n = 10000
# 预分配数组内存
result = np.empty(n)
for i in range(n):
# 进行一些计算并将结果存储到预分配的数组中
result[i] = i ** 2 + 2 * i + 1
# 如果不预分配内存,而是动态扩展数组,性能会差很多
# result_dynamic = []
# for i in range(n):
# result_dynamic.append(i ** 2 + 2 * i + 1)
# result_dynamic = np.array(result_dynamic)
预分配内存的方式可以大大减少内存操作的开销,从而提高代码的执行效率。
-
使用合适的数据类型 :选择合适的数据类型可以减少内存占用,提高计算速度。例如,如果数据的取值范围较小,可以使用较小的数据类型(如 int8、float32)而不是较大的数据类型(如 int64、float64)。通过使用 np.dtype 来指定数组的数据类型,或者在创建数组时通过 dtype 参数来选择合适的数据类型:
import numpy as np
# 使用 int8 数据类型创建数组
array_int8 = np.array([1, 2, 3, 4, 5], dtype=np.int8)
print("int8 数组占用的内存字节数:", array_int8.nbytes) # 输出 5
# 使用 int64 数据类型创建相同的数组
array_int64 = np.array([1, 2, 3, 4, 5], dtype=np.int64)
print("int64 数组占用的内存字节数:", array_int64.nbytes) # 输出 40
在这个例子中,int8 数组占用的内存仅为 int64 数组的 1/8,在处理大规模数据时,这种内存节省是非常可观的,同时也可以加速内存访问和计算过程。
-
利用广播机制 :合理利用广播机制可以避免创建不必要的中间数组,减少内存消耗和计算步骤。例如,将一个二维数组的每一列都加上一个不同的偏置值:
import numpy as np
data = np.random.rand(1000, 100)
biases = np.random.rand(100)
# 利用广播机制添加偏置(推荐)
data_with_bias = data + biases
# 不利用广播机制,手动复制偏置数组(不推荐)
# biases_replicated = np.tile(biases, (1000, 1))
# data_with_bias_no_broadcast = data + biases_replicated
在这里,第一种方法利用广播机制直接将形状为 (100,) 的偏置数组与形状为 (1000, 100) 的数据数组相加,得到正确的结果,而无需显式地将偏置数组复制为 (1000, 100) 的形状。这不仅节省了内存(避免创建 biases_replicated 数组),还减少了计算量,提高了代码的性能。
-
使用 NumPy 的数学函数 :NumPy 提供的数学函数通常经过高度优化,能够利用 SIMD(单指令多数据)指令集等硬件特性来加速计算。在可能的情况下,尽量使用 NumPy 的内置数学函数,而不是自己编写类似的计算逻辑。例如,计算数组元素的平方根时,使用 np.sqrt() 函数而不是自己编写平方根计算代码:
import numpy as np
array = np.random.rand(1000000)
# 使用 NumPy 的 sqrt 函数(推荐)
sqrt_array = np.sqrt(array)
# 自己编写平方根计算逻辑(不推荐)
# sqrt_array_manual = array ** 0.5
虽然在这个例子中,自己编写 array ** 0.5 也能得到正确的结果,但 NumPy 的 np.sqrt() 函数在底层进行了更多的优化,执行速度通常会更快,尤其是在处理大规模数组时。
-
使用视图(View)代替副本(Copy) :在某些操作中,如数组的切片操作,尽量使用视图(即原数组的一个引用)而不是创建副本,这样可以节省内存并提高程序的响应速度。只有在确实需要修改数据且不希望影响原数组的情况下,才考虑创建副本。例如:
import numpy as np
original_array = np.array([1, 2, 3, 4, 5])
# 使用视图进行切片操作(推荐)
view_array = original_array[1:4]
print("视图数组:", view_array)
# 修改视图数组会影响原数组
view_array[0] = 10
print("修改后的原数组:", original_array)
# 如果需要副本,可以显式地创建(不推荐,除非必要)
# copy_array = original_array[1:4].copy()
# copy_array[0] = 20
# print("修改副本后的原数组:", original_array) # 原数组不会改变
在上面的代码中,view_array 是 original_array 的一个视图,对 view_array 的修改会直接反映到 original_array 中。如果使用 copy() 方法创建副本,则副本与原数组的数据独立,修改副本不会影响原数组。在内存和性能敏感的应用场景中,合理选择视图或副本是非常重要的。
九、NumPy 与其他库的集成
NumPy 作为 Python 科学计算生态系统的基石,与其他众多库紧密集成,共同构成了强大的数据处理和分析工具链。
-
与 Pandas 集成 :Pandas 是一个用于数据分析和处理的 Python 库,它在内部大量使用 NumPy 数组来存储数据。Pandas 的 DataFrame 和 Series 数据结构实际上是建立在 NumPy 的 ndarray 基础之上的。例如,我们可以将 NumPy 数组转换为 Pandas 的 DataFrame:
import numpy as np
import pandas as pd
# 创建一个 NumPy 数组
array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# 将 NumPy 数组转换为 Pandas DataFrame
df = pd.DataFrame(array, columns=['A', 'B', 'C'])
print(df)
A B C
0 1 2 3
1 4 5 6
2 7 8 9
同样,也可以将 Pandas 的 DataFrame 或 Series 转换回 NumPy 数组:
# 将 Pandas DataFrame 转换为 NumPy 数组
numpy_array = df.values
print(numpy_array)
[[1 2 3]
[4 5 6]
[7 8 9]]
这种无缝的转换使得我们可以充分利用 NumPy 的高效数组运算能力和 Pandas 的数据处理功能,实现复杂的数据分析任务。
-
与 Matplotlib 集成 :Matplotlib 是一个广泛使用的 Python 绘图库,它与 NumPy 配合默契,可以方便地将 NumPy 数组中的数据可视化。例如,绘制一个简单的折线图:
import numpy as np
import matplotlib.pyplot as plt
# 创建 NumPy 数组作为数据
x = np.linspace(0, 10, 100)
y = np.sin(x)
# 使用 Matplotlib 绘制折线图
plt.plot(x, y)
plt.xlabel('x')
plt.ylabel('sin(x)')
plt.title('Sine Wave')
plt.show()
输出结果为一张显示正弦波的折线图。在这个例子中,我们使用 NumPy 的 linspace() 函数生成 x 值数组,使用 sin() 函数计算对应的 y 值数组,然后将这两个数组传递给 Matplotlib 的 plot() 函数进行绘图。通过这种方式,我们可以轻松地将数据计算和可视化结合起来,直观地展示数据的规律和趋势。
-
与 SciPy 集成 :SciPy 是一个基于 NumPy 构建的科学计算库,它提供了更多高级的数学函数和算法,如信号处理、积分、优化、插值等。NumPy 作为 SciPy 的基础,为 SciPy 提供了高效的数组支持。例如,使用 SciPy 的 integrate 模块进行数值积分:
import numpy as np
from scipy import integrate
# 定义被积函数
def integrand(x):
return np.exp(-x**2)
# 使用 SciPy 的 quad 函数计算定积分
result, error = integrate.quad(integrand, -np.inf, np.inf)
print("积分结果:", result)
print("估计误差:", error)
在这个例子中,我们使用 NumPy 的 exp() 函数定义了被积函数,然后调用 SciPy 的 quad() 函数计算该函数在整个实数范围上的定积分。NumPy 的高效数组运算能力与 SciPy 的专业数学算法相结合,使得我们可以轻松地处理复杂的科学计算问题。
-
与 Scikit-learn 集成 :Scikit-learn 是一个流行的机器学习库,它基于 NumPy、SciPy 和 Matplotlib 构建。在 Scikit-learn 中,数据通常以 NumPy 数组的形式传递,这使得我们可以方便地将 NumPy 的数据处理功能与 Scikit-learn 的机器学习算法结合起来。例如,使用 Scikit-learn 的线性回归模型:
import numpy as np
from sklearn.linear_model import LinearRegression
# 生成训练数据
X = np.array([[1], [2], [3], [4], [5]])
y = np.array([2, 4, 6, 8, 10])
# 创建线性回归模型并拟合数据
model = LinearRegression()
model.fit(X, y)
# 预测新数据
X_new = np.array([[6], [7]])
y_pred = model.predict(X_new)
print("预测结果:", y_pred)
在这个例子中,我们使用 NumPy 数组存储训练数据和目标变量,然后将其传递给 Scikit-learn 的 LinearRegression 模型进行训练和预测。通过这种方式,我们可以充分利用 NumPy 的数据处理能力和 Scikit-learn 的机器学习算法,快速构建和评估各种机器学习模型。
十、NumPy 的局限性与注意事项
尽管 NumPy 在数值计算方面表现出色,但它也有一些局限性和需要注意的地方。
-
内存消耗 :NumPy 数组在内存中存储为连续的块,虽然这提高了访问速度,但对于大规模数据来说,可能会消耗大量的内存。如果处理的数据量超过了系统内存的容量,可能会导致内存溢出错误或显著的性能下降。在这种情况下,可以考虑使用其他技术,如数据分块处理、利用磁盘存储(如使用 HDF5 文件格式)或使用分布式计算框架(如 Dask)来处理大规模数据。
-
并行计算的局限性 :虽然 NumPy 的底层运算利用了多线程来加速计算(特别是对于支持多线程的 BLAS/LAPACK 库),但在某些情况下,它可能无法充分发挥多核 CPU 的并行计算能力。对于需要高度并行化的复杂计算任务,可能需要借助其他专门的并行计算库(如 multiprocessing、joblib 或 CUDA)来实现更高效的并行计算。
-
数据类型转换的潜在问题 :在使用 NumPy 与其他 Python 库或函数交互时,需要注意数据类型转换的问题。NumPy 的数据类型与 Python 内置的类型之间存在差异,自动转换可能会导致意外的结果或性能损失。例如,将一个 NumPy 整数数组传递给一个期望 Python 整数列表的函数,可能会引发类型错误或计算结果不准确。因此,在进行数据交换时,需要显式地进行类型转换,以确保数据的正确性和一致性。
-
与 Python 原生数据结构的兼容性 :虽然 NumPy 提供了高效的数组对象,但在与一些只支持 Python 原生数据结构(如列表、元组)的库或函数交互时,可能需要将 NumPy 数组转换为这些原生结构。这种转换会带来一定的性能开销,特别是在频繁转换大规模数据时。因此,在设计数据处理流程时,应尽量减少不必要的数据类型转换,或者选择能够直接支持 NumPy 数组的工具和库,以提高整体性能。