Python版本:Python 3.12+
NumPy版本:NumPy 2.0+
开发工具:PyCharm 或 VS Code
操作系统:Windows / macOS / Linux (通用)


摘要:NumPy是Python数据科学领域的基石库,它提供了高效的多维数组对象和丰富的数学运算工具。本章将从零基础出发,深入讲解NumPy数组的创建与属性、索引切片技巧、广播机制原理以及性能优化方法,帮助你掌握数据科学中最核心的数值计算技能。


学习目标

完成本章学习后,你将能够:

  • 理解NumPy数组与Python列表的本质区别,掌握数组的内存布局原理
  • 熟练创建各种维度的NumPy数组,并根据场景选择合适的数据类型
  • 掌握数组的基础索引、布尔索引和花式索引等高级切片技巧
  • 深入理解广播机制的工作原理,能够运用广播简化复杂运算
  • 运用向量化操作和内存优化技巧显著提升代码执行效率
  • 识别并避免NumPy使用中的常见陷阱和性能误区

1. NumPy简介与安装

1.1 为什么需要NumPy

在数据科学的世界里,我们每天都要处理大量的数值数据。Python原生的列表(list)虽然灵活,但在数值计算方面存在明显的性能瓶颈。

Python列表 vs NumPy数组:核心差异对比

特性Python列表NumPy数组
存储方式存储对象指针,元素分散存储连续内存块存储原始数据
数据类型可混合存储任意类型同类型元素,类型固定
运算效率需Python循环遍历C语言级别的向量化运算
内存占用大(每个元素额外开销)小(紧凑存储)
数学运算不支持直接运算支持元素级和矩阵运算

类比理解

把Python列表想象成一个储物柜,每个格子存放一个指向物品的地址标签。要找东西,得先查标签,再去对应位置取物。而NumPy数组像是一个整齐的仓库,同类物品紧密排列在一起,可以直接批量搬运。

import numpy as np
import time

# 性能对比:百万级数据求和
size = 1_000_000
python_list = list(range(size))
numpy_array = np.arange(size)

# Python列表求和
start = time.time()
sum(python_list)
python_time = time.time() - start

# NumPy数组求和
start = time.time()
np.sum(numpy_array)
numpy_time = time.time() - start

print(f"Python列表耗时: {python_time:.4f}秒")
print(f"NumPy数组耗时: {numpy_time:.4f}秒")
print(f"加速比: {python_time/numpy_time:.0f}倍")

1.2 NumPy 2.0新特性(2025年更新)

NumPy 2.0于2024年正式发布,是自2006年以来首次重大版本更新,带来了多项重要改进。

核心新特性一览

特性说明应用场景
StringDType可变长度UTF-8字符串类型文本数据处理,替代object dtype
数组API标准支持主命名空间支持Array API标准跨库兼容(CuPy、JAX等)
SIMD加速排序Intel x86-simd-sort + Google Highway大规模数据排序性能提升
macOS Acceleratearm64支持,线性代数运算加速macOS用户性能提升达10倍
matrix_transpose矩阵转置专用函数深度学习、线性代数运算
最大维度扩展从32维扩展到64维高维张量计算

StringDType:文本数据处理的新选择

# NumPy 2.0新增:StringDType用于高效存储变长字符串
from numpy.dtypes import StringDType

# 传统方式:使用object dtype存储字符串(效率低)
strings_old = np.array(["apple", "banana", "cherry"], dtype=object)
print(f"object dtype内存: {strings_old.nbytes} 字节")

# NumPy 2.0新方式:使用StringDType
strings_new = np.array(["apple", "banana", "cherry"], dtype=StringDType())
print(f"StringDType内存: {strings_new.nbytes} 字节")

# 字符串操作ufunc加速
result = np.strings.upper(strings_new)
print(f"大写转换结果: {result}")

数组API标准兼容性

# NumPy 2.0主命名空间支持Array API标准
# 这意味着代码可以更容易地在NumPy、CuPy、JAX之间迁移

# Array API标准函数示例
arr = np.array([[1, 2], [3, 4]])

# 矩阵转置(新函数)
result = np.matrix_transpose(arr)
print(f"矩阵转置:\n{result}")

# 矩阵范数(新函数)
norm = np.linalg.matrix_norm(arr)
print(f"矩阵范数: {norm}")

# 向量范数(新函数)
vec_norm = np.linalg.vector_norm(arr)
print(f"向量范数: {vec_norm}")

1.3 安装与导入

# 使用conda安装(推荐)
# conda install numpy>=2.0

# 使用pip安装
# pip install numpy>=2.0

# 导入NumPy并查看版本
import numpy as np
print(f"NumPy版本: {np.__version__}")

# 检查SIMD优化支持
from numpy.lib.introspect import opt_func_info
print(f"\n优化函数信息:\n{opt_func_info('add')}")

2. 数组创建与属性

2.1 数组创建方法

NumPy的核心是ndarray(N-dimensional array)对象。理解数组的创建方式是掌握NumPy的第一步。

创建方式速查表

函数功能示例输出形状
np.array()从列表/元组创建与输入相同
np.zeros()创建全零数组(rows, cols)
np.ones()创建全一数组(rows, cols)
np.empty()创建未初始化数组(rows, cols)
np.eye()创建单位矩阵(n, n)
np.arange()等差数列(stop-start)//step
np.linspace()等间隔数列(num,)
np.random.rand()均匀分布随机数(d0, d1, ...)

从Python列表创建数组

import numpy as np

# 创建一维数组
arr1 = np.array([1, 2, 3, 4, 5])
print(f"一维数组: {arr1}")

# 创建二维数组(矩阵)
arr2 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f"二维数组:\n{arr2}")

# 创建三维数组
arr3 = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(f"三维数组形状: {arr3.shape}")

# 指定数据类型创建数组
arr_float = np.array([1, 2, 3], dtype=np.float32)
print(f"指定float32类型: {arr_float.dtype}")

创建特殊值数组

# 创建全零数组,常用于初始化权重或占位
zeros = np.zeros((3, 4))  # 3行4列的全零数组
print(f"全零数组:\n{zeros}")

# 创建全一数组,常用于掩码操作
ones = np.ones((2, 3, 4))  # 2个3行4列的三维数组
print(f"全一数组形状: {ones.shape}")

# 创建空数组,未初始化,值是内存中的随机残留数据
# 适用于需要立即填充数据的场景,比创建后再赋值更高效
empty = np.empty((2, 2))
print(f"空数组:\n{empty}")

# 创建单位矩阵,线性代数中常用
identity = np.eye(3)
print(f"3x3单位矩阵:\n{identity}")

# 创建对角矩阵
diag = np.diag([1, 2, 3, 4])
print(f"对角矩阵:\n{diag}")

创建等间隔数列

# arange生成等差数列,类似于Python的range但返回数组
# 参数:起始值、结束值(不包含)、步长
arange_arr = np.arange(0, 10, 2)  # 从0到10,步长为2
print(f"arange结果: {arange_arr}")

# linspace生成等间隔数列,包含起始和结束值
# 参数:起始值、结束值、元素个数
linspace_arr = np.linspace(0, 1, 5)  # 0到1之间均匀分布5个数
print(f"linspace结果: {linspace_arr}")

