Skip to content

第 12 章 综合实践:用 NumPy 解决真实问题

学习目标

  • 能把全书知识组合成完整程序(设计 → 实现 → 验证)
  • 会阅读 AI 生成的 NumPy 代码,按固定顺序审查
  • 会用边界测试发现隐藏 bug
  • 树立「理解并验证后再使用」的态度

12.1 综合实践的思路:拆解问题

遇到真实问题时,先拆解,再编码。任何 NumPy 任务都按三步走:

  1. 数据是什么形状? 一维(序列)、二维(表格)、三维(图像/时间序列)?先想清楚,再想索引、轴、广播。
  2. 要做什么运算? 聚合(统计)、逐元素(ufunc)、形状变换、掩码筛选、矩阵运算?对号入座找工具。
  3. 如何验证? 至少运行一次,用已知结果核对;写边界测试。

本章用两个完整项目演示这个过程,再用一套「AI 代码审查流程」收尾——它也是结业项目的评审标准。

12.2 项目一:气温数据分析

问题:temps.csv 存了 3 个城市 30 天的气温(3 列),其中混入了异常读数(如 ±99)。任务是读入、清洗、统计各城市平均气温。

第一步,看形状。 30 行 × 3 列 → 二维数组,shape = (30, 3)

第二步,选运算。 读取用 loadtxt;清洗用掩码替换为 nan;统计用 nanmean(axis=0)(忽略 nan 的列平均)。

第三步,编码并验证。

python
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(把值限制在区间内)。

python
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) 的三维数组,同样的代码直接可用——逐元素运算与维度无关:

python
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

其他常见图像操作,都是「形状 + 逐元素」的组合:

python
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 写出:

python
def normalize(data):
    return data / data.max()

normalize —— 猜意图:把数据缩放到 [0, 1]。参数是数组,返回同形状数组。

第二步,看结构。 data.max() 不带 axis,是全局最大值;除法用广播(第 7 章)。问题:对多列数据,这是「整个矩阵除以全局最大值」,而不是「每列除以自己列的最大值」。

第三步,验证(重点)。 用边界测试检验:

python
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 用错或漏用

修复:按列求最大值并保持维度:

python
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 归一化遇到「常数列」:

python
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(或保持不变):

python
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 代码,逐条打勾:

  1. 导入规范:import numpy as np,没有多余导入。
  2. 形状意识:每个数组的 shape 是否清楚?axis 是否正确?聚合是否用对 keepdims?
  3. 广播正确:能否广播?是否依赖了隐式广播导致结果错误?
  4. dtype 安全:整数溢出(如 uint8 加法)、浮点转整数的截断是否处理?
  5. 边界测试:空、单行、常数列、极端值、缺失值都跑过了吗?
  6. 验证输出:打印关键中间结果,与已知值核对。

记住本书的态度:理解并验证后再使用。AI 生成的代码可以成为你的起点,但只有你亲手跑过、测过、改过的代码,才算你的代码。

动手实践

  1. 自己造一份 temps.csv(30 行 3 列,混入 2 个 ±99 异常值),完整跑一遍 12.2 的清洗与统计流程。
  2. 对一个 5×5 的随机灰度图做:提亮 40、二值化(阈值 128)、反色,打印三个结果。
  3. 运行 12.4 的 normalize,测它的边界:单列、常数列、含负数,记录 bug。
  4. 用 12.6 清单审查自己写的任一函数(比如第 11 章的 simulate_dice),补上没测过的边界。

常见错误

错误写法现象原因
data.max() 用于多列归一化各列缩放比例错误全局最大值 ≠ 列最大值;要 axis=0 + keepdims=True
uint8 数组直接加 60结果溢出回绕astype(np.float64) 再运算,最后转回
min-max 归一化遇常数列产生 nan + RuntimeWarning除以 0;先处理分母
统计含 nan 的数据用 mean结果是 nannp.nanmean 或先掩码过滤
只测常规输入多列/边界输入出 bug必须做边界测试清单

章末练习

基础

  1. 读取一份 CSV(自行生成),用 nanmean 计算每列平均,验证与手动结果一致。
  2. 对 3×3 灰度图 np.arange(9).reshape(3, 3) 做提亮 20 与二值化(阈值 4),打印结果。
  3. 运行 normalize(12.4 的原始版本)对 np.array([[1, 100], [2, 200]]),写出输出,并解释 bug。

提高

  1. 修复 12.4 的 normalize,写一个 normalize_columns(data) 函数,要求:每列缩放到 [0, 1],常数列输出 0,含 nan 时报错或跳过(二选一,说明你的选择)。
  2. 写一个函数 analyze_weather(path):读 CSV → 清洗异常 → 返回每列平均、最大、标准差。用自己造的数据验证。

挑战

  1. 写一个「蒙特卡洛+广播」的综合程序:模拟 10000 个随机点估算 π(第 11 章),再比较 N=10³、10⁴、10⁵、10⁶ 的误差,输出误差随 N 变化的规律。
  2. 选一个真实数据问题(气温、成绩、价格任一),用 NumPy 完成「读入 → 清洗 → 分析 → 输出报告」的完整程序,按 12.6 清单自评。

章末自测

每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。

  1. 综合实践的第一步是?
    • A. 立刻写代码
    • B. 分析数据形状与要做的运算
    • C. 找 AI 要代码
    • D. 安装库
  2. data.max(axis=0, keepdims=True) 的结果形状是?
    • A. 标量
    • B. (n,)
    • C. (1, n)
    • D. (m, n)
  3. np.clip(x, 0, 255) 的作用是?
    • A. 把 x 四舍五入
    • B. 把 x 限制在 [0, 255] 区间
    • C. 把 x 转成整数
    • D. 反转 x
  4. uint8 数组 [250] 加 30 的问题在于?
    • A. 结果变负数
    • B. 可能溢出回绕
    • C. 变浮点
    • D. 没有问题
  5. AI 生成的 NumPy 代码最常见的一类 bug 是?
    • A. 拼写错误
    • B. axis 用错或漏用
    • C. 导入错误
    • D. 注释太少
  6. np.nanmean(a, axis=0)a.mean(axis=0) 的区别是?
    • A. 前者忽略 nan
    • B. 前者更快
    • C. 没有区别
    • D. 前者返回整数
  7. min-max 归一化遇常数列会?
    • A. 返回全 1
    • B. 产生 nan(除以 0)
    • C. 报语法错误
    • D. 返回原数据
  8. 边界测试不包括下列哪项?
    • A. 空数组
    • B. 常数列
    • C. 超大数据量
    • D. 含缺失值
  9. normalize 的修复(12.4)关键在于?
    • A. 用全局最大值
    • B. 加 axis=0keepdims=True
    • C. 转成列表
    • D. 用 for 循环
  10. 对待 AI 生成的代码,正确的态度是?
    • A. 直接使用并信任
    • B. 理解、验证、修改后再使用
    • C. 一律不用
    • D. 只看注释