| Day 22 | NumPy:ndarray 与向量化运算 |

1 ndarray —— 为什么数组比列表快几十倍

是什么?为什么学?

ndarray(N-dimensional array,N 维数组)是 NumPy 的核心数据类型,可以理解成「装了加速引擎的超级列表」。你平时用 list 存数据、写循环处理,数据量大了就慢;ndarray 把所有元素连续存放在内存里,一次对整个数组做运算,速度能快几十上百倍。Pandas 的底层数据也是 ndarray,学不会它,后面处处受阻。今天开始进入数据分析阶段,NumPy 是 Pandas、Matplotlib 等所有数据分析库的地基。

底层原理

列表 [1, 2, 3, 4] 里每个元素是独立的 Python 对象,分散在内存各处,每次访问都要走一层「对象封装」;ndarray 把元素按固定大小连续排列在一块内存里(这叫连续内存布局),CPU 可以批量搬运、批量计算。同时 ndarray 的所有元素必须是同一种类型(由 dtype 决定),类型统一才能批量运算——这解释了为什么混入小数后整个数组会自动变成浮点型。运算时「对整个数组同时算」叫向量化:循环是「一个个来」,向量化是「一起上」。

生活类比

列表像一个萝卜一个坑的散装仓库:每个萝卜装在独立的盒子里,搬一车货要挨个盒子拆开检查;ndarray 像码得整整齐齐的集装箱货柜:所有货物规格统一、紧挨着码放,叉车一次就能处理整柜。

注意事项总结

1 创建数组 + 看「长相」(形状、类型)

import numpy as np
arr = np.array([1, 2, 3, 4])   # 把列表转成数组
print(arr)
print(type(arr))               # 类型是 numpy.ndarray
print(arr.shape)               # 形状:一维,4 个元素
print(arr.dtype)               # 元素类型:64 位整数

运行结果:

[1 2 3 4]
<class 'numpy.ndarray'>
(4,)
int64

np.array(列表) 一行完成「列表 → 数组」;shape 是元组,一维数组写成 (4,)(逗号不能省);dtype 是元素类型。注意打印数组时元素之间没有逗号,这是 NumPy 的显示风格。

2 类型自动统一 + 数组乘法和列表乘法的区别

mixed = np.array([1, 2.5, 3])      # 混入小数
print(mixed, mixed.dtype)          # 整个数组自动变浮点

arr = np.array([1, 2, 3, 4, 5])
print(arr * 2)                     # 数组:每个元素乘 2
print([n * 2 for n in [1, 2, 3, 4, 5]])   # 列表:循环

运行结果:

[1.  2.5 3. ] float64
[ 2  4  6  8 10]
[2, 4, 6, 8, 10]

数组里混入 2.5 后,整个数组自动统一成 float64(浮点);arr * 2 一次乘完所有元素,而列表 * 2 是「复制整个列表」(不是乘元素),必须写列表推导式——数组的运算写在「整体」层面,列表的运算写在「元素」层面

3 批量生成数组的常用函数

print(np.zeros(3))            # 全 0
print(np.ones((2, 2)))        # 全 1,2 行 2 列
print(np.arange(5))           # 0 到 4
print(np.arange(2, 10, 2))    # 从 2 开始,步长 2,到 10 之前
print(np.linspace(0, 1, 5))   # 0 到 1 之间平均分 5 个数

易错点

  • 错误写法:[1, 2, 3] * 3 期待得到 [3, 6, 9] → 问题:列表 * 3 是把列表重复 3 遍,得到 [1, 2, 3, 1, 2, 3, 1, 2, 3] → 正确写法:先转成数组 np.array([1, 2, 3]) * 3
  • 错误写法:写 import numpy 后直接 array([1, 2]) → 问题:没导入别名,array 未定义 → 正确写法:统一 import numpy as np,用 np.array
  • 错误写法:期待 np.array([1, 2.5, 3]) 中 1 还是整数 → 问题:数组类型必须统一,自动提升为浮点 → 正确写法:想强制类型用 np.array([1, 2.5, 3], dtype=float)

记忆口诀:数组是集装箱,列表是散装仓;类型要统一,运算整批上。

2 shape 与 dtype —— 数组的「身份证」

是什么?为什么学?

shape(形状)描述数组「几行几列、几维」,**dtype(数据类型)**描述「每个元素是什么类型」。拿到任何数组,第一件事就是看这两个属性——它们是数组的「身份证」。99% 的报错(形状不匹配、类型不对)都能靠先看身份证定位。