# logspace生成对数间隔数列,常用于频率分析
# 参数:起始指数、结束指数、元素个数(以10为底)
logspace_arr = np.logspace(0, 2, 5)  # 10^0到10^2之间5个数
print(f"logspace结果: {logspace_arr}")

# geomspace生成几何间隔数列
geomspace_arr = np.geomspace(1, 100, 5)  # 1到100之间几何分布5个数
print(f"geomspace结果: {geomspace_arr}")

创建随机数组

# 设置随机种子以保证结果可复现
np.random.seed(42)

# 生成0到1之间的均匀分布随机数
random_arr = np.random.rand(3, 3)
print(f"均匀分布随机数组:\n{random_arr}")

# 生成指定范围内的随机整数
# 参数:最小值、最大值(不包含)、数组形状
randint_arr = np.random.randint(0, 10, size=(3, 3))
print(f"随机整数数组:\n{randint_arr}")

# 生成标准正态分布随机数(均值为0,标准差为1)
normal_arr = np.random.randn(3, 3)
print(f"标准正态分布:\n{normal_arr}")

# 生成指定范围和分布的随机数
uniform_arr = np.random.uniform(0, 1, size=(3, 3))
print(f"指定范围均匀分布:\n{uniform_arr}")

# 生成正态分布随机数(指定均值和标准差)
normal_custom = np.random.normal(loc=5, scale=2, size=(2, 3))
print(f"均值为5标准差为2的正态分布:\n{normal_custom}")

2.2 数组属性详解

了解数组的属性对于后续操作至关重要,这些属性描述了数组的结构和内存特征。

关键属性速查表

属性说明示例值
ndim数组维度数2
shape每个维度的大小(3, 4)
size元素总数12
dtype元素数据类型float64
itemsize每个元素字节数8
nbytes总内存字节数96
strides每个维度步长(字节)(32, 8)
# 创建一个示例数组用于演示属性
arr = np.array([[1, 2, 3], [4, 5, 6]])

print(f"数组内容:\n{arr}")
print(f"维度数 (ndim): {arr.ndim}")        # 数组的维度数量
print(f"形状 (shape): {arr.shape}")        # 每个维度的大小
print(f"元素总数 (size): {arr.size}")      # 数组中所有元素的数量
print(f"数据类型 (dtype): {arr.dtype}")    # 数组元素的数据类型
print(f"元素字节数 (itemsize): {arr.itemsize}")  # 每个元素占用的字节数
print(f"总字节数 (nbytes): {arr.nbytes}")  # 数组占用的总内存字节数
print(f"步长 (strides): {arr.strides}")    # 每个维度上移动到下一个元素的字节数

数据类型系统详解

选择合适的dtype可以显著节省内存并提升计算效率。

类型说明取值范围典型应用场景
int8/int16/int32/int64有符号整数-128~127 / -32768~32767 / 约正负21亿 / 约正负9e18计数、索引、类别编码
uint8/uint16/uint32/uint64无符号整数0~255 / 0~65535 / 0~约42亿 / 0~约1.8e19图像像素值、二进制数据
float16/float32/float64浮点数半精度/单精度/双精度深度学习(float16/float32)、科学计算(float64)
complex64/complex128复数单精度/双精度复数信号处理、量子计算
bool布尔值True/False掩码、条件筛选
StringDType可变长度字符串UTF-8编码文本数据处理(NumPy 2.0新增)
# 数据类型选择示例
# 图像处理中,像素值范围是0-255,使用uint8最合适
image_data = np.zeros((1080, 1920, 3), dtype=np.uint8)
print(f"图像数据内存占用: {image_data.nbytes / 1024 / 1024:.2f} MB")

# 如果使用float64存储图像,内存将浪费8倍
image_float64 = np.zeros((1080, 1920, 3), dtype=np.float64)
print(f"float64图像数据内存占用: {image_float64.nbytes / 1024 / 1024:.2f} MB")

# 科学计算通常需要高精度,使用float64
scientific_data = np.array([1.0, 2.0, 3.0], dtype=np.float64)
print(f"科学计算数据类型: {scientific_data.dtype}")

2.3 数组变形操作

数组变形是数据处理中的常见需求,NumPy提供了多种方式在不改变数据内容的情况下改变数组形状。

变形操作速查表

操作函数/属性是否返回视图说明
reshapearr.reshape()通常返回视图改变数组形状
ravelarr.ravel()返回视图展平为一维
flattenarr.flatten()返回副本展平为一维
transposearr.T / arr.transpose()返回视图转置数组
expand_dimsnp.expand_dims()返回视图增加维度
squeezenp.squeeze()返回视图删除大小为1的维度

reshape改变形状

# 创建一个一维数组
arr = np.arange(12)  # 生成0到11的数组
print(f"原始数组: {arr}")
print(f"原始形状: {arr.shape}")

# reshape改变形状为3行4列
reshaped = arr.reshape(3, 4)
print(f"reshape为(3,4):\n{reshaped}")

# 使用-1让NumPy自动计算该维度大小
auto_reshape = arr.reshape(3, -1)  # -1表示自动计算,这里等同于reshape(3, 4)
print(f"自动计算维度(3,-1):\n{auto_reshape}")

# 多维reshape
multi_dim = arr.reshape(2, 2, 3)
print(f"三维reshape(2,2,3):\n{multi_dim}")

展平操作

# ravel返回视图,不复制数据,修改会影响原数组
flattened_view = reshaped.ravel()
print(f"ravel结果: {flattened_view}")

# flatten返回副本,修改不会影响原数组
flattened_copy = reshaped.flatten()
print(f"flatten结果: {flattened_copy}")

# 验证视图和副本的区别
flattened_view[0] = 999
print(f"修改视图后原数组:\n{reshaped}")  # 原数组被修改

flattened_copy[1] = 888
print(f"修改副本后原数组:\n{reshaped}")  # 原数组不变

转置和轴交换

# 创建示例矩阵
arr = np.array([[1, 2, 3], [4, 5, 6]])
print(f"原始矩阵:\n{arr}")
print(f"原始形状: {arr.shape}")

# 使用.T属性进行转置
transposed = arr.T
print(f"转置后:\n{transposed}")
print(f"转置后形状: {transposed.shape}")

# 使用transpose函数,可以指定轴的顺序
# 对于二维数组,transpose(1, 0)等同于.T
transposed_func = arr.transpose(1, 0)
print(f"使用transpose:\n{transposed_func}")

# swapaxes交换指定的两个轴
swapped = arr.swapaxes(0, 1)  # 交换轴0和轴1
print(f"使用swapaxes:\n{swapped}")

# NumPy 2.0新增:matrix_transpose用于矩阵转置
matrix = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
result = np.matrix_transpose(matrix)
print(f"matrix_transpose结果形状: {result.shape}")

添加和删除维度

# 创建一维数组
arr = np.array([1, 2, 3])
print(f"原始数组形状: {arr.shape}")

# 使用np.newaxis增加维度
# 在行方向增加维度,形状从(3,)变为(1, 3)
row_vector = arr[np.newaxis, :]
print(f"增加行维度后形状: {row_vector.shape}")

# 在列方向增加维度,形状从(3,)变为(3, 1)
col_vector = arr[:, np.newaxis]
print(f"增加列维度后形状: {col_vector.shape}")

# 使用expand_dims函数增加维度
expanded = np.expand_dims(arr, axis=0)
print(f"使用expand_dims后形状: {expanded.shape}")

