Claude Media
Claude CodeでEvidentlyのデータドリフトを読み、再学習の要否を判断する

Claude CodeでEvidentlyのデータドリフトを読み、再学習の要否を判断する

Evidentlyのドリフトレポートは1000行を境に判定方法が変わります。JSONで出した列ごとの結果をclaude -pに読ませ、再学習するかどうかの判断メモを構造化出力で受け取る手順をまとめます。

Evidentlyのデータドリフトレポートを使うと、参照データと直近データの分布のずれを列ごとに調べられます。ただし、そのレポートに書かれた数値は「再学習するか」の答えではありません。ずれた列がモデルにとって重要か、精度は落ちているか、データ側の欠損が原因ではないか。これらを読み分けて初めて判断になります。

この記事では、ドリフトレポートをJSONで出し、列ごとの結果を claude -p に読ませて、再学習の要否を判断メモとして受け取る流れを組みます。数値の出し方はEvidently 0.7.23の手元実行です。Claudeには数えさせず、読ませて整理させます。

ドリフトの判定方法は1000行を境に変わる

最初に押さえたいのは、DataDriftPresetの既定が「p値」ではない場合があることです。公式の説明では、参照データが1000行以下なら数値列にK-S検定、カテゴリ列にカイ二乗検定を使い、スコアはp値で0.05以下がドリフトです。1000行を超えると、数値列はWasserstein距離、カテゴリ列はJensen-Shannon距離になり、スコアは距離で0.1以上がドリフトです。

同じ列でも、行数によってスコアの意味が変わります。Claudeに「p値が小さい列を挙げて」と頼む手順は、1000行を超えるデータでは噛み合いません。

これを確かめるため、合成データで両方を出しました。参照データと直近データを各2000行作り、monthly_spend(対数正規分布の平均を4.0から4.4へ)と plan の構成比(free 50%、standard 35%、premium 15% を30%、40%、30% へ)だけをずらしています。age と visits は同じ分布です。

列先頭500行どうし(既定)2000行どうし(既定)2000行どうし(PSI)
age先頭500行どうし(既定)K-S p値0.3292000行どうし(既定)Wasserstein 0.0892000行どうし(PSI)0.021
monthly_spend先頭500行どうし(既定)K-S p値3.5e-312000行どうし(既定)Wasserstein 0.9632000行どうし(PSI)0.607
visits先頭500行どうし(既定)K-S p値0.999972000行どうし(既定)Wasserstein 0.0212000行どうし(PSI)0.0096
plan先頭500行どうし(既定)カイ二乗p値1.2e-212000行どうし(既定)Jensen-Shannon 0.1512000行どうし(PSI)0.185

ドリフトの判定結果は3通りとも monthly_spend と plan の2列で一致しました。スコアの桁と向きが違うだけです。PSI(Population Stability Index)は既定ではなく、method="psi" で指定したときに使われ、しきい値は0.1以上です。

つまり、列ごとのp値とPSIを並べて読ませる前に、どの方法で出したスコアかを固定する必要があります。この記事ではPSIに揃えます。

ドリフトレポートをJSONで出す

インストールは pip install evidently です。手元では、pandas 3.1.0の環境で from evidently.presets import DataDriftPreset が ImportError: cannot import name 'infer_dtype_from_object' で止まりました。pandasを2.3.3に下げると通りました。環境を作るときは pandas<3 を指定しておくと迷いません。

次のスクリプトが、参照データと直近データのCSVからPSIのレポートを作ります。

python -m venv .venv && source .venv/bin/activate
pip install evidently "pandas<3"
# drift_report.py
import pandas as pd
from evidently import Report, Dataset, DataDefinition
from evidently.presets import DataDriftPreset
 
ref = pd.read_csv("reference.csv")
cur = pd.read_csv("current.csv")
dd = DataDefinition(
    numerical_columns=["age", "monthly_spend", "visits"],
    categorical_columns=["plan"],
)
report = Report([DataDriftPreset(method="psi")], include_tests=True)
result = report.run(
    Dataset.from_pandas(cur, data_definition=dd),
    Dataset.from_pandas(ref, data_definition=dd),
)
result.save_json("drift.json")

report.run の第1引数が直近データ、第2引数が参照データです。逆にすると、参照と直近が入れ替わって比較されます。列の型は DataDefinition で明示します。公式も明示を勧めていて、省略すると数値とカテゴリは自動判定になり、テキスト列は必ず指定が必要です。

JSONには判定が入らない場合がある

