前処理の事故には特徴があります。コードは何も変えていないのに、ある日結果が壊れる。原因は入力側の変化です。
この回は ── 入ってくるデータ自体を検査する仕組み ── を扱います。コードのテストとは別に必要なものです。
1. コードのテストでは捕まらない
LLM時代のテスト戦略 で扱ったテストは、処理が正しいかを見るものでした。前処理では、それだけでは足りません。
| 事故の原因 | コードのテストで捕まるか |
|---|---|
| 処理の書き間違い | 捕まる |
| 上流の列名が変わった | 捕まらない |
| 値の意味が変わった | 捕まらない |
| 件数が急に減った | 捕まらない |
| 欠損が急に増えた | 捕まらない |
| 新しい区分値が現れた | 捕まらない |
2行目以降は、コードが正しくても起きます。しかも処理はエラーにならず、静かに間違った結果を出すことが多い。これが最も厄介な点です。
エラーで止まってくれれば、その時点で気づけます。前処理の事故は止まりません。空の集計、半分の件数、全部が同じ区分。下流まで流れてから「数字がおかしい」と言われるという形で発覚します。
2. まず件数を見る
最も単純で、最も多くの異常を捉えるのが件数です。ここから始めてください。
import numpy as np
def check_row_count(current: int, history: list[int], tolerance: float = 3.0) -> tuple[bool, str]: """件数が過去の実績から外れていないかを見る。
history: 直近の日次件数(新しい順でも古い順でもよい) tolerance: 何倍のばらつきまで許すか """ if len(history) < 7: return True, "履歴が不足(判定なし)"
h = np.array(history, dtype=float) median = np.median(h) # 外れ値に強い散らばりの指標を使う mad = np.median(np.abs(h - median)) spread = mad * 1.4826 or 1.0 # 0除算を避ける
z = abs(current - median) / spread
if z > tolerance: direction = "多すぎ" if current > median else "少なすぎ" return False, (f"件数が{direction}: {current:,} 件 " f"(平常時 {median:,.0f} 件 / 乖離 {z:.1f})") return True, f"正常: {current:,} 件"
history = [10200, 9800, 10100, 10500, 9900, 10300, 10000]for n in [10150, 5200, 0, 30000]: ok, msg = check_row_count(n, history) print(f" {'OK ' if ok else 'NG '} {msg}")中央値と、外れ値に強い散らばりの指標を使っているのが要点です。平均と標準偏差だと、過去に一度でも異常があると、その分だけ判定が緩くなります。一度大きく外れた日があると、その後しばらく検知できなくなる ── という形で、異常が異常を隠します。
そして0件も必ず検知してください。「取り込みが失敗して0件」は最も多い事故の1つですが、処理としてはエラーになりません。空のデータを正常に処理して、空の結果を出します。
3. 何を検査するか
件数の次に見るべき項目を、効果の大きい順に並べます。
| 検査 | 捉えられる異常 | 手間 |
|---|---|---|
| 件数 | 取り込み失敗、重複、欠落 | 小さい |
| 列の有無と型 | 上流のスキーマ変更 | 小さい |
| 欠損率 | 取得できなくなった項目 | 小さい |
| 値の範囲 | 単位の変更、桁の誤り(EP.21) | 中 |
| 区分値の集合 | 新しい区分の出現 | 中 |
| 主キーの重複 | 結合で件数が増える原因 | 中 |
| 集計値の水準 | 全体的な異常 | 中 |
3行目が実務で効きます。ある日から特定の列が全部空になるというのは、上流の変更でよく起きます。件数は変わらないので、件数の検査では捕まりません。
5行目も重要です。新しい区分値が現れると、それを想定していない後段の処理が誤動作します。EP.19 で扱った「学習時に存在しなかった群」と同じ問題です。
from dataclasses import dataclass
@dataclassclass CheckResult: name: str passed: bool message: str severity: str # "error" は止める / "warn" は通す
def run_checks(df, spec) -> list[CheckResult]: results = []
# 1. 必要な列があるか missing = set(spec["required_columns"]) - set(df.columns) results.append(CheckResult( "必須列", not missing, f"欠けている列: {sorted(missing)}" if missing else "すべて存在", "error", )) if missing: return results # 列が無ければ以降は判定できない
# 2. 欠損率 for col, max_null in spec.get("max_null_rate", {}).items(): rate = df[col].isna().mean() results.append(CheckResult( f"欠損率:{col}", rate <= max_null, f"{rate:.1%}(上限 {max_null:.0%})", "error", ))
# 3. 値の範囲 for col, (lo, hi) in spec.get("ranges", {}).items(): out = df[(df[col] < lo) | (df[col] > hi)][col] results.append(CheckResult( f"範囲:{col}", out.empty, f"範囲外 {len(out)} 件" if len(out) else "すべて範囲内", "error", ))
# 4. 想定外の区分値(警告に留める) for col, allowed in spec.get("allowed_values", {}).items(): unknown = set(df[col].dropna().unique()) - set(allowed) results.append(CheckResult( f"区分値:{col}", not unknown, f"未知の値: {sorted(unknown)}" if unknown else "既知のみ", "warn", ))
# 5. 主キーの重複 if key := spec.get("primary_key"): dup = df.duplicated(subset=key).sum() results.append(CheckResult( "主キー重複", dup == 0, f"重複 {dup} 件", "error", ))
return results`severity` で止めるか通すかを分けているのが要点です。次の節で扱いますが、全部を止める設定にすると、運用が回らなくなります。
4. 止めるか、通すか
検査に引っかかったとき、処理を止めるか、警告して通すかを決める必要があります。
| 状況 | 判断 | 理由 |
|---|---|---|
| 必須列が無い | 止める | 処理のしようがない |
| 件数が0 | 止める | 取り込み失敗の可能性が高い |
| 主キーが重複 | 止める | 下流で件数が膨らむ |
| 範囲外の値 | 止める | 集計が狂う |
| 新しい区分値 | 警告 | 正当な追加かもしれない |
| 欠損率がやや上昇 | 警告 | 一時的な変動の可能性 |
判断の基準は、壊れたデータが下流へ流れる影響です。集計値が広く使われるなら止める、参考情報なら警告に留める。
厳しくしすぎると、しょっちゅう止まるようになります。そして「またか」と再実行で通す運用になり、本物の異常も同じ扱いになります。LLM時代のテスト戦略 EP.11 で扱った「赤に慣れる」問題と、まったく同じ構造です。
5. 上流と取り決める
検査は事後の防御です。根本的には、上流と取り決めておくほうが効きます。 という考え方です。
- 列名と型 — 変えるときは事前に連絡する
- 必須かどうか — 空になりうる列を明示
- 取りうる値 — 区分を増やすときは連絡
- 単位と桁 — EP.21 で扱った内容
- 件数の目安 — 大きく変わるときは連絡
取り決めがあると、変わったこと自体が検知できます。取り決めがなければ、変わったかどうかも分かりません。「上流が黙って変えた」という話は、たいてい取り決めがなかったという話でもあります。
現実には、上流が社外だったり、こちらから依頼できなかったりします。その場合でも、こちらの想定を文書として持っておく価値はあります。検査の仕様が、実質的な取り決めの記録になります。
6. 検知したあとの動線
検知しても、誰も見なければ意味がありません。動線まで設計してください。
- 1誰に知らせるか — 担当を決める。全員宛は誰も見ない
- 2何を見れば判断できるか — 検査の結果と、その時のデータの様子
- 3止まったときの手順 — 誰が判断し、どう再開するか
- 4警告が続いたときの扱い — 放置されないよう、期限を設ける
4番目が抜けやすい。警告は止まらないので、放置されます。「新しい区分値が出ました」という警告が数か月出続けているのに、誰も対応していない ── これはよくある状態です。警告にも期限を設けるか、定期的に棚卸ししてください。
そして、壊れたまま流れた場合にどう気づくかも考えておいてください。検査は完全ではありません。レガシー再生の現場 EP.06 で扱った突き合わせのように、下流側での確認も組み合わせると、取りこぼしが減ります。入口で全部を止めようとするより、入口と出口の両方で見るほうが現実的です。
前処理の事故は入力の変化から起きるので、コードのテストでは捕まらない。しかも止まらずに静かに壊れる。まず件数を見る(0件も必ず検知)。次に列・欠損率・範囲・区分値・主キー。止めるか通すかは下流への影響で分け、全部止めると無視されるようになる。根本的には上流との取り決めが効きます。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。