你手里那条列
二十二章走完了一条列的一生:一块连续的字节,被贴上名字,被接到眼睛上,被压成矩形,最后变成一个分数。这一章不讲新东西,做三件事:把五层的交接单收成一张表,把全书那些「不报错但错了」的地方收成一张自查表,然后说说这条列往下通向哪里。
最后一题,考的是这本书的主线。下面四段代码都能跑通,都不报错,都会给出一个看起来正常的结果。
问:哪一段的错误,代价最大?
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 | 一块连续内存 + shape/strides/dtype | dtype 混杂 → 整列退化成 <U21;广播出一个 (n, n) | 只有完全对不上才报 |
| pandas | 一堆列 + 一个 index | index 对不上 → 静默产生 NaN;merge 静默放大行数 | 几乎不检查 |
| seaborn | 长表:一行一个观测,一列一个变量 | 宽表 → 参数填不出来(不是报错,是没东西可指) | 列名不存在时报 |
| matplotlib | Figure → Axes → Artist | 状态机 → 画到别人身上;图不 close 会攒着 | 不检查 |
| sklearn | X: (n_samples, n_features)、y: (n_samples,) | 一维 X、有 NaN、没 fit 就 predict | ★ 全书唯一认真检查的 |
看这一列「谁检查」。五层里只有一层会真的拦你。这不是设计失误——numpy 和 pandas 的定位是「随你怎么摆」,它们不知道你打算拿这些数做什么。而这也正是这本书要写的理由:没有人替你把关的地方,得你自己知道该看什么。
二、十二条自查
全书那些「不报错但错了」的地方。每一条都是一句能在十秒内做完的检查:
| # | 写完这种代码之后 | 问一句 | 章 |
|---|---|---|---|
| 1 | np.array(...) | .dtype 是我要的吗?(混进字符串会让整列变 <U21) | 1 |
| 2 | 转置、切片、reshape 之后还要做重活 | .flags['C_CONTIGUOUS'] 还是 True 吗? | 2 |
| 3 | 任何一次 .sum(axis=)/.mean(axis=) | 结果还要参与逐元素运算吗?要就加 keepdims=True | 3 |
| 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 |
| 8 | fillna 或 dropna | 这些格子为什么是空的?我这个填法假设了什么? | 8 |
| 9 | groupby(...).agg(...) | assert 结果.sum() == 原表.sum()(键为 NaN 的行被丢了) | 9 |
| 10 | merge | 写了 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 官方文档的 Broadcasting 和 Copies 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 的文档 |
| 因果 | 相关不是因果——这本书里每一个「特征重要性」都停在相关上。想跨过那道线,看《拨动》 |
三个值得动手的项目,按难度排:
- 拿你自己的数据跑一遍第 22 章那七步。手机导出的记账数据、Strava 的运动记录、一份工作上的表——不需要多大,重点是每一步打印形状。
- 亲手复现第 18 章那个 98.9%。然后改数字:列数从 5000 降到 50,样本从 50 升到 500,看泄漏怎么消失。这是全书最值得动手的二十行。
- 把一份 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 章的事故,没有一个是因为不会用 API:merge 谁都会写,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% 会让你把一个毫无价值的模型推上线、写进论文、拿去做决策;而且泄漏永远让分数变好看,所以没有任何东西会提醒你。前三个错误会让你的报表出错,第四个会让你相信一件不存在的事。
- 《抹零》(浮点数)——这本书里所有的
float64,那本书讲它们什么时候会骗你。第 21 章那个「三家答案差 4.3 × 10⁻¹⁴」,出处在那儿。 - 《意外》(信息论)——第 20 章的基尼不纯度、决策树的分裂准则,用的都是熵。而第 21 章那个「
qty只占 3.7% 字节」,讲的是同一件事:可预测的东西不占地方。 - 《拨动》(因果推断)——这本书停在相关性上:特征重要性告诉你「哪一列和标签一起变」,不告诉你「改它会不会改变结果」。那本书从这里开始。
- 《撞墙》(排队与拥塞)——第 21 章那些性能数字背后是同一套直觉:瓶颈在哪、余量还剩多少。
- 《快照》(Git)——第 21 章的 Parquet 和 Git 的对象库有同一个设计:不可变的块 + 一份指向它们的元数据。
如果这本书只留下一句话:
面对任何一段数据代码,先问「我手上这堆东西是什么形状」,再问「下一层要求它长成什么样」。
这两个问题不需要记住任何 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,是在每一次交接处问一句「现在是什么形状」——因为五层里只有一层会替你把关,其余四层出了问题都是安静的。