底层原理

shape 返回一个元组,元组的长度就是维度数(ndim),每一维的数字是该维的元素个数:(4,) 是一维 4 个元素,(2, 3) 是 2 行 3 列,(2, 3, 4) 是「2 个 3×4 的块」。dtype 常见值:int64(整数)、float64(浮点)、bool(布尔)。reshape(行, 列) 不改变数据,只重新排列形状(元素总数必须一致);ndim 直接给出维度数。整个二维数组在内存里仍是「按行排成一串」的,reshape 只是换了个「怎么看」的方式。

生活类比

shape相册的尺寸标注:「2 页 × 3 张」;dtype相纸的材质:「普通纸 / 光面 / 防水」。同样的照片,不同材质存放和处理方式不同——知道尺寸和材质,才敢对相册动手。

注意事项总结

1 看形状、维度、类型,reshape 重排

import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]])   # 2 行 3 列
print("shape:", a.shape)               # 形状
print("行数:", a.shape[0])             # 第一个数字是行数
print("列数:", a.shape[1])             # 第二个数字是列数
print("dtype:", a.dtype)               # 元素类型
print("维度:", a.ndim)                 # 几维数组

b = np.array([1, 2, 3], dtype=float)   # 指定元素类型
print(b, b.dtype)

运行结果:

shape: (2, 3)
行数: 2
列数: 3
dtype: int64
维度: 2
[1. 2. 3.] float64

2 arange + reshape —— 一长串数据排成表格

m = np.arange(1, 10).reshape(3, 3)   # 1~9 排成 3 行 3 列
print(m)

运行结果:

[[1 2 3]
 [4 5 6]
 [7 8 9]]

np.arange(1, 10) 生成 1 到 9 共 9 个数的一维数组.reshape(3, 3) 按「先填满第一行、再第二行」的顺序重排成 3×3——元素总数 9 = 3×3 才能重排,否则报 ValueError

易错点

  • 错误写法:np.arange(10).reshape(3, 3) → 问题:10 个元素排不进 3×3(只能放 9 个),报 ValueError → 正确写法:保证元素总数 = 行数 × 列数。
  • 错误写法:把 shape 当函数调用:arr.shape() → 问题:shape 是属性不是方法,报 TypeError → 正确写法:直接 arr.shape
  • 错误写法:二维数组 arr.shape[1]IndexError → 问题:一维数组只有一个维度,shape[1] 越界 → 正确写法:先 print(arr.ndim, arr.shape) 确认是几维。

记忆口诀:shape 管长相,dtype 管材质;先看身份证,再动手干活。

3 索引与切片 —— 数组的「取数四件套」

是什么?为什么学?

索引arr[2])取一个元素,切片arr[2:6])取一段,二维数组用 [行, 列] 逗号分隔,倒序用 **[::-1]**。取数是数据分析第一高频动作:取一列成绩、取某几行记录、倒序排名……全都靠它。

底层原理

一维索引切片和列表语法一致:[起:止:步长]含起不含止arr[2:6] 取下标 2~5)。二维数组本质是「数组的数组」,m[行, 列] 是 NumPy 特有的写法——逗号左边管行、右边管列,冒号表示「整行/整列」:m[0] 取第 1 行,m[:, 1] 取第 2 列,m[1, 2] 取第 2 行第 3 列。关键区别:NumPy 的切片返回的是「视图」(view)——它不复制数据,只是「换个角度看同一块内存」,所以改视图会改原数组;.copy() 才产生独立副本。

生活类比

切片像给你一把尺子去量货架:尺子指到哪里,看到的就是哪里的货——你换把尺子(视图)不会改变货架,但如果你顺手把货换了(改视图元素),货架上的货也就真变了。想要「拍张照片再改」(独立副本),就要 .copy()

注意事项总结

1 一维取数四件套

arr = np.arange(10)      # [0 1 2 ... 9]
print(arr[2])            # 第 3 个元素(下标从 0 开始)
print(arr[2:6])          # 下标 2 到 5
print(arr[:4])           # 前 4 个(下标 0~3)
print(arr[::-1])         # 倒序

运行结果:

2
[2 3 4 5]
[0 1 2 3]
[9 8 7 6 5 4 3 2 1 0]

arr[2:6] 含 2 不含 6;arr[::-1] 步长为 -1 从尾往头取,一行实现倒序——**列表做倒序要 reversed 或切片,数组直接 [::-1]**。