# squeeze删除大小为1的维度
squeezed = row_vector.squeeze()
print(f"squeeze后形状: {squeezed.shape}")

3. 数组索引与切片

3.1 基础索引

NumPy的索引和切片比Python列表更强大,支持多维索引和高级切片语法。

索引方式对比

方式语法返回值典型应用
基础索引arr[i, j]标量访问单个元素
切片arr[start:end:step]子数组(视图)获取数据子集
布尔索引arr[condition]一维数组(副本)条件筛选
花式索引arr[[i, j, k]]新数组(副本)按索引选取元素

一维数组索引

# 创建一维数组
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 基础索引
print(f"第一个元素: {arr[0]}")      # 索引从0开始
print(f"最后一个元素: {arr[-1]}")   # 负数索引从末尾开始

# 切片操作 [起始:结束:步长]
print(f"索引2到5(不含5): {arr[2:5]}")
print(f"从头到尾步长为2: {arr[::2]}")   # 每隔一个元素取一个
print(f"数组反转: {arr[::-1]}")       # 步长为-1表示反转
print(f"从索引3到末尾: {arr[3:]}")    # 省略结束表示到末尾
print(f"从头到索引5: {arr[:5]}")      # 省略起始表示从开头

多维数组索引

# 创建二维数组(矩阵)
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f"矩阵:\n{matrix}")

# 访问单个元素
print(f"第0行第0列: {matrix[0, 0]}")
print(f"第2行第1列: {matrix[2, 1]}")

# 访问整行
print(f"第0行: {matrix[0]}")
print(f"第1行: {matrix[1, :]}")  # 等同于matrix[1]

# 访问整列
print(f"第0列: {matrix[:, 0]}")
print(f"第2列: {matrix[:, 2]}")

# 访问子矩阵(切片)
sub_matrix = matrix[0:2, 1:3]
print(f"子矩阵(0-1行,1-2列):\n{sub_matrix}")

3.2 布尔索引

布尔索引是NumPy的强大特性,可以根据条件筛选元素,避免了显式循环。

布尔索引操作

# 创建示例数组
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

# 基础布尔索引:筛选大于5的元素
mask = arr > 5
print(f"布尔掩码: {mask}")
print(f"大于5的元素: {arr[mask]}")

# 直接在使用时创建条件
print(f"大于5的元素: {arr[arr > 5]}")

# 复合条件:使用&(与)、|(或)、~(非)
# 注意:复合条件需要用括号包裹
result = arr[(arr > 3) & (arr < 8)]
print(f"大于3且小于8的元素: {result}")

# 或条件
result = arr[(arr < 3) | (arr > 8)]
print(f"小于3或大于8的元素: {result}")

# 取反
result = arr[~(arr > 5)]
print(f"不大于5的元素: {result}")

# 多维数组布尔索引
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f"矩阵中大于5的元素: {matrix[matrix > 5]}")

3.3 花式索引

花式索引使用整数数组进行索引,可以选取任意位置的元素,不受连续性的限制。

# 创建示例数组
arr = np.array([10, 20, 30, 40, 50])

# 使用索引数组选取特定位置的元素
indices = [0, 2, 4]
result = arr[indices]
print(f"选取索引0,2,4的元素: {result}")

# 二维数组花式索引
matrix = np.array([[1, 2], [3, 4], [5, 6]])
print(f"矩阵:\n{matrix}")

# 同时指定行和列索引
rows = [0, 2]  # 选取第0行和第2行
cols = [1, 0]  # 分别选取第1列和第0列
result = matrix[rows, cols]
print(f"花式索引结果: {result}")  # 返回matrix[0,1]和matrix[2,0]

# 使用np.ix_进行网格索引,获取子矩阵
result = matrix[np.ix_([0, 2], [0, 1])]
print(f"使用ix_获取子矩阵:\n{result}")

# 花式索引与切片结合
result = arr[[0, 2, 4]][:2]
print(f"花式索引后切片: {result}")

3.4 视图与副本

理解视图和副本的区别对性能优化和避免意外修改至关重要。

视图 vs 副本对比

特性视图(View)副本(Copy)
内存共享与原数组共享数据独立的数据副本
修改影响修改视图会影响原数组修改副本不影响原数组
创建方式切片、reshape、ravelcopy方法、flatten
性能快(无数据复制)慢(需要数据复制)
使用场景临时查看、内存优化需要独立数据时

视图(View)

# 创建原数组
arr = np.array([1, 2, 3, 4, 5])

# 切片操作返回视图
view = arr[1:4]
print(f"视图: {view}")

# 修改视图
view[0] = 100
print(f"修改视图后,原数组: {arr}")  # 原数组也被修改

# 验证是否为视图
print(f"视图.base是原数组: {view.base is arr}")

副本(Copy)

# 创建原数组
arr = np.array([1, 2, 3, 4, 5])

# 使用copy方法创建副本
copy = arr[1:4].copy()
print(f"副本: {copy}")

# 修改副本
copy[0] = 100
print(f"修改副本后,原数组: {arr}")  # 原数组不变
print(f"修改后副本: {copy}")

# 验证是否为副本
print(f"副本.base是原数组: {copy.base is arr}")

判断方法总结

# 创建数组
arr = np.array([1, 2, 3, 4, 5])

# 切片返回视图
view = arr[1:4]
print(f"切片是视图: {view.base is arr}")  # True

# copy返回副本
copy = arr[1:4].copy()
print(f"copy是副本: {copy.base is arr}")  # False

# reshape通常返回视图
reshaped = arr.reshape(5, 1)
print(f"reshape通常是视图: {reshaped.base is arr}")  # True

# flatten返回副本
flattened = reshaped.flatten()
print(f"flatten是副本: {flattened.base is arr}")  # False

# ravel返回视图
raveled = reshaped.ravel()
print(f"ravel是视图: {raveled.base is arr}")  # True

4. 数组形状操作

4.1 reshape与resize

reshape和resize都可以改变数组形状,但它们的行为有重要区别。

reshape vs resize对比

特性reshaperesize
返回值返回新数组(视图或副本)无返回值,修改原数组
原数组不改变直接修改
元素数量必须与原数组相同可扩展(填充0)或截断
使用场景临时变形查看数据永久改变数组大小
# reshape不改变原数组,返回新数组(视图或副本)
arr = np.arange(6)
print(f"原数组: {arr}")

reshaped = arr.reshape(2, 3)
print(f"reshape后:\n{reshaped}")
print(f"原数组不变: {arr}")

# resize改变原数组本身,不返回新数组
arr_resize = np.arange(6)
arr_resize.resize(2, 3)  # 注意:没有返回值
print(f"resize后的数组:\n{arr_resize}")

# resize可以填充或截断数据
arr_small = np.array([1, 2, 3])
arr_small.resize(2, 3)  # 扩展到6个元素,新位置填充0
print(f"扩展resize:\n{arr_small}")

4.2 数组拼接与分割

在实际数据处理中,经常需要合并多个数组或分割大数组。

拼接函数速查表

函数拼接方向适用场景
np.vstack()垂直方向axis=0按行拼接
np.hstack()水平方向axis=1按列拼接
np.dstack()深度方向axis=2三维数组深度拼接
np.concatenate()指定轴任意通用拼接

数组拼接

# 创建示例数组
arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([[5, 6], [7, 8]])

