ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

NumPy数组创建与索引:从基础概念到高效数据操作实战

NumPy数组创建与索引:从基础概念到高效数据操作实战 1. 从“列表”到“数组”为什么我们需要NumPy如果你刚开始用Python处理数据尤其是数值计算大概率是从Python内置的list列表开始的。列表很灵活可以存放不同类型的数据比如[1, ‘hello‘, 3.14]。但当你尝试用它来做点正经的数学运算比如把两个长度相同的列表对应元素相加你会发现事情没那么简单。你得写个循环或者用列表推导式[ab for a, b in zip(list1, list2)]。这还只是加法要是矩阵乘法、求标准差、傅里叶变换呢代码会立刻变得臃肿且低效。这里就引出了NumPy的核心价值它提供了一个名为ndarrayN-dimensional arrayN维数组的高性能多维数组对象。这个对象和Python列表有本质区别。列表更像一个“容器”里面可以放任何东西而ndarray是一个“同质化”的、连续内存块里面所有元素必须是同一种数据类型比如全是float64或int32。这种设计带来了两个压倒性优势极致的计算速度和极其便捷的向量化操作语法。速度的提升源于底层是用C语言实现的并且针对现代CPU的向量化指令如SSE, AVX做了优化。向量化操作则允许你用近乎数学公式的简洁语法来完成复杂计算比如C A BA, B, C都是数组就完成了整个数组的逐元素加法完全不用写循环。这种语法不仅写起来快读起来也直观更关键的是它把循环的任务交给了底层高度优化的C/Fortran代码执行效率比Python的for循环高出成百上千倍。所以无论你是做科学计算、数据分析、机器学习还是图像处理只要涉及批量数值运算NumPy都是你绕不开的基石。它就像Python数据科学生态里的“钢筋水泥”Pandas、SciPy、Scikit-learn、TensorFlow/PyTorch这些高楼大厦都建立在它的地基之上。今天我们就从最核心的ndarray创建和取值操作开始把这块地基打牢。2. 创建你的第一个NumPy数组不止于np.array要使用NumPy首先得导入它。社区惯例是import numpy as np。创建数组最直接的方法是使用np.array()它可以将一个序列如列表、元组转换为ndarray。import numpy as np # 从列表创建一维数组 list_data [1, 2, 3, 4, 5] arr_1d np.array(list_data) print(arr_1d) # 输出: [1 2 3 4 5] print(type(arr_1d)) # 输出: class numpy.ndarray # 从嵌套列表创建二维数组矩阵 list_2d [[1, 2, 3], [4, 5, 6], [7, 8, 9]] arr_2d np.array(list_2d) print(arr_2d) # 输出: # [[1 2 3] # [4 5 6] # [7 8 9]]但np.array()只是起点。在实际项目中我们经常需要快速创建具有特定形态或内容的数组NumPy提供了一系列非常高效的“工厂函数”。2.1 创建占位数组zeros,ones,empty,full这些函数用于快速创建填充了特定值的数组在初始化权重矩阵、掩码或预分配内存空间时非常有用。# 创建一个3行4列元素全为0的浮点数数组 arr_zeros np.zeros((3, 4)) print(arr_zeros) # 创建一个形状为(2, 3, 4)的三维数组元素全为1并指定数据类型为int arr_ones np.ones((2, 3, 4), dtypenp.int32) print(arr_ones.shape) # 输出: (2, 3, 4) # 创建一个2x2的未初始化数组。内容取决于内存当时的状态速度最快但需立即填充 arr_empty np.empty((2, 2)) print(arr_empty) # 内容随机不可预测 # 创建一个5x5元素全为9.5的数组 arr_full np.full((5, 5), 9.5) print(arr_full)注意np.empty返回的数组并非“空”其内存已分配但未被初始化不执行置零操作。这意味着它的元素值是内存中残留的任意数据。除非你确定接下来会覆盖所有元素否则更安全的做法是使用np.zeros或np.ones。2.2 创建序列数组arange,linspace,logspace这些函数用于生成具有规律性数值的数组类似于Python的range但功能更强大且直接生成数组。# np.arange(start, stop, step) 类似于range但生成数组 # 生成从0到9不包括10的整数 arr_range np.arange(10) print(arr_range) # [0 1 2 3 4 5 6 7 8 9] # 生成从1.5到9.5步长为2的数组 arr_range_float np.arange(1.5, 10, 2) print(arr_range_float) # [1.5 3.5 5.5 7.5 9.5] # np.linspace(start, stop, num) 生成等间隔的num个点包含终点 # 在0到1之间生成5个等间隔的点 arr_lin np.linspace(0, 1, 5) print(arr_lin) # [0. 0.25 0.5 0.75 1. ] # np.logspace(start, stop, num, base10) 生成在对数尺度上等间隔的num个点 # 生成从10^1到10^3即10到1000之间的4个点 arr_log np.logspace(1, 3, 4) print(arr_log) # [ 10. 46.41588834 215.443469 1000. ]linspace在需要固定采样点数量的场景下如绘制函数图像特别方便因为你确切知道会得到多少个点。而arange更关注步长但处理浮点数时可能因为精度问题漏掉终点需要注意。2.3 创建特殊数组eye,identity,diag这些函数用于快速创建单位矩阵、对角矩阵等。# 创建一个3x3的单位矩阵主对角线为1其余为0 arr_eye np.eye(3) print(arr_eye) # [[1. 0. 0.] # [0. 1. 0.] # [0. 0. 1.]] # np.identity是np.eye的一个特例始终创建方阵 arr_identity np.identity(3) # 同上 # 从给定的一维数组创建对角方阵或提取矩阵的对角线 diag_elements [1, 2, 3] arr_diag np.diag(diag_elements) print(arr_diag) # [[1 0 0] # [0 2 0] # [0 0 3]] # 提取上面arr_2d矩阵的对角线 diag_of_arr2d np.diag(arr_2d) print(diag_of_arr2d) # [1 5 9]2.4 理解数组的属性shape,dtype,ndim,size创建数组后了解其元信息至关重要。这几个属性你会频繁用到。arr np.array([[1, 2, 3], [4, 5, 6]]) print(‘形状 (shape):‘, arr.shape) # (2, 3) 表示2行3列 print(‘数据类型 (dtype):‘, arr.dtype) # int64 (取决于系统和Python版本可能是int32) print(‘维度数 (ndim):‘, arr.ndim) # 2 (二维数组) print(‘元素总数 (size):‘, arr.size) # 6 (2*3) print(‘每个元素字节数 (itemsize):‘, arr.itemsize) # 8 (对于int64) print(‘总字节数 (nbytes):‘, arr.nbytes) # 48 (6*8)dtype是NumPy高效和灵活的关键。你可以显式指定它来控制内存占用和精度。例如对于大型数据集使用np.float32而不是默认的np.float64可以节省一半内存但会损失一些精度。在图像处理中像素值常用np.uint80-255的无符号8位整数。# 创建时指定数据类型 arr_float32 np.array([1, 2, 3], dtypenp.float32) arr_int8 np.ones((5,), dtypenp.int8) # 转换现有数组的数据类型 arr_float64 arr_float32.astype(np.float64) # 注意astype会创建新数组3. 数组索引与切片精准获取数据的艺术如果说创建数组是准备原料那么索引和切片就是烹饪中的“切配”工序决定了你如何获取和处理数据。NumPy的索引语法在形式上借鉴了Python列表但能力强大得多尤其是支持在多维数组上直接操作。3.1 一维数组与列表相似但更强大对于一维数组索引和切片与Python列表几乎一致。arr np.arange(10) # [0 1 2 3 4 5 6 7 8 9] # 基础索引获取单个元素 print(arr[5]) # 5 print(arr[-1]) # 9 (负索引从末尾开始) # 基础切片arr[start:stop:step] print(arr[2:7]) # [2 3 4 5 6] (从索引2到7不包括7) print(arr[::2]) # [0 2 4 6 8] (步长为2) print(arr[::-1]) # [9 8 ... 0] (反转数组) # 与列表的关键区别NumPy切片返回的是视图(view)而非副本(copy) arr_slice arr[2:7] arr_slice[0] 100 # 修改切片 print(arr) # [0 1 100 3 4 5 6 7 8 9] 原数组也被修改了核心概念视图(View) vs 副本(Copy)。这是NumPy初学者最容易困惑也最容易出错的地方。NumPy的切片操作默认返回一个视图即与原数组共享底层数据内存。修改视图原数组也会变。这种行为是为了极致的内存效率因为复制大数组开销很大。如果你需要一份独立的副本必须显式调用.copy()方法arr_copy arr[2:7].copy()。3.2 多维数组用逗号分隔各维度索引对于二维数组矩阵索引需要两个坐标[row, column]。NumPy使用逗号分隔不同维度的索引。arr_2d np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) # 获取单个元素第2行索引1第3列索引2 print(arr_2d[1, 2]) # 7 # 切片获取前两行的所有列 print(arr_2d[:2, :]) # [[1 2 3 4] # [5 6 7 8]] # 切片获取所有行的第2和第3列 print(arr_2d[:, 1:3]) # [[ 2 3] # [ 6 7] # [10 11]] # 切片获取一个子矩阵第1、2行第0、2列 print(arr_2d[0:2, [0, 2]]) # [[1 3] # [5 7]]对于更高维数组原理相同。例如一个三维数组arr_3d其形状为(depth, height, width)你可以用arr_3d[z, y, x]来访问特定体素。3.3 花式索引用整数数组或布尔数组进行索引这是NumPy索引的精华所在它允许你以非常灵活、非连续的方式选取数组元素。整数数组索引传入一个整数列表或数组按位置选取元素。arr np.arange(10, 20) # [10 11 12 13 14 15 16 17 18 19] # 选取第1 5 7个元素 indices [1, 5, 7] print(arr[indices]) # [11 15 17] # 在多维数组中可以为每个维度指定一个整数数组 arr_2d np.array([[1, 2], [3, 4], [5, 6]]) # 选取(0,1), (1,0), (2,1)位置的点 rows [0, 1, 2] cols [1, 0, 1] print(arr_2d[rows, cols]) # [2 3 6] # 这等价于 arr_2d[[0,1,2], [1,0,1]]布尔数组索引掩码索引这是数据清洗和条件筛选的利器。你提供一个与数组形状相同的布尔数组或能产生布尔数组的条件表达式True的位置对应的元素将被选中。arr np.array([3, 1, 4, 1, 5, 9, 2, 6]) # 创建一个布尔掩码元素值大于3 mask arr 3 print(mask) # [False False True False True True False True] # 使用掩码索引获取所有大于3的元素 print(arr[mask]) # [4 5 9 6] # 更简洁的写法 print(arr[arr 3]) # 效果同上 # 结合逻辑运算符进行复杂筛选 # 选取大于2且小于7的元素 condition (arr 2) (arr 7) # 注意必须用 , |, ~而不是 and, or, not print(arr[condition]) # [3 4 5 6] # 选取小于3或等于9的元素 condition2 (arr 3) | (arr 9) print(arr[condition2]) # [1 1 9 2]重要提示在NumPy中对数组进行逻辑运算,|,~是逐元素的返回布尔数组。Python关键字and,or,not是用于标量的直接用在数组上会引发ValueError。记住这个区别能避免很多坑。布尔索引同样适用于多维数组它会返回一个一维数组包含所有满足条件的元素。arr_2d np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # 找出所有大于5的元素 print(arr_2d[arr_2d 5]) # [6 7 8 9] # 找出所有偶数 print(arr_2d[arr_2d % 2 0]) # [2 4 6 8]3.4 结合切片与花式索引你可以自由组合基本切片和花式索引实现更复杂的选取逻辑。arr_2d np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) # 选取前两行中第0列和第2列的元素 result arr_2d[:2, [0, 2]] print(result) # [[1 3] # [5 7]] # 选取所有行中第1列元素大于6的那些行 mask arr_2d[:, 1] 6 # 第1列大于6的掩码 print(mask) # [False False True] (第01行False第2行True) print(arr_2d[mask, :]) # 选取掩码为True的行所有列 # [[ 9 10 11 12]]4. 高级索引机制与内存布局探秘当你熟练使用基础索引后可能会遇到一些更复杂的场景或者对索引背后的原理产生好奇。理解这些能让你写出更高效、更不易出错的代码。4.1 索引结果的维度规律索引操作如何影响输出数组的维度这里有个简单的规律传入标量索引会减少一个维度。arr_2d[1, 2]得到标量7维度为0。传入切片:会保留该维度。arr_2d[1, :]得到一维数组[5,6,7,8]形状为(4,)。传入整数数组输出形状与索引数组的形状一致。arr_2d[[0,2], :]中[0,2]是形状为(2,)的数组所以输出是(2,4)的数组。传入布尔数组输出是一维的长度为True的个数。4.2 使用np.ix_函数进行网格索引当你想从多维数组中选取一个矩形区域而行和列的索引都是整数数组时直接写arr[rows, cols]会触发“整数数组索引”的配对规则如前文的arr_2d[rows, cols]返回一维数组。如果你想要的是这些行和列所有组合的笛卡尔积即一个子矩阵就需要np.ix_函数。arr np.arange(12).reshape(3, 4) print(arr) # [[ 0 1 2 3] # [ 4 5 6 7] # [ 8 9 10 11]] rows [0, 2] cols [1, 3] # 错误/非预期的做法配对索引 print(arr[rows, cols]) # [1 11] 选取了(0,1)和(2,3)两个点 # 正确的做法使用np.ix_生成网格索引 print(arr[np.ix_(rows, cols)]) # [[ 1 3] # [ 9 11]] # 这选取了第0行和第2行以及第1列和第3列交叉形成的2x2子矩阵np.ix_(rows, cols)返回一个元组其行为相当于(rows[:, np.newaxis], cols)将行索引扩展为列向量列索引保持为行向量从而触发广播机制得到所有组合。4.3 索引与内存布局C顺序与F顺序NumPy数组在内存中是连续存储的。多维数组的元素排列有两种主要方式C顺序行优先最右侧的索引变化最快。这是NumPy和C语言的默认方式。arr[i, j]在内存中是挨着arr[i, j1]存储的。F顺序列优先最左侧的索引变化最快。这是Fortran和MATLAB的默认方式。arr[i, j]在内存中是挨着arr[i1, j]存储的。创建数组时可以指定顺序arr_c np.array([[1,2,3],[4,5,6]], order‘C‘) # 默认 arr_f np.array([[1,2,3],[4,5,6]], order‘F‘)为什么关心这个因为某些操作如重塑reshape、转置T、切片[:]返回的视图其内存布局会影响后续操作的性能。连续内存的数组无论是C连续还是F连续在进行向量化运算时通常更快。你可以用arr.flags查看属性print(arr_c.flags) # C_CONTIGUOUS : True # F_CONTIGUOUS : False print(arr_f.flags) # C_CONTIGUOUS : False # F_CONTIGUOUS : True4.4 使用np.take和np.put进行扁平化索引有时将多维数组视为扁平化的一维数组来索引会更方便。np.take和np.put就是为此设计的。arr np.arange(12).reshape(3, 4) # [[ 0 1 2 3] # [ 4 5 6 7] # [ 8 9 10 11]] # 将数组展平逻辑上然后按一维索引取值 indices_flat [0, 5, 10] # 对应展平后的位置 print(np.take(arr, indices_flat)) # [ 0 5 10] # 等价于 arr.flat[[0,5,10]] 或 arr.flatten()[[0,5,10]]但flatten返回副本 # np.put 将给定值放入展平后的指定位置 np.put(arr, [0, 5, 10], [100, 200, 300]) print(arr) # [[100 1 2 3] # [ 4 200 6 7] # [ 8 9 300 11]]这在处理某些需要线性索引的算法时很有用但要注意它破坏了多维结构的直观性。5. 实战避坑索引操作中的常见“雷区”与最佳实践掌握了各种索引方法不等于就能用好。在实际编码中我踩过不少坑也总结了一些经验。5.1 视图与副本的混淆意外的数据修改这是最经典的错误。你本想取一份数据出来处理却不小心修改了原数据。original_data np.random.rand(100, 100) # 假设我们想分析前10行 analysis_data original_data[:10, :] # 这是一个视图 analysis_data[0, :] 0 # 修改分析数据 # 糟糕original_data的第一行也被清零了解决方案明确意图如果你只是想查看或读取数据用视图没问题效率高。如果你要修改数据且不希望影响原数组务必使用.copy()。safe_copy original_data[:10, :].copy() safe_copy[0, :] 0 # 原数据安然无恙养成习惯在对切片结果进行赋值操作前先问自己“我是否允许原数据被改变”如果答案是否定的就加上.copy()。5.2 布尔索引的赋值一个强大的特性布尔索引不仅可以用来选取数据还可以用来批量赋值这是数据清洗的利器。arr np.array([1, 2, np.nan, 4, np.nan, 6]) # 将所有的NaN替换为0 arr[np.isnan(arr)] 0 print(arr) # [1. 2. 0. 4. 0. 6.] # 将数组中所有大于阈值的值进行截断 arr np.array([10, 25, 3, 48, 12]) arr[arr 20] 20 print(arr) # [10 20 3 20 12]注意赋值语句左侧的布尔索引必须能明确指向一个可修改的内存位置。你不能用布尔索引去创建一个新数组只能修改现有数组中满足条件的元素。5.3 整数数组索引的维度陷阱如前所述arr[[row_indices], [col_indices]]是配对索引返回一维数组。而arr[np.ix_(row_indices, col_indices)]返回子矩阵。混淆两者会导致结果与预期不符。当你需要矩形区域时想想np.ix_。5.4 处理NaN和inf值时的索引NumPy提供了np.isnan(),np.isinf(),np.isfinite()函数来检测非数值。但要注意NaNNot a Number与任何值包括它自己的比较结果都是False。arr np.array([1, 2, np.nan, 4]) print(arr np.nan) # [False False False False] 这没用 print(np.isnan(arr)) # [False False True False] 正确方法 # 筛选出所有有效非NaN的值 valid_values arr[~np.isnan(arr)] print(valid_values) # [1. 2. 4.]5.5 性能考量花式索引 vs 切片花式索引整数数组、布尔数组虽然灵活但通常比简单的切片产生更大的开销因为它需要收集分散在各处的元素可能返回一个副本而非视图。在性能关键的循环中如果可能优先使用切片。对于布尔索引如果条件复杂且数组很大可以考虑使用np.where(condition)来获取索引有时能带来更清晰的代码或微小的性能提升。arr np.random.rand(10000) condition (arr 0.3) (arr 0.7) # 方法1直接布尔索引常用清晰 result1 arr[condition] # 方法2使用np.where获取索引 indices np.where(condition) result2 arr[indices] # 结果与result1相同np.where在只需要索引位置时特别有用例如你想知道满足条件的元素在原数组中的坐标。6. 综合案例用索引操作处理小型数据集让我们通过一个模拟的小型数据集串联起所学的索引知识。假设我们有一个5名学生3门课程的成绩表以及一个对应的姓名列表。import numpy as np # 模拟数据5名学生3门课程数学、物理、化学的成绩 scores np.array([ [85, 90, 78], [92, 88, 95], [78, 85, 80], [60, 72, 65], [95, 91, 98] ]) student_names np.array([‘Alice‘, ‘Bob‘, ‘Charlie‘, ‘David‘, ‘Eva‘]) course_names np.array([‘Math‘, ‘Physics‘, ‘Chemistry‘]) print(“原始成绩表“) print(scores) print(“\n学生姓名“, student_names) print(“课程名称“, course_names)任务1找出数学成绩第一列高于90分的学生。# 方法布尔索引 math_scores scores[:, 0] # 所有行第0列 high_math_mask math_scores 90 high_math_students student_names[high_math_mask] high_math_scores math_scores[high_math_mask] print(f“\n数学高于90分的学生{high_math_students}“) print(f“对应成绩{high_math_scores}“) # 输出数学高于90分的学生[‘Bob‘ ‘Eva‘] # 对应成绩[92 95]任务2找出所有科目均高于85分的学生全科优秀。# 方法布尔索引结合all函数沿轴1即列方向判断是否全为True all_above_85_mask np.all(scores 85, axis1) excellent_students student_names[all_above_85_mask] print(f“\n所有科目均高于85分的学生{excellent_students}“) # 输出所有科目均高于85分的学生[‘Bob‘ ‘Eva‘]任务3计算每个学生的平均分并找出平均分高于班级平均分的学生。# 计算每个学生的平均分沿轴1求平均 student_avg np.mean(scores, axis1) class_avg np.mean(scores) # 不指定axis计算所有元素的平均值 above_avg_mask student_avg class_avg above_avg_students student_names[above_avg_mask] above_avg_scores student_avg[above_avg_mask] print(f“\n班级平均分{class_avg:.2f}“) print(f“高于班级平均分的学生{above_avg_students}“) print(f“他们的平均分{above_avg_scores}“)任务4David同学索引3觉得化学考砸了申请复查后成绩更正为80分。# 使用精确索引进行赋值 scores[3, 2] 80 # 第4行David第3列Chemistry print(“\n更正David的化学成绩后“) print(scores)任务5老师需要一份报告只包含Bob, Charlie, Eva三位同学索引1,2,4的数学和化学成绩第0,2列。# 使用整数数组索引选取特定行和列 selected_rows [1, 2, 4] selected_cols [0, 2] report scores[np.ix_(selected_rows, selected_cols)] # 使用ix_获取子矩阵 report_students student_names[selected_rows] report_courses course_names[selected_cols] print(f“\n特定学生报告 - 学生{report_students}“) print(f“课程{report_courses}“) print(report) # 输出 # 特定学生报告 - 学生[‘Bob‘ ‘Charlie‘ ‘Eva‘] # 课程[‘Math‘ ‘Chemistry‘] # [[92 95] # [78 80] # [95 98]]通过这个案例你可以看到NumPy的索引操作不仅仅是获取数据更是进行数据筛选、清洗、分析和转换的核心工具。结合np.mean,np.sum,np.all,np.any等聚合函数你就能以非常简洁、高效的方式完成复杂的数据处理逻辑。记住在NumPy的世界里尽量用向量化操作和索引来替代显式的Python循环这是写出高性能数值计算代码的关键。
返回列表