2 二维数组 [行, 列] 取数

m = np.arange(1, 10).reshape(3, 3)
print("第 2 行第 3 列:", m[1, 2])
print("第 1 行:", m[0])
print("第 2 列:", m[:, 1])      # 冒号 = "整行/整列"

运行结果:

第 2 行第 3 列: 6
第 1 行: [1 2 3]
第 2 列: [2 5 8]

m[1, 2] 取第 2 行第 3 列(都是 0 起);m[0] 只给行号时取整行;m[:, 1] 逗号左边冒号表示「所有行」、右边 1 表示第 2 列——这是和列表最大的区别:二维数组取数要写逗号

3 切片是「视图」——改视图会改原数组

arr = np.arange(10)
view = arr[2:6]       # 切片得到视图
view[0] = 999         # 修改视图
print(arr)            # 原数组被改了!

arr2 = np.arange(10)
copied = arr2[2:6].copy()   # 想要独立副本:.copy()
copied[0] = 999
print(arr2)           # 原数组不受影响

运行结果:

[  0   1 999   3   4   5   6   7   8   9]
[0 1 2 3 4 5 6 7 8 9]

arr[2:6] 得到的是视图,view[0] = 999 实际改的是原数组第 3 个位置——这是 NumPy 新手第一大坑.copy() 产生独立副本,改副本不影响原数组。经验法则:只想「看一眼」用视图,要「改了不牵连原数据」就用 .copy()

易错点

  • 错误写法:二维数组写 m[1][2] → 问题:能跑但丑,且和 NumPy 语义不一致 → 正确写法:写 m[1, 2](逗号分隔行和列)。
  • 错误写法:改切片想不影响原数组:view = arr[2:6]; view[0] = 999 → 问题:切片是视图,原数组被连带修改 → 正确写法:需要独立数据时 arr[2:6].copy()
  • 错误写法:arr[2:6] 取「第 2 到第 6 个」却总差一个 → 问题:切片含起不含止,2:6 是下标 2~5 → 正确写法:记住「含头不含尾」,要 5 个元素写 arr[2:7]

记忆口诀:[行, 列] 中间逗,含起不含止;切片是视图,独立要 copy。

4 布尔索引 —— 按条件「一键筛人」

是什么?为什么学?

布尔索引是用「由 True/False 组成的数组」当下标,只取满足条件的元素:arr[arr > 30] 一行筛出所有大于 30 的数。这是 NumPy 最常用的招数——筛选及格成绩、找出异常值、统计达标人数,全指望它。

底层原理

比较运算(><==)作用在数组上时,对每个元素分别比较,生成一个同形状的布尔数组:np.array([10, 25, 30]) > 20 得到 [False True True]。把这个布尔数组放进 arr[...] 里当下标,NumPy 就只取对应位置为 True 的元素。多个条件用 &(与)、|(或)、~(非) 连接,每个条件要加括号(因为 & 的优先级比比较运算低)。np.where(条件, 是, 否) 则返回一个与条件同形状的数组,按条件填「是」或「否」——适合「把筛选结果变成标记」。

生活类比

布尔索引像地铁安检口:每个乘客过闸机时被自动判断「是否带违禁品」(得到一串 True/False),True 的放行、False 的拦下——不用安检员一个个喊名字,闸机批量判断、批量放行。

注意事项总结

1 比较生成布尔数组 + 布尔索引筛选

arr = np.array([10, 25, 30, 45, 50])
print(arr > 30)            # 比较得到布尔数组
print(arr[arr > 30])       # 只取大于 30 的元素
print(arr[(arr > 20) & (arr < 50)])   # 多个条件用 & 连接

运行结果:

[False False False  True  True]
[45 50]
[25 30 45]

arr > 30 逐元素比较,得到布尔数组;arr[布尔数组] 只留下 True 位置的元素;两个条件 (arr > 20) & (arr < 50)各自加括号再与 & 连接——列表要实现同样的筛选必须写循环,数组一行搞定

2 取反 ~ 与 np.where 标记

arr = np.array([10, 25, 30, 45, 50])
print(arr[~(arr > 30)])          # ~ 取反:不大于 30 的
print(np.where(arr > 30, "高", "低"))   # 大于 30 标"高",否则"低"

运行结果:

[10 25 30]
['低' '低' '低' '高' '高']

~(arr > 30) 把布尔数组取反;np.where(条件, 高, 低) 不筛数据,而是按条件逐元素打标签——数据分析里常用于把数值转成「及格/不及格」「高/低」这类标记列。