print(f"数组1:\n{arr1}")
print(f"数组2:\n{arr2}")

# 垂直拼接(沿轴0拼接)
vstack_result = np.vstack((arr1, arr2))
print(f"垂直拼接:\n{vstack_result}")

# 水平拼接(沿轴1拼接)
hstack_result = np.hstack((arr1, arr2))
print(f"水平拼接:\n{hstack_result}")

# 使用concatenate,更通用的拼接方式
concat_axis0 = np.concatenate((arr1, arr2), axis=0)
print(f"沿轴0拼接:\n{concat_axis0}")

concat_axis1 = np.concatenate((arr1, arr2), axis=1)
print(f"沿轴1拼接:\n{concat_axis1}")

# 深度拼接(沿轴2,用于三维数组)
arr3d_1 = np.array([[[1, 2]], [[3, 4]]])
arr3d_2 = np.array([[[5, 6]], [[7, 8]]])
dstack_result = np.dstack((arr3d_1, arr3d_2))
print(f"深度拼接形状: {dstack_result.shape}")

数组分割

# 创建示例数组
arr = np.arange(12).reshape(3, 4)
print(f"原数组:\n{arr}")

# 水平分割(沿列分割)
h_split = np.hsplit(arr, 2)  # 分成2份
print(f"水平分割结果:")
for i, part in enumerate(h_split):
    print(f"  第{i}部分:\n{part}")

# 垂直分割(沿行分割)
v_split = np.vsplit(arr, 3)  # 分成3份
print(f"垂直分割结果:")
for i, part in enumerate(v_split):
    print(f"  第{i}部分: {part.flatten()}")

# 使用split进行更灵活的分割
split_result = np.split(arr, [1, 2], axis=0)  # 在第1行和第2行处分割
print(f"在第1、2行处分割,共{len(split_result)}部分")

4.3 数组重复与平铺

# 创建示例数组
arr = np.array([[1, 2], [3, 4]])

# repeat重复元素
repeated = np.repeat(arr, 2)  # 每个元素重复2次
print(f"repeat结果: {repeated}")

# 沿指定轴重复
repeated_axis0 = np.repeat(arr, 2, axis=0)  # 沿行重复
print(f"沿轴0 repeat:\n{repeated_axis0}")

# tile平铺整个数组
tiled = np.tile(arr, 2)  # 整体重复2次
print(f"tile结果:\n{tiled}")

# 二维平铺
tiled_2d = np.tile(arr, (2, 3))  # 2行3列平铺
print(f"二维tile结果:\n{tiled_2d}")

5. 广播机制深度讲解

5.1 广播的概念与原理

广播是NumPy最强大的特性之一,它允许不同形状的数组进行数学运算,避免了显式地复制数据来匹配形状。

什么是广播?

广播(Broadcasting)是NumPy用于处理不同形状数组之间运算的机制。它通过逻辑上扩展数组的维度,使运算能够在不实际复制数据的情况下进行。

类比理解:课堂发书

想象一个教室(二维数组)里有很多排座位。要给每个座位发一本书:

  • 没有广播:老师逐个座位递书(Python循环)
  • 有广播:老师喊一声"每人一本",书自动复制到每个座位(NumPy广播)

广播机制就像是自动化的"复制分发系统",它让代码更简洁,执行更高效。

为什么广播如此重要

场景无广播方案有广播方案
数据标准化显式循环遍历每个元素(data - mean) / std
距离计算嵌套循环计算每对距离利用广播一次性计算
加权计算手动扩展权重数组scores * weights
内存占用需要显式复制数据逻辑扩展,无额外内存

5.2 广播规则详解

广播遵循以下三条规则,按顺序应用:

规则一:维度对齐

如果两个数组的维度数不同,会在较小维度数组的形状前面补1,直到维度数相同。

规则二:维度兼容

如果两个数组在某个维度的大小相同,或者其中一个数组在该维度的大小为1,则这两个数组在该维度上是兼容的

规则三:结果形状

如果两个数组在所有维度上都兼容,则可以进行广播。广播后,每个数组的形状变为两个输入数组在各维度上的最大值

广播规则可视化

数组A形状:    (2, 3)
数组B形状:      (3,)
              ↓ 规则一:B前面补1
B扩展为:     (1, 3)
              ↓ 规则二:检查兼容性
维度0: 2 vs 1 → 兼容(B会沿此维度复制)
维度1: 3 vs 3 → 兼容
              ↓ 规则三:结果形状
结果形状:    (2, 3)

广播示例代码

# 规则演示:标量与数组的广播
arr = np.array([1, 2, 3])
result = arr + 10
print(f"标量广播: {result}")
# 标量10被逻辑上扩展为[10, 10, 10],然后与arr相加

# 规则演示:一维数组与二维数组的广播
matrix = np.array([[1, 2, 3], [4, 5, 6]])  # 形状(2, 3)
vector = np.array([10, 20, 30])             # 形状(3,)
result = matrix + vector
print(f"一维广播到二维:\n{result}")
# vector的形状从(3,)扩展为(1, 3),再扩展为(2, 3)

# 规则演示:列向量与行向量的广播
col = np.array([[1], [2], [3]])  # 形状(3, 1)
row = np.array([10, 20, 30])      # 形状(3,)
result = col + row
print(f"列向量与行向量广播:\n{result}")
# col扩展为(3, 3),row扩展为(1, 3)再扩展为(3, 3)

5.3 广播实战应用

数据标准化

在机器学习中,经常需要对数据进行标准化处理,广播可以简化这个过程。

# 创建示例数据:3个样本,每个样本4个特征
data = np.array([[1, 2, 3, 4],
                 [5, 6, 7, 8],
                 [9, 10, 11, 12]], dtype=float)
print(f"原始数据:\n{data}")

# 计算每列的均值和标准差
mean = np.mean(data, axis=0)  # 形状(4,)
std = np.std(data, axis=0)    # 形状(4,)
print(f"列均值: {mean}")
print(f"列标准差: {std}")

# 使用广播进行标准化 (x - mean) / std
# data形状(3, 4),mean形状(4,),广播后mean扩展为(3, 4)
normalized = (data - mean) / std
print(f"标准化后的数据:\n{normalized}")

距离计算

使用广播可以高效计算样本之间的距离,这在聚类算法中非常有用。

# 计算欧几里得距离示例
points = np.array([[1, 2], [3, 4], [5, 6]])  # 3个点
query = np.array([0, 0])                      # 查询点

# 使用广播计算每个点到查询点的距离
# 扩展query到(3, 2)形状进行计算
diff = points - query  # 广播:query从(2,)扩展到(3, 2)
distances = np.sqrt(np.sum(diff ** 2, axis=1))
print(f"各点到查询点的距离: {distances}")

# 计算所有点对之间的距离
# 使用广播技巧:扩展维度后相减
points_expanded = points[:, np.newaxis, :]  # 形状(3, 1, 2)
all_diffs = points_expanded - points         # 广播:形状(3, 3, 2)
all_distances = np.sqrt(np.sum(all_diffs ** 2, axis=2))
print(f"所有点对之间的距离矩阵:\n{all_distances}")

加权计算

广播在加权计算中非常实用,如加权平均、加权求和等。

# 学生成绩数据:3个学生,5门课程
scores = np.array([[85, 90, 78, 92, 88],
                   [76, 85, 90, 85, 79],
                   [92, 88, 85, 90, 95]], dtype=float)

