「データドリフト (data drift)」は本番運用中にモデル入力の統計分布が学習時から徐々にずれる現象。 共変量シフト (P(X) 変化)・ラベルシフト (P(Y) 変化)・コンセプトドリフト (P(Y|X) 変化) の 3 種に分類される。 本ページでは KL ダイバージェンス・PSI (Population Stability Index)・KS 検定によるドリフト検出、 SHAP 値による特徴量ベースの監視、 再学習トリガーの設計を整理する。
これらのキーワードは「ドリフトを 3 タイプで識別 → 統計量で検出 → SHAP で原因特定 → 再学習で対応」というモデル運用の中核ループを構成する。
🍰 まずはやさしく
データの傾向が変わることです。
AIの予測ミスを防ぐために使います。
流行の服が時間で変わるようなものです。
結論を短くまとめて説明します。
データドリフト:入力データの分布が時間とともに変化する現象
🍰 まずはやさしく
AIを使い続けるための仕組みです。
AIの性能を保つために使います。
スマホアプリの更新のような作業です。
この言葉がどこで使われるか書きます。
この用語は MLOps カテゴリに属します。 関連する別称・略号:(なし)。
論文・実務レポートで データドリフト が登場したら、 まず本ページの「30秒で分かる結論」と「直感で掴む」を読めば、 その文脈で何を言っているか把握できます。
位置づけ:機械学習モデルを 運用フェーズに投入したあと、 入力データ分布や入出力関係が時間とともに変化する現象。 SSDSE-B-2026 のような年次更新公的統計でも、 各年のデータが微妙に変化することによりモデル性能が劣化する。 monitoring の対象。
🍰 まずはやさしく
AIが古い世界にいる状態です。
なぜ予測が外れるかを知るために使います。
冬の服で夏を過ごすような違和感です。
直感的に分かる例と図で説明します。
電力需要予測モデルを春に学習した。 夏になって気温が上がり、 学習時にはほぼ見なかった「猛暑日のエアコン需要」が頻発するようになる。 入力 (気温) の分布も、 入力→出力 (気温→需要) の関係性も変化した。 これが データドリフト。 モデルは古い世界しか知らないので、 そのままでは精度が落ちる。
データドリフトの主要 3 型を図で押さえる。 「共変量シフト」「ラベルシフト」「コンセプトドリフト」の違いを視覚化する。
💬 ①入力分布シフト(人口統計の変化など)、 ②クラス比率の変化(不正取引率の上昇)、 ③関係性そのものの変化(顧客行動様式の変化)。 検知手法も対応策も異なる。
この節では、 「データドリフト」を曖昧な概念で済ませず、 ①ドリフトの分類 (covariate / label / concept)、 ②検出統計量の選択 (PSI / KS / Wasserstein / JS divergence)、 ③閾値設計、 ④再学習トリガ、 ⑤運用時の人間介入ポイント、 という 5 段階で SSDSE-B-2026 や e-Stat 系の公的データを例にしながら整理する。
「データドリフト」と一括りにされがちだが、 厳密には少なくとも 3 種類ある。 (a) covariate shift = 入力分布 P(X) のみ変化、 P(Y|X) は不変、 (b) label shift / prior shift = P(Y) が変化、 P(X|Y) は不変、 (c) concept drift = P(Y|X) そのものが変化。 SSDSE-B-2026 の都道府県データで「総人口 (A1101) → 出生数 (A4101)」を予測するモデルを考えると、 (a) は都市部への人口移動で年齢構成が変わるケース、 (b) は全国的に出生数が一律に下押しされるケース、 (c) は少子化の要因構造そのものが変化して同じ人口でも出生数が違うケース。 検出手法も対処法も異なるので、 まず種類を切り分けることが第一歩。
| ドリフト種別 | 変化する分布 | 検出指標 | 対応策 |
|---|---|---|---|
| covariate shift | P(X) | PSI / KS / Wasserstein | 重要度重みづけ・再学習 |
| label shift | P(Y) | 事前確率比較 | prior 補正・再較正 |
| concept drift | P(Y|X) | 精度低下監視 | 再学習・再設計 |
| virtual drift | P(X) のみ・P(Y|X) 不変 | PSI | 監視のみ (再学習不要) |
PSI (Population Stability Index) は信用スコアリングで広く使われ、 ビン化したヒストグラム間の対称 KL ダイバージェンス。 KS (Kolmogorov-Smirnov) は累積分布関数の最大差分で、 連続値に対する古典的検定。 Wasserstein 距離 (Earth Mover's Distance) は分布間の「最適輸送コスト」を測り、 形状の違いを直感的に反映。 JS divergence (Jensen-Shannon) は対称化された KL で、 値域が [0, log2] に収まるため複数特徴の総合判定に向く。 表で特性を整理する。
| 指標 | 対称性 | 数値型 | カテゴリ型 | 代表的閾値 |
|---|---|---|---|---|
| PSI | ✓ | ○ (ビン化) | ○ | 0.1 警戒 / 0.25 警報 |
| KS 統計量 | ✓ | ◎ | × | p<0.05 |
| Wasserstein | ✓ | ◎ | ○ (順序あり) | サンプル依存 |
| JS divergence | ✓ | ○ (ビン化) | ○ | 0.1 警戒 |
| χ² | ✓ | ○ (ビン化) | ◎ | p<0.05 |
このコードでやること: SSDSE-B-2026 の人口分布を「過去年」「現在年」で比較し、 PSI と KS で検出する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import pandas as pd import numpy as np from scipy.stats import ks_2samp df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) # 総人口 A1101 を「過去年=2017年度」「現在年=2023年度」で比較(6年差) old = df[df['SSDSE-B-2026'] == 2017]['A1101'].values new = df[df['SSDSE-B-2026'] == 2023]['A1101'].values # KS 検定 ks_stat, ks_p = ks_2samp(old, new) print(f'KS 統計量 = {ks_stat:.4f}, p = {ks_p:.4f}') # PSI (10 ビン) bins = np.quantile(old, np.linspace(0, 1, 11)) old_cnt, _ = np.histogram(old, bins=bins) new_cnt, _ = np.histogram(new, bins=bins) p_old = old_cnt / old_cnt.sum() + 1e-6 p_new = new_cnt / new_cnt.sum() + 1e-6 psi = np.sum((p_old - p_new) * np.log(p_old / p_new)) print(f'PSI = {psi:.4f}') |
📤 出力例:
💬 都道府県人口は 6 年で大きく動かないので KS の p が 0.99、 PSI も 0.07 と低く(警戒閾値 0.1 未満)、 ドリフトなしと判定。 一方、 全国コロナ患者数のような変動の激しいデータでは PSI が 0.5 を超えることもある。
「PSI > 0.1 で警戒、 0.25 で警報」は業界慣例だが、 ビジネスコストに応じて調整する必要がある。 警告が頻発するとアラート疲労 (alert fatigue) を引き起こし、 本当のドリフトを見逃す。 逆に閾値が高すぎると検出が遅れる。 経験則として、 (a) 重要な特徴量だけ厳しめ (PSI 0.1)、 (b) 補助的な特徴量はやや緩め (PSI 0.25)、 (c) アンサンブル化して複数指標が同時に警戒域に入ったら警報、 という階層化が運用しやすい。
再学習のトリガ設計は 3 種類。 (1) 時間ベース: 毎週 / 毎月のような定期再学習、 シンプルで運用しやすいが、 ドリフトが発生していなくても再学習で計算資源を消費する。 (2) 性能ベース: バリデーション精度が閾値を下回ったら再学習。 真のラベルが遅れて到着するケース (信用デフォルト判定で 3 ヶ月遅れ等) では応答が遅れる。 (3) ドリフト検出ベース: PSI 等の入力分布監視で警報が出たら再学習。 ラベル遅延に強い。 ハイブリッド (最低毎月 + ドリフト時即時) が実務的にバランスが良い。
| トリガ | 応答速度 | 運用負荷 | 推奨用途 |
|---|---|---|---|
| 時間ベース (週次) | 中 | 低 | 緩やかなドリフト |
| 性能ベース | 遅 | 中 | ラベル即時取得可 |
| ドリフト検出ベース | 速 | 中 | ラベル遅延あり |
| ハイブリッド | 速 | 高 | クリティカル |
「ドリフト検出 → 自動再学習 → 自動デプロイ」 を全自動で組むのは危険。 ドリフトが「データ不具合 (パイプ破損・スキーマ変更)」 と「真の変化」のどちらか判別できないため、 不具合データで学習してしまうリスクがある。 推奨フローは「検出 → 警報 → 人間レビュー → 承認 → 再学習 → カナリアデプロイ → 段階的展開」。 SSDSE-B-2026 のような公的データを使う教育用システムなら全自動で構わないが、 production の与信モデルや医療モデルでは人間 in the loop が必須。
PSI / KS / Wasserstein のいずれかを毎日計算してダッシュボード化し、 閾値超過時にアラート、 真のドリフトと判定したら人間レビュー後に再学習、 カナリアデプロイで段階展開、 という運用パイプラインを設計するのがプロの仕事。 単に「ドリフトを検出します」 と謳う製品は多いが、 実務的価値は「検出後のワークフロー」 が定義されているかで決まる。
PSI 警報が出たとき、 最初にやるべきは「データパイプ自体が壊れていないか」 の確認。 ETL 失敗・スキーマ変更・タイムゾーン誤り・型変換ミス — これらは「データドリフト」 と区別困難な signature を持つ。 (a) 上流データソースに直接アクセスして spot check、 (b) パイプライン各ステージのレコード数・欠損率・型を比較、 (c) 異常検知された特徴量だけに絞って数値を眺める — これらの「データフォレンジック」 を 10 分以内に終えてから、 本物のドリフトと判定する。 「警報 = 再学習」 を反射的に行うと、 壊れたデータでモデルを更新して状況を悪化させる。
単変量 PSI を全特徴量で並列計算すると、 多重比較問題で false alarm が増える。 多変量を一度に判定する手法として、 (a) PCA で次元削減してから KL 距離、 (b) Maximum Mean Discrepancy (MMD) で高次元分布間距離、 (c) Domain classifier (新旧データを判別する分類器の AUC が 0.5 から離れたらドリフト)、 (d) Population Stability Index の多変量拡張、 などがある。 「相関構造が崩れる」 タイプのドリフト (特徴間の関係が変化) は単変量検出では捕捉できないため、 多変量手法は補助的に重要。
| 手法 | 特性 | 計算コスト | 解釈性 |
|---|---|---|---|
| PCA + KL | 線形相関の崩れに敏感 | 低 | 主成分で解釈可 |
| MMD (RBF カーネル) | 非線形変化検出 | 中 (O(n²)) | 低 |
| Domain classifier | 汎用・直感的 | 中 | 特徴量重要度で解釈 |
| エネルギー距離 | 対称・距離計量 | 中 | 中 |
P(Y|X) の変化は、 真のラベル Y が得られないと直接観測できない。 一方、 ラベルは数日〜数ヶ月遅れて到着するケースが多い (与信判定なら 3 ヶ月後、 医療診断なら 1 年後)。 この遅延に対処するため、 (a) 早期シグナル (一部ラベルが早く到着、 e.g. 即日デフォルト) を高速チャネルとして利用、 (b) プロキシ指標 (予測信頼度の平均が下がっているか、 等) で間接的に検出、 (c) DDM (Drift Detection Method) や ADWIN のようなオンライン統計手法で逐次検定、 という戦略を組み合わせる。
バッチ再学習の代わりに、 「常時オンライン学習」 でドリフトに追従する設計もある。 SGD 系最適化器 (Vowpal Wabbit, river ライブラリ) は新しい観測 1 件ずつをモデルに反映できる。 利点は応答速度と計算コストの最小化、 欠点は「壊れたデータでも学習してしまう」 「過去の傾向を忘れすぎる (catastrophic forgetting)」 リスク。 対策として、 (a) ラベル品質のオンライン検証、 (b) 学習率の動的調整、 (c) 過去モデルとのアンサンブル、 などを組み込む。 与信や医療より、 推薦システムやオンライン広告で実用化が進んでいる。
全体としてはドリフト無しだが、 特定サブグループ (年齢層・性別・地域) でだけドリフトが起きるケースは公平性問題の原因になりやすい。 SSDSE-B-2026 で「高齢者比率の高い県」 だけ予測精度が悪化するパターンなど。 監視時には全体 PSI だけでなく、 セグメント別 PSI / セグメント別精度を必ず計算し、 一部だけでドリフトが起きているなら個別対応 (該当セグメントの再学習、 features の追加) を検討する。
技術的検出ができても、 「誰が見て・誰が判断し・誰が再学習を承認するか」 が組織として定義されていないと、 警報が鳴っても無視される。 推奨は「MLOps エンジニア = 検出設定・閾値調整」「データサイエンティスト = 警報レビュー・再学習判断」「プロダクトオーナー = ビジネス影響評価・再デプロイ承認」 の三役分担。 SaaS 系 ML プラットフォーム (Datadog ML Monitoring, Arize AI, WhyLabs 等) はこの分担をワークフロー化するツールを提供している。
重大なドリフトが発生して再学習に至った際は、 必ず post-mortem (事後分析報告) を書く。 「いつ何が起きたか・どう検出されたか・原因は何か・どう対処したか・再発防止策」 を 1 ページにまとめ、 チーム共有する。 これを年単位で蓄積すると、 ドリフトの季節パターン (年度替わり・コロナ波・選挙期) が見えてきて、 予測的監視が可能になる。 「学習する組織」 を作る上で MLOps の post-mortem 文化は決定的に重要。
ドリフトは時間スケールによって対応戦略が異なる。 (a) 短期 (時間〜日): トラフィックスパイク・大規模キャンペーン・ニュース報道。 一時的な現象なので再学習せず monitoring で十分。 (b) 中期 (週〜月): 季節性・流行・経済指標変化。 月次再学習サイクルで吸収。 (c) 長期 (年単位): 法規制変更・人口構造変化・産業転換。 モデル設計そのものを見直す必要。 SSDSE-B-2026 のような年次データでは長期ドリフトが主、 月次データでは中期、 リアルタイムログでは短期が主役。
全体ドリフトを検出したら、 「どの特徴量が原因か」 を特定する。 各特徴量の PSI を個別計算し、 上位を可視化。 SHAP 値の分布シフトを比較する手法 (SHAP drift) も近年注目されている。 SSDSE-B-2026 で「出生数 (A4101)」 を予測するモデルなら、 「総人口 (A1101)」 が安定で「高齢者人口 (A1303)」 が大きくドリフトしている場合、 少子高齢化の構造変化が原因と推測できる。 こうした特徴量ごとの解像度が、 ドメイン専門家との議論を可能にする。
過去のドリフト履歴から「このモデルは何ヶ月稼働できるか」 を予測する研究もある。 サバイバル分析 (Survival Analysis) のフレームワークで、 モデルが SLA 違反に達するまでの hazard rate をモデリング。 これにより、 「半年後に再学習が必要そう」 と事前予算策定ができる。 SSDSE-B-2026 のようなゆっくり変化するデータでは寿命 1 年、 マーケティング系では 1 ヶ月、 株価予測では 1 日、 といった経験則がある。
分布シフトを幾何学的に分解すると、 (1) 平行移動 (mean shift): 分布全体が右や左に動く、 (2) スケール変化 (scale shift): 分散が拡大 / 縮小、 (3) 形状変化 (shape shift): 歪度・尖度・多峰性が変わる、 という 3 形式。 平均と分散だけ監視していると形状変化を見逃すので、 ヒストグラム形状そのものを比較する KL/JS や Wasserstein 距離が必要になる。 SSDSE-B-2026 で「コロナ後の都道府県別観光客数」 は明らかな形状変化を示す例。
OSS: Evidently AI (Python, PSI/KS 多数指標), NannyML (univariate / multivariate drift), Alibi Detect (Seldon の adversarial / drift detector)。 SaaS: Arize AI, WhyLabs, Fiddler AI, Datadog ML Monitoring。 OSS は無料で柔軟だが運用負荷あり、 SaaS は月数万〜数百万円だがオンボーディングが速い。 大規模本番では SaaS、 教育・PoC では OSS が現実的。 SSDSE-B-2026 を題材に Evidently AI で PSI 監視を試すのは、 学生にとって最初の MLOps 体験として最適。
2020 年のコロナ禍は、 ML モデルにとって史上最大のドリフトイベントだった。 (a) 需要予測モデル: 観光・外食はゼロ近くまで急減、 EC は急増。 既存モデルは全く役に立たず、 大量の人手介入が必要に。 (b) 与信モデル: 失業率急変で従来の信用スコアの予測力が低下。 (c) 医療画像: マスク着用や感染防護で撮影条件が変化。 教訓は「想定外のドリフトに対し、 自動再学習だけでは追従できない」 「人間判断のフォールバックを必ず用意しておく」 こと。 SSDSE-B-2026 の 2020 年代データを比較するだけでも、 都道府県別にこの混乱の痕跡が見える。
学術的には「ドリフトの因果構造を明示的にモデリング」 する causal drift detection や、 「ドリフトに対する継続学習 (continual learning) で破滅的忘却を回避」 する手法が活発に研究されている。 後者は、 新しい分布を学びながら過去の分布も保持する難しい問題で、 Elastic Weight Consolidation (EWC), Progressive Networks, Memory Replay 等のアプローチが提案。 SSDSE-B のような長期時系列パネルデータは、 こうした最先端研究のテストベッドとしても有望。
2024 年以降、 LLM や時系列基盤モデル (TimesFM, Chronos) が登場し、 「ドメイン横断で事前学習されたモデルはドリフトに強い」 という観察が増えてきた。 zero-shot / few-shot で新しい分布に適応できる能力 (in-context learning) が、 従来の re-training パラダイムを補完する可能性がある。 教育・研究の場では、 古典的 ML モデルと基盤モデルを SSDSE-B-2026 のような同じデータで対決させ、 ドリフト耐性を比較する課題が、 学生のリテラシー向上に最適。
このコードでやること: 2020 年版と 2026 年版の都道府県データを各変数ごとに PSI 比較し、 もっとも変化した変数を特定する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import pandas as pd import numpy as np df_old = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8-sig') df_new = pd.read_csv('data/raw/SSDSE-B-2026-prefecture.csv', encoding='utf-8-sig') common_cols = [c for c in df_old.columns if c in df_new.columns and df_old[c].dtype != 'object'] def psi(old, new, bins=10): cuts = np.quantile(old, np.linspace(0, 1, bins+1)) cuts[0] -= 1e-6; cuts[-1] += 1e-6 o = np.histogram(old, bins=cuts)[0] / len(old) + 1e-6 n = np.histogram(new, bins=cuts)[0] / len(new) + 1e-6 return float(np.sum((o - n) * np.log(o / n))) results = [] for c in common_cols: results.append((c, psi(df_old[c].dropna(), df_new[c].dropna()))) results.sort(key=lambda r: -r[1]) print('PSI 上位 5 変数 (ドリフト最大):') for col, p in results[:5]: print(f' {col}: PSI={p:.4f}') |
📤 出力例:
💬 観光業関連が最も大きく変化 (コロナ禍の影響推測)、 続いて少子高齢化に関する変数が変化。 SSDSE は年次データなので、 6 年差は社会変化を素直に反映する。 このようなドリフト解析は、 単独の予測モデル評価より社会動向の理解にも役立つ。
「ドリフト検出された!」 と思っても、 実は技術的アーティファクトだったケースが現場では非常に多い。 (1) サンプリングバイアス: 旧データと新データで母集団が異なる、 (2) 欠損値処理の不一致: 旧は drop, 新は 0 埋め等、 (3) カテゴリ符号化の変化: 同じ「東京」 でも 13 と 130000 で違う、 (4) タイムゾーン処理: UTC と JST が混在、 (5) センサー / 計測機器の校正変更: 同じ値の計測条件が変化。 検出された PSI が高い変数は、 ドリフト警報を出す前に必ずこの 5 つをチェックする。
データドリフトの背後には、 多くの場合「介入」 や「環境変化」 という因果的要因がある。 SSDSE-B-2026 で観光客数のドリフトが観測されたとき、 「コロナ禍」 「インバウンド政策」 「為替変動」 などの因果要因を切り分けるには、 因果推論の枠組み (DAG, do-calculus, counterfactual reasoning) が役立つ。 ドリフト検出が「異常を見つける」 段階だとすれば、 因果分析は「なぜ異常になったか」 を明らかにする段階。 両者を組み合わせると「ドリフトの真因」 にたどり着き、 単なる再学習を超えた根本対策が打てる。
具体的には、 (a) 介入時期の前後比較 (DID; Difference in Differences)、 (b) 同期間の対照群との比較 (合成統制法; Synthetic Control)、 (c) ベイズ構造時系列モデル (CausalImpact)、 などの手法が有効。 SSDSE-B-2026 の都道府県データはパネル構造を持つので、 「ある県で発生したドリフトが他県と比較してどう異なるか」 を統計的に厳密に分析できる素晴らしい教材。 例えば、 ある県の観光客数の急減を、 全国平均との差で比較 (DID) すれば、 コロナ影響の都道府県別差異を定量化できる。
さらに進めば、 「ドリフトが起きたとき、 もし何もしなかったら何が起きていたか」 という反事実的シナリオ (counterfactual) を予測する研究もある。 Causal Forest, Double Machine Learning, T-learner / S-learner / X-learner などの ML ベース因果推論手法が、 大規模データに対するドリフト因果分析の現代的解。 SSDSE-B-2026 の小サンプル (n=47) では古典的 DID が現実的だが、 全国市町村レベルに広げれば ML 因果推論が威力を発揮する。 こうした統計と ML の融合的視点を持つ学生は、 卒業後の業界でも高く評価される。
| 項目 | 準備状況 | 担当 |
|---|---|---|
| PSI 等指標を毎日計算 | cron / Airflow ジョブ設定済か | MLOps |
| 閾値設定 | 業務影響に基づく値か | PdM + DS |
| アラート通知先 | Slack / PagerDuty 接続済か | SRE |
| 再学習パイプライン | 1 コマンドで実行可能か | DS + Eng |
| カナリアデプロイ | 段階展開の手順書あるか | SRE |
| ロールバック | 10 分以内に戻せるか | SRE |
| post-mortem テンプレ | 準備されているか | マネージャ |
💬 全 7 項目が「準備済み」 になっていない MLOps システムは、 ドリフト発生時に対応できず事故が大きくなりやすい。 デプロイ前のチェックリストとして必ず確認する。
PSI 警報が出たとき、 「本物のドリフト」 と「データ品質劣化」 を切り分ける手順は重要。 (a) 欠損率の急変 はパイプライン障害の典型サイン (実環境変化では欠損率は緩やかに動く)、 (b) 同一値の急増 はセンサー故障やデフォルト値挿入の可能性、 (c) 型の混在 (数値カラムに突然文字列が混入) はスキーマ変更、 (d) タイムスタンプの巻き戻り は ETL バグ、 (e) 異常値率の急増 は校正不良。 これらは「本物のドリフト」 と異なる対策 (再学習でなく、 上流のデータ修復) が必要。 SSDSE-B-2026 のような公的データでも、 取得時の文字コード違いや列追加で「擬似ドリフト」 が観測されることがあるので、 メタデータ管理が重要。
ドリフトとデータ品質を切り分ける手段として、 (1) 上流ソースに直接アクセスして spot check、 (2) Great Expectations 等の data validation framework で常時監視、 (3) Apache Atlas 等の data lineage tool で変更履歴を追跡、 (4) Data Contract (上下流間の API スキーマ合意) を運用、 などの仕組みを組み合わせる。 これらは MLOps だけでなく DataOps の領域でもあり、 データエンジニアと密接に協力するべき。
現場で繰り返し見られる失敗パターンを 7 つ整理しておく。 (1) 全データ再学習スパイラル: ドリフト警報のたびに全データで再学習し、 結果的に最新データの過学習。 古いデータの重みを下げる exponential weighting が解。 (2) 閾値ハッキング: 警報が多いから閾値を緩める → ドリフトを検出できなくなる本末転倒。 まずアラート疲労の原因 (誤検知が多すぎる、 アクションが定義されていない等) を解く。 (3) 監視のサイロ化: データチーム・MLチーム・SRE チームが別々のダッシュボードを見ている → 障害時にコミュニケーションが断絶。 単一ダッシュボードに統合。 (4) ラベルなしで監視終了: 入力分布だけ監視して P(Y|X) を測らない → コンセプトドリフトを完全に見逃す。 遅延ラベルでも数値化できる proxy を必ず置く。
続けて、 (5) ベースラインの更新忘れ: 6 ヶ月前の比較対象データを使い続け、 「徐々にドリフト」 を全く検出できない。 ベースライン自体を定期更新する仕組み (rolling window) が必要。 (6) 再学習データの品質チェック欠落: 「ドリフト → 自動再学習」 で、 壊れた最新データでモデルを更新してしまい状況悪化。 必ず再学習前にデータ品質ゲートを通す。 (7) ドリフト = モデル劣化と誤解: 入力分布が変化しても、 P(Y|X) が安定なら予測精度は落ちない (covariate shift では予測精度はむしろ向上することもある)。 ドリフト = 即再学習 ではなく、 「精度ベース」 と「分布ベース」 の両指標で総合判断する。 これらアンチパターンを回避するだけで、 ML 運用の事故率は大幅に下がる。
最終的な教訓として、 「ドリフト検出は『監視のための監視』 になりがちな技術であり、 ビジネス価値に結びついた KPI と一体で運用することが重要」 という点を強調したい。 SSDSE-B-2026 のような公的データから始めて、 ドリフト検出ツールの仕組みを理解し、 産業界で必要とされる成熟した MLOps 思考を身につけてもらいたい。 一度学んだドリフト思考は、 ML 以外の領域 (ソフトウェア品質モニタリング・ビジネスインテリジェンス・組織変革) でも応用が効く汎用的な分析の枠組みである。
この用語ページ全体を通じて、 単なる「PSI を計算する技術」 ではなく、 「変化する現実を継続的にモデルに反映する組織と仕組み」 という視点を強調したい。 SSDSE-B-2026 のデータが毎年更新されるように、 現実世界は止まることがない。 その動的な対象に対して、 静的なモデルで対応するのは無理がある。 ドリフト対応は、 ML を生きた技術として育てるための基本姿勢であり、 学生時代から意識しておきたい大切な考え方である。
訓練時の入力分布 (青) と運用時の入力分布 (赤) を並べて表示します。 スライダー、 または左のグラフを左右にドラッグして運用分布の 平均 と 分散 をずらすと、 両分布の乖離 (ドリフト) と、 訓練分布に最適化されたモデルの誤差がリアルタイムに更新されます。 分布が離れるほど予測が外れることを体感してください。 ドリフト検知指標 (平均の差・PSI・KL 距離) と、 訓練時 MSE を基準としたモデル性能の劣化も同時に表示します。
💬 判定は PSI の慣用帯域(<0.1 なし/0.1–0.25 中程度/≥0.25 重大)で色分けしています。 右図のモデルは真の非線形関係 g(x)=x+0.5x² を訓練分布 N(0,1) 上で最小二乗フィットした線形近似 m(x)=0.5+x です。 平均ずれ Δμ=0・σ=1 のとき運用時 MSE は基準の 0.500(線形近似の限界による既約誤差)ですが、 運用分布が訓練域から離れるほど外挿誤差が急増します。 これは 共変量シフトでもモデルが誤設定(線形≠真の非線形)だと精度が落ちる という現実の姿です。 逆に P(y|x) を完全に捉えたモデルなら共変量シフトだけでは精度が落ちない、 という対比も本ページの他節で扱っています。
🍰 まずはやさしく
データの分布(偏り)の変化です。
正しく変化を測るために使います。
テストの点数の傾向が変わるようなものです。
数式を使って正確な意味を説明します。
学習時の同時分布 $P_{\text{train}}$ と運用時の $P_{\text{prod}}$ が異なる。 分解すると共変量シフト $P(\mathbf{x})$ 変化、 概念ドリフト $P(y|\mathbf{x})$ 変化に分けられる。
数式に出てくる記号の意味を 1 つずつ確認しましょう。
データドリフト検出で使う主な統計量を「言葉」と「数式」の両方で読み解く。
言葉:参照分布 P_ref と現分布 P_curr の 差を、 対数比で重み付けして合計。 二つの確率分布の「ズレ」を測る。 PSI < 0.1 で安定、 0.1-0.25 でやや変化、 > 0.25 で大変化。 SSDSE-B-2026 の年次推移で、 2020 と 2026 の人口分布を比較すれば PSI が計算できる。
言葉:累積分布関数(CDF)の最大差。 ノンパラメトリックで頑健。 D が大きいほど分布の違い大。 「2 標本 KS 検定」として確率分布の同一性を統計的に検定。
言葉:分布 P で見た「P から Q への情報損失」。 非対称(KL(P||Q) ≠ KL(Q||P))、 非負、 P = Q のとき 0。 PSI は KL の対称化版に近い。
言葉:KL を対称化し、 0-1 で正規化。 ドリフト検出の標準指標の一つ。
言葉:「土を運ぶコスト」のメタファー。 分布 P を Q に変形するための最小作業量。 連続変数のドリフト検知に有用。
これら 5 統計量を SSDSE-B-2026 の年次推移に適用すると、 「人口」「出生数」「高齢化率」など各列の ドリフト量が定量化される。 例えば「2010 → 2026 の総人口分布」で PSI = 0.12(やや変化)、 「2010 → 2026 の高齢化率分布」で PSI = 0.34(大変化)など、 列ごとにドリフトの強さが見える。 これらは MLOps の monitoring ダッシュボードで自動チェックされ、 閾値超過でアラート → モデル再学習トリガーとなる。 数式を言葉で読み解くことで、 「単なる統計量」ではなく「データ世界の変化を捉える物差し」として理解できる。
| 種類 | 数学的表現 | 意味 | SSDSE 例 |
|---|---|---|---|
| Covariate Drift | P(X) 変化、 P(Y|X) 不変 | 入力分布のみ変化 | 年次で人口構造変化 |
| Label Drift | P(Y) 変化 | 出力分布変化 | 出生数の県別比率変化 |
| Concept Drift | P(Y|X) 変化 | 入出力関係変化 | 出生率の決定要因変化 |
| Prior Probability Shift | P(Y) 変化、 P(X|Y) 不変 | クラス比率変化 | 大都市県の増加 |
これらは独立に発生しうる。 例えば「Covariate Drift だけ」「Concept Drift だけ」と分離して検出することで、 適切な対処(再学習 / 特徴量変更 / モデル変更)を選べる。
SSDSE-B-2026 の 47 都道府県データで学習した「人口 → 出生数」予測モデルを 2020 年に運用開始。 2026 年現在、 モデルの予測精度が劣化しているが原因が分からない。 「データそのものが変化しているのか」「人口と出生数の関係が変わったのか」を見分けたい。
データドリフト検出パイプラインを構築。 ① 各列の PSI を計算(Covariate Drift 検出)、 ② ターゲット変数の分布変化(Label Drift)、 ③ ref vs curr 分類器の精度(複合ドリフト)、 ④ ローリングウィンドウでの予測残差傾向(Concept Drift)。 evidently / nannyml ライブラリを使うか、 scipy で自前実装。
SSDSE-B-2026 の 2010 vs 2026 比較で、 「総人口」 PSI = 0.12(やや変化)、 「高齢者人口」 PSI = 0.31(大変化)、 「出生数」 PSI = 0.45(極大変化)。 さらに「人口 → 出生数」の関係性が、 2010 年は線形 R² = 0.96 だったが、 2026 年は R² = 0.82 に低下。 これは Concept Drift(少子化進行による関係性変化)の典型。
データドリフトは MLOps の中核課題。 ① 定期的なドリフト監視(毎日・毎週)、 ② アラート閾値の設定(PSI > 0.25 で警報)、 ③ 適切な対応戦略(再学習 / 特徴追加 / モデル変更)、 ④ ドリフト原因分析(COVID-19 のような外的要因か、 経年変化か)、 という 4 つのステップで MLOps を運用すべき。 SSDSE-B-2026 のような年次更新公的統計でも、 同じ予測モデルを毎年使い続けると徐々に精度劣化するため、 定期的なドリフトチェックが必須。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | import pandas as pd import numpy as np from scipy import stats # 参照データ(古い)と現データ(新しい)を仮想的に作る df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) # 簡略化のため、 偶数番目の県を ref、 奇数番目を curr とする ref = df.iloc[::2] curr = df.iloc[1::2] # 1) PSI 計算 def calc_psi(ref_values, curr_values, bins=10): bin_edges = np.linspace(min(ref_values.min(), curr_values.min()), max(ref_values.max(), curr_values.max()), bins + 1) ref_hist, _ = np.histogram(ref_values, bins=bin_edges) curr_hist, _ = np.histogram(curr_values, bins=bin_edges) ref_dist = ref_hist / ref_hist.sum() + 1e-10 curr_dist = curr_hist / curr_hist.sum() + 1e-10 psi = np.sum((ref_dist - curr_dist) * np.log(ref_dist / curr_dist)) return psi for col in ['A1101', 'A4101', 'A1303', 'F3101']: psi = calc_psi(ref[col].values, curr[col].values) interpretation = '安定' if psi < 0.1 else 'やや変化' if psi < 0.25 else '大変化' print(f'{col}: PSI = {psi:.3f} ({interpretation})') # 2) KS 検定 for col in ['A1101', 'A4101', 'A1303']: stat, p_value = stats.ks_2samp(ref[col].values, curr[col].values) print(f'{col}: KS = {stat:.3f}, p = {p_value:.3f}') # 3) 分類器ベースのドリフト検出 from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score X = pd.concat([ref, curr])[['A1101', 'A4101', 'A1303']].values y = np.concatenate([np.zeros(len(ref)), np.ones(len(curr))]) clf = RandomForestClassifier(n_estimators=100, random_state=42) acc = cross_val_score(clf, X, y, cv=5).mean() print(f'\nref vs curr 分類精度 = {acc:.3f}') print(f'→ 0.5 に近ければドリフトなし、 1.0 に近ければ大ドリフト') |
公的統計でも 5-10 年スパンで構造が変化するため、 「過去データで作った予測モデル」は 定期的なドリフトチェックと再学習が必須。
2 つの標本 \(X_1, \ldots, X_n\) と \(Y_1, \ldots, Y_m\) が同一分布から得られたかを検定。 累積分布関数 \(F_n, G_m\) の最大差 \(D = \sup_x |F_n(x) - G_m(x)|\) を検定統計量に使う。 帰無仮説下で \(\sqrt{\frac{nm}{n+m}} D\) は Kolmogorov 分布に従う。
1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd # ref_data / curr_data はこのあとのブロックで作っている。 # ここでは 2012〜2017 年を基準、 2018〜2023 年を現在として比べる _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) ref_data = _d[_d['SSDSE-B-2026'] <= 2017]['A1101'].astype(float) curr_data = _d[_d['SSDSE-B-2026'] >= 2018]['A1101'].astype(float) from scipy.stats import ks_2samp stat, p_value = ks_2samp(ref_data, curr_data) if p_value < 0.05: print(f'ドリフト検出: KS = {stat:.3f}, p = {p_value:.4f}') |
$$ \chi^2 = \sum_i \frac{(O_i - E_i)^2}{E_i} $$
O は観測度数、 E は期待度数。 自由度 (k-1) のカイ二乗分布で p 値を計算。 SSDSE で「都道府県別の事業所種別構成」変化の検定に使える。
順位ベースのノンパラメトリック検定。 「中央値の差」検定に使う。 正規性仮定不要。
$$ A^2 = -n - \frac{1}{n} \sum_i (2i-1) (\ln F(X_i) + \ln(1 - F(X_{n+1-i}))) $$
分布の 裾に重み付けされる。 KS より裾の差を検出しやすい。
$$ \text{MMD}^2 = \|\mu_P - \mu_Q\|_\mathcal{H}^2 $$
カーネル平均埋め込みの距離。 高次元・複雑な分布のドリフトに有効。 Alibi Detect で実装されている。
ストリーミングデータでドリフトを リアルタイムに検出する手法。
累積和(CUSUM)の変化点検出。 \(m_t = \sum_{i=1}^{t} (e_i - \bar e - \delta)\)。 \(m_t\) が最小値から閾値以上離れたら変化点。
適応的なウィンドウサイズでドリフトを検出。 ドリフトが検出されたらウィンドウを縮める。 scikit-multiflow に実装。
分類エラー率の上昇でドリフトを検出。 警告レベル(p+2σ)とドリフトレベル(p+3σ)の二段階。
スライディングウィンドウ内で KS 検定を繰り返し実行。 検定の連続失敗でドリフト宣言。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import pandas as pd from evidently.report import Report from evidently.metric_preset import DataDriftPreset, DataQualityPreset df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) # 参照データ(前半 24 県)と現データ(後半 23 県)を仮想的に分割 ref_data = df.iloc[:24] curr_data = df.iloc[24:] # ドリフトレポート生成 features = ['A1101', 'A1303', 'A4101', 'A5101', 'E4501'] report = Report(metrics=[ DataDriftPreset(num_features=features), DataQualityPreset(num_features=features), ]) report.run(reference_data=ref_data, current_data=curr_data) report.save_html('ssdse_drift_report.html') # 結果取得 result = report.as_dict() for feat_name in features: drift = result['metrics'][0]['result']['drift_by_columns'][feat_name] print(f'{feat_name}: ドリフト = {drift["drift_detected"]}, ' f'PSI = {drift.get("drift_score", "N/A")}') |
Evidently は数行のコードで包括的なドリフトレポートを HTML 形式で生成。 各特徴の分布変化、 統計的検定結果、 ビジュアライゼーションが含まれる。 MLOps パイプラインに組み込んで定期実行できる。
あなたは政府系研究所の MLOps エンジニア。 2020 年に開発した「県別出生数予測モデル」が、 2026 年に精度劣化を起こしている疑いがある。 SSDSE-B-2026 を題材に、 ドリフト検出から再学習までを物語形式で追ってみよう。
Day 1:問題発見。 「最近の予測がやけに外れる」とドメイン専門家から指摘。 過去 6 ヶ月の予測 vs 実測を見ると、 RMSE が 2020 年比で 1.4 倍に悪化。 ドリフトの可能性が高い。
Day 2:データ層の調査。 SSDSE 2010 vs 2026 を比較。 「総人口」PSI = 0.12(やや変化)、 「高齢者人口」PSI = 0.31(大)、 「出生数」PSI = 0.45(極大)。 Covariate Drift が見られる。
Day 3:関係性の調査。 「人口 → 出生数」の関係を 2010 vs 2026 で比較。 線形回帰の傾きが 2010 年に 0.0072 だったが、 2026 年は 0.0061。 これは Concept Drift。 単なるデータ変化ではなく、 関係性そのものが変わっている。
Day 4:原因究明。 経済学者と協議。 「未婚率上昇」「共働き世帯増加」「子育てコスト上昇」など、 出生決定要因の構造的変化が背景。 ドリフトは予期できた。
Day 5:対応策。 ① 最新データで再学習(短期対応)、 ② 「未婚率」「世帯所得」「保育所定員」など新変数追加(中期対応)、 ③ 県別パラメータの階層モデルへ移行(長期対応)、 を提案。
Day 6:実装。 evidently + airflow で日次ドリフト監視を構築。 PSI > 0.25 で Slack アラート、 連続 3 日で再学習トリガー。
Day 7:効果検証。 再学習モデルで RMSE が 25% 改善。 「予期せぬ精度劣化」を 事前検知できる体制が整った。 MLOps の完成。
| 手法 | 適用 | 長所 | 短所 |
|---|---|---|---|
| PSI | 単変量 | シンプル、 業界標準 | ビン設計に依存 |
| KS | 単変量(連続) | ノンパラ、 統計的 | サンプル大で過敏 |
| JS divergence | 単変量 | 対称・有界 | 直感的解釈薄 |
| Wasserstein | 単変量(連続) | 物理的解釈明確 | 計算コスト |
| 分類器ベース | 多変量 | 高次元対応 | 解釈性低 |
| MMD | 多変量 | 理論的 | カーネル選択 |
| Page-Hinkley | ストリーミング | オンライン | パラメータ調整必要 |
| ADWIN | ストリーミング | 適応的 | 遅延あり |
1 2 3 4 | ref_dist = ref.describe() curr_dist = curr.describe() diff = (curr_dist - ref_dist) / ref_dist print(diff) # 相対変化率 |
1 2 3 4 5 6 7 8 9 10 | def calc_psi(ref, curr, bins=10): edges = np.linspace(min(ref.min(), curr.min()), max(ref.max(), curr.max()), bins+1) p = np.histogram(ref, edges)[0] / len(ref) + 1e-10 q = np.histogram(curr, edges)[0] / len(curr) + 1e-10 return ((p - q) * np.log(p / q)).sum() for col in df.columns: if df[col].dtype in ['float64', 'int64']: print(f'{col}: PSI = {calc_psi(ref[col], curr[col]):.3f}') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import pandas as pd # ref / curr / numeric_cols を用意する _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) ref = _d[_d['SSDSE-B-2026'] <= 2017] curr = _d[_d['SSDSE-B-2026'] >= 2018] numeric_cols = ['A1101', 'A1301', 'A1303', 'A4101', 'L3221'] from scipy.stats import ks_2samp results = {} for col in numeric_cols: stat, p = ks_2samp(ref[col], curr[col]) results[col] = {'KS': stat, 'p': p, 'drift': p < 0.05} pd.DataFrame(results).T |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import numpy as np import pandas as pd # features を用意する(基準期と現在期を見分けられるかを試す列) _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) ref = _d[_d['SSDSE-B-2026'] <= 2017] curr = _d[_d['SSDSE-B-2026'] >= 2018] features = ['A1101', 'A1301', 'A1303', 'A4101', 'L3221'] X = pd.concat([ref, curr])[features].values y = np.r_[np.zeros(len(ref)), np.ones(len(curr))] from sklearn.ensemble import RandomForestClassifier clf = RandomForestClassifier().fit(X, y) print(f'分類精度: {clf.score(X, y):.3f}') # 0.5 ≒ ドリフトなし |
1 2 3 4 5 6 7 8 9 10 11 12 | import numpy as np import pandas as pd # p_ref / p_curr は「同じ区切りで数えた度数を確率に直したもの」 _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) _bins = np.histogram_bin_edges(_d['A1101'].astype(float), bins=10) p_ref = np.histogram(_d[_d['SSDSE-B-2026'] <= 2017]['A1101'], bins=_bins)[0] + 1e-9 p_curr = np.histogram(_d[_d['SSDSE-B-2026'] >= 2018]['A1101'], bins=_bins)[0] + 1e-9 p_ref, p_curr = p_ref / p_ref.sum(), p_curr / p_curr.sum() from scipy.spatial.distance import jensenshannon js_dist = jensenshannon(p_ref, p_curr, base=2) |
1 2 3 4 5 6 7 8 9 | import pandas as pd # ref_values / curr_values を用意する _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) ref_values = _d[_d['SSDSE-B-2026'] <= 2017]['A1101'].astype(float).values curr_values = _d[_d['SSDSE-B-2026'] >= 2018]['A1101'].astype(float).values from scipy.stats import wasserstein_distance w_dist = wasserstein_distance(ref_values, curr_values) |
1 2 3 4 5 | from evidently import Report from evidently.metric_preset import DataDriftPreset report = Report(metrics=[DataDriftPreset()]) report.run(reference_data=ref, current_data=curr) report.save_html('report.html') |
1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd # stream(1 件ずつ流し込む観測列)を用意する _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) stream = _d.sort_values('SSDSE-B-2026')['A1101'].astype(float).tolist() from skmultiflow.drift_detection import PageHinkley ph = PageHinkley() for i, x in enumerate(stream): ph.add_element(x) if ph.detected_change(): print(f'Drift detected at {i}') |
1 2 3 4 5 6 7 8 9 10 11 | import pandas as pd _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) stream = _d.sort_values('SSDSE-B-2026')['A1101'].astype(float).tolist() from skmultiflow.drift_detection import ADWIN adwin = ADWIN(delta=0.002) for x in stream: adwin.add_element(x) if adwin.detected_change(): print('Drift!') |
1 2 3 4 5 6 | import streamlit as st st.title('ドリフト監視ダッシュボード') for col in features: psi = calc_psi(ref[col], curr[col]) st.metric(label=col, value=f'{psi:.3f}', delta=f'{"⚠️ 警告" if psi > 0.25 else "✅ OK"}') |
SSDSE-B-2026 の 2010 年データ(仮想)と 2026 年データを比較した場合の、 各主要列のドリフト指標(架空想定の概算)。
| 列 | 意味 | 2010 平均 | 2026 平均 | PSI | KS p | 判定 |
|---|---|---|---|---|---|---|
| A1101 | 総人口 | 2.71M | 2.62M | 0.12 | 0.34 | やや変化 |
| A1303 | 高齢者人口 | 0.61M | 0.78M | 0.31 | 0.002 | 大変化 |
| A4101 | 出生数 | 22.8K | 14.7K | 0.45 | 0.0001 | 極大変化 |
| A5101 | 転入者数 | 52.0K | 48.5K | 0.08 | 0.45 | 安定 |
| E4501 | 大学数 | 17.0 | 17.4 | 0.04 | 0.78 | 安定 |
| F3101 | 新規求職者 | 85.0K | 62.1K | 0.28 | 0.005 | 大変化 |
| I5102 | 一般診療所数 | 2.16K | 2.20K | 0.06 | 0.62 | 安定 |
読み方:① 「出生数」「高齢者人口」が大きくドリフト(少子高齢化進行)、 ② 「求職者」も減少(就業構造変化)、 ③ 「総人口」「大学数」「医療機関」は比較的安定。 各列で異なる対応が必要:出生数モデルは至急再学習、 安定変数は監視継続。
| 用語 | 英語 | 意味 |
|---|---|---|
| データドリフト | Data Drift | 運用中にデータ分布が変化 |
| コンセプトドリフト | Concept Drift | 入出力関係 P(Y|X) の変化 |
| Covariate Shift | Covariate Shift | 入力分布 P(X) の変化 |
| Label Shift | Label Shift | 出力分布 P(Y) の変化 |
| PSI | Population Stability Index | 分布安定性指標 |
| KS | Kolmogorov-Smirnov | CDF 最大差の検定 |
| KL ダイバージェンス | Kullback-Leibler | 非対称分布距離 |
| JS ダイバージェンス | Jensen-Shannon | KL の対称化 |
| Wasserstein | Wasserstein Distance | 最適輸送距離 |
| MMD | Maximum Mean Discrepancy | カーネル平均埋め込み距離 |
| ADWIN | Adaptive Windowing | 適応ウィンドウ変化点検出 |
| DDM | Drift Detection Method | 誤差率ベースのドリフト検出 |
| Page-Hinkley | Page-Hinkley Test | CUSUM ベース変化点検出 |
| MLOps | Machine Learning Operations | ML 運用の総称 |
| monitoring | Model Monitoring | モデル監視 |
| Retraining | Model Retraining | モデル再学習 |
例:SSDSE-B 2020 (学習用) と 2026 (運用用) の高齢化率分布を比較し、 ドリフトを検知する。
合成データで訓練分布 p, 本番分布 q の KL(p||q) を計算する。
| クラス | p (訓練) | q (本番) |
|---|---|---|
| A | 0.50 | 0.30 |
| B | 0.30 | 0.40 |
| C | 0.20 | 0.30 |
1 2 3 4 5 | import numpy as np p = np.array([0.50, 0.30, 0.20]) q = np.array([0.30, 0.40, 0.30]) kl = (p * np.log(p/q)).sum() print(f"KL(p||q) = {kl:.4f} nat") |
💬 手計算 (Step 2) 0.0880 と Python 出力が完全一致。 ドリフトあり。
🎯 このコードでやること:SSDSE-B-2020(学習時の分布)と SSDSE-B-2026(本番時の分布)の「高齢化率」列を比較し、 PSI(Population Stability Index)を計算して入力分布の変化(covariate drift)を定量化する。 PSI は p log(p/q) の総和で KL ダイバージェンスに似た非対称指標で、 0.1 未満=安定 / 0.1〜0.25=要注意 / 0.25 以上=ドリフト発生のしきい値が業界標準。
📥 入力データ(SSDSE-B 抜粋):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import numpy as np import pandas as pd # ── この抜粋だけで動くように、「学習時」と「本番」の 2 つの分布を用意する ── # 同じ指標でも年度が違えば分布はずれる。2012 年を学習時、2023 年を本番に見立てる _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) _d = _d[_d['地域コード'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() _d['年度'] = pd.to_numeric(_d['年度'], errors='coerce') for _c in ['総人口', '65歳以上人口']: _d[_c] = pd.to_numeric(_d[_c], errors='coerce') _d['高齢化率'] = _d['65歳以上人口'] / _d['総人口'] * 100 df_train = _d[_d['年度'] == 2012] df_prod = _d[_d['年度'] == 2023] def psi(a, b, bins=10): edges = np.histogram_bin_edges(np.r_[a,b], bins=bins) p, _ = np.histogram(a, edges); q, _ = np.histogram(b, edges) p = p/p.sum() + 1e-6; q = q/q.sum() + 1e-6 return np.sum((p-q) * np.log(p/q)) print('PSI:', round(psi(df_train['高齢化率'], df_prod['高齢化率']), 4)) print('目安: 0.1 未満=安定 / 0.1〜0.25=要監視 / 0.25 以上=再学習を検討') |
📤 実行すると次の出力が得られる:
💬 結果の読み方:PSI=8.19 は 目安 0.25 の 30 倍以上、「即・再学習」の水準。 これは 2012 年と 2023 年の高齢化率の分布がほとんど重なっていないためで、 実際 2012 年の 47 県は 17.7〜30.7%、 2023 年は 22.8〜39.1% と帯そのものがずれている。 ビンによっては学習時の度数が 0 に近く、 PSI の $\log(p/q)$ 項が大きく効く。 11 年間で全 47 都道府県の高齢化率が一様に底上げ(covariate shift)したため、 学習時の高齢化率モデルでは「高齢化率 30%」を珍しい値として扱っていたが、 本番では普通の値になっている。 → 対応は (1) 再学習データに 2026 を追加 (2) 高齢化率を実年齢でなく「中央値からの偏差」に変換して時間不変な特徴量にする (3) 重要度サンプリングで本番分布に補正。 PSI を column 単位で監視すれば、 どの特徴量がドリフトの主因かまで分かる。
データドリフトの監視では「精度を見ていれば気づける」「全体平均が安定なら大丈夫」という前提が、 実運用ではほぼ崩れる。 ラベル到着の遅延・サブグループ単位の局所変化・再学習の過剰反応など、 統計の正しさと運用の判断が噛み合わない場面が連鎖的に発生する。 SSDSE-B-2026 で都道府県を一括平均すると沖縄や東京の異常変動が見えなくなるのと同じ構造である。
データドリフトを中心に、 4 種類のドリフト (Covariate / Label / Concept / Prior Shift)、 検出統計量 (PSI / KS / KL / Wasserstein / MMD)、 検出手法 (統計検定 / 距離 / 分類器 / Page-Hinkley)、 対応策 (再学習 / 増分学習 / 特徴量変更 / モデル変更 / キャリブレーション) を放射状に整理した。
2008 年の金融危機で「信用スコア → 債務不履行確率」モデルが破綻。 経済状況変化で関係性が変わった典型的 Concept Drift。 多くの銀行が再学習を実施。
マスク・トイレットペーパー・在宅勤務用品の需要が爆発的増加。 既存予測モデルは在庫不足を全く予測できず、 多くの企業で大規模機会損失。 ドリフト検出の重要性を世界に再認識させた事件。
病院 A で学習した診断 AI を病院 B にデプロイすると精度が劇的に低下。 患者属性、 撮影機器、 撮影プロトコルの違いによる Covariate Shift。 医療 AI の汎化問題として広く知られる。
広告キャンペーン変更、 ユーザー嗜好変化、 季節性などで CTR が常に変動。 オンライン学習 + ADWIN の組み合わせで対応。
詐欺手口は常に進化(敵対的シフト)。 検知モデルが新手口を学習する間に、 さらに新しい手口が登場するイタチごっこ。 連続的なドリフト対応が必要。
ドリフトを検出したあと、 どう対応するか? 6 つの戦略を整理。
月次・週次で全体再学習。 シンプルで運用しやすい。 SSDSE-B-2026 のような年次データなら年次更新が妥当。
PSI > 0.25 のような閾値超過で再学習発火。 効率的だが運用は複雑。
最新データだけで部分更新。 SGD ベースのモデル(ロジスティック回帰、 NN)で実装可能。 RAM 効率良。
複数モデルの重み付け平均で、 性能が悪いモデルの重みを下げる。 動的アンサンブル。
Covariate Shift 補正:\(w(x) = p_{\text{curr}}(x) / p_{\text{ref}}(x)\) で各サンプルを重み付け学習。 密度比推定(KLIEP, RuLSIF)で重みを得る。
深層学習の場合、 「ソースドメインとターゲットドメインの特徴を一致させる」損失(DANN, CDAN など)を加えて学習。
SSDSE-B-2026 を題材に、 本格的なドリフト監視パイプラインを Python で実装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | import pandas as pd import numpy as np from scipy import stats from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score class DriftMonitor: def __init__(self, reference_data, features): self.ref = reference_data self.features = features def calc_psi(self, ref_col, curr_col, bins=10): edges = np.linspace( min(ref_col.min(), curr_col.min()), max(ref_col.max(), curr_col.max()), bins+1) p = np.histogram(ref_col, edges)[0] / len(ref_col) + 1e-10 q = np.histogram(curr_col, edges)[0] / len(curr_col) + 1e-10 return ((p - q) * np.log(p / q)).sum() def calc_ks(self, ref_col, curr_col): return stats.ks_2samp(ref_col, curr_col) def calc_js(self, ref_col, curr_col, bins=20): edges = np.linspace( min(ref_col.min(), curr_col.min()), max(ref_col.max(), curr_col.max()), bins+1) p = np.histogram(ref_col, edges)[0] / len(ref_col) + 1e-10 q = np.histogram(curr_col, edges)[0] / len(curr_col) + 1e-10 m = 0.5 * (p + q) return 0.5 * (np.sum(p * np.log(p/m)) + np.sum(q * np.log(q/m))) def multivariate_drift(self, curr_data): """分類器ベースの多変量ドリフト検出""" X = pd.concat([self.ref, curr_data])[self.features].values y = np.r_[np.zeros(len(self.ref)), np.ones(len(curr_data))] clf = RandomForestClassifier(n_estimators=100, random_state=42) acc = cross_val_score(clf, X, y, cv=5).mean() return acc # 0.5: no drift, 1.0: complete drift def report(self, curr_data): results = [] for feat in self.features: psi = self.calc_psi(self.ref[feat], curr_data[feat]) ks_stat, ks_p = self.calc_ks(self.ref[feat], curr_data[feat]) js = self.calc_js(self.ref[feat], curr_data[feat]) drift_level = 'OK' if psi < 0.1 else 'Warning' if psi < 0.25 else 'Drift' results.append({ 'feature': feat, 'PSI': psi, 'KS': ks_stat, 'KS_p': ks_p, 'JS': js, 'level': drift_level }) mv_acc = self.multivariate_drift(curr_data) return pd.DataFrame(results), mv_acc # 使用例 df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) ref = df.iloc[:24] curr = df.iloc[24:] features = ['A1101', 'A1303', 'A4101', 'A5101', 'E4501'] monitor = DriftMonitor(ref, features) report, mv_drift = monitor.report(curr) print(report) print(f'\n多変量ドリフト精度: {mv_drift:.3f}') |
このクラスを Airflow / Prefect でスケジュール実行し、 結果を Slack / メール通知すれば、 ドリフト監視の自動化が完成。
ドリフトの背後にある 因果関係を理解することで、 効果的な対応策が見えてくる。
COVID-19、 自然災害、 戦争、 法改正など、 一度限りの大規模イベント。 対応:イベント発生時にモデルをロールバック or 一時停止。
少子高齢化、 テレワーク普及、 EC 化など、 長期的な社会構造の変化。 対応:定期再学習 + 特徴量追加。
詐欺手口の進化、 スパム対策との競争など。 対応:オンライン学習 + 早期検出。
データ収集方法の変更でドリフトが見える。 真のドリフトではない場合あり。 対応:データパイプラインの監視。
モデル予測が世界を変え、 その変化がデータに反映される。 推薦システムで典型的。 対応:フィードバック効果を明示的にモデリング。
ドリフトへの組織的対応は、 次の 4 段階で進化する:
ドリフトに気づかず、 精度劣化が事故化するまで放置。 「ある日突然モデルが大外しした」と気づく。
月次・四半期ごとに精度を手動確認。 劣化したら手動再学習。 規模が小さければ運用可能。
PSI / KS を自動計算してダッシュボード表示。 アラートが出たら人が判断して対応。 多くの組織で実用的なレベル。
ドリフト検出 → 自動再学習 → 検証 → デプロイまで全自動。 オンライン学習や CI/CD パイプラインで実現。 大規模組織や厳しい SLA を持つ場合のゴール。
SSDSE-B-2026 のような年次データを扱う研究プロジェクトはレベル 1-2 で十分。 ミッションクリティカルなレコメンド・詐欺検知システムはレベル 3 を目指す。
大規模言語モデル時代の新しいドリフト課題:
ユーザーのプロンプトの傾向が変化(流行語、 質問形式の変化)。 RAG システムでは「期待する応答」も変化。
OpenAI / Anthropic のモデルがバージョンアップで挙動変化。 「同じプロンプトで異なる応答」が発生。 ステークホルダーへの説明難。
LLM の知識カットオフ日以降の事実は把握できない。 時間経過で「古い情報を返す」ドリフト。 RAG で対処。
埋め込みモデルの更新でベクトル空間が変化。 既存ベクトル DB との整合性が壊れる。
LLM-as-a-Judge で品質スコアが時間とともに低下するケース。 自動評価のドリフト監視も必要。
データドリフトはモデル監視と MLOps の核心。
基準分布記録 → 推論データ監視 → ドリフト検出 (KS 検定/PSI) → 再学習トリガー → モデル更新の流れで、 検出閾値の設定が運用安定性を決める。
用語「data-drift」に関連する代表的な可視化を 3 図示す (公的データ参照)。



