mcpField
S
smoke-test-ml-pipeline

smoke-test-ml-pipeline

github.com/probabl-ai测试与质量144143 次安装1 天前更新
代码仓库

关于 smoke-test-ml-pipeline

smoke-test-ml-pipeline 是用于完整评估前检查 ML 工作流的技能,使用少量真实数据运行测试。其 pytest 测试核对预测行数,也可将指标与交叉验证均值比较。

主要功能

  1. 01生成基于真实数据的 pytest 测试
  2. 02检查预测行数是否完全一致
  3. 03可选地比较指标与交叉验证结果
  4. 04使用项目的数据加载器
  5. 05运行冒烟测试命令

适用场景

  1. 01发现特征处理导致的预测行丢失
  2. 02用少量真实数据检查工作流
  3. 03检测推理时预测质量下降

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/probabl-ai/skore-skills --skill smoke-test-ml-pipeline

安装后在 Claude Code 中输入/smoke-test-ml-pipeline确认已加载。

详细介绍

ML 流水线冒烟测试

在迭代阶段捕获“加载 → 特征化 → 划分”反模式、避免其进入生产环境的最简 pytest。

先交付。 完成预检清单后,用户首先看到的内容必须是完整的测试文件,放在围栏代码块中 (assert len(predictions) == n_predict_grid_rows,预测环境不含历史缓冲区,使用真实的 data/ 数据源,将 CV 均值硬编码为字面量的软断言,不导入 skore)。 然后运行 python -m skore_skills smoke run --stem <NN>_<short_name>。以 JSON 中的 action 为准。不要停留在计划阶段,也不要只把文件留在思考频道中。绝不要让用户或 CI 之后再运行 pytest。明确写出这条确切的 smoke run 命令。 即使是没有工具的环境,也必须提供一份完整的测试文件(使用假定的日志 / 实验 / 包信息;不要使用 <FILL_…>) 以及该命令。可以说明本轮无法执行 pytest;但不能指示用户去运行。此处不要针对 evaluate 使用 AskUserQuestion——该门槛属于 build-ml-pipeline,且只有在 smoke run 返回 proceed 后才适用。运行通过后,由父流程负责面向用户的收尾说明(叙述 + 链接);本技能不负责讲述流水线。