# 每门课程的权重
weights = np.array([0.2, 0.2, 0.3, 0.2, 0.1])

# 使用广播计算加权分数
# scores形状(3, 5),weights形状(5,),广播后weights扩展为(3, 5)
weighted_scores = scores * weights
print(f"加权后的分数:\n{weighted_scores}")

# 计算每个学生的加权平均分
weighted_avg = np.sum(weighted_scores, axis=1)
print(f"每个学生的加权平均分: {weighted_avg}")

5.4 广播失败的情况

了解广播失败的情况有助于调试代码。

广播失败示例

# 广播失败的例子
a = np.array([[1, 2, 3], [4, 5, 6]])  # 形状(2, 3)
b = np.array([1, 2])                   # 形状(2,)

# a + b 会报错,因为:
# b的形状(2,)会被扩展为(1, 2)
# 但a的形状是(2, 3),最后一个维度3和2不匹配
# 尝试:result = a + b  # ValueError

# 解决方案:调整b的形状使维度兼容
b_reshaped = b.reshape(-1, 1)  # 形状变为(2, 1)
result = a + b_reshaped
print(f"调整形状后广播成功:\n{result}")

# 或者调整b的方向
b_row = np.array([1, 2, 3])  # 形状(3,)
result = a + b_row  # 可以广播,b_row扩展为(2, 3)
print(f"匹配最后一维:\n{result}")

广播调试技巧

问题原因解决方案
ValueError: operands could not be broadcast维度不兼容检查形状,使用reshape调整
结果形状不符合预期维度对齐错误从右向左检查维度匹配
运算结果错误广播方向错误明确使用np.newaxis增加维度

6. 数学运算与向量化

6.1 基本算术运算

NumPy支持元素级的算术运算,运算符会被重载为对应元素的运算。

算术运算符速查表

运算符功能示例
+元素级加法a + b
-元素级减法a - b
*元素级乘法a * b
/元素级除法a / b
**幂运算a ** 2
%取模运算a % 2
//整数除法a // 2
# 创建示例数组
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

# 元素级加法
print(f"a + b = {a + b}")

# 元素级减法
print(f"a - b = {a - b}")

# 元素级乘法(注意:不是矩阵乘法)
print(f"a * b = {a * b}")

# 元素级除法
print(f"a / b = {a / b}")

# 幂运算
print(f"a ** 2 = {a ** 2}")

# 取模运算
print(f"a % 2 = {a % 2}")

# 复合运算
print(f"(a + b) * 2 = {(a + b) * 2}")

6.2 矩阵运算

NumPy提供了专门的矩阵运算函数,与元素级运算区分开来。

矩阵运算对比

运算运算符/函数说明
元素乘法* / np.multiply()对应位置相乘
矩阵乘法@ / np.dot() / np.matmul()线性代数矩阵乘法
转置.T / np.transpose()行列互换
逆矩阵np.linalg.inv()矩阵求逆
行列式np.linalg.det()计算行列式
# 创建矩阵
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

print(f"矩阵a:\n{a}")
print(f"矩阵b:\n{b}")

# 元素乘法(Hadamard积)
print(f"元素乘法:\n{a * b}")

# 矩阵乘法(点积)
# 方法1:使用@运算符(Python 3.5+)
print(f"矩阵乘法(@):\n{a @ b}")

# 方法2:使用dot函数
print(f"矩阵乘法(dot):\n{np.dot(a, b)}")

# 方法3:使用matmul函数
print(f"矩阵乘法(matmul):\n{np.matmul(a, b)}")

# 矩阵转置与乘法结合
result = a.T @ b
print(f"a的转置乘以b:\n{result}")

6.3 通用函数(ufunc)

通用函数是NumPy提供的对数组进行元素级操作的函数,底层由C语言实现,执行效率极高。

ufunc分类速查表

类别函数示例功能
数学函数np.sin, np.cos, np.exp, np.log三角函数、指数对数
统计函数np.sum, np.mean, np.std聚合统计
比较函数np.greater, np.equal, np.maximum元素比较
位运算np.bitwise_and, np.left_shift位操作
字符串(NumPy 2.0)np.strings.upper, np.strings.find字符串处理

数学函数

# 创建示例数组
arr = np.array([0, np.pi/4, np.pi/2, np.pi])

# 三角函数
print(f"sin: {np.sin(arr)}")
print(f"cos: {np.cos(arr)}")
print(f"tan: {np.tan(arr)}")

# 指数和对数函数
arr_pos = np.array([1, 2, 3, 4])
print(f"exp: {np.exp(arr_pos)}")
print(f"log: {np.log(arr_pos)}")
print(f"log10: {np.log10(arr_pos)}")
print(f"log2: {np.log2(arr_pos)}")

# 其他数学函数
print(f"sqrt: {np.sqrt(arr_pos)}")
print(f"square: {np.square(arr_pos)}")
print(f"abs: {np.abs([-1, -2, 3, -4])}")

统计函数

# 创建示例矩阵
arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f"数组:\n{arr}")

# 求和
print(f"总和: {np.sum(arr)}")
print(f"按列求和: {np.sum(arr, axis=0)}")
print(f"按行求和: {np.sum(arr, axis=1)}")

# 平均值
print(f"平均值: {np.mean(arr)}")
print(f"按列平均: {np.mean(arr, axis=0)}")

# 标准差和方差
print(f"标准差: {np.std(arr):.4f}")
print(f"方差: {np.var(arr):.4f}")

# 最值
print(f"最小值: {np.min(arr)}")
print(f"最大值: {np.max(arr)}")
print(f"最小值索引: {np.argmin(arr)}")
print(f"最大值索引: {np.argmax(arr)}")

# 中位数和百分位数
print(f"中位数: {np.median(arr)}")
print(f"25%分位数: {np.percentile(arr, 25)}")
print(f"75%分位数: {np.percentile(arr, 75)}")

聚合函数

# 创建示例数组
arr = np.array([1, 2, 3, 4, 5])

# 累积和
print(f"累积和: {np.cumsum(arr)}")

# 累积积
print(f"累积积: {np.cumprod(arr)}")

# 累积最值
print(f"累积最大值: {np.maximum.accumulate(arr)}")
print(f"累积最小值: {np.minimum.accumulate([5, 4, 3, 4, 5])}")

6.4 线性代数运算

NumPy的linalg模块提供了丰富的线性代数函数。

线性代数函数速查表

函数功能数学表示
np.linalg.det()行列式det(A)
np.linalg.inv()逆矩阵A^(-1)
np.linalg.eig()特征值和特征向量Av = λv
np.linalg.solve()解线性方程组Ax = b
np.linalg.svd()奇异值分解A = UΣV^T
np.linalg.norm()矩阵/向量范数
np.linalg.matrix_rank()矩阵秩rank(A)
# 创建矩阵
a = np.array([[4, 2], [3, 1]])

# 计算行列式
det = np.linalg.det(a)
print(f"行列式: {det:.4f}")

# 计算逆矩阵
inv = np.linalg.inv(a)
print(f"逆矩阵:\n{inv}")

# 验证逆矩阵
print(f"a @ inv:\n{a @ inv}")  # 应该接近单位矩阵

# 计算特征值和特征向量
eigenvalues, eigenvectors = np.linalg.eig(a)
print(f"特征值: {eigenvalues}")
print(f"特征向量:\n{eigenvectors}")

