第 12 章 综合实践:用 NumPy 解决真实问题
学习目标
- 能把全书知识组合成完整程序(设计 → 实现 → 验证)
- 会阅读 AI 生成的 NumPy 代码,按固定顺序审查
- 会用边界测试发现隐藏 bug
- 树立「理解并验证后再使用」的态度
12.1 综合实践的思路:拆解问题
遇到真实问题时,先拆解,再编码。任何 NumPy 任务都按三步走:
- 数据是什么形状? 一维(序列)、二维(表格)、三维(图像/时间序列)?先想清楚,再想索引、轴、广播。
- 要做什么运算? 聚合(统计)、逐元素(ufunc)、形状变换、掩码筛选、矩阵运算?对号入座找工具。
- 如何验证? 至少运行一次,用已知结果核对;写边界测试。
本章用两个完整项目演示这个过程,再用一套「AI 代码审查流程」收尾——它也是结业项目的评审标准。
12.2 项目一:气温数据分析
问题:temps.csv 存了 3 个城市 30 天的气温(3 列),其中混入了异常读数(如 ±99)。任务是读入、清洗、统计各城市平均气温。
第一步,看形状。 30 行 × 3 列 → 二维数组,shape = (30, 3)。
第二步,选运算。 读取用 loadtxt;清洗用掩码替换为 nan;统计用 nanmean(axis=0)(忽略 nan 的列平均)。
第三步,编码并验证。
import numpy as np
# 1. 读入(带表头的文件:city1,city2,city3)
data = np.loadtxt("temps.csv", delimiter=",", skiprows=1)
print("形状:", data.shape)
# 2. 清洗:绝对值超过 50 视为异常
clean = np.where(np.abs(data) > 50, np.nan, data)
print("异常值个数:", np.isnan(clean).sum())
# 3. 统计:每列(每城市)平均气温,忽略 nan
city_mean = np.nanmean(clean, axis=0)
print("各城市平均气温:", np.round(city_mean, 1))输出(数据不同则数值不同,结构一致):
形状: (30, 3)
异常值个数: 2
各城市平均气温: [20.3 19.3 20.4]知识点回顾:读文件(第 9 章)、掩码与 where(第 8 章)、axis 统计(第 6 章)、nanmean 处理缺失值。一个真实任务,把四个章节串了起来。
12.3 项目二:图像亮度调整
问题:一张灰度图(0~255 的二维数组)太暗,要整体提亮 30 个灰度级,并保证不溢出 255。
第一步,看形状。 灰度图是 (高, 宽) 二维数组;彩色图是 (高, 宽, 3)。用小例子理解,再推广到真实尺寸。
第二步,选运算。 提亮是逐元素加法;防溢出用 np.clip(把值限制在区间内)。
import numpy as np
img = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]], dtype=np.uint8)
brighter = np.clip(img.astype(np.float64) + 30, 0, 255).astype(np.uint8)
print(brighter)输出:
[[ 40 50 60]
[ 70 80 90]
[100 110 120]]为什么要 astype(np.float64)?uint8(0~255 无符号整数)加 30 可能溢出回绕(如 250+30 变成 20);先转成浮点运算,clip 到 [0, 255] 后再转回 uint8。
推广到真实图像:真实图片是 (100, 100, 3) 的三维数组,同样的代码直接可用——逐元素运算与维度无关:
import numpy as np
rng = np.random.default_rng(2)
img = rng.integers(0, 256, size=(100, 100, 3)).astype(np.uint8)
bright = np.clip(img.astype(np.float64) + 60, 0, 255).astype(np.uint8)
print("原图范围:", img.min(), "-", img.max())
print("提亮后范围:", bright.min(), "-", bright.max())输出:
原图范围: 0 - 255
提亮后范围: 60 - 255其他常见图像操作,都是「形状 + 逐元素」的组合:
import numpy as np
img = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]], dtype=np.uint8)
print(np.where(img > 50, 255, 0)) # 二值化:大于 50 变白
print(255 - img) # 反色
print(img[:, ::-1]) # 水平翻转输出:
[[ 0 0 0]
[ 0 0 255]
[255 255 255]]
[[245 235 225]
[215 205 195]
[185 175 165]]
[[30 20 10]
[60 50 40]
[90 80 70]]12.4 阅读 AI 生成的 NumPy 代码
AI 生成的代码通常能运行,但可能有隐藏 bug。本书最后一个技能:按固定顺序审查 AI 代码。
第一步,看意图。 函数名、参数、返回值。例如 AI 写出:
def normalize(data):
return data / data.max()normalize —— 猜意图:把数据缩放到 [0, 1]。参数是数组,返回同形状数组。
第二步,看结构。 data.max() 不带 axis,是全局最大值;除法用广播(第 7 章)。问题:对多列数据,这是「整个矩阵除以全局最大值」,而不是「每列除以自己列的最大值」。
第三步,验证(重点)。 用边界测试检验:
import numpy as np
m = np.array([[10, 100],
[20, 200]])
print(normalize(m))输出:
[[0.05 0.5 ]
[0.1 1. ]]bug 出现了:第 0 列最大只有 20,应缩放到 1,结果却是 0.1。因为分母用了全局最大值 200。常规输入(单列数据)一切正常,多列输入暴露问题——这正是 AI 代码最常见的 bug 类型:axis 用错或漏用。
修复:按列求最大值并保持维度:
import numpy as np
def normalize(data):
return data / data.max(axis=0, keepdims=True)
m = np.array([[10, 100],
[20, 200]])
print(normalize(m))输出:
[[0.5 0.5]
[1. 1. ]]现在每列都缩放到 0~1:第 0 列 [10, 20] 变成 [0.5, 1],正确。
12.5 边界测试清单
边界条件是容易出错的特殊输入。审查 AI 代码时,至少测以下五类:
| 类别 | 例子 | 易暴露的问题 |
|---|---|---|
| 空数组 | np.array([]) | 除以 0、sum/len 出错 |
| 常数列 | 某列全部相同 | min-max 归一化除以 0,产生 nan |
| 极端值 | 含 0、负数、超大数 | 对数、开方产生 nan |
| 单行/单列 | (1, n) 或 (n, 1) | axis、keepdims 用错 |
| 含缺失值 | 数据里混入 nan | 统计结果变 nan |
例如,min-max 归一化遇到「常数列」:
import numpy as np
def minmax(data):
return (data - data.min(axis=0)) / (data.max(axis=0) - data.min(axis=0))
c = np.array([[5, 1],
[5, 2]])
print(minmax(c))输出(伴随 RuntimeWarning: invalid value encountered in divide):
[[nan 0.]
[nan 1.]]第 0 列全是 5,max - min = 0,除以 0 产生 nan。改进:先判断分母是否为 0,为 0 时整列赋 0(或保持不变):
import numpy as np
def minmax(data):
denom = data.max(axis=0) - data.min(axis=0)
denom[denom == 0] = 1 # 常数列:分母置 1,分子为 0 → 结果 0
return (data - data.min(axis=0)) / denom
c = np.array([[5, 1], [5, 2]])
print(minmax(c))输出:
[[0. 0.]
[0. 1.]]12.6 审查清单(结业项目也用这份)
审查任何 NumPy 代码,逐条打勾:
- 导入规范:
import numpy as np,没有多余导入。 - 形状意识:每个数组的 shape 是否清楚?axis 是否正确?聚合是否用对
keepdims? - 广播正确:能否广播?是否依赖了隐式广播导致结果错误?
- dtype 安全:整数溢出(如 uint8 加法)、浮点转整数的截断是否处理?
- 边界测试:空、单行、常数列、极端值、缺失值都跑过了吗?
- 验证输出:打印关键中间结果,与已知值核对。
记住本书的态度:理解并验证后再使用。AI 生成的代码可以成为你的起点,但只有你亲手跑过、测过、改过的代码,才算你的代码。
动手实践
- 自己造一份
temps.csv(30 行 3 列,混入 2 个 ±99 异常值),完整跑一遍 12.2 的清洗与统计流程。 - 对一个 5×5 的随机灰度图做:提亮 40、二值化(阈值 128)、反色,打印三个结果。
- 运行 12.4 的
normalize,测它的边界:单列、常数列、含负数,记录 bug。 - 用 12.6 清单审查自己写的任一函数(比如第 11 章的
simulate_dice),补上没测过的边界。
常见错误
| 错误写法 | 现象 | 原因 |
|---|---|---|
data.max() 用于多列归一化 | 各列缩放比例错误 | 全局最大值 ≠ 列最大值;要 axis=0 + keepdims=True |
uint8 数组直接加 60 | 结果溢出回绕 | 先 astype(np.float64) 再运算,最后转回 |
| min-max 归一化遇常数列 | 产生 nan + RuntimeWarning | 除以 0;先处理分母 |
统计含 nan 的数据用 mean | 结果是 nan | 用 np.nanmean 或先掩码过滤 |
| 只测常规输入 | 多列/边界输入出 bug | 必须做边界测试清单 |
章末练习
基础
- 读取一份 CSV(自行生成),用
nanmean计算每列平均,验证与手动结果一致。 - 对 3×3 灰度图
np.arange(9).reshape(3, 3)做提亮 20 与二值化(阈值 4),打印结果。 - 运行
normalize(12.4 的原始版本)对np.array([[1, 100], [2, 200]]),写出输出,并解释 bug。
提高
- 修复 12.4 的
normalize,写一个normalize_columns(data)函数,要求:每列缩放到 [0, 1],常数列输出 0,含 nan 时报错或跳过(二选一,说明你的选择)。 - 写一个函数
analyze_weather(path):读 CSV → 清洗异常 → 返回每列平均、最大、标准差。用自己造的数据验证。
挑战
- 写一个「蒙特卡洛+广播」的综合程序:模拟 10000 个随机点估算 π(第 11 章),再比较 N=10³、10⁴、10⁵、10⁶ 的误差,输出误差随 N 变化的规律。
- 选一个真实数据问题(气温、成绩、价格任一),用 NumPy 完成「读入 → 清洗 → 分析 → 输出报告」的完整程序,按 12.6 清单自评。
章末自测
每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。
- 综合实践的第一步是?
- A. 立刻写代码
- B. 分析数据形状与要做的运算
- C. 找 AI 要代码
- D. 安装库
data.max(axis=0, keepdims=True)的结果形状是?- A. 标量
- B.
(n,) - C.
(1, n) - D.
(m, n)
np.clip(x, 0, 255)的作用是?- A. 把 x 四舍五入
- B. 把 x 限制在 [0, 255] 区间
- C. 把 x 转成整数
- D. 反转 x
uint8数组[250]加 30 的问题在于?- A. 结果变负数
- B. 可能溢出回绕
- C. 变浮点
- D. 没有问题
- AI 生成的 NumPy 代码最常见的一类 bug 是?
- A. 拼写错误
- B. axis 用错或漏用
- C. 导入错误
- D. 注释太少
np.nanmean(a, axis=0)与a.mean(axis=0)的区别是?- A. 前者忽略 nan
- B. 前者更快
- C. 没有区别
- D. 前者返回整数
- min-max 归一化遇常数列会?
- A. 返回全 1
- B. 产生 nan(除以 0)
- C. 报语法错误
- D. 返回原数据
- 边界测试不包括下列哪项?
- A. 空数组
- B. 常数列
- C. 超大数据量
- D. 含缺失值
normalize的修复(12.4)关键在于?- A. 用全局最大值
- B. 加
axis=0与keepdims=True - C. 转成列表
- D. 用
for循环
- 对待 AI 生成的代码,正确的态度是?
- A. 直接使用并信任
- B. 理解、验证、修改后再使用
- C. 一律不用
- D. 只看注释
