第125回 実務で回すデータドリフト検出と自動リトレーニングワークフロー — Pythonで作る指標計算・閾値判定・スケジューリング手順

運用中のモデルで「最近性能が落ちている気がする」「データ分布が変わったかもしれない」と悩むことは少なくありません。本記事は、そうしたつまずきに寄り添い、具体的な手順と短いPythonスニペットで「検出→判定→自動トリガー→安全な再学習・展開」までを実務で回せる形に落とし込みます。

1) 概要とユースケース — いつ自動化すべきか

すべてを自動化すればよいわけではありません。自動化の検討ポイントは次の通りです。

  • 入力データが定期的に到着し、遅延ラベル(ラベルが後から付く)がある場合は自動化を段階的に行う。
  • ビジネスへの影響が高く、人的監視コストが高い場合は自動トリガーを検討する。
  • モデル更新の安全策(ステージング検証・小規模ロールアウト)を確保できること。

運用方針に応じて「検出は自動、最終的なデプロイは承認制」などのハイブリッド運用も有効です。

2) ドリフト指標の選び方と実装(PSI, KL, KS, feature-wise要約)

まずは代表的な指標と用途を整理します。

指標 用途 特徴
PSI(Population Stability Index) カテゴリ・連続の分布変化 直感的で閾値運用がしやすい(業務でよく使われる)
KLダイバージェンス 分布差の情報量的評価 扱いやすいがゼロ除算に注意
KS検定 2つのサンプル分布の差の検定 統計的検定で有意差を判定できる(サンプル数依存)

Pythonでの簡潔な実装例

以下は最小限の依存で動くスニペットです(pandas, numpy が必要)。実際は欠損処理やカテゴリの取り扱いを追加してください。

import numpy as np
import pandas as pd

EPS = 1e-8

def psi(expected, actual, bins=10):
    """簡易PSI実装。連続値をbinsで分割して計算。"""
    expected = np.asarray(expected)
    actual = np.asarray(actual)
    # binsはexpectedの分位で作るのが実務で安定
    quantiles = np.linspace(0, 1, bins + 1)
    bins_edges = np.unique(np.quantile(expected, quantiles))
    if len(bins_edges) <= 1:
        return 0.0
    e_counts, _ = np.histogram(expected, bins=bins_edges)
    a_counts, _ = np.histogram(actual, bins=bins_edges)
    e_pct = e_counts / (e_counts.sum() + EPS)
    a_pct = a_counts / (a_counts.sum() + EPS)
    e_pct = np.clip(e_pct, EPS, 1.0)
    a_pct = np.clip(a_pct, EPS, 1.0)
    return np.sum((e_pct - a_pct) * np.log(e_pct / a_pct))

def kl_divergence(p, q):
    p = np.asarray(p, dtype=float)
    q = np.asarray(q, dtype=float)
    p = p / (p.sum() + EPS)
    q = q / (q.sum() + EPS)
    p = np.clip(p, EPS, 1.0)
    q = np.clip(q, EPS, 1.0)
    return np.sum(p * np.log(p / q))

# pandasを使った読み込み例
from pathlib import Path

def load_data(path: str):
    p = Path(path)
    if p.suffix == '.csv':
        return pd.read_csv(p)
    elif p.suffix in ('.parquet', '.pq'):
        return pd.read_parquet(p)
    else:
        raise ValueError('unsupported file type')

特徴量ごとの要約出力

実務では全特徴量をループして指標化した結果をCSV/Parquetに出力し、ダッシュボードや監査ログに組み込みます。

def feature_drift_report(ref_df, cur_df, features, bins=10):
    rows = []
    for f in features:
        try:
            psi_v = psi(ref_df[f].dropna(), cur_df[f].dropna(), bins=bins)
        except Exception as e:
            psi_v = None
        rows.append({'feature': f, 'psi': psi_v})
    return pd.DataFrame(rows)

3) 閾値設計とアラート連携(統計的検定+実務的ルール)

閾値は単一の数値に頼らず、複数のルールを組み合わせることを推奨します。例:

レベル PSI 対応
良好 < 0.1 通常通りモニタリング
注意 0.1 ~ 0.2 担当者に通知、追加検査
要対応 > 0.2 自動トリガーの候補、ステージングで再学習

実務ルール例(複合判定):

  • PSI > 0.2 かつ(KL > 0.5 または KSのp値 < 0.01)→ 再学習トリガー
  • 単一特徴量のPSIのみ高い場合はサンプリングや入力処理の変化を疑う
  • ラベル遅延がある場合は、予測分布のみで判定して暫定アラートに留める

4) 再学習トリガーと安全確認フロー(ステージング検証・スモールロールアウト)