# 解线性方程组 ax = b
b = np.array([1, 2])
x = np.linalg.solve(a, b)
print(f"方程组的解: {x}")
print(f"验证: a @ x = {a @ x}")

# 矩阵分解
u, s, vh = np.linalg.svd(a)  # 奇异值分解
print(f"SVD奇异值: {s}")

# NumPy 2.0新增:svdvals直接计算奇异值
singular_values = np.linalg.svdvals(a)
print(f"svdvals结果: {singular_values}")

7. 性能优化技巧

7.1 向量化操作 vs Python循环

向量化操作是NumPy性能优势的核心,应尽量避免使用Python循环处理数组数据。

为什么向量化更快?

因素Python循环NumPy向量化
执行方式Python解释器逐行执行编译后的C代码
CPU利用单指令单数据SIMD单指令多数据
内存访问分散访问(列表指针)连续访问(数组)
类型检查每次循环检查类型类型确定后批量处理

一句话总结:向量化操作就像是"批量处理订单",而Python循环是"逐个处理订单"。

import time

# 创建大数据
size = 1_000_000
arr = np.random.rand(size)

# 方法1:Python循环(慢)
start = time.time()
result_loop = []
for x in arr:
    result_loop.append(x ** 2)
result_loop = np.array(result_loop)
python_time = time.time() - start

# 方法2:NumPy向量化(快)
start = time.time()
result_vectorized = arr ** 2
numpy_time = time.time() - start

print(f"数组大小: {size:,}")
print(f"Python循环耗时: {python_time:.4f}秒")
print(f"NumPy向量化耗时: {numpy_time:.4f}秒")
print(f"加速比: {python_time/numpy_time:.0f}倍")

# 验证结果相同
print(f"结果是否相同: {np.allclose(result_loop, result_vectorized)}")

7.2 内存布局优化

NumPy数组的内存布局(C-order vs F-order)会影响计算性能。

内存布局对比

布局全称存储顺序最优操作
C-order行优先(C语言风格)行连续存储按行操作(axis=1)
F-order列优先(Fortran风格)列连续存储按列操作(axis=0)

类比理解:C-order像横着写字(从左到右,从上到下),F-order像竖着写字(从上到下,从左到右)。

# C-order(行优先):按行连续存储,是NumPy默认布局
arr_c = np.ones((1000, 1000), order='C')
print(f"C-order是否行连续: {arr_c.flags['C_CONTIGUOUS']}")

# F-order(列优先):按列连续存储
arr_f = np.ones((1000, 1000), order='F')
print(f"F-order是否列连续: {arr_f.flags['F_CONTIGUOUS']}")

# 按行求和在C-order上更快
start = time.time()
for _ in range(100):
    _ = np.sum(arr_c, axis=1)
c_time = time.time() - start

# 按列求和在F-order上更快
start = time.time()
for _ in range(100):
    _ = np.sum(arr_f, axis=0)
f_time = time.time() - start

print(f"C-order按行求和: {c_time:.4f}秒")
print(f"F-order按列求和: {f_time:.4f}秒")

# 确保数组连续
non_contiguous = arr_c[::2, ::2]  # 步长不为1,不连续
print(f"切片后是否连续: {non_contiguous.flags['C_CONTIGUOUS']}")

contiguous = np.ascontiguousarray(non_contiguous)
print(f"ascontiguousarray后是否连续: {contiguous.flags['C_CONTIGUOUS']}")

7.3 选择合适的数据类型

根据数据范围选择合适的数据类型可以显著节省内存。

数据类型选择决策树

数据类型选择
├── 整数?
│   ├── 范围0-255 → uint8
│   ├── 范围-32768~32767 → int16
│   ├── 范围约正负21亿 → int32
│   └── 更大范围 → int64
├── 浮点数?
│   ├── 深度学习 → float32
│   ├── 科学计算 → float64
│   └── 量化推理 → float16
└── 字符串?
    ├── 固定长度 → bytes_/str_
    └── 可变长度 → StringDType (NumPy 2.0)
# 大数组使用不同dtype的内存对比
size = 10_000_000

# float64:双精度浮点,默认类型
arr_float64 = np.ones(size, dtype=np.float64)
print(f"float64内存占用: {arr_float64.nbytes / 1e6:.1f} MB")

# float32:单精度浮点,精度足够时优先使用
arr_float32 = np.ones(size, dtype=np.float32)
print(f"float32内存占用: {arr_float32.nbytes / 1e6:.1f} MB")

# int8:8位整数,适合小范围整数
arr_int8 = np.ones(size, dtype=np.int8)
print(f"int8内存占用: {arr_int8.nbytes / 1e6:.1f} MB")

# 图像处理示例:图像像素值范围0-255,使用uint8最合适
image = np.random.randint(0, 256, size=(1080, 1920, 3), dtype=np.uint8)
print(f"1080p图像uint8内存占用: {image.nbytes / 1024 / 1024:.2f} MB")

# 如果错误地使用float64存储图像
image_float64 = image.astype(np.float64)
print(f"1080p图像float64内存占用: {image_float64.nbytes / 1024 / 1024:.2f} MB")

7.4 使用视图减少内存复制

在可能的情况下使用视图而非副本,可以节省大量内存。

视图 vs 副本选择指南

场景推荐方式原因
临时查看数据视图节省内存
切片后修改数据副本避免意外修改原数组
reshape查看视图不改变数据内容
数据预处理视图为先,必要时copy平衡性能与安全
# 创建大数组
arr = np.random.rand(1000, 1000)
print(f"原数组内存: {arr.nbytes / 1e6:.2f} MB")

# 创建副本(占用双倍内存)
copy = arr[::2, ::2].copy()
print(f"副本内存: {copy.nbytes / 1e6:.2f} MB")

# 创建视图(共享内存,几乎不占用额外内存)
view = arr[::2, ::2]
print(f"视图内存(共享): {view.nbytes / 1e6:.2f} MB")
print(f"视图实际新分配内存: 接近0 MB(共享原数组数据)")

# 注意:修改视图会影响原数组
view[0, 0] = 999
print(f"修改视图后原数组[0,0]: {arr[0, 0]}")  # 也被修改了

7.5 预分配内存与in-place操作

预分配内存和in-place操作可以避免动态内存分配的开销。

优化策略对比

策略实现方式优势
预分配np.empty() + 填充避免动态扩容
in-placearr *= 2 vs arr = arr * 2节省内存分配
批量处理向量化替代循环减少Python开销
# 不好的做法:动态扩展列表再转数组
start = time.time()
result = []
for i in range(100000):
    result.append(i ** 2)
result = np.array(result)
dynamic_time = time.time() - start

# 好的做法:预分配NumPy数组
start = time.time()
result = np.empty(100000)
for i in range(100000):
    result[i] = i ** 2
prealloc_time = time.time() - start

# 最好的做法:完全向量化
start = time.time()
result = np.arange(100000) ** 2
vectorized_time = time.time() - start

print(f"动态扩展: {dynamic_time:.4f}秒")
print(f"预分配: {prealloc_time:.4f}秒")
print(f"完全向量化: {vectorized_time:.4f}秒")

# in-place操作节省内存
arr = np.random.rand(1000000)

# 非in-place:创建新数组
result_new = arr * 2  # 分配新内存

