NumPy 数组计算
NumPy 是 Python 数值计算的基础库。Pandas、Scikit-learn、图像处理、机器学习、深度学习框架底层都会大量使用数组计算。学习 NumPy 的重点不是背 API,而是理解:
NumPy 把大量同类型数据放在连续内存中,用底层高效循环一次性处理,避免 Python 解释器逐个元素执行。
如果你只知道 np.array()、mean(),但不理解 shape、dtype、广播、轴、切片视图、拷贝和向量化,后面学 Pandas、机器学习、AI 数据处理都会经常卡住。
学习目标
学完本页,你应该能回答:
- Python
list为什么不适合大规模数值计算。 ndarray的shape、dtype、ndim分别是什么。- NumPy 向量化为什么比 Python for 循环快。
- 广播机制怎么工作,为什么形状不匹配会报错。
axis=0、axis=1到底沿哪个方向聚合。- 切片为什么可能是视图,什么时候需要
.copy()。 - 金额、缺失值、浮点精度在商业数据里要注意什么。
- 能写一个可运行的商业数组计算 Demo。
- 面试时能说清 NumPy 原理、常见坑和排查方法。
为什么 Python list 不够用
Python list 很灵活,可以混放整数、字符串、对象:
data = [1, "2", {"score": 3}]灵活的代价是:每个元素都是 Python 对象,列表里存的是对象引用。做数值计算时,解释器要不断处理对象、类型和循环。
NumPy 数组要求同一个数组内元素通常是同一种类型,它可以把数据紧凑地放进连续内存。
flowchart TD
A["Python list"] --> B["保存一组对象引用"]
B --> C["每个元素可能是不同 Python 对象"]
C --> D["for 循环逐个进入解释器"]
E["NumPy ndarray"] --> F["连续内存保存同类型数据"]
F --> G["底层 C 循环批量计算"]
G --> H["减少 Python 层解释器开销"]| 对比 | Python list | NumPy ndarray |
|---|---|---|
| 元素类型 | 可以混合 | 通常同类型 |
| 存储方式 | 存对象引用 | 连续内存存原始值 |
| 计算方式 | Python 循环逐个处理 | 底层批量处理 |
| 多维数据 | 嵌套 list,容易不规则 | 原生支持多维数组 |
| 适合场景 | 小数据、业务对象、控制流 | 大量数值、矩阵、向量计算 |
结论:不是 list 没用,而是 list 的目标是通用容器;NumPy 的目标是高效数值计算。
ndarray 是什么
ndarray 是 NumPy 的核心对象,意思是 N-dimensional array,多维数组。
import numpy as np
arr = np.array([
[80, 90, 75],
[88, 92, 70],
])
print(arr)
print("shape:", arr.shape)
print("ndim:", arr.ndim)
print("dtype:", arr.dtype)
print("size:", arr.size)输出类似:
[[80 90 75]
[88 92 70]]
shape: (2, 3)
ndim: 2
dtype: int32
size: 6几个属性必须掌握:
| 属性 | 含义 | 例子 |
|---|---|---|
shape | 每个维度长度 | (2, 3) 表示 2 行 3 列 |
ndim | 维度数量 | 二维数组是 2 |
dtype | 元素类型 | int64、float64、object |
size | 元素总数 | 2 * 3 = 6 |
遇到任何 NumPy 问题,第一步先打印:
print(arr.shape, arr.dtype, arr.ndim)很多 bug 不是算法错,而是形状或类型和你想的不一样。
数组计算完整流程
flowchart TD
A["原始 Python 数据或文件列"] --> B["转换成 ndarray"]
B --> C["检查 shape、dtype、缺失值"]
C --> D["按业务含义对齐维度"]
D --> E["向量化计算或广播"]
E --> F["按 axis 聚合统计"]
F --> G["输出结果或交给 Pandas"]每一步的意义:
| 步骤 | 为什么要做 | 不做会怎样 |
|---|---|---|
| 转数组 | 使用 NumPy 高效计算能力 | 仍然在 Python 循环里慢慢算 |
| 检查 shape | 确认维度是否符合业务 | 广播错误或结果对错方向 |
| 检查 dtype | 确认数值能正常计算 | 字符串拼接、object 慢、统计错误 |
| 对齐维度 | 保证行列含义正确 | 把列权重乘到行上 |
| 向量化 | 减少解释器循环开销 | 大数据下性能差 |
| 聚合统计 | 得到业务指标 | 轴选错导致统计口径错 |
最小 Demo:成绩数组计算
import numpy as np
scores = np.array([80, 90, 75, 100])
print("平均分:", scores.mean())
print("最高分:", scores.max())
print("是否优秀:", scores >= 90)
print("优秀分数:", scores[scores >= 90])输出:
平均分: 86.25
最高分: 100
是否优秀: [False True False True]
优秀分数: [ 90 100]重点看这一句:
scores >= 90它不是写了一个显式 for 循环,而是一次性对数组中所有元素做比较,返回布尔数组。再用布尔数组筛选:
scores[scores >= 90]这就是 NumPy 的核心思维:不要逐个元素想,尽量按整列、整行、整块数组想。
向量化为什么快
Python for 循环:
result = []
for item in data:
result.append(item * 2)NumPy 向量化:
result = arr * 2二者看起来都在乘 2,但执行路径不同。
flowchart TD
A["Python for 循环"] --> B["取一个元素"]
B --> C["解释器判断对象和类型"]
C --> D["执行乘法"]
D --> E["追加结果"]
E --> B
F["NumPy 向量化"] --> G["一次提交整块数组运算"]
G --> H["底层循环处理连续内存"]
H --> I["返回新数组"]Python 慢的关键不是“乘法慢”,而是循环次数多时,每次都要经过解释器、对象访问、类型处理和函数调用。NumPy 把循环放到底层实现里,Python 层只发出一次操作命令。
性能对比 Demo:
import time
import numpy as np
n = 1_000_000
data = list(range(n))
arr = np.arange(n)
start = time.perf_counter()
result1 = [x * 2 for x in data]
print("list cost:", time.perf_counter() - start)
start = time.perf_counter()
result2 = arr * 2
print("numpy cost:", time.perf_counter() - start)注意:小数据量时 NumPy 不一定明显快,因为创建数组也有成本。NumPy 的优势在“大量同类型数据批量计算”。
dtype 为什么重要
dtype 决定数组元素怎么存、怎么计算、占多少内存。
import numpy as np
a = np.array([1, 2, 3])
b = np.array(["1", "2", "3"])
print(a.dtype)
print(b.dtype)如果读入的是字符串:
b = np.array(["1", "2", "3"])
print(b + b)可能不是你想要的数值加法。真实数据里,金额列经常包含 "120元"、" 99.5 "、空字符串,直接转数组会变成字符串或 object。
正确思路:
raw = np.array(["1", "2", "3"])
numbers = raw.astype(np.int64)
print(numbers + numbers)常见 dtype:
| dtype | 含义 | 适合 |
|---|---|---|
int64 | 整数 | 数量、次数、编号内部计算 |
float64 | 浮点数 | 科学计算、均值、比例 |
bool | 布尔 | 条件筛选 |
datetime64 | 日期时间 | 时间数组 |
object | Python 对象 | 混合类型,但性能差 |
商业提醒:
- 财务金额的最终结算不要依赖二进制浮点,应该用数据库 decimal 或 Python
Decimal。 - 手机号、身份证号、资产编码不要随便转数值。
object类型通常意味着数据不干净或混合类型,计算会慢。
shape 和 reshape
shape 表示数组形状。
import numpy as np
arr = np.arange(12)
print(arr.shape)
matrix = arr.reshape(3, 4)
print(matrix)
print(matrix.shape)输出:
(12,)
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
(3, 4)reshape(3, 4) 的前提是元素总数对得上。12 个元素可以改成 (3, 4)、(4, 3)、(2, 6),但不能改成 (5, 3)。
错误示例:
arr.reshape(5, 3)这会报错,因为需要 15 个元素。
业务上更重要的是:不要为了让代码不报错而乱 reshape。先问清楚每个维度含义。
| 形状 | 可能含义 |
|---|---|
(100,) | 100 个订单金额 |
(100, 3) | 100 个订单,每个订单 3 个指标 |
(7, 24) | 7 天,每天 24 小时 |
(1000, 768) | 1000 个文本,每个 768 维 embedding |
axis 到底怎么理解
axis 是 NumPy 初学者最容易混乱的点。
二维数组:
import numpy as np
scores = np.array([
[80, 90, 70],
[88, 92, 76],
])
print(scores.sum(axis=0))
print(scores.sum(axis=1))可以这样理解:
| 写法 | 沿哪个维度压缩 | 结果含义 |
|---|---|---|
axis=0 | 压缩行方向,按列聚合 | 每一列求和 |
axis=1 | 压缩列方向,按行聚合 | 每一行求和 |
流程图:
flowchart TD
A["二维数组 2 行 3 列"] --> B["axis=0"]
A --> C["axis=1"]
B --> D["把多行压成一行,得到每列统计"]
C --> E["把多列压成一列,得到每行统计"]业务例子:
sales = np.array([
[100, 120, 130],
[80, 90, 110],
])
print("每天总销售:", sales.sum(axis=0))
print("每个门店总销售:", sales.sum(axis=1))如果行代表门店,列代表日期:
| axis | 业务含义 |
|---|---|
axis=0 | 按日期统计所有门店 |
axis=1 | 按门店统计所有日期 |
所以不要死记“0 是列、1 是行”,要理解它是在压缩哪个维度。
广播机制
广播是 NumPy 的重要机制:不同形状的数组在满足规则时,可以自动扩展后计算。
标量广播
import numpy as np
prices = np.array([100, 200, 300])
discount = 0.8
print(prices * discount)标量 0.8 会作用到每个元素上。
一维数组广播到二维
sales = np.array([
[100, 120, 130],
[80, 90, 110],
])
weights = np.array([0.2, 0.3, 0.5])
print(sales * weights)sales 形状是 (2, 3),weights 形状是 (3,)。NumPy 会把 weights 当成每一列的权重,广播到每一行。
flowchart TD
A["sales shape=(2, 3)"] --> C["逐列相乘"]
B["weights shape=(3,)"] --> C
C --> D["weights 作用到每一行"]广播规则
从右往左比较两个数组的维度:
- 两个维度相等,可以广播。
- 其中一个维度是 1,可以广播。
- 否则不能广播。
| 形状 A | 形状 B | 是否可广播 |
|---|---|---|
(2, 3) | (3,) | 可以 |
(2, 3) | (1, 3) | 可以 |
(2, 3) | (2, 1) | 可以 |
(2, 3) | (2,) | 不可以 |
(3,) | () 标量 | 可以 |
错误例子:
a = np.array([1, 2, 3])
b = np.array([10, 20])
print(a + b)(3,) 和 (2,) 既不相等,也没有一个是 1,所以不能广播。
布尔索引和条件筛选
布尔索引是 NumPy 做筛选的核心。
import numpy as np
amounts = np.array([99.5, 120.0, -80.0, 300.0])
paid_mask = amounts > 0
print(paid_mask)
print(amounts[paid_mask])多个条件要用 &、|,并且每个条件加括号:
amounts = np.array([99.5, 120.0, -80.0, 300.0])
counts = np.array([1, 2, 1, 5])
mask = (amounts > 0) & (counts >= 2)
print(amounts[mask])常见错误:
mask = amounts > 0 and counts >= 2这会报错,因为 and 是 Python 标量布尔逻辑,不适合数组逐元素判断。数组条件要用 & 和 |。
切片、视图和拷贝
NumPy 的切片经常返回视图,而不是新数组。视图和原数组共享底层数据。
import numpy as np
arr = np.array([1, 2, 3, 4])
part = arr[1:3]
part[0] = 99
print(arr)输出:
[ 1 99 3 4]为什么?因为 part 是 arr 的视图,修改 part 会影响原数组。
flowchart TD
A["原数组 arr"] --> B["底层数据"]
C["切片 part"] --> B
D["修改 part"] --> B
B --> E["arr 也看到变化"]如果你需要独立数据,用 .copy():
part = arr[1:3].copy()商业数据处理里,视图和拷贝的区别很重要。你以为只是在修改临时结果,实际可能改了原始数据。
缺失值和 NaN
NumPy 用 np.nan 表示浮点缺失值。
import numpy as np
amounts = np.array([100.0, np.nan, 200.0])
print(amounts.mean())
print(np.nanmean(amounts))普通 mean() 遇到 NaN 会得到 NaN;np.nanmean() 会忽略 NaN。
常用函数:
| 函数 | 作用 |
|---|---|
np.isnan(arr) | 判断是否 NaN |
np.nanmean(arr) | 忽略 NaN 求平均 |
np.nansum(arr) | 忽略 NaN 求和 |
np.nan_to_num(arr) | 把 NaN 转成指定值 |
注意:忽略 NaN 不是永远正确。金额缺失、日期缺失、状态缺失都可能代表数据质量问题。是否忽略要看业务。
浮点精度问题
print(0.1 + 0.2)输出可能是:
0.30000000000000004这是二进制浮点表示限制,不是 NumPy 特有问题。
NumPy 适合统计、分析、科学计算,但如果是支付、结算、发票金额这类强精度业务,最终落库和结算应使用数据库 decimal 或 Python Decimal。
from decimal import Decimal
amount = Decimal("0.1") + Decimal("0.2")
print(amount)商业 Demo:资产评分批量计算
假设医疗数据资产平台要给一批数据资产打分,评分由三部分组成:
- 数据完整率
completeness,权重 40%。 - 更新及时性
freshness,权重 30%。 - 访问热度
usage,权重 30%。
每一行代表一个数据资产。
import numpy as np
asset_names = np.array(["门诊明细", "检验报告", "药品库存", "设备台账"])
# columns: completeness, freshness, usage
metrics = np.array([
[0.95, 0.90, 0.80],
[0.88, 0.70, 0.92],
[0.76, 0.85, 0.60],
[0.99, 0.60, 0.40],
])
weights = np.array([0.4, 0.3, 0.3])
scores = (metrics * weights).sum(axis=1)
qualified_mask = scores >= 0.8
for name, score, qualified in zip(asset_names, scores, qualified_mask):
status = "合格" if qualified else "待治理"
print(f"{name}: {score:.3f}, {status}")这段 Demo 涉及:
| 知识点 | 在 Demo 中的作用 |
|---|---|
| 二维数组 | 保存多个资产的多个指标 |
| 广播 | metrics * weights 把列权重应用到每一行 |
axis=1 | 每个资产按列求加权总分 |
| 布尔索引 | 判断哪些资产合格 |
| 向量化 | 不写循环逐项计算分数 |
如果不用 NumPy,你可能会写多层循环。数据量大时,代码更慢也更容易写错。
商业 Demo:批量识别异常订单金额
import numpy as np
order_ids = np.array(["1001", "1002", "1003", "1004", "1005"])
amounts = np.array([99.5, 120.0, -80.0, 30000.0, np.nan])
invalid_mask = np.isnan(amounts) | (amounts < 0) | (amounts > 10000)
print("异常订单:")
for order_id, amount in zip(order_ids[invalid_mask], amounts[invalid_mask]):
print(order_id, amount)这个例子说明:NumPy 不只是科学计算,也可以用于批量业务校验。但真实项目如果涉及表格字段、列名、导出错误文件,Pandas 会更方便。
NumPy 和 Pandas 怎么配合
Pandas 的一列 Series 可以转成 NumPy 数组:
amount_array = df["amount"].to_numpy()常见配合方式:
| 场景 | 做法 |
|---|---|
| 读取 CSV/Excel | Pandas |
| 清洗字段、去重、分组 | Pandas |
| 对数值列做批量计算 | NumPy 或 Pandas 向量化 |
| 构造机器学习输入矩阵 | NumPy |
| 输出报表 | Pandas |
不要为了用 NumPy,把有列名、有业务语义的表格硬转成裸数组。裸数组快,但列名和业务含义容易丢失。
常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
不看 shape | 维度错,广播错 | 每次关键计算前打印 shape |
不看 dtype | 字符串、object 导致计算错或慢 | 转换并校验类型 |
axis 选错 | 把行统计成列,口径错 | 明确每个维度业务含义 |
| 切片视图被修改 | 原数组被意外改变 | 需要独立数据时 .copy() |
用 and/or 判断数组 | 报错或逻辑错 | 用 &、` |
| 无脑忽略 NaN | 掩盖数据质量问题 | 先分析缺失原因 |
| 财务金额用 float 结算 | 精度风险 | 结算用 Decimal 或数据库 decimal |
| 为小数据强行向量化 | 代码难读,收益小 | 先正确,再优化 |
生产排查流程
NumPy 计算结果不对时,按这个顺序查:
flowchart TD
A["NumPy 结果不对"] --> B["打印 shape、dtype、ndim"]
B --> C{"shape 是否符合业务维度"}
C -- "否" --> D["检查 reshape、转置、输入数据"]
C -- "是" --> E{"dtype 是否为预期数值类型"}
E -- "否" --> F["清洗字符串、object、缺失值"]
E -- "是" --> G{"广播是否按预期发生"}
G -- "否" --> H["检查维度对齐和权重方向"]
G -- "是" --> I{"axis 是否选对"}
I -- "否" --> J["重新确认行列含义"]
I -- "是" --> K["检查 NaN、异常值、精度和业务口径"]排查时常用:
print(arr.shape)
print(arr.dtype)
print(arr[:5])
print(np.isnan(arr).sum())
print(arr.min(), arr.max())面试标准回答
NumPy 为什么比 list 快?
可以这样答:
Python list 是通用容器,里面存的是 Python 对象引用,做数值计算时通常要通过解释器循环逐个处理元素。NumPy ndarray 通常把同类型数据放在连续内存中,数组运算会交给底层 C 实现批量处理,减少 Python 层循环、对象访问和类型判断开销。所以大量数值计算时 NumPy 通常比 list 快。shape、dtype、axis 怎么理解?
shape 表示数组每个维度的长度,dtype 表示元素类型,axis 表示沿哪个维度进行聚合或操作。二维数组里 axis=0 通常是压缩行方向得到每列统计,axis=1 是压缩列方向得到每行统计。真正使用时不能死记,要结合业务含义判断行和列分别代表什么。
广播机制是什么?
广播是 NumPy 在数组形状不完全相同时自动扩展较小数组的机制。从右往左比较维度,维度相等或其中一个为 1 时可以广播,否则报错。广播能让列权重、标量折扣、批量偏移等计算不用手写循环,但前提是维度语义正确。
视图和拷贝有什么区别?
NumPy 切片很多时候返回视图,视图和原数组共享底层数据,修改视图可能影响原数组;拷贝是独立数据,修改不会影响原数组。数据处理里如果只是临时查看可以用视图,如果要独立修改一份数据,应该显式 .copy()。
关联知识点
| 知识点 | 作用 |
|---|---|
| Python 数据处理总览 | 理解数据从读取到导出的完整流程 |
| Pandas 清洗分析 | 学习带列名和业务语义的表格处理 |
| 端到端实践 | 完整跑通 CSV 清洗和报表输出 |
| Python 数据结构 | 对比 list、dict、set 和数组思维 |
| Python 面试题 | 查看标准回答和追问 |
本章小结
NumPy 的核心不是函数名,而是数组思维。你要能看懂 shape、dtype、axis,理解向量化为什么快、广播怎么对齐、切片什么时候是视图、NaN 和浮点精度有什么风险。掌握这些后,再学 Pandas、机器学习和 AI 数据处理时,底层数组计算就不会是黑盒。