易错点

  • 错误写法:arr[arr > 20 and arr < 50] → 问题:Python 的 and 不能用于数组,报 ValueError: The truth value of an array... → 正确写法:用 &arr[(arr > 20) & (arr < 50)]
  • 错误写法:arr[arr > 20 & arr < 50] 忘加括号 → 问题:& 优先级高于比较,被解释成 arr > (20 & arr) < 50,结果全错 → 正确写法:每个条件都加括号。
  • 错误写法:用 or 连接「或」条件 → 问题:数组不支持 or → 正确写法:用 |arr[(arr < 20) | (arr > 45)]

记忆口诀:比较得布尔,布尔当下标;与 & 或 | 非 ~,条件记得加括号。

5 广播与聚合 —— 一次算一批的「加速键」

是什么?为什么学?

广播(broadcasting)让不同形状的数组也能做运算(小数×数组、行向量+列向量),聚合(aggregation)把整批数据「汇总成一个数」(总和、平均、标准差)或「按方向汇总」(每列之和)。批量运算 + 汇总统计,是数据分析的基本功——算总销售额、找最高分、统计平均工资,全靠这两个机制。

底层原理

广播的规则:运算时 NumPy 从最后一个维度开始对齐,维度相同或其中一个是 1 就可以扩展:数组 * 0.8 把标量 0.8 扩展成「每个元素都乘」;(3,1) 的列向量 + (3,) 的行向量,分别扩展成 (3,3) 再相加。规则一句话:从右往左对,尺寸相等或为 1 就能广播,不满足就报 ValueError: operands could not be broadcast together。聚合函数带 axis 参数控制方向:axis=0 沿着行方向「往下压」,得到每列的结果(形状 = 列数);axis=1 沿着列方向「往右压」,得到每行的结果(形状 = 行数)。argmax 返回最大值的下标而非值;布尔数组 sum() 直接数 True 的个数。

生活类比

广播像食堂打饭:师傅(标量)给每个餐盘都打一勺——不用一个个单独打;聚合像汇总成绩单:把全班每科成绩「压」成一列平均分(axis=0 按科压)或一行总分(axis=1 按人压)。「压」的方向不同,得到的汇总表就不同。

注意事项总结

1 广播——列向量 + 行向量扩展成矩阵

col = np.array([[1], [2], [3]])    # 3 行 1 列
row = np.array([10, 20, 30])       # 1 行 3 列(一维)
print(col + row)                   # 广播成 3×3

base = np.full((3, 3), 100)        # 3×3 全是 100
print(base + np.array([1, 2, 3]))  # 每一行都加同一个向量

运行结果:

[[11 21 31]
 [12 22 32]
 [13 23 33]]
[[101 102 103]
 [101 102 103]
 [101 102 103]]

col(3,1)row(3,),从右往左对齐后都扩展成 (3,3) 对应相加;base + np.array([1, 2, 3]) 是「每一行都加同一个向量」——写一次,作用于整张表,这就是广播的威力。

2 聚合 + axis 方向 + argmax

arr = np.array([3, 1, 4, 1, 5, 9, 2, 6])
print("总和:", arr.sum())
print("平均:", arr.mean())
print("标准差:", arr.std())
print("最大值下标:", arr.argmax())

m = np.array([[1, 2, 3], [4, 5, 6]])
print("按列求和(axis=0):", m.sum(axis=0))   # 每列加起来
print("按行求和(axis=1):", m.sum(axis=1))   # 每行加起来

运行结果:

总和: 31
平均: 3.875
标准差: 2.5708704751503917
最大值下标: 5
按列求和(axis=0): [5 7 9]
按行求和(axis=1): [ 6 15]

sum / mean / std(标准差,衡量数据波动)把一维数组汇总成一个数;argmax 返回最大值 9 的下标 5(不是值 9!);m.sum(axis=0) 沿行方向压得每列之和;m.sum(axis=1) 沿列方向压得每行之和。

3 布尔数组的 sum —— 直接数 True 的个数

passed = np.array([True, True, False, True])
print("True 的个数:", passed.sum())

运行结果:

True 的个数: 3

布尔值参与运算时 True 当 1、False 当 0,所以 passed.sum() 直接得到 True 的个数——「统计及格人数」就是「布尔数组求和」,一行完成。