# in-place:原地修改
arr *= 2  # 不分配新内存
print(f"in-place操作不分配新内存,更高效")

7.6 使用NumPy的高级索引技巧

# 使用where进行条件赋值
arr = np.array([1, 2, 3, 4, 5])

# 将大于3的元素设为0
result = np.where(arr > 3, 0, arr)
print(f"where条件赋值: {result}")

# 使用clip限制值范围
arr = np.array([-1, 0, 5, 10, 15])
clipped = np.clip(arr, 0, 10)
print(f"clip到[0,10]: {clipped}")

# 使用select进行多条件选择
conditions = [arr < 0, (arr >= 0) & (arr < 10), arr >= 10]
choices = ['negative', 'small', 'large']
result = np.select(conditions, choices, default='unknown')
print(f"多条件选择: {result}")

# 使用unique去重和计数
arr = np.array([1, 2, 2, 3, 3, 3, 4])
unique_vals, counts = np.unique(arr, return_counts=True)
print(f"唯一值: {unique_vals}")
print(f"出现次数: {counts}")

# NumPy 2.0新增:unique_all等函数
unique_all_result = np.unique_all(arr)
print(f"unique_all结果: {unique_all_result}")

8. 避坑小贴士

8.1 修改切片时意外修改原数组

这是NumPy新手最常犯的错误之一,切片返回的是视图而非副本。

问题本质

原数组: [1, 2, 3, 4, 5]
           ↑ 切片 [2, 3, 4](视图)
         
修改视图[0] = 100
           ↓
原数组: [1, 100, 3, 4, 5]  (原数组也被修改!)
# 错误示例
arr = np.array([1, 2, 3, 4, 5])
slice_arr = arr[1:4]  # 这是视图,不是副本
slice_arr[0] = 100    # 修改视图
print(f"原数组被意外修改: {arr}")  # 输出[1, 100, 3, 4, 5]

# 正确做法:显式创建副本
arr = np.array([1, 2, 3, 4, 5])
slice_arr = arr[1:4].copy()  # 创建副本
slice_arr[0] = 100           # 修改副本
print(f"原数组保持不变: {arr}")  # 输出[1, 2, 3, 4, 5]

8.2 整数除法与浮点除法混淆

Python 3中除法行为与Python 2不同,NumPy也遵循这一规则。

除法行为对比

运算符Python 2Python 3 / NumPy结果示例
/整数除法真除法(浮点)5/2 = 2.5
//地板除地板除5//2 = 2
# 普通除法返回浮点数
result = np.array([1, 2, 3]) / 2
print(f"普通除法: {result}")  # [0.5, 1. , 1.5]

# 整数除法使用//运算符
result = np.array([1, 2, 3]) // 2
print(f"整数除法: {result}")  # [0, 1, 1]

# 注意负数除法的区别
print(f"-3 // 2 = {-3 // 2}")  # 向下取整,结果为-2
print(f"int(-3/2) = {int(-3/2)}")  # 向零取整,结果为-1

8.3 广播维度不匹配

广播失败是常见的错误,需要理解广播规则来避免。

常见错误模式

数组A: (2, 3)    数组B: (2,)
                    ↓ 前面补1
B变为:  (1, 2)
                    ↓ 检查兼容性
维度0: 2 vs 1 → 兼容
维度1: 3 vs 2 → 不兼容!(都不是1)
                    ↓
            ValueError: 广播失败
# 广播失败的例子
a = np.array([[1, 2, 3], [4, 5, 6]])  # 形状(2, 3)
b = np.array([1, 2])                   # 形状(2,)

# a + b 会报错,因为维度不匹配
# 错误信息:ValueError: operands could not be broadcast together

# 解决方案:调整形状使维度兼容
b_correct = b.reshape(-1, 1)  # 形状变为(2, 1)
result = a + b_correct
print(f"调整形状后广播成功:\n{result}")

# 或者调整b的方向
b_row = np.array([1, 2, 3])  # 形状(3,)
result = a + b_row  # 可以广播,b_row扩展为(2, 3)
print(f"匹配最后一维:\n{result}")

8.4 频繁的小数组操作

频繁创建小数组会导致性能问题,应该尽量批量处理。

性能对比

方式时间复杂度适用场景
频繁创建小数组O(n) + 内存分配开销避免使用
预分配大数组O(n)中等规模数据
完全向量化O(1)(Python层面)首选方案
import time

# 低效做法:频繁创建小数组
start = time.time()
for i in range(10000):
    arr = np.array([i, i+1, i+2])
    _ = arr.sum()
frequent_time = time.time() - start

# 高效做法:预分配大数组
start = time.time()
arr = np.empty((10000, 3))
for i in range(10000):
    arr[i] = [i, i+1, i+2]
_ = arr.sum(axis=1)
batch_time = time.time() - start

# 最高效做法:完全向量化
start = time.time()
indices = np.arange(10000)
arr = np.column_stack([indices, indices+1, indices+2])
_ = arr.sum(axis=1)
vectorized_time = time.time() - start

print(f"频繁创建小数组: {frequent_time:.4f}秒")
print(f"预分配大数组: {batch_time:.4f}秒")
print(f"完全向量化: {vectorized_time:.4f}秒")

8.5 不必要的类型转换

混合类型运算会导致隐式类型转换,可能影响性能或结果精度。

类型提升规则(NEP 50)

NumPy 2.0采用NEP 50改进了类型提升行为,使结果更可预测。

# 创建int32数组
arr = np.array([1, 2, 3], dtype=np.int32)
print(f"原数组类型: {arr.dtype}")

# 与浮点数运算会导致类型提升
result = arr + 1.5
print(f"运算后类型: {result.dtype}")  # 提升为float64

# 避免方法:预先转换为合适的类型
arr_float = arr.astype(np.float32)
result = arr_float + 1.5
print(f"预转换后类型: {result.dtype}")  # 保持float32

# 注意精度损失
large_int = np.array([2**31-1], dtype=np.int32)
print(f"大整数: {large_int[0]}")
# 转换为float32可能丢失精度
print(f"转float32: {large_int.astype(np.float32)[0]}")
# 转换为float64保持精度
print(f"转float64: {large_int.astype(np.float64)[0]}")

8.6 混淆reshape和resize

reshape和resize都可以改变数组形状,但行为不同。

reshape vs resize 关键区别

特性reshaperesize
返回值返回新数组无返回值
原数组不改变直接修改
元素数量必须相同可扩展/截断
# reshape不改变原数组,返回新数组
arr = np.arange(6)
reshaped = arr.reshape(2, 3)
print(f"reshape后原数组: {arr}")  # 不变
print(f"reshape返回: \n{reshaped}")

# resize改变原数组本身,不返回新数组
arr_resize = np.arange(6)
arr_resize.resize(2, 3)  # 没有返回值
print(f"resize后原数组:\n{arr_resize}")  # 已改变

# resize可以扩展或截断
arr_small = np.array([1, 2, 3])
arr_small.resize(2, 3)  # 扩展到6个元素
print(f"resize扩展:\n{arr_small}")  # 新位置填充0

9. 实战练习

练习1:数组创建与属性探索

import numpy as np

# 任务1:创建一个3x4的随机整数数组(范围0-100)
arr = np.random.randint(0, 100, size=(3, 4))
print("原始数组:")
print(arr)
print(f"数组形状: {arr.shape}")
print(f"数组维度: {arr.ndim}")
print(f"元素总数: {arr.size}")

