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)
能说出 shape 和 dtype 分别表示什么,reshape 前后元素总数必须一致
会用 [行, 列] 索引、切片、布尔索引取数据;知道切片是「视图」,要独立改数据用 .copy()
会用 & / | / ~ 组合布尔条件,会用 np.where 按条件打标记
理解广播规则(尺寸相等或为 1 就能扩展),会用 axis 控制聚合方向
会用 sum / mean / std / argmax 聚合,会用布尔数组 sum() 数 True 的个数
成绩分析脚本运行正常,排名与统计结果正确
| 报错 | 原因 | 修复 |
|---|---|---|
ModuleNotFoundError: No module named 'numpy' | 没安装 NumPy | pip 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) 确认结构 |