停止条件——请先阅读

  • 设置仍在等待处理。 先运行 python -m skore_skills status。如果 status.setup.pending 非空,且 status.skills.setup-ml-project 为 true,则加载 setup-ml-project 并停止。不要启动此技能。它返回后再继续。本轮不要再次加载它。如果未安装此技能,用一行列出待处理项并停止。不要臆造 git init、scaffold 或 env init。如果 status.setup.env 或 status.setup.workspace 为 declined,用一行说明并停止。git 或 editable 若已拒绝,就不要再次询问;继续。
  • 没有获批的设计说明和脚本,就不要进行冒烟测试。 配对规则是硬性要求:运行 python -m skore_skills design consent --stem <stem>。只有当该命令返回的 JSON 为 proceed,并且 experiments/NN_<short_name>.py 存在且 stem 匹配时,tests/smoke/test_NN_<short_name>.py 才能存在。若结果为 ask / stop,则不要编写冒烟测试。
  • 缺少 pytest。 如果项目环境中无法导入 pytest,立即停止。针对 pytest,在 default 上加载 add-python-package(确认;env route 会将 pytest 映射到 --feature agent 之外)。不要运行 pip install pytest,也不要将其放到 agent 功能上(使用组合的 dev 环境,即 default + agent)。
  • 禁止凭记忆写符号。 冒烟测试中写入的任何 skrub / scikit-learn 名称,都必须来自本轮运行的 python -m skore_skills api get <dotted>... 或本轮读取的匹配缓存。多个符号放在一次调用中。冒烟测试文件虽小,但会导入预测包的 API 表面;同样适用禁止凭记忆的规则。
  • 不要降低断言要求。 硬性断言要求行数完全相等。不能是“近似相等”,也不能是“至少达到预期行数的 80%”。行数不匹配正是冒烟测试要捕获的失败模式。放宽断言会悄悄让这个 bug 再次出现。
  • 也不要减少预期数量。 断言由一对内容组成:== 和它右侧的数字。将 n_predict_grid_rows 重新定义为流水线实际输出的行数(“predict grid 减去 embargo”“减去 cold-start 窗口”),即使保留相等判断,也仍是同一个缺陷。如果你认为这个差值是有意设计的,这是一个关于 build-ml-pipeline 的 X-marker 问题:说明情况并停止。不要提出数字。
  • 附带条件的提议也不代表内容获准。 有条件的提议仍是提议:“如果这 24 行最终证实是有文档记录的 embargo,那么正确的修改是……”这仍然是在提议被禁止的修改。拒绝时应在拒绝加上退回路径后结束;不要附加会导向同一结果的保留意见。
  • 不要合成测试数据。 冒烟测试读取真实的 data/ 源数据。合成数据看起来没问题,但会跳过生产环境中实际出错的加载器。
  • 不要使用包装器、NaN 处理或 eval_mode 变通方案。 如果冒烟测试只有在包装预测器或根据 eval_mode 添加条件后才能通过,那就是流水线有问题。返回 build(§ 返回 build),修正 X-marker 的位置。包装器只是掩盖失败模式,并不能解决问题。
  • 冒烟测试只能使用预测包的 API。 对于 build-ml-pipeline 生成的 SkrubLearner,这意味着 skrub 的 fit / predict /(可选的)score,以及软断言使用的任何指标对应的 sklearn.metrics。不要在测试文件中导入 skore(或任何其他跟踪 / 报告库)。只要环境可以 import skrub + import sklearn,冒烟测试就必须能够运行;skore Project 是附带产物,不是测试依赖项。软断言的基线(CV-mean MAE 等)必须硬编码自设计说明中的 Status.headline,并添加指向设计说明的注释;实验的 headline 数值变更时,手动更新。
  • 不要过滤警告。 不要使用 @pytest.mark.filterwarnings(...),不要在测试主体中使用 warnings.filterwarnings(...),也不要在 pytest.ini / pyproject.toml 中使用 filterwarnings = [...],除非用户明确要求。参见 python -m skore_skills style § 停止条件。

执行前

确认设计与脚本配对无误,且 pytest 可用后,在写入测试文件前输出 1–3 个自然句子。 说明这是一次本地真实数据冒烟计算:它将在一小部分真实数据上拟合声明的学习器,在不使用预先历史缓冲区的情况下预测一个不重叠的数据切片,并检查输出的精确行数以及可选的软指标。注明 tests/smoke/test_<stem>.py 和准确的 smoke run 命令。

说明该数据切片有意远小于全数据集交叉验证,但运行时间仍取决于加载器、特征图和学习器。除非已有实测时长,否则不要承诺几分钟内完成;也绝不要为了缩短预估时间而用合成行替代真实数据。此预览只输出一次,不要在每个 pytest 操作前重复输出。如果有强制门禁尚未通过,可以预览可能的测试,但不要编写或运行测试。此技能不进行 LLM 研究,也不执行完整评估。

预检 — 在任何测试代码之前以可见文本输出此清单

Pre-flight (smoke-test-ml-pipeline):
- [ ] Tier 1 mandatory libs importable: pytest + sklearn + skrub
      (stage libraries per `python -m skore_skills env stack`). **Not skore** —
      see the Stop conditions; the smoke test is intentionally
      portable to any skrub-capable environment
- [ ] API confirmed for skrub / sklearn symbols used in
      the test: <symbols, or "none">
      Evidence: python -m skore_skills api get <dotted>...
                | Read scratch/api/<lib>/<version>/<topic>.md (this turn)
                | Write scratch/api/<lib>/<version>/<topic>.md (this turn)
                | "n/a — test only uses symbols already present in
                  src/<pkg>/ (build_learner / load_training_table / etc.)"
- [ ] `journal/NN_<short_name>.md` read this turn (frozen sections:
      Question, Method) so the test asserts what the experiment claims
- [ ] `experiments/NN_<short_name>.py` skimmed this turn for the env-dict
      keys `build_learner` consumes (`data_dir` / `start` + `end` /
      `raw_frame` / etc.)