include_tests=True を付けずに保存したJSONは、1288バイトでした。中身は列ごとのスコアだけで、たとえば ValueDrift(column=monthly_spend,method=psi,threshold=0.1) の値は 0.6072185426864555 です。ドリフトしたかどうかの真偽値は入っていません。

テストを有効にすると、tests に列ごとの結果が入ります。手元の出力は次のとおりです(4489バイト)。

Share of Drifted Columns: Less 0.500 | Share of Drifted Columns: Actual value 0.500 >= 0.500 | FAIL
Value Drift for column age | Drift score is 0.02. The drift detection method is PSI. The drift threshold is 0.10. | SUCCESS
Value Drift for column monthly_spend | Drift score is 0.61. ... PSI. The drift threshold is 0.10. | FAIL
Value Drift for column visits | Drift score is 0.01. ... PSI. The drift threshold is 0.10. | SUCCESS
Value Drift for column plan | Drift score is 0.18. ... PSI. The drift threshold is 0.10. | FAIL

4列中2列がドリフトしたので、ドリフトした列の割合はちょうど0.5です。公式は「列の50%以上がドリフトするとデータセット全体のドリフト」としており、手元でも0.5がFAILになりました。境界上の結果だと分かった状態で読ませたいので、割合の数字は判断メモに書かせます。

判定をClaudeに再計算させる必要はありません。しきい値の比較はEvidentlyの status に任せ、Claudeにはその結果の意味づけを任せます。

claude -pに列ごとの結果を読ませる

claude -p は標準入力を読むので、JSONをパイプで渡せます。ファイルを読む権限やBashの権限は要りません。公式のhow-toでも、git diff main をパイプで渡すと「ClaudeにBashの権限が要らない」と説明されています。入力の上限は10MBです。超えるとエラーで終了するので、列数が多いときはJSONを列ごとに絞ってから渡します。

判断メモの形は --json-schema で固定します。結果は structured_output に入ります。

cat drift.json | claude -p \
  "このドリフトレポートを読み、再学習の要否を判断してください" \
  --append-system-prompt-file drift-policy.md \
  --tools "" \
  --max-turns 3 \
  --output-format json \
  --json-schema '{
    "type":"object",
    "properties":{
      "decision":{"type":"string","enum":["retrain","investigate","hold"]},
      "drifted_columns":{"type":"array","items":{"type":"object",
        "properties":{"column":{"type":"string"},"score":{"type":"number"}},
        "required":["column","score"]}},
      "rationale":{"type":"string"},
      "checks_before_retrain":{"type":"array","items":{"type":"string"}}
    },
    "required":["decision","drifted_columns","rationale","checks_before_retrain"]
  }' | jq '.structured_output'

--tools "" で組み込みツールをすべて無効にしています。レポートを読んで文章にするだけの処理に、ファイルの読み書きやコマンド実行は要りません。--max-turns 3 は、上限に達するとエラーで終了するための歯止めです。

返ってくるJSONは、たとえば次のような形になります(スキーマに沿った例で、Claudeの実出力ではありません)。

{
  "decision": "investigate",
  "drifted_columns": [
    {"column": "monthly_spend", "score": 0.61},
    {"column": "plan", "score": 0.18}
  ],
  "rationale": "4列中2列がドリフトし、割合は境界の0.5。...",
  "checks_before_retrain": ["直近の精度指標の確認", "欠損率の別検査"]
}

スクリプトから使うときは、structured_output が存在するかを先に確かめます。公式は、Claudeが依頼を断るなどして構造化出力が得られなかったとき、このフィールド自体が無くなると書いています。jq -e '.structured_output' の終了コードで分岐させると、空の判断メモを後段に流しません。

判断の基準はポリシーに書いて渡す

スキーマは出力の形を決めるだけで、retrain と investigate の線引きは決めません。線引きはClaudeの裁量に任せず、drift-policy.md に書いて --append-system-prompt-file で渡します。次は一例です。

# ドリフト判定ポリシー
- ドリフトの有無は status の SUCCESS / FAIL をそのまま使い、スコアから再判定しない
- 方法は PSI、しきい値は 0.1 以上。方法が違うレポートが来たら decision は investigate にする
- ドリフトした列が dataset 全体の 50% 以上なら、割合が境界でも「ドリフトあり」と書く
- ドリフトだけで retrain にしない。直近の精度指標が入力に含まれないときは investigate にする
- 重要度の高い列: monthly_spend, plan。この2列が両方ドリフトしたら理由に必ず書く
- 推測で数値を補わない。入力にない精度や件数は「入力になし」と書く

