NumPy(Numerical Python)是Python科学计算的核心库。它提供了一个高性能的多维数组对象 ndarray 以及用于处理这些数组的工具。无论是数据分析、机器学习还是图像处理,NumPy都是不可或缺的底层支撑。
一、为什么选择NumPy?(高效性分析)
1.1 Python原生循环的瓶颈
在Python中,使用原生 for 循环处理大量数据时,由于动态类型检查和解释器开销,速度非常慢。
【代码实践】对比测试:
import numpy as np
import time
# 生成100万个数据
data_list = list(range(1000000))
data_array = np.arange(1000000)
# 测试Python原生循环
start_time = time.time()
result_list = [x * 2for x in data_list]
end_time = time.time()
print(f"Python列表推导式耗时: {end_time - start_time:.4f}秒")
# 测试NumPy向量化运算
start_time = time.time()
result_array = data_array * 2
end_time = time.time()
print(f"NumPy向量化运算耗时: {end_time - start_time:.4f}秒")
# 验证结果一致性
print(f"结果一致: {np.allclose(result_list, result_array)}")
运行结果示例:
Python列表推导式耗时: 0.0800 秒
NumPy向量化运算耗时: 0.0020 秒
结论:NumPy的速度通常是原生Python的几十倍甚至上百倍。
1.2 NumPy高效的三大秘诀
- 底层C语言实现
- 内存连续性:数组元素在内存中连续存储,且类型统一(如全是
int32),减少了内存碎片和类型判断开销。 - 向量化(Vectorization):将循环操作转化为底层的SIMD(单指令多数据)指令集并行执行。
二、NumPy数组的创建方法
2.1 从Python列表创建
import numpy as np
# 从列表创建
arr1 = np.array([1, 2, 3, 4])
arr2 = np.array([[1, 2], [3, 4]], dtype=float) # 指定数据类型为浮点型
print(f"一维数组: {arr1}")
print(f"二维数组:\n{arr2}")
print(f"数据类型: {arr2.dtype}")
2.2 初始化特定数组
NumPy提供了多种快速生成特定模式数组的函数。
| | | |
|---|
np.zeros | | np.zeros((3, 4)) | |
np.ones | | np.ones((2, 2), dtype=int) | |
np.full | | np.full((2, 2), 7) | |
np.eye | | np.eye(3) | |
np.arange | | np.arange(0, 10, 2) | [0, 2, 4, 6, 8] |
np.linspace | | np.linspace(0, 1, 5) | [0., 0.25, 0.5, 0.75, 1.] |
np.logspace | | np.logspace(0, 2, 3) | [1, 10, 100] |
【代码实践】:
# 创建一个3x3的单位矩阵
identity = np.eye(3)
print("单位矩阵:\n", identity)
# 创建从0到1均匀分布的5个数
linear = np.linspace(0, 1, 5)
print("线性空间:", linear)
2.3 随机数组生成
np.random 模块是模拟数据和初始化的利器。
# 设置随机种子,保证结果可复现
np.random.seed(42)
# 1. 均匀分布 [0, 1)
rand_arr = np.random.rand(2, 3)
print("均匀分布:\n", rand_arr)
# 2. 标准正态分布 (均值0, 方差1)
randn_arr = np.random.randn(2, 2)
print("正态分布:\n", randn_arr)
# 3. 随机整数 [low, high)
randint_arr = np.random.randint(1, 10, size=(2, 3))
print("随机整数:\n", randint_arr)
# 4. 随机重排 (不改变原数组,返回新数组)
perm_arr = np.random.permutation([1, 2, 3, 4, 5])
print("随机重排:", perm_arr)
# 5. 随机采样 (有放回/无放回)
# 从 [0, 1, 2, 3] 中随机选2个,概率分别为 [0.1, 0.1, 0.4, 0.4]
choice_arr = np.random.choice([0, 1, 2, 3], size=2, p=[0.1, 0.1, 0.4, 0.4])
print("加权采样:", choice_arr)
三、数组的属性与核心操作
3.1 基础属性
arr = np.array([[1, 2, 3], [4, 5, 6]])
print(f"形状 (shape): {arr.shape}") # (2, 3) -> 2行3列
print(f"维度 (ndim): {arr.ndim}") # 2
print(f"元素总数 (size): {arr.size}") # 6
print(f"数据类型 (dtype): {arr.dtype}") # int32 或 int64
3.2 索引与切片(重点)
NumPy的切片返回的是视图(View),修改切片会影响原数组。如果需要副本,请使用 .copy()。
【代码实践】:
data = np.arange(1, 10).reshape(3, 3)
# data = [[1, 2, 3],
# [4, 5, 6],
# [7, 8, 9]]
# 1. 基本索引:取第2行第3列的元素 (索引从0开始)
val = data[1, 2]
print(f"特定元素: {val}") # 6
# 2. 切片:取前两行,所有列
sub_arr = data[:2, :]
print("前两行:\n", sub_arr)
# 3. 步长切片:取所有行,每隔一列取一个 (第0列, 第2列...)
step_arr = data[:, ::2]
print("隔列取样:\n", step_arr)
# 4. 视图特性验证
view = data[0:1, 0:1]
view[0, 0] = 999
print(f"修改视图后原数组:\n{data}") # 原数组第一个元素也会变成999
# 5. 获取副本
copy_arr = data[0:1, 0:1].copy()
copy_arr[0, 0] = 0
print(f"修改副本后原数组不变:\n{data}")
3.3 数组变形(Reshape)
改变数组形状,但元素总数必须一致。
arr = np.arange(1, 7) # [1, 2, 3, 4, 5, 6]
# 变为 2行3列
reshaped = arr.reshape(2, 3)
print("重塑后:\n", reshaped)
# 增加维度 (一维转二维列向量)
col_vec = arr[np.newaxis, :] # 或者 arr.reshape(1, -1)
print("行向量形状:", col_vec.shape) # (1, 6)
# 展平
flat = reshaped.ravel() # 返回视图
flat_copy = reshaped.flatten() # 返回副本
print("展平:", flat)
3.4 拼接与分裂
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
# 1. 垂直拼接 (上下叠放) -> axis=0
v_stack = np.vstack((a, b))
# 或者 np.concatenate((a, b), axis=0)
print("垂直拼接:\n", v_stack)
# 2. 水平拼接 (左右并排) -> axis=1
h_stack = np.hstack((a, b))
# 或者 np.concatenate((a, b), axis=1)
print("水平拼接:\n", h_stack)
# 3. 分裂
# 将 v_stack 沿行方向平均分成2份
split_arr = np.vsplit(v_stack, 2)
print("分裂结果1:\n", split_arr[0])
四、NumPy四大运算体系
4.1 向量化运算(元素级)
直接对数组整体进行数学运算,无需循环。
arr = np.array([1, 2, 3, 4])
print("加法:", arr + 10) # [11, 12, 13, 14]
print("平方:", arr ** 2) # [1, 4, 9, 16]
print("正弦:", np.sin(arr)) # 对每个元素求sin
print("比较:", arr > 2) # [False, False, True, True]
4.2 矩阵化运算(线性代数)
区分 元素乘法 (*) 和 矩阵乘法 (dot 或 @)。
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# 元素对应相乘
elem_mult = A * B
# [[1*5, 2*6], [3*7, 4*8]] = [[5, 12], [21, 32]]
print("元素乘法:\n", elem_mult)
# 矩阵乘法 (行乘列)
mat_mult = np.dot(A, B)
# 或者 A @ B
# 第一行第一列: 1*5 + 2*7 = 19
print("矩阵乘法:\n", mat_mult)
# 转置
print("A的转置:\n", A.T)
4.3 广播机制(Broadcasting)
当两个数组形状不同时,NumPy会自动扩展较小的数组以匹配较大的数组,前提是满足广播规则:
【图解案例】:
# 场景1:标量与数组
arr = np.array([1, 2, 3])
print(arr + 5) # 5被广播为 [5, 5, 5] -> [6, 7, 8]
# 场景2:(3, 3) 与 (1, 3)
matrix = np.ones((3, 3))
row_vec = np.array([0, 1, 2]) # 形状 (3,) 被视为 (1, 3)
result = matrix + row_vec
# row_vec 被复制3次变成 (3, 3) 然后相加
print("广播相加:\n", result)
# 场景3:外积效果 (3, 1) + (1, 3) -> (3, 3)
col_vec = np.array([[0], [10], [20]]) # 形状 (3, 1)
row_vec = np.array([0, 1, 2]) # 形状 (3,) -> (1, 3)
outer_sum = col_vec + row_vec
print("外积式广播:\n", outer_sum)
# 结果:
# [[ 0, 1, 2],
# [10, 11, 12],
# [20, 21, 22]]
4.4 比较运算与布尔掩码
利用布尔数组筛选数据,是数据清洗的核心技巧。
data = np.random.randint(0, 10, size=(3, 4))
print("原始数据:\n", data)
# 1. 生成布尔掩码
mask = data > 5
print("大于5的掩码:\n", mask)
# 2. 统计满足条件的个数
count = np.sum(mask)
print(f"大于5的元素个数: {count}")
# 3. 筛选数据 (返回一维数组)
filtered = data[data > 5]
print(f"大于5的元素集合: {filtered}")
# 4. 原地修改满足条件的值
data[data > 5] = -1
print("将大于5的值替换为-1:\n", data)
五、花哨索引(Fancy Indexing)
与普通切片不同,花哨索引通过传入索引数组来访问数据,返回的永远是副本。
5.1 一维花哨索引
arr = np.array([10, 20, 30, 40, 50])
indices = [0, 2, 4]
# 提取第0, 2, 4个元素
result = arr[indices]
print(result) # [10, 30, 50]
5.2 二维花哨索引
需要同时提供行索引数组和列索引数组,它们会配对使用。
matrix = np.arange(1, 10).reshape(3, 3)
# matrix = [[1, 2, 3],
# [4, 5, 6],
# [7, 8, 9]]
# 想要提取 (0, 1), (1, 2), (2, 0) 这三个位置的元素 -> 对应值 2, 6, 7
rows = [0, 1, 2]
cols = [1, 2, 0]
result = matrix[rows, cols]
print(f"花哨索引结果: {result}") # [2, 6, 7]
# 结合广播:选取特定行和特定列的笛卡尔积(需配合np.ix_)
# 选取第0行和第2行,以及第1列和第2列的交叉点
sub_matrix = matrix[np.ix_([0, 2], [1, 2])]
print("子矩阵:\n", sub_matrix)
# 输出:
# [[2, 3],
# [8, 9]]
六、常用通用函数(Universal Functions)
6.1 排序
arr = np.array([3, 1, 4, 1, 5, 9, 2])
# 1. 返回排序后的副本
sorted_arr = np.sort(arr)
print("排序后:", sorted_arr)
# 2. 原地排序
arr.sort()
print("原地排序后:", arr)
# 3. 获取排序后的索引位置 (argsort)
original = np.array([30, 10, 20])
indices = np.argsort(original)
# 最小值10在原索引1,次小20在索引2...
print("排序索引:", indices) # [1, 2, 0]
print("通过索引还原排序:", original[indices]) # [10, 20, 30]
6.2 统计函数与轴(Axis)的概念
axis=0 代表跨行操作(即按列计算),axis=1 代表跨列操作(即按行计算)。
data = np.array([[1, 2, 3],
[4, 5, 6]])
# 全局统计
print("总和:", np.sum(data)) # 21
print("均值:", np.mean(data)) # 3.5
# 按轴统计
print("每列的和 (axis=0):", np.sum(data, axis=0)) # [5, 7, 9] -> 1+4, 2+5, 3+6
print("每行的和 (axis=1):", np.sum(data, axis=1)) # [6, 15] -> 1+2+3, 4+5+6
# 其他常用统计
print("最大值:", np.max(data))
print("最大值索引:", np.argmax(data)) # 展平后的索引
print("标准差:", np.std(data))
print("中位数:", np.median(data))
七、综合实战案例:模拟学生成绩分析
假设我们有一个班级5名学生的3门课成绩(语文、数学、英语)。
import numpy as np
# 1. 生成模拟数据 (5行3列),分数在60-100之间
np.random.seed(100)
scores = np.random.randint(60, 101, size=(5, 3))
subjects = ['语文', '数学', '英语']
students = ['张三', '李四', '王五', '赵六', '孙七']
print("=== 原始成绩表 ===")
print(f"{'姓名':<6}{'语文':<6}{'数学':<6}{'英语':<6}")
for i, name inenumerate(students):
print(f"{name:<6}{scores[i][0]:<6}{scores[i][1]:<6}{scores[i][2]:<6}")
# 2. 计算每个学生的平均分 (按行计算 axis=1)
avg_scores = np.mean(scores, axis=1)
print("\n=== 学生平均分 ===")
for name, avg inzip(students, avg_scores):
print(f"{name}: {avg:.2f}")
# 3. 计算每门课的最高分 (按列计算 axis=0)
max_scores = np.max(scores, axis=0)
print("\n=== 各科最高分 ===")
for subj, max_s inzip(subjects, max_scores):
print(f"{subj}: {max_s}")
# 4. 找出所有数学成绩大于85分的学生姓名
math_col = scores[:, 1] # 提取数学列
high_math_mask = math_col > 85
high_math_students = np.array(students)[high_math_mask]
print(f"\n数学成绩大于85分的学生: {high_math_students}")
# 5. 给所有低于65分的成绩加分至65分 (及格线保护)
scores[scores < 65] = 65
print("\n=== 补考加分后的第一行数据 ===")
print(f"{students[0]}: {scores[0]}")
回顾
- 效率:NumPy利用C底层和向量化,比Python循环快数十倍。
- 对象:
ndarray 是核心,注意其 shape, dtype, ndim 属性。 - 操作:掌握切片(视图)、变形(reshape)、拼接(concatenate)和广播机制。
- 运算:分清元素运算(
*)与矩阵运算(dot/@),善用布尔掩码筛选数据。 - 统计:理解
axis 参数,熟练使用 sum, mean, sort, argsort。