为什么大多数数据科学 Notebook 在第一天后就死了:如何构建能存活下来的 Notebook
内容提要
文章介绍六个让 Jupyter Notebook 保持可运行的习惯:将配置和随机种子放在首格;函数复制输入、不修改全局变量;用校验函数检查数据契约;在笔记本内编写小型测试夹具和断言;用 doctest 执行文档示例;用 main() 和名称保护使笔记本可导出为脚本。作者以奥运会运动员数据集为例,发现三行重复记录和两条测试哨兵数据,说明这些习惯能避免结果出错。
延伸解读
为什么“重启内核并全部运行”是 Notebook 的生死线
文章开篇指出,Notebook 的死亡瞬间是“重启内核并全部运行”失败。由于输出单元格仍显示旧结果,Notebook 表面正常却已无法运行。这种问题往往一周后才被发现,导致分析结果不可信。因此,定期执行“重启并全部运行”是验证 Notebook 可复现性的最低成本方法,也是所有习惯的最终检验标准。
自然键选择:数据验证中最易出错的一步
文章以奥运会数据集为例,强调重复检查必须基于自然键(如 id、games、event),而非单一 id。若仅用 id 检查,会误判 16 条合法记录为重复,因为同一运动员可参加多个项目。选错键会导致删除真实数据,而正确的自然键能准确识别 3 条重复记录。这提醒读者,数据验证前必须理解数据的业务含义。
哨兵数据:隐藏的测试夹具如何扭曲分析结果
数据集中混入了两条哨兵记录(id 999998 和 999999),名为 John Testman,是测试夹具误入正式数据。它们仅占 0.6%,在 head、describe 和空值统计中均不可见,却使 Athletics 奖牌率从 0.245 降至 0.213,造成 13% 的误差。文章说明,简单的三行校验就能捕获此类问题,避免结论被污染。
函数复制输入:让单元格顺序不再重要
文章建议每个函数第一行使用 .copy() 复制输入,避免修改全局变量或参数。这样,无论单元格以何种顺序运行,函数多次执行结果一致,消除了 Notebook 常见的“第三次运行结果不同”的故障。同时,将配置和随机种子集中在首格,确保任何采样或分割可复现,增强 Notebook 的可靠性。
Q&A
为什么大多数数据科学 Notebook 在第一天后就无法运行了?
因为当“重启内核并全部运行”失败时,Notebook 就死了。常见原因是单元格执行顺序混乱,例如重命名或删除了列后,前面的单元格会抛出 KeyError,但输出单元格仍显示旧结果,导致 Notebook 看起来正常却无法运行。
如何让 Jupyter Notebook 保持可运行?有哪些关键习惯?
文章介绍了六个习惯:1. 将配置和随机种子放在第一个单元格;2. 每个函数复制输入,不修改全局变量;3. 用校验函数检查数据契约;4. 在 Notebook 内编写小型测试夹具和断言;5. 用 doctest 执行文档示例;6. 用 main() 和名称保护使 Notebook 可导出为脚本。
在 Notebook 中如何正确设置随机种子?为什么重要?
将随机种子放在第一个单元格,例如 RANDOM_SEED = 42。因为任何 df.sample()、训练/测试分割或 k-means 初始化都会从全局随机状态抽取,如果不固定种子,Notebook 在不同时间运行会得到不同结果,导致不可信。
如何编写函数以避免 Notebook 中的单元格顺序问题?
每个函数应在第一行使用 .copy() 复制输入,并且绝不修改全局变量或参数。这样重新运行函数多次会得到相同结果,单元格顺序不再影响输出,避免了经典 Notebook 失败。
数据验证在 Notebook 中如何实现?能发现哪些常见问题?
编写 validate_raw 函数,检查缺失列、重复行、哨兵 ID、非法性别/奖牌值以及数值范围。例如在奥运会数据集中,它发现了 3 行重复记录和 2 条测试哨兵数据(ID 999998 和 999999),这些数据会导致奖牌率计算错误。
如何在 Notebook 内编写和运行测试?
创建一个小的测试夹具 DataFrame,然后编写包含断言的测试函数,例如检查 clean() 去重、缺失奖牌转为 'None'、add_features() 不修改输入等。使用 assert_frame_equal 确保函数不突变输入。运行所有断言,快速验证代码正确性。
doctest 在 Notebook 中如何使用?有什么好处?
在函数 docstring 中编写示例,然后使用 doctest.run_docstring_examples(bmi, globals(), name='bmi', verbose=True) 执行。这样文档示例会被实际运行,确保文档和行为一致,避免注释过时。
如何让 Notebook 能作为脚本运行?
在最后一个单元格定义 main() 函数,包含加载数据、验证、清洗、特征工程和生成报告等步骤,并使用 if __name__ == '__main__': 保护。这样通过 jupyter nbconvert --to script 导出的脚本可以正常导入,不会在导入时执行顶层代码。