# 任务2:将其变形为2x6的数组
reshaped = arr.reshape(2, 6)
print("\n变形为2x6:")
print(reshaped)

# 任务3:提取第二行所有元素
second_row = reshaped[1, :]
print(f"\n第二行: {second_row}")

# 任务4:找出所有大于50的元素
large_values = reshaped[reshaped > 50]
print(f"大于50的元素: {large_values}")

# 任务5:计算每列的平均值
col_means = np.mean(reshaped, axis=0)
print(f"每列平均值: {col_means}")

练习2:广播机制应用

import numpy as np

# 数据:3个学生的5门课程成绩
scores = np.array([
    [85, 90, 78, 92, 88],  # 学生1
    [76, 85, 90, 85, 79],  # 学生2
    [92, 88, 85, 90, 95]   # 学生3
], dtype=float)

# 每门课程的权重
weights = np.array([0.2, 0.2, 0.3, 0.2, 0.1])

# 计算加权平均分(使用广播)
weighted_scores = scores * weights  # 广播:(3,5) * (5,) -> (3,5)
weighted_avg = np.sum(weighted_scores, axis=1)
print("每个学生的加权平均分:")
for i, avg in enumerate(weighted_avg):
    print(f"  学生{i+1}: {avg:.2f}")

# 计算每门课程的全班平均分
class_avg = np.mean(scores, axis=0)
print(f"\n每门课程全班平均分: {class_avg}")

# 计算每个学生与全班平均分的差距(使用广播)
diff_from_avg = scores - class_avg  # 广播:(3,5) - (5,) -> (3,5)
print(f"\n与全班平均分的差距:\n{diff_from_avg}")

练习3:性能优化对比

import numpy as np
import time

# 创建大数组
size = 10_000_000
arr = np.random.rand(size)

# 方法1:Python循环(慢)
def python_square(arr):
    result = []
    for x in arr:
        result.append(x ** 2)
    return np.array(result)

start = time.time()
result1 = python_square(arr)
python_time = time.time() - start

# 方法2:NumPy向量化(快)
start = time.time()
result2 = arr ** 2
numpy_time = time.time() - start

# 方法3:使用np.power函数
start = time.time()
result3 = np.power(arr, 2)
np_power_time = time.time() - start

print(f"数组大小: {size:,}")
print(f"Python循环: {python_time:.4f}秒")
print(f"NumPy向量化: {numpy_time:.4f}秒")
print(f"np.power函数: {np_power_time:.4f}秒")
print(f"向量化加速比: {python_time/numpy_time:.1f}倍")

# 验证结果一致性
print(f"\n结果是否一致: {np.allclose(result1, result2) and np.allclose(result2, result3)}")

练习4:综合应用 - 数据标准化与距离计算

import numpy as np

# 生成模拟数据:100个样本,每个样本有5个特征
data = np.random.randn(100, 5)

# 任务1:数据标准化(Z-score标准化)
mean = np.mean(data, axis=0)
std = np.std(data, axis=0)
data_normalized = (data - mean) / std
print("标准化后数据的均值(应接近0):")
print(np.mean(data_normalized, axis=0))
print("标准化后数据的标准差(应接近1):")
print(np.std(data_normalized, axis=0))

# 任务2:计算样本间的欧氏距离矩阵
# 使用广播技巧高效计算
diff = data[:, np.newaxis, :] - data[np.newaxis, :, :]
distances = np.sqrt(np.sum(diff ** 2, axis=2))
print(f"\n距离矩阵形状: {distances.shape}")
print(f"距离矩阵对角线(应为0): {np.diag(distances)[:5]}")

# 任务3:找出每个样本最近的3个邻居
nearest_indices = np.argsort(distances, axis=1)[:, 1:4]  # 排除自己
print(f"\n前5个样本的3个最近邻居索引:\n{nearest_indices[:5]}")

# 任务4:使用布尔索引筛选异常值(超过3个标准差)
outlier_mask = np.abs(data_normalized) > 3
outliers = data[outlier_mask]
print(f"\n异常值数量: {len(outliers)}")

练习5:NumPy 2.0新特性实践

import numpy as np
from numpy.dtypes import StringDType

# 任务1:体验StringDType
print("=== StringDType 体验 ===")
names = np.array(["Alice", "Bob", "Charlie", "Diana"], dtype=StringDType())
print(f"字符串数组: {names}")
print(f"字符串类型: {names.dtype}")

# 使用NumPy 2.0新增的strings命名空间
upper_names = np.strings.upper(names)
print(f"大写转换: {upper_names}")

# 任务2:体验矩阵转置新函数
print("\n=== matrix_transpose 体验 ===")
matrix = np.random.rand(2, 3, 4)
result = np.matrix_transpose(matrix)
print(f"原矩阵形状: {matrix.shape}")
print(f"转置后形状: {result.shape}")

# 任务3:体验新的unique函数
print("\n=== unique_all 体验 ===")
arr = np.array([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])
unique_result = np.unique_all(arr)
print(f"唯一值: {unique_result.values}")
print(f"计数: {unique_result.counts}")
print(f"逆索引: {unique_result.inverse_indices}")

# 任务4:体验数组API标准函数
print("\n=== 数组API标准函数体验 ===")
arr = np.array([[1, 2], [3, 4]])
print(f"矩阵范数: {np.linalg.matrix_norm(arr)}")
print(f"向量范数: {np.linalg.vector_norm(arr)}")
print(f"svdvals: {np.linalg.svdvals(arr)}")

10. 本章小结

本章我们全面深入地学习了NumPy数值计算库的核心知识,从基础概念到高级技巧,从原理讲解到性能优化。

核心知识点回顾

主题关键要点
数组基础NumPy数组连续存储、同类型元素,性能远超Python列表
数组创建掌握 array, zeros, ones, arange, linspace, random等函数
索引切片基础索引、布尔索引、花式索引三种方式,理解视图与副本区别
形状操作reshape, transpose, concatenate等函数灵活运用
广播机制三条规则:维度对齐、维度兼容、结果形状,简化复杂运算
数学运算元素级运算、矩阵运算、ufunc函数,底层C语言实现
性能优化向量化、内存布局、数据类型选择、视图使用、in-place操作

NumPy 2.0重要更新

  • StringDType:可变长度字符串类型,替代object dtype
  • 数组API标准:主命名空间支持,提高跨库兼容性
  • SIMD优化:排序和字符串操作性能大幅提升
  • 新函数matrix_transpose, svdvals, unique_all

学习建议

  1. 理论与实践结合:完成本章所有练习,动手实验每个代码示例
  2. 性能意识:处理大规模数据时,始终考虑向量化方案
  3. 避免陷阱:牢记切片返回视图、广播规则、数据类型转换等易错点
  4. 持续学习:关注NumPy官方文档,了解最新特性和最佳实践

NumPy是Python数据科学生态的基石,熟练掌握NumPy将为你后续学习Pandas、Scikit-learn、TensorFlow等高级库打下坚实基础。


上一章回顾

在上一章中,我们完成了Python数据科学环境的搭建,包括Anaconda安装、Jupyter配置和VS Code设置。如果你还没有完成环境搭建,建议先回顾第1章内容,确保开发环境准备就绪。


参考资源


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