卷 V · 合上书之后CH 23深度 23/23

你手里那条列

二十二章走完了一条列的一生:一块连续的字节,被贴上名字,被接到眼睛上,被压成矩形,最后变成一个分数。这一章不讲新东西,做三件事:把五层的交接单收成一张表,把全书那些「不报错但错了」的地方收成一张自查表,然后说说这条列往下通向哪里。

五张交接单十二条自查继续往下走

▷ 先猜一下

最后一题,考的是这本书的主线。下面四段代码都能跑通,都不报错,都会给出一个看起来正常的结果

问:哪一段的错误,代价最大?

A df["rank"] = top["score"].rank().to_numpy()——四个人的名次全部串位 B cust.merge(orders, on="cid")["age"].mean()——平均年龄 40.00 变 29.09 C sel.fit(X, y); cross_val_score(clf, sel.transform(X), y)——纯随机数据报出 98.9% D sq / sq.sum(axis=1)——方阵上做成了列归一化

一、五张交接单

这本书的贯穿组件,每章末尾那个「⌗ 交接单」,收在这里:

这一层它要什么形状形状不对时会怎样谁检查
numpy一块连续内存 + shapestridesdtypedtype 混杂 → 整列退化成 <U21;广播出一个 (n, n)只有完全对不上才报
pandas一堆列 + 一个 indexindex 对不上 → 静默产生 NaN;merge 静默放大行数几乎不检查
seaborn长表:一行一个观测,一列一个变量宽表 → 参数填不出来(不是报错,是没东西可指)列名不存在时报
matplotlibFigure → Axes → Artist状态机 → 画到别人身上;图不 close 会攒着不检查
sklearnX: (n_samples, n_features)y: (n_samples,)一维 X、有 NaN、没 fit 就 predict★ 全书唯一认真检查的

看这一列「谁检查」。五层里只有一层会真的拦你。这不是设计失误——numpy 和 pandas 的定位是「随你怎么摆」,它们不知道你打算拿这些数做什么。而这也正是这本书要写的理由:没有人替你把关的地方,得你自己知道该看什么。

二、十二条自查

全书那些「不报错但错了」的地方。每一条都是一句能在十秒内做完的检查:

#写完这种代码之后问一句
1np.array(...).dtype 是我要的吗?(混进字符串会让整列变 <U211
2转置、切片、reshape 之后还要做重活.flags['C_CONTIGUOUS'] 还是 True 吗?2
3任何一次 .sum(axis=).mean(axis=)结果还要参与逐元素运算吗?要就加 keepdims=True3
4两个形状不同的数组做运算np.broadcast_shapes(a.shape, b.shape) 是什么?4
5把数组传进一个会修改它的函数这是视图还是副本?np.shares_memory(a, b)5
6.to_numpy().values两边的行顺序真的一致吗?6
7过滤或排序之后接下来按名字还是按位置?(要按位置就 reset_index(drop=True)7
8fillnadropna这些格子为什么是空的?我这个填法假设了什么?8
9groupby(...).agg(...)assert 结果.sum() == 原表.sum()(键为 NaN 的行被丢了)9
10merge写了 validate= 吗?前后 len() 变了吗?10
11画完一张柱状图y 轴从 0 开始了吗?点会不会互相盖住?15
12报一个模型分数每一步都在 Pipeline 里吗?报的是分布还是一个数?跑过 dummy 基线吗?18–20

如果只记一条,记第 12 条。它对应全书代价最大的那个错误——那个让纯随机数据给出 98.9% 的位置。

三、这本书没讲的

诚实地划一下边界。以下这些是这条线上真实存在、但本书没有覆盖的:

  • 统计推断本身。本书讲了「怎么算」,没讲「算出来的差异是不是真的」——假设检验、置信区间、多重比较。第 19 章那个「差距有没有超过一个标准差」只是一个粗糙的替身。
  • 时间序列。重采样、滚动窗口、季节性分解、自相关。pandas 有一整套为它设计的 API(它当初就是为金融时序而生的),值得单独学。
  • 深度学习。PyTorch 的张量和 numpy 的数组共享同一套形状直觉,但训练循环、自动微分、GPU 内存管理是另一套东西。
  • 数据工程。调度、增量更新、数据质量监控、血缘追踪。第 22 章那些 print 变成生产系统里的 assert,就是这个领域的入口。

四、继续往下走

想往哪走看什么
把 numpy 这一层吃透NumPy 官方文档的 BroadcastingCopies and views 两节(都很短);numpy.lib.stride_tricks.sliding_window_view 是理解 strides 的最好玩具
pandas 的深水区Wes McKinney《Python for Data Analysis》第 3 版;官方的 Comparison with SQL 一页能省很多摸索
可视化Wilkinson《The Grammar of Graphics》(理论源头);Tufte《The Visual Display of Quantitative Information》(第 15 章那些概念的出处);Claus Wilke《Fundamentals of Data Visualization》(免费在线,最实用)
建模与评估Hastie 等《The Elements of Statistical Learning》(免费 PDF,第 7.10.2 节就是第 18 章那个实验);sklearn 官方的 Common pitfalls and recommended practices 一页,是这本书卷 IV 的浓缩版
列式与大数据Arrow 官方的 Columnar Format 规范(比想象中好读);polars 的 User Guide;duckdb 的文档
因果相关不是因果——这本书里每一个「特征重要性」都停在相关上。想跨过那道线,看《拨动》

三个值得动手的项目,按难度排:

  1. 拿你自己的数据跑一遍第 22 章那七步。手机导出的记账数据、Strava 的运动记录、一份工作上的表——不需要多大,重点是每一步打印形状。
  2. 亲手复现第 18 章那个 98.9%。然后改数字:列数从 5000 降到 50,样本从 50 升到 500,看泄漏怎么消失。这是全书最值得动手的二十行。
  3. 把一份 CSV 转成 Parquet,用 duckdb 直接查它。三行代码,但它会改变你对「数据放在哪」这件事的默认选择。
▸ 在现实里

这本书里那条列,走出 Python 之后还在同样的地方出现——它们的共同点是「一整队同类型的东西挨着放,然后对整队下一次命令」

  • SQL 的一个字段就是一列,而 GROUP BY 就是第 9 章那三步。窗口函数是 transform,普通聚合是 agg
  • GPU 的一个 warp 是 32 条线程跑同一条指令——第 1 章那个「让循环消失」的硬件版本。
  • 列式数据库(ClickHouse、BigQuery)把第 21 章那个决定做到了整个系统的地基上。
  • PyTorch 的张量和 numpy 的数组共享同一套形状直觉,连内存协议都是通的。你在第 2 到 4 章学的 shape/广播,在深度学习里一天要用二十次。
  • Excel 的一列一个公式,和 Kotlin 的 IntArray、响应式流的一条 Flow——同一个想法的不同实现。
⌨ 自己跑一遍

最后一段代码,不是新东西——是把这本书的自查表压成一个能贴进任何项目的小工具:

import pandas as pd

def trace(df, label, expect_rows=None, expect_cols=None):
    """每一次形状变化都过一遍这里。它是这本书唯一的产出物。"""
    n, k = df.shape
    print(f"{label:<20} ({n}, {k})")
    if expect_rows is not None:
        assert n == expect_rows, f"{label}: 行数 {n} != 预期 {expect_rows}"
    if expect_cols is not None:
        assert k == expect_cols, f"{label}: 列数 {k} != 预期 {expect_cols}"
    return df

# 用法:串在每一步之间,行数一变就当场停
raw   = trace(pd.read_csv("orders.csv"), "读进来")
clean = trace(raw.dropna(subset=["金额"]), "丢缺失")
wide  = trace(clean.merge(users, on="uid", validate="many_to_one"),
              "补用户信息", expect_rows=len(clean))     # ← 合并不许改行数

第三行那个 expect_rows=len(clean) 就是第 10 章那一整章:「补充信息」这个意图,意味着行数不该变;把这个意图写成断言,一对多的事故就再也发生不了。

python3 -c "import pandas as pd,numpy;print('pandas',pd.__version__,'| numpy',numpy.__version__)"

先确认版本。这本书写在 pandas 3.0.5 上,而 3.0 是一次大改动——网上大量教程还停在 2.x。

✗ 这个直觉是错的

「这套工具我已经会用了——教程都跑通了,项目也做过,剩下的查文档就行。」

「会用」和「知道它什么时候会安静地骗你」是两件事。这本书里 23 章的事故,没有一个是因为不会用 APImerge 谁都会写,dropna 谁都会写,cross_val_score 谁都会写。出事的地方全在「这一步之后,形状变成了什么」

而这类问题查文档查不到,因为文档写的是「这个函数做什么」,不是「你这次调用之后,一行还代表不代表原来那个东西」。merge 的文档不会告诉你平均年龄会从 40.00 变成 29.09。

判据只有那两句话:我手上这堆东西是什么形状?下一层要求它长成什么样?成本是一行 print(df.shape),而它覆盖了这本书里绝大多数事故。

◇ 揭晓

正确答案是 C数据泄漏。因为另外三个错的是一个数,而它错的是你对整件事的判断

A 索引串位(第 6 章)——四个名次全错,但一眼能看出来:最低分拿了第一名。而且它只影响这一列,改回来就好了。 B 合并放大行数(第 10 章)——这个更贵,因为 29.09 是个合理的年龄,不会引起怀疑。但它仍然是「一个数算错了」,一旦被发现就能修,而且 validate= 能当场拦住。 D 广播方向反了(第 3 章)——同样是一个数算错了,而且长方形矩阵上会报错,只有方阵会放过你。加 keepdims=True 就修好了。 C 数据泄漏(第 18 章)——它错的不是某个数,是「这个模型有没有用」这个结论。98.9% 会让你把一个毫无价值的模型推上线、写进论文、拿去做决策;而且泄漏永远让分数变好看,所以没有任何东西会提醒你。前三个错误会让你的报表出错,第四个会让你相信一件不存在的事。
◆ 合上书之后

如果这本书只留下一句话:

面对任何一段数据代码,先问「我手上这堆东西是什么形状」,再问「下一层要求它长成什么样」。

这两个问题不需要记住任何 API,但它们覆盖了这本书里绝大多数事故——因为那些事故的共同点不是「写错了」,是「形状变了而没人告诉你」

而做这件事的成本,是一行 print(df.shape)

本书的数字是怎么来的

正文里每一个数字都是本机跑出来的,环境如下:

Python      3.12.10        (Apple 芯片的 Mac)
numpy       2.5.2
pandas      3.0.5
matplotlib  3.11.1
seaborn     0.13.2
scikit-learn 1.9.0
polars 1.44.1 · duckdb 1.5.5 · pyarrow 25.0.1

耗时类的数字(毫秒、倍数)和你的机器一定不同,但相对关系是稳的:数组表达式比 Python 循环快、连着读比跳着读快、Parquet 比 CSV 快、内置聚合比 apply 快。而那些非耗时的数字(39.3%、11 列、98.9%、0.7119)在同版本下应该逐位复现——如果你跑出来不一样,那更值得研究,多半是版本行为变了。

另外提醒一句:pandas 3.0 是一次大改动(写时复制成为唯一模式、字符串列默认换成 Arrow)。网上大量教程写于 2.x 时代,遇到不一致时先 print(pd.__version__)

这一章的一句话

这五个库共用的东西不是 Python,是一条列;而学会它们,学的不是 API,是在每一次交接处问一句「现在是什么形状」——因为五层里只有一层会替你把关,其余四层出了问题都是安静的。

⌗ 最后一张交接单
二十三章,五层,一条列。 一套问形状的习惯。 你自己。这也是这本书从头到尾唯一想说的那件事。