自動で再学習する場合でも、安全策を設けます。代表的なフローは次の通りです。

  1. 検出(PSI等)→ しきい値超過でトリガー発行(自動)
  2. ステージング環境での再学習とバリデーション(自動)
  3. 自動ABテスト(トラフィックの一部で新モデルを比較)
  4. 一定期間で性能が改善されれば段階的ロールアウト、問題があればロールバック

ステージング検証のチェック例:

  • 学習データのサイズと分布チェック
  • 特徴量の欠損率・カテゴリの増減
  • 学習・推論スクリプトの冪等性確認(何度実行しても同じ状態に戻る)

トリガー発行の簡易CLIスクリプト

#!/usr/bin/env python3
import json
from pathlib import Path
from datetime import datetime

def write_trigger(target_dir='triggers', payload=None):
    p = Path(target_dir)
    p.mkdir(parents=True, exist_ok=True)
    if payload is None:
        payload = {}
    payload.setdefault('ts', datetime.utcnow().isoformat())
    fname = p / f'trigger_{payload["ts"].replace(":","-")}.json'
    with open(fname, 'w') as f:
        json.dump(payload, f)
    print('wrote', fname)

if __name__ == '__main__':
    write_trigger(payload={'reason': 'psi_over_threshold', 'psi': 0.23})

5) スケジューリングと実行例(cron, systemd timer, Prefect/軽量キュー)

定期的に指標計算を走らせる例を示します。

cronの例(毎日00:30に実行)

# crontab -e
30 0 * * * /usr/bin/python3 /opt/monitoring/run_drift_check.py >> /var/log/drift_check.log 2>&1

Prefectを使った軽量ワークフロー(例)

from prefect import flow, task

@task
def compute_and_maybe_trigger():
    # ここに指標計算・閾値判定を入れる
    pass

@flow
def daily_drift_flow():
    compute_and_maybe_trigger()

# PrefectのスケジューラやUIからdaily_drift_flowを登録して実行

Prefectは再試行や依存関係、通知との連携が容易で運用向きです。小規模ならcron + CLIでも十分動きます。

6) ロギング・可観測性・監査ログの出力例

判定の透明性を担保するため、各実行のメタ情報を保存します。例:

{
  "ts": "2026-08-01T00:30:00Z",
  "ref_dataset": "ref_2026-07-01.parquet",
  "current_dataset": "batch_2026-08-01.parquet",
  "metrics": {"feature_a": {"psi": 0.25}, "feature_b": {"psi": 0.05}},
  "decision": "triggered",
  "action": "wrote trigger file trigger_2026-08-01T00-30-00.json"
}

ログはJSONLで保存し、ELKやCloudログに取り込むと可視化が容易です。

7) テスト・デバッグ・よくある失敗と対処

現場で遭遇しやすい問題と対処を表でまとめます。

問題 原因 対処
PSIが頻繁に振れる サンプリングが不安定、季節性 集計ウィンドウを長めにする、サンプリング方法を固定化
ゼロ頻度カテゴリでKLが発散 ゼロ確率の扱い不足 平滑化(EPS追加)を行う
再学習で性能が落ちる データ品質・バイアス・リーク ステージングでのABテスト、データバリデーション強化
ジョブが二重起動 スケジューラ設定ミス、冪等性不足 ロックファイルやDBフラグで排他制御

次の一歩(読後アクション)

  • 付属サンプルリポジトリをクローンして、自分のデータでPSI/KLを計算してみる。
  • 閾値を決めたらcron/Prefectにジョブを登録してステージングで自動再学習を試す。
  • 小規模ABテストで新モデルを比較し、問題なければ段階的にロールアウトする。

まとめ

本記事では、データドリフトの実務ワークフローを「指標計算(PSI/KL/KS)→閾値判定→トリガー発行→ステージングでの再学習→安全なロールアウト」という一連の流れで示しました。コードスニペットは最小実装なので、実際にはデータ検証(第118回)、大規模CSV処理(第122回)、可観測性(第123回)、耐障害性や再試行(第124回)と組み合わせて運用に落とし込んでください。実務では誤検出(偽陽性)と見逃し(偽陰性)のバランスを取り、段階的な自動化を進めることが鍵です。

参考(関連記事)

編集メモ

  • トーン:落ち着いた実務寄り。読者がそのまま試せる手順を重視。
  • シリーズ位置:第123(可観測性)・第124(耐障害性)の次。自動対応にフォーカス。
  • 重複最小化:第118・122回の理論は参照に留め、実務手順を中心に。
  • コード:短めのPythonスニペット中心。pandas/numpy前提で最小実装を提示。
  • 図ではなくテーブル中心:指標比較や問題対処を表で示す。
  • 安全策強調:ステージング、ABテスト、ロールバック手順を明記。
  • 運用例:cronとPrefect両方の例を提示し、運用規模での選び方に言及。
  • 付録案内:付属のサンプルリポジトリで実装を試すことを次の一歩に設定。