- [ ] `src/<pkg>/data.py` skimmed this turn for the loader signature
      (so the predict-env construction matches the loader's expectations)
- [ ] Test category & stem decided: `tests/smoke/test_NN_<short_name>.py`
- [ ] Predict-grid size decided: smallest window that still triggers
      the failure mode (default: a single horizon-length slice; for
      time series, the most recent N steps such that the target is
      *just* observable for assertion)
- [ ] Hard assertion wired: `len(predictions) == n_predict_grid_rows`,
      plus output width (1 for one target; `translation.targets`
      for multi-output regression, including column names)
- [ ] Soft assertion wired (or explicitly skipped): smoke MAE within
      `3 × CV_MEAN_HARDCODED_FROM_PLAN` (or task-appropriate
      analogue). Value is a literal pulled from the matching
      `journal/NN_<short_name>.md` § Status.headline; the test does
      not import `skore` / read the project store at runtime.
- [ ] Pytest run this turn on `tests/smoke/test_NN_<short_name>.py`.
      Red → back to build (§ Back to build). Green (sub-step) →
      return to build for the design HITL.

冒烟测试断言内容

两个断言,两种严重性:

硬性 — 行数检查
assert len(predictions) == n_predict_grid_rows

这是一个结构正确性断言。它只有通过或失败两种结果,也是冒烟测试的核心目的。正确构建的流水线(依据 build-ml-pipeline 的 X-marker 规则)会轻松满足此条件。先加载、再提取特征、然后拆分的流水线则会失败,因为预测环境在预测时进行特征提取时没有预先历史缓冲区,冷启动行会被静默丢弃。

n_predict_grid_rows 是预测环境声称需要预测的行数,通常是预测时网格中的目标时间行数。如果流水线的源绑定是原始文件目录,那么该值就是预测时由预测环境生成的监督数据帧行数(可通过 build_supervised_frame(predict_dir) 获取)。

len 统计行数。还要断言输出宽度:单个目标时为 1。对于多输出回归,宽度以及(当 predict 返回 DataFrame 时)列名都必须与 translation.targets 相等。行数正确但输出错误的矩阵仍属于形状错误。

软性 — 指标与 CV 的差距
smoke_mae = mean_absolute_error(y_true, predictions)
assert smoke_mae < 3 * cv_mae_mean, (
    f"smoke MAE {smoke_mae:.0f} is more than 3× the CV mean "
    f"({cv_mae_mean:.0f}); predictions may be NaN-poisoned even "
    f"though the count matches."
)

指标差距可以捕捉第二级故障模式:预测数量正确,但由于某些特征在预测时为 NaN,数值毫无意义(例如,编码器尚未见过新类别,或因上游历史引用配置不正确而导致滞后值为空)。3× 上限是一个初始启发式值;请根据任务调整。冒烟窗口只有一个季节性数据切片,因此上限必须足够宽松,避免合理的困难季节窗口触发失败。

软性断言是默认启用、可主动退出:只有当任务没有明显的指标与 CV 比较项时才跳过(例如,冒烟测试夹具刻意不包含真实值)。如果跳过,请在测试文件中留下注释说明原因。

按构造即可诊断的特性

该夹具专门设计为在错误形态下失败,在正确形态下通过。这是冒烟测试最重要的特性;如果你走了夹具构造的捷径,却没有这个特性,那么测试就毫无价值。

具体来说,预测时的环境字典只包含我们要进行预测的行,不带任何超出预测时已知特征绝对必要范围的历史缓冲。这会产生两个结果:

  • 较晚调用 mark_as_X 的流水线:特征仅使用预测环境中的数据在图内计算。回看滞后值、滚动窗口和目标偏移在冷启动行处为 NaN。标记前的 drop_nulls(或模型不接受 NaN)会丢弃这些行。len(predictions) < n_predict_grid_rows。测试失败。
  • 较早调用 mark_as_X 的流水线:标记会落在预测网格节点上(build-ml-pipeline 规则 2 的第 2 层);依赖历史的特征会将上游历史 DataOp 作为额外的 apply_func 参数。预测时,历史节点会解析为完整可用历史(从与训练环境相同的数据源绑定),并且每个特征步骤中的连接都会为预测网格中的每一行生成实际值。len(predictions) == n_predict_grid_rows。测试通过。

这两种结果都是确定的。冒烟测试不可能“时好时坏”——如果行数差 1,说明流水线有问题。

预测网格大小方面:越小越好。使用仍能真实代表预测时网格的最小预测窗口。一个等于预测跨度的切片(例如,针对 t+24 模型的一天)足以暴露问题;更大的窗口只会用数据量掩盖问题。

Fixture 构建——data/ 是数据源

Fixture 从真实的 data/ 数据源读取,而不是使用合成生成器或已提交到代码库的 fixture 文件。实验使用的加载器,就是冒烟测试必须实际调用的加载器。合成 fixture 会让测试失去意义。

构建方式取决于实验的数据源绑定(阅读 experiments/NN_*.py,了解 build_learner 会读取哪些 env-dict 键),但结构始终相同:

  1. 确定预测网格的时间边界(predict_start、predict_end)。对于时间序列,使用数据中最近的、与预测时域等长的窗口。
  2. 确定训练 env。最清晰的选择是使用严格早于 predict_start - HORIZON 的所有数据(embargo 长度等于预测时域)。对于表格 IID 数据,只需排除预测网格中的行。
  3. 构建两个 env-dict:
    • train_env:采用实验拟合绑定所使用的形状,并限制为 embargo 之前的数据。
    • predict_env:预测网格的描述,不添加任何额外的历史填充(这是诊断属性;如果添加填充,测试就会产生误通过)。
  4. 独立于预测结果计算 n_predict_grid_rows——行数来自 predict_env 的监督表示(而不是预测结果本身)。
  5. 从 predict_env 的监督表示计算 y_true(软断言的真实值)。

Fixture 不得向 data/holdout/、data/train/ 等目录写入派生文件。这些是由项目设置脚本拥有的工作区级产物;冒烟测试 fixture 是临时的。如果实验的数据源绑定需要磁盘输入,请使用 tmp_path(pytest 内置的临时目录 fixture)。

三种常见的数据源绑定形状——冒烟测试 fixture 必须与实验实际使用的形状一致:

绑定形状预测 env 构建方式n_predict_grid_rows
原始文件目录——build_learner 绑定一个 data_dir 类变量;加载器会从中 glob / 读取文件。在测试中创建一个包含按时间切片的原始文件的小型临时目录(使用 pytest 内置的 tmp_path)。将其绑定为 data_dir。predict_env 监督表示的行数(例如,在 load-forecasting 中使用 len(build_supervised_frame(predict_dir))),可根据切片预先确定。
预测网格 + 原始历史数据源——build-ml-pipeline 规则 2 中的 early-mark 形状:predict_grid 加上单独的 history_source / weather_source / 等变量。构建内存中的 predict_grid 值(时间戳列表、面板键网格等)和数据源标识符。无需写入文件。len(predict_grid)。
物化的 (X, y) IID 数据——build_learner 直接绑定 X 和 y(或绑定一个映射到 {"X": ..., "y": ...} 的 data env-dict)。拟合前,从物化的 X 中留出一小部分行(以及对应的 y);其余部分放入 train_env,留出部分放入 predict_env。len(predict_subset)。

对于第二种形状(预测网格 + 原始历史数据源),三个层——数据源 → 预测网格 + 对齐 + mark_as_X → 特征(以历史数据作为上游引用)——在 build-ml-pipeline 规则 2 中有说明,并在 build-ml-pipeline/references/layer_examples.md 中提供了完整代码示例。为 early-mark 流水线构建预测 env 之前,请先阅读该参考文档。

IID 平面表格问题——烟雾测试仍能发挥的作用

对于没有跨行依赖的管道(逐行运算、在拟合时学习并在预测时逐行应用的有状态编码器、没有滞后项 / 滚动计算 / 与历史数据联接),烟雾测试可简化为:“在训练子集上拟合,在留出子集上预测,并断言 len(predictions) == len(predict_subset)”。

按构造即可诊断的属性不适用——没有跨行访问可供测试破坏,因此,对于构建正确的管道和存在错误的管道,硬断言都会通过。烟雾测试在 IID 场景下仍能捕获:

  • 加载器在比 CV 使用的数据更小的输入上丢弃或重复行的错误。
  • learner.predict(env) 的输出与 predict-env 行数不匹配。DataFrame 的 len 表示行数,因此还要检查输出宽度。单个目标的宽度为 1。多输出回归的宽度和列名都必须与 translation.targets 一致。
  • 编码器从未见过预测子集中存在的类别,导致意外的 NaN 污染(烟雾测试 MAE 与 CV 均值的软断言可以捕获这种情况;请保留该断言)。

将 IID 烟雾测试视为健全性检查,而非 CV 替代方案。对于跨行管道,烟雾测试才承担 CV 替代方案的作用;此时,按构造即可诊断的属性是关键保障。

pytest 的标准形式

每个烟雾测试文件包含一个测试函数。函数名与实验名称相同,让 pytest 输出一目了然。

"""Smoke test for `experiments/NN_<short_name>.py`."""

# stdlib + numpy first
import pytest

from <pkg> import PROJECT_ROOT
from <pkg>.pipeline import build_learner
# additional imports per the experiment's binding shape

DATA_DIR = PROJECT_ROOT / "data"


@pytest.fixture
def train_predict_envs(tmp_path):
    """Build a (train_env, predict_env, n_predict_grid_rows, y_true) tuple.

    Diagnostic by construction: predict_env carries only the
    rows we want predictions for, with no pre-history padding.
    """
    # ... per-experiment fixture construction ...
    return train_env, predict_env, n_predict_grid_rows, y_true


def test_NN_<short_name>(train_predict_envs):
    """Predict-time replay must produce one prediction per predict-grid row."""
    train_env, predict_env, n_predict_grid_rows, y_true = train_predict_envs

    learner = build_learner()
    learner.fit(train_env)
    predictions = learner.predict(predict_env)

    # HARD: structural correctness. n_outputs is 1 for one target.
    # Multi-output regression sets n_outputs and target_names from
    # translation.targets.
    n_outputs = 1
    target_names: list[str] = []
    assert len(predictions) == n_predict_grid_rows, (
        f"got {len(predictions)} predictions for "
        f"{n_predict_grid_rows} predict-grid rows — pipeline is "
        f"dropping cold-start rows; check `mark_as_X` placement "
        f"and that history-dependent features reference an "
        f"upstream history node, not a per-slice computation."
    )
    width = predictions.shape[1] if getattr(predictions, "ndim", 1) == 2 else 1
    assert width == n_outputs, (
        f"got {width} outputs, expected {n_outputs}"
    )
    if target_names:
        assert list(predictions.columns) == target_names, list(predictions.columns)

    # SOFT: predictions are not NaN-poisoned.
    from sklearn.metrics import mean_absolute_error
    smoke_mae = mean_absolute_error(y_true, predictions)
    # CV_MAE_MEAN is hardcoded at the top of the file from
    # `journal/NN_<short_name>.md` § Status.headline. The smoke test
    # uses only the predicting package's API (skrub/sklearn) —
    # no skore import, so it runs anywhere skrub does.
    assert smoke_mae < 3 * CV_MAE_MEAN, (
        f"smoke MAE {smoke_mae:.0f} > 3 × CV mean "
        f"({CV_MAE_MEAN:.0f}) — predictions may be NaN-poisoned."
    )

tmp_path 是 pytest 内置的、每个测试专用的临时目录;只要实验的源绑定需要磁盘输入,就使用它。

失败语义

烟雾测试失败说明是管道形状问题,而不是指标问题。

  • 硬断言失败(行数)→ 管道已损坏。返回构建阶段(§ 返回构建阶段),检查 X 标记的位置以及依赖历史数据的特征步骤。不要调整模型;不要放宽断言;不要添加包装器。修复形状。
  • 软断言失败(指标偏差很大)→ 预测结果存在,但在烟雾测试窗口上的表现毫无意义。最常见的原因是:上游历史节点在预测时未能正确解析,因此滞后列悄然变成 NaN。检查 learner.skb.full_report(),查找预测时的值与拟合时所见值不匹配的节点。
  • 失败会阻止状态变为 done。 manage-ml-backlog 的 record-outcome 会拒绝将实验状态改为 done,直到对应的烟雾测试通过。evaluate-ml-pipeline 也会在 smoke run 为 stop 时停止(如果依赖历史数据的管道缺少烟雾测试文件,也会停止)。

返回构建阶段

管道修复应由 build-ml-pipeline 处理。如何返回取决于加载此技能的方式:

  • 子步骤——build-ml-pipeline 加载了此技能:返回该技能。它的指令仍处于加载状态;继续执行其循环。
  • 直接调试——此技能是单独加载的(例如“为什么烟雾测试失败?”):当 status.skills.model-ml-pipeline 为 true 时,加载 model-ml-pipeline,并注明名称。它会恢复已批准的设计,并在 build 中重新进入修复流程,使用 build 声明的模型层级。不要从这里加载 build-ml-pipeline:此技能加载的子步骤最多只能运行在此技能自身的层级。缺少 model-ml-pipeline 时 → 用一句话说明管道需要在 build 阶段修复,然后停止。

如果直接调试请求只问烟雾测试为何失败,诊断就是答案;只有用户希望修复管道时才返回 build。

此技能不会做什么

  • 编写设计说明或实验脚本。这些由 model-ml-pipeline 和 setup-workspace / build-ml-pipeline 负责。
  • 操作 skore Project。烟雾测试不会调用 project.put——它是飞行前检查,不是指标 工件。交叉验证指标由 evaluate-ml-pipeline 提供。
  • 定义什么是“良好的指标”。硬性断言检查的是结构; 软性断言是合理性边界,不是 性能目标。根据 triage-ml-task 中由用户判断结果的规则, 性能由用户评判。
  • 询问“评估(推荐)/ 修改 / 停止”。执行 smoke run 后,返回 build-ml-pipeline(父技能),或针对 直接调试请求报告通过/失败。

运行 pytest

这是可执行的验证。编写或更新测试文件后,运行 python -m skore_skills smoke run --stem <NN>_<short_name>。它会流式运行 tests/smoke/test_NN_<short_name>.py 中的 pytest,然后打印 JSON。没有 action: proceed 就不要声称测试通过。stop / red → 返回 build 修改流水线(§ 返回 build); 不要开始评估。proceed → 如果此技能作为子步骤加载,则返回 build 执行设计 HITL。

配套技能

  • build-ml-pipeline — 父技能。负责烟雾测试断言的 X-marker 放置 规则,以及通过后的 HITL。烟雾测试失败时,返回该技能修复流水线形状;直接调试时则通过 model-ml-pipeline 处理(§ 返回 build)。 pytest 是迭代循环:smoke run 显示 red → 修改流水线 → 再次运行 smoke run。
  • manage-ml-backlog — record-outcome 会写入 done。实验状态改为 done 前,烟雾测试必须通过。
  • evaluate-ml-pipeline — 负责交叉验证。不要从这里加载它。
  • python -m skore_skills api get — 获取烟雾测试所用预测包 API 的 符号参考。在测试主体中指定任何导入函数前, 请先查阅。 python -m skore_skills api get 不是烟雾测试的依赖项——参见上文 “不导入 skore”停止条件。优先检查缓存: 在 WebSearching 前检查 scratch/api/<lib>/<version>/; 将新发现缓存回该目录(依据 python -m skore_skills api get Shape 0/3)。
  • python -m skore_skills env stack — 为使用此技能的任何工作区声明 pytest 为 阶段库。
  • python -m skore_skills style — 编写或 编辑 tests/smoke/test_NN_*.py 后,必须调用。若不调用此技能而直接运行特定管理器的 ruff 检查,会静默丢失技术栈所要求的 NumPyDoc 文档字符串规范:ruff 的 D 规则允许只有一行摘要的文档字符串通过,但只有技能正文会说明测试夹具和 测试函数应采用的参数类型槽位形状及章节 布局(Parameters / Returns / Notes)。

需要包?

缺少导入时,如果 status.skills.add-python-package 为 true,请加载 add-python-package。该技能负责 env add 和非托管依赖请求。不要在这里运行 env add。 如果未安装该技能,请指出包名并停止。

评论

支持 Markdown