易错点

  • 错误写法:np.array([1, 2, 3]) + np.array([[1, 2], [3, 4]]) → 问题:形状 (3,)(2,2) 无法对齐,报 ValueError → 正确写法:先 reshape 让维度对齐,如 np.array([1, 2, 3]).reshape(3, 1)
  • 错误写法:想要每行之和却写 m.sum(axis=0) → 问题:axis=0 是「沿行方向压」,得到的是每列之和 → 正确写法:记「axis=0 压行得列,axis=1 压列得行」。
  • 错误写法:arr.argmax() 期望返回最大值 9 → 问题:argmax 返回的是下标 5 → 正确写法:想要值用 arr.max(),想要位置用 arr.argmax()

记忆口诀:广播看维度:相同或为 1,从右往左对;聚合按 axis:0 压行得列,1 压列得行。

6 动手实践:成绩分析脚本

做一个「成绩分析」脚本:用 NumPy 存一个班级的 4 科成绩(5 名学生),完成创建、查看形状、按科索引、条件筛选、总分排名、整体统计一整套练习。

# score_analysis.py —— 数组计算练习
import numpy as np

# 1. 创建:5 名学生 × 4 科(语文、数学、英语、Python)
scores = np.array([
    [88, 92, 85, 90],
    [75, 80, 78, 95],
    [90, 88, 92, 96],
    [60, 65, 70, 72],
    [95, 100, 90, 98],
])
names = ["张三", "李四", "王五", "赵六", "孙七"]
subjects = ["语文", "数学", "英语", "Python"]

print("=== 数组形状与类型 ===")
print("shape:", scores.shape, "dtype:", scores.dtype)

print("\n=== Python 科成绩(第 4 列)===")
print(scores[:, 3])

print("\n=== 总分与平均分 ===")
total = scores.sum(axis=1)
print("每人总分:", total)
print("全科平均:", scores.mean())
print("Python 科平均:", scores[:, 3].mean())

print("\n=== 总分排名(从高到低)===")
order = total.argsort()[::-1]          # 下标从大到小
for rank, idx in enumerate(order, 1):
    print(f"第{rank}名: {names[idx]} {total[idx]}分")

print("\n=== 布尔索引:Python 及格的名单 ===")
passed = scores[:, 3] >= 60
print(passed)
print("及格人数:", passed.sum())

运行结果:

=== 数组形状与类型 ===
shape: (5, 4) dtype: int64

=== Python 科成绩(第 4 列)===
[90 95 96 72 98]

=== 总分与平均分 ===
每人总分: [355 328 366 267 383]
全科平均: 84.95
Python 科平均: 90.2

=== 总分排名(从高到低)===
第1名: 孙七 383分
第2名: 王五 366分
第3名: 张三 355分
第4名: 李四 328分
第5名: 赵六 267分

=== 布尔索引:Python 及格的名单 ===
[ True  True  True  True  True]
及格人数: 5

练习里用到的知识点串联:axis=1 按行求和、[:, 3] 取第 4 列、>= 生成布尔数组、布尔数组的 sum() 直接数出 True 的个数。

升级挑战:用 scores.max(axis=1) 找出每人最高分科目;用 scores > 90 生成一张「哪些人哪科超过 90 分」的布尔表。

今日总结

能熟练创建数组(array / zeros / ones / arange / linspace) 能说出 shapedtype 分别表示什么,reshape 前后元素总数必须一致 会用 [行, 列] 索引、切片、布尔索引取数据;知道切片是「视图」,要独立改数据用 .copy() 会用 & / | / ~ 组合布尔条件,会用 np.where 按条件打标记 理解广播规则(尺寸相等或为 1 就能扩展),会用 axis 控制聚合方向 会用 sum / mean / std / argmax 聚合,会用布尔数组 sum() 数 True 的个数 成绩分析脚本运行正常,排名与统计结果正确

报错原因修复
ModuleNotFoundError: No module named 'numpy'没安装 NumPypip install numpy(确认在正确的虚拟环境里)
ValueError: shape mismatch: objects cannot be broadcast两个数组形状不同还做运算检查两边 shape 是否匹配,或统一形状
IndexError: index 4 is out of bounds for axis 0 with size 4下标越界,超出了数组长度下标从 0 开始,最大是 shape - 1
ValueError: operands could not be broadcast together广播规则不满足例如 (3,) 的数组和 (2, 3) 相加要确认维度对齐
TypeError: 'numpy.float64' object is not subscriptable把单个数值当数组用了检查是否忘了取整列/整行,先 print(arr.shape) 确认结构
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