「ドリフトだけで retrain にしない」の1行が、この記事で最も効く規則です。Evidentlyの公式も、特徴量のドリフトを「真のラベルが無いときの代理指標」と位置づけ、ドリフトが出たら再ラベリングと再学習を始めるか、別の判断方法に切り替えると説明しています。ドリフトが無ければ環境は安定していて、再学習は不要かもしれない、とも書いています。

分布のずれは、精度の低下とは別の話です。ずれた列がモデルでほとんど使われていなければ、再学習しても得るものは小さくなります。逆にずれが小さくても、重要な1列だけが動けば精度は落ちえます。重要度の情報はレポートに入っていないので、ポリシーに書くか、入力に付け足します。ポリシーファイルはコードと同じくGitで管理し、変更をレビューに通すと、判定基準をいつ変えたかが履歴に残ります。

プロジェクトで繰り返し使うなら、同じ内容を CLAUDE.md に置く手もあります。ただし claude -p は、--bare を付けない限りCLAUDE.mdやフック、MCPサーバーを自動で読み込みます。CIや定期ジョブで毎回同じ結果を得たいときは、--bare を付けてポリシーを --append-system-prompt-file で明示する方が、手元と実行環境の差が出ません。--bare はサブスクリプションのログインを使わないので、ANTHROPIC_API_KEY の設定が前提です。

ドリフト検定が見ない欠損は別の検査で拾う

公式の説明では、ドリフト検定は欠損値を除いて分布を比べ、欠損が増えても反応しません。欠損率の悪化が原因でモデルが壊れるケースは、このレポートだけでは見逃します。欠損と入力の品質は、Evidentlyの DataSummaryPreset や、Great Expectationsの期待値による検査のように、別の検査として組みます。

ポリシーにも「欠損率は入力に無いので、decisionがretrainのときは検査を checks_before_retrain に入れる」と書いておくと、判断メモの抜けを減らせます。学習データを作る前処理側のスキーマ検証は、panderaでJSONLのスキーマを固定する手順で扱っています。

実行の回数とコストを見る

--output-format json のレスポンスには、total_cost_usd とモデルごとの内訳が含まれます。スクリプトから呼ぶ場合は、費用を記録しておけます。ただし公式は、この値をクライアント側の推定で、請求額とは異なりうると説明しています。

ドリフトレポートの実行は日次や週次のバッチで回すのが一般的です。その場合は、claude -p を呼ぶ前に、status に一つでもFAILがあるときだけ呼ぶ条件をシェルに書くと無駄がありません。全列がSUCCESSなら hold で確定なので、Claudeに読ませる意味が薄くなります。

実験やモデルの記録は別の道具が担います。runごとの精度やパラメータの比較は、MLflowの実験管理の記事の手順と組み合わせられます。Claude Codeの非対話実行そのもののオプションは、claude -pオプションの解説にまとめています。

つまずきやすいところ

  • PSIとp値を同じ列に混ぜる: method を指定しないと、行数によってp値と距離が混在します。ポリシーに方法を書くか、スクリプトで method を固定します
  • 参照と直近を逆に渡す: report.run の第1引数が直近データです。入れ替えると、スコアは出ても意味が逆になります
  • JSONだけで判定を求める: include_tests=True なしのJSONはスコアだけです。しきい値の比較をClaudeに任せることになり、判定が揺れます
  • 境界上の結果を断定する: 4列中2列のように割合がちょうど50%のときは、hold か retrain かを二択で断定せず、investigate に倒すとポリシーに書いておきます
  • pandas 3系を入れる: 手元ではEvidently 0.7.23とpandas 3.1.0の組み合わせでimportが失敗しました。pandas<3 を指定します

まとめ

Evidentlyのドリフトレポートは、行数によってp値と距離のどちらで判定するかが変わるので、claude -p に読ませる前に方法を固定します。PSIに揃えて include_tests=True で保存したJSONをパイプで渡し、スキーマで判断メモの形を、ポリシーファイルで線引きを決めれば、再学習の要否が毎回同じ形式で残ります。

最後に残る判断は精度の側です。ドリフトは再学習の理由ではなく、再学習を検討する合図です。精度指標をポリシーと入力に加えるところまで組めて、はじめて「ドリフトしたから学習し直す」から「落ちているから学習し直す」に近づきます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. code.claude.com/docs/en/cli-reference
  3. docs.evidentlyai.com/metrics/explainer_drift.md
  4. docs.evidentlyai.com/metrics/preset_data_drift.md
  5. docs.evidentlyai.com/metrics/customize_data_drift.md
  6. docs.evidentlyai.com/docs/library/output_formats.md
この記事を共有:XはてブLinkedIn