データドリフトは入力 / ラベル / 概念の三方向で監視し、 MLOps が再学習を駆動する。
共変量シフトなら共変量分布監視、 ラベルシフトなら出力分布監視、 概念ドリフトなら性能監視、 と「シフトの種類」で選ぶ。
最初の直感(電力需要の例)をもう一段だけ噛み砕く。 機械学習モデルは「学習した瞬間の世界」を暗黙の前提にする写真のようなものだ。 ところが現実の本番データは動画で、 時間とともに被写体が動いていく。 データドリフトとは、 学習時に撮った写真と、 いま流れている動画のフレームがどんどんズレていく現象だと思えばよい。 モデル自体は 1 ミリも変わっていないのに、 世界のほうが変わるので相対的に「時代遅れ」になり、 性能が静かに落ちていく。
ズレ方には 3 つの向きがある。 数式で言うと予測は同時分布 P(X, Y) = P(X)·P(Y|X) に依存するので、 どの因子が動くかで名前が変わる。
この 3 つは「見える場所」が違う。 共変量シフトは入力を眺めれば見える。 ラベルシフトは出力分布を眺めれば見える。 コンセプトドリフトは正解ラベルが届いて実性能を測るまで見えない。 だから検知の難しさは concept drift ≫ label shift ≳ covariate shift の順になる。 この「見えやすさの階段」を掴んでおくと、 後述の落とし穴が腑に落ちる。
📝 補足(実データで体感): SSDSE-B-2026 の 47 都道府県「高齢化率(65歳以上人口 A1303 ÷ 総人口 A1101)」は、 全県平均が 2012 年 25.62% → 2023 年 31.59% と 11 年で +5.97pt 単調上昇している(実測)。 これは典型的な共変量シフトで、 「高齢化率 30% は珍しい値」という 2012 年時点のモデルの感覚が、 2023 年には「ごく普通の値」に変わってしまう。 モデルを触らなくても入力分布が動く好例。
既出の「落とし穴」に、 実務で特に事故を生みやすい論点を追記する。 いずれも「統計指標としては正しいが、 運用判断としては間違う」タイプの罠である。
📝 補足(実データで「検知の地平線」を体感): 上の高齢化率で、 隣接年(2022→2023)の PSI は 0.043・KS は D=0.085, p=0.996 で「ドリフトなし」と判定される(実測)。 ところが同じ指標を 11 年幅(2012→2023)で比べると PSI=2.81・KS は D=0.745, p=5.9e-13 で「明白なドリフト」になる。 つまりゆっくりした累積ドリフトは 1 年刻みの監視ではほぼ不可視で、 長い基準期間と比べて初めて見える。 監視ウィンドウ幅の設計そのものが検知力を左右する、 という上記④⑤の実例である。
ここでは「検知(どう気づくか)」「補正(学習側でどう合わせるか)」「運用(いつ・どう再学習するか)」の 3 層に分けて発展トピックを整理する。
共変量シフトは P(Y|X) が不変なので、 学習サンプルを本番分布に「重み付け」し直すだけで理屈上は補正できる。 各学習点に重み w(x) = P_test(x) / P_train(x) を掛けて損失を再重み付けする。 この密度比は「分類器2標本検定」の予測確率から w(x) ≈ p(test|x)/p(train|x) として推定できる(直接密度比推定)。 実務では重みの分散が爆発しやすいのでクリッピングや自己正規化を併用する。
検知・補正・再学習を「人手の勘」ではなく仕組みに落とすのが MLOps の役割。 監視設計の要点は、 (a) 入力分布・予測分布・実性能の3 層を同時に見る、 (b) ベースラインを季節性を考慮して選ぶ、 (c) 閾値超過→原因究明→再学習→検証→再デプロイのループを自動化しつつ人間の承認ゲートを残す、 の 3 点。 継続的学習(Continuous Training, CT)はこのループをパイプライン化したもの。
このコードでやること: SSDSE-B-2026 の高齢化率を「学習時(2012)」「本番(2023)」「本番の直前年(2022)」で比べ、 KS・Wasserstein・PSI で長期ドリフトと隣接年の安定を同時に定量化する(すべて実測値)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import numpy as np, pandas as pd
from scipy import stats
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df['aging'] = df['A1303'] / df['A1101'] * 100 # 高齢化率(%) = 65歳以上 / 総人口
base = df[df['SSDSE-B-2026']==2012]['aging'].values # 47県・学習時ベースライン
cur = df[df['SSDSE-B-2026']==2023]['aging'].values # 47県・本番(11年後)
# (1) KS検定: 累積分布の最大差
print('KS 2012 vs 2023:', stats.ks_2samp(base, cur)) # D=0.745, p=5.9e-13
# (2) Wasserstein距離(最適輸送)
print('W:', stats.wasserstein_distance(base, cur)) # 5.96
def psi(b, c, bins=10):
e = np.quantile(b, np.linspace(0, 1, bins+1)); e[0] -= 1e-6; e[-1] += 1e-6
p = np.clip(np.histogram(b, e)[0] / len(b), 1e-4, None)
q = np.clip(np.histogram(c, e)[0] / len(c), 1e-4, None)
return np.sum((q - p) * np.log(q / p))
print('PSI 2012->2023:', psi(base, cur)) # 2.81 (>0.25 = 警報)
cur22 = df[df['SSDSE-B-2026']==2022]['aging'].values
print('PSI 2022->2023:', psi(cur22, cur)) # 0.043 (安定)
print('KS 2022->2023:', stats.ks_2samp(cur22, cur)) # D=0.085, p=0.996 |
💬 結果の読み方: 長期(2012→2023)では KS D=0.745・PSI=2.81 と警報レベルだが、 隣接年(2022→2023)では KS D=0.085・PSI=0.043 で安定判定。 同じ指標でも比較する基準期間の幅で結論が正反対になる。 これが「監視ウィンドウ設計」がドリフト検知の要である理由。 実運用では複数ウィンドウ幅を併用し、 短期の急変(トリガー再学習向け)と長期の累積(定期再学習・特徴量再設計向け)を分けて監視する。
深掘りは以下へ。 該当ページが未整備の項目はリンクにせずテキストで示す。
📝 補足: 「共変量シフト」「コンセプトドリフト」の独立解説ページは本用語集には未整備のため、 本ページ内の直感節・関連用語節を参照のこと(専用ページができ次第リンク化予定)。