「roc curve」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「roc curve」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。
これらのキーワードは「roc curve の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。
🍰 まずはやさしく
判定の正しさを測るグラフです。
モデルの性能を比べるために使います。
迷惑メールの判定などで役立ちます。
結論として大切なポイントを読みましょう。
閾値を変えたときの真陽性率vs偽陽性率の曲線
🍰 まずはやさしく
分類の評価に使う道具です。
どこで線を引くか決めるために使います。
病気の検査などの判定に似ています。
この用語の詳しい使い方を学びましょう。
医療診断(病気の検出)、 不正検知、 迷惑メール分類 — 「感度と特異度のバランスをどこに置くか」を決めるときの定番ツール。
このページは ROC 曲線 を解説する用語ページです。
カテゴリ:分類評価
ジャストインタイム型データサイエンス教育の一環として、必要な時に参照し、関連概念とともに学べる構成になっています。
基準ページ:correlation.html(149KB、12セクション、SSDSE-B 実値計算)と同等以上の品質を目指しています。
🍰 まずはやさしく
判定の厳しさを表す地図のようなものです。
正解と間違いのバランスを見るために使います。
スマホの顔認証の厳しさに似ています。
図を使って直感的に理解しましょう。
分類モデルは「スコア(0〜1)」を出すだけ。 「スコア > 閾値 → 陽性」と決めるのは人間です。
このトレードオフを「全閾値で図示」したのが ROC 曲線です。
ROC 曲線の位置取りと AUC の意味を、 3 点のインライン SVG で押さえる。
左上に近いほど良いモデル。 対角線(偶然ライン)は AUC=0.5。
AUC=0.95 ほぼ完璧/0.75 中程度/0.5 偶然と同じ。
閾値を下げると TPR ↑(取りこぼし減)/FPR ↑(誤検知増)。 用途で選ぶ。
スライダーを動かすと その場で 分布・ROC 曲線・AUC・混同行列が再計算されます。 「分離度を上げると曲線が左上に張り出して AUC が 1 に近づく」「閾値を動かすと曲線上の現在点(TPR, FPR)と混同行列が連動する」——この2つの感覚を手で掴んでください。 陽性 100 件・陰性 100 件の合成データ(等分散の正規分布)を、 台形則で数値積分して AUC を求めています。
閾値より右(スコア高)を「陽性」と予測。 陽性分布の右側=TP(緑)、 陰性分布の右側=FP(赤)。 重なりが大きいほど誤りが増えます。
| 予測 | |||
| 陽性 | 陰性 | ||
| 実際 | 陽性 | TP 50 |
FN 50 |
| 陰性 | FP 2 |
TN 98 |
|
ROC 曲線上の 1 点は、 ある閾値での 混同行列 を (FPR, TPR) の 2 数に圧縮したものです。 閾値を高→低に動かすと混同行列の 4 マスが連続的に変わり、 その軌跡が ROC 曲線になります。 上のミニ実験で「閾値スライダーを動かす=混同行列を組み替える=曲線上を移動する」が同じ操作だと確認できます。 縦軸 TPR = TP/(TP+FN)(=再現率/感度)、 横軸 FPR = FP/(FP+TN)(=1−特異度)。
ROC の横軸 FPR = FP/(FP+TN) は陰性の総数で割るため、 陰性が圧倒的に多い クラス不均衡 では FP が増えても FPR がなかなか上がらず、 ROC-AUC が楽観的に高く出ます。 「陽性を当てる質」を見たいときは、 分母に陰性を含まない Precision = TP/(TP+FP) を縦軸に取る PR 曲線(PR-AUC)を併用します。 目安として、 クラスがおおむね均衡なら ROC-AUC、 陽性がまれ(数% 以下)で「陽性の取りこぼし/誤検出」が主関心なら PR-AUC。 このページ下部の「実値で計算してみる」に、 SSDSE-B-2026 実データでの ROC-AUC と PR-AUC の比較例があります。
🍰 まずはやさしく
2つの割合を線で結んだグラフです。
正しく判定できた率を確認するために使います。
テストの合格ラインを決めることに似ています。
計算式と定義について詳しく読みましょう。
ROC曲線(ROC Curve):閾値を変えたときの真陽性率vs偽陽性率の曲線
ROC 曲線 の中心となる数式・定義は次の通りです。
$$ \mathrm{TPR} = \frac{TP}{TP+FN}, \quad \mathrm{FPR} = \frac{FP}{FP+TN} $$
| 記号 | 意味 |
|---|---|
| TP/FP/FN/TN | 混同行列の4要素 |
| TPR | 真陽性率=感度=Recall |
| FPR | 偽陽性率=1−特異度 |
| AUC | ROC曲線下面積(モデルの総合性能) |
ROC曲線(ROC Curve)は、 単に用語の定義を覚えるだけでは本当には理解できません。 なぜこの概念が生まれたのか、 どんな問題を解決するために導入されたのか、 類似の手法とどう違うのか — これらを意識することで、 初めて「使える知識」になります。
数式や Python コードはあくまで 道具。 道具の使い方を覚える前に、 その道具で何をしたいか(目的) を明確にすることが、 データサイエンス学習の鉄則です。
この用語は、 単独で存在するわけではなく、 多くの関連概念とネットワークを形成しています。 上の「関連用語」セクションに挙げたリンク先を1つずつ辿ると、 全体像が見えてきます。 特に:
理論を学ぶことと、 実務で使えることは別物です。 公的統計(SSDSE、 e-Stat 等)の実データで実装・実験することで、 教科書だけでは見えない罠 に気付けます。 たとえば:
これらは ROC曲線 に限った話ではなく、 データサイエンス全般に共通する作法です。 「落とし穴」セクションの内容と合わせて、 自分なりのチェックリストを作るとよいでしょう。
ROC曲線 を使った分析の 正しさを担保する ためには、 以下の観点で検証するのが定番です。
| 確認する点 | ROC曲線 で何を見るか |
|---|---|
| クラス不均衡で AUC を過信 | 極端な不均衡では PR-AUC の方が情報豊富。 |
| 運用閾値を曲線から選ばない | ROCはトレードオフ表示。 実運用閾値はビジネス制約で決める。 |
| AUC同じでも形が違う | AUC=0.8 の2モデルでも、 高 FPR 域 vs 低 FPR 域で性能差。 |
| 学習データの AUC | テストデータで評価しないと意味なし。 |
| 再現性 | 同じデータ・同じコードで同じ結果が出るか。このページの ▶ 実行ボタンで確かめられます |
ROC曲線 は分野横断で活躍する概念です。 業界別に見ると以下のような使われ方があります。
ROC曲線 を実際のデータで学ぶときは、 SSDSE(教育用標準データセット、 総務省統計局)が便利です。
これらは 統計センターの SSDSE ページ から CSV で直接ダウンロードできます。 上の Python コード例で data/raw/SSDSE-B-2026.csv としているのが、 まさにこれです。
実データで動かすことで、 教科書の例題では見えない 実務的な気づき(欠損のパターン、 単位の混在、 都道府県名の表記揺れ等)が得られます。
pip install pandas numpy scikit-learn matplotlib で揃います。utf-8 ではなく shift_jis や cp932 の場合がある(古い日本の公的統計に多い)。 encoding='cp932' を試してください。%matplotlib inline、 スクリプト実行なら plt.show() を忘れずに。 日本語フォントは matplotlib 用に別途設定(japanize-matplotlib 等)が必要。政府統計の総合窓口 e-Stat が公開する SSDSE-B-2026.csv(47都道府県×項目)を用いた具体的計算例を示します。
SSDSE-B-2026 から「人口100万超 = 陽性」のラベルを作り、出生数(A4101)で予測。閾値を変えて TPR/FPR を計算すると AUC ≒ 1.00(実測 0.997)と高い分離性能を得る(東京・大阪は明らかに高人口、地方は低人口で線形分離可能)。
| 項目 | 値・指標 |
|---|---|
| データ件数 | 47 都道府県 |
| 対象指標 | 人口・出生数・日本人人口など |
| 計算結果 | 上記説明参照 |
SSDSE-B-2026 から「人口 100 万人未満の県(少数クラス)」を予測する例を考える。 ROC は陽性率と陰性率の比率で評価するため、 クラス不均衡には鈍感だが、 PR 曲線(Precision-Recall)はクラス比率に敏感である。 ROC-AUC が 0.90 でも、 陽性が 5% しかない場合、 Precision は 0.3 程度しか出ないことも珍しくない。
🎯 このコードでやること:SSDSE-B-2026 の都道府県データを使って「人口 100 万人未満かどうか」を予測し、 ROC-AUC と PR-AUC を比較する。
📥 入力データ(SSDSE-B-2026 抜粋、 A1101=総人口、 A4101=出生数):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, average_precision_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) # 目的変数:総人口 100 万人未満(少数クラス=不均衡) y = (d['A1101'] < 1_000_000).astype(int).values # 説明変数:出生数(A4101) X = d[['A4101']].values clf = LogisticRegression(max_iter=1000).fit(X, y) prob = clf.predict_proba(X)[:, 1] print(f"ROC-AUC = {roc_auc_score(y, prob):.3f}") print(f"PR-AUC = {average_precision_score(y, prob):.3f}") print(f"陽性率 = {y.mean():.3f}") |
📤 実行すると次の出力が得られる:
💬 結果の読み方: ROC-AUC が 0.997 と非常に高く、 PR-AUC も 0.991 とほぼ同水準。 ただし不均衡が強くなる(陽性率が 5% 以下になる)ほど、 PR-AUC の方が先に低下し、 この差は拡大する。 ROC は「全体としての分離性能」、 PR は「陽性の取りこぼし/誤検出」を見るための指標と覚えるとよい。
ROC の縦軸 TPR = TP/(TP+FN) は「真陽性のうち何 % 拾えたか」(再現率)、 横軸 FPR = FP/(FP+TN) は「陰性のうち誤検知した割合」。 PR 曲線の縦軸 Precision = TP/(TP+FP) は「陽性と予測したもののうち本物は何 %」。 つまり ROC は陰性側の挙動も評価するが、 PR は陽性側のみに焦点を当てる。
AUC / Precision / Recall / F1 / 混同行列 / クラス不均衡 / ロジスティック回帰 / 交差検証 / ブートストラップ
合成 (FPR, TPR) ペアから AUC を計算する。
| 点 | FPR | TPR |
|---|---|---|
| 1 | 0.0 | 0.0 |
| 2 | 0.2 | 0.7 |
| 3 | 0.5 | 0.9 |
| 4 | 1.0 | 1.0 |
1 2 3 4 5 | import numpy as np fpr = np.array([0.0, 0.2, 0.5, 1.0]) tpr = np.array([0.0, 0.7, 0.9, 1.0]) auc = np.trapezoid(tpr, fpr) print(f"AUC: {auc}") |
💬 手計算 (Step 2) 0.785 と Python 出力が完全一致。
SSDSE-B-2026 などの実データを使った最小コード(12行):
1 2 3 4 5 6 7 8 9 10 11 12 13 | import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, roc_curve df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num = df.select_dtypes('number').dropna(axis=1) X = num.iloc[:, 1:5] y = (num.iloc[:, 0] > num.iloc[:, 0].median()).astype(int) m = LogisticRegression(max_iter=1000).fit(X, y) proba = m.predict_proba(X)[:, 1] # roc_auc_score は Python の float を返すので .round() ではなく round() を使う print('AUC:', round(roc_auc_score(y, proba), 3)) fpr, tpr, thr = roc_curve(y, proba) print('ROC点数:', len(fpr), '最初の閾値5個:', thr[:5].round(2)) |
※ data/raw/SSDSE-B-2026.csv は e-Stat SSDSE から取得した実データを想定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd import numpy as np from sklearn.metrics import roc_curve, roc_auc_score from sklearn.linear_model import LogisticRegression df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.dropna() y = (df.iloc[:, 3] > 1_000_000).astype(int) X = df.iloc[:, 4:5] model = LogisticRegression().fit(X, y) proba = model.predict_proba(X)[:, 1] fpr, tpr, _ = roc_curve(y, proba) print('AUC:', roc_auc_score(y, proba)) |
上記コードは pandas / numpy / scipy / sklearn / statsmodels の標準的なライブラリを用い、SSDSE-B-2026.csv を直接読み込んで計算します(合成データ不使用)。
ROC 曲線を中心に、 前提となる混同行列・TPR (再現率)・FPR、 一点で要約した AUC、 不均衡データで補完的な PR 曲線、 閾値選択 (Youden 指数)、 並列の Precision/Recall、 派生の感度・特異度との関係を 6 方向に配置した。 SSDSE-B-2026 から作る 2 値判定 (例: 人口減少県の予測) で、 閾値選択と AUC 評価の位置づけを俯瞰できる。
ROC 曲線を中心に、 上下左右に「学問領域・実務・公的統計」が並ぶ。 医学診断(病気あり/なし)、 信用与信(債務不履行あり/なし)、 不正検知(不正取引あり/なし)、 マーケティング(CV あり/なし)、 公衆衛生(陽性/陰性)はすべて ROC で評価される。 SSDSE-B-2026 では県別データを「特定指標が閾値超え/未満」で二値化することで ROC を作れる(例: 県別失業率 3% 以上を陽性と定義し、 別の説明変数から確率分類器を学習)。
この概念マップが示す本質は、 「学問領域は違っても二値判別の閾値性能を可視化する道具は同じ」という点。 ROC を 1 つ覚えれば、 ドメインを越えて分類器の品質会話が成立する。 同時に、 不均衡データでは PR 曲線への切り替えが必須という限界も常に意識しておく。
「ROC 曲線」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。
「閾値を動かすと診断性能がどう変わるか」 を 1 つの曲線で可視化する ROC は、 がん診断・スパム判定など二値分類の標準ツールであり、 閾値選択の意思決定を支える。
「ROC 曲線」を分類モデル評価に使うとき、 クラス不均衡と運用閾値で判定する。
SSDSE 系の社会データで分類モデルを作るとき、 まず ROC 曲線を描いて AUC を見るのは標準。 ただし「東京都を当てるか?」のような極端な不均衡では PR-AUC を併記する。 scikit-learn では roc_curve・roc_auc_score で計算可能。
ここから先は、 ROC 曲線を「単に知っている」から「使える」レベルへ引き上げるための濃密な解説です。 SSDSE-B-2026(47 都道府県×多変量)を題材に、 数式・実装・落とし穴を統合的に扱います。
ROC 曲線の正式定義を、 確率変数の言葉で書き直すと次のようになります。
$$ \mathrm{ROC}(t) = \big(\mathrm{FPR}(t),\; \mathrm{TPR}(t)\big), \quad t \in [0, 1] $$
$$ \mathrm{TPR}(t) = P(\hat{p}(X) > t \mid Y = 1), \quad \mathrm{FPR}(t) = P(\hat{p}(X) > t \mid Y = 0) $$
$$ \mathrm{AUC} = \int_0^1 \mathrm{TPR}(\mathrm{FPR}^{-1}(u))\, du = P(\hat{p}(X_+) > \hat{p}(X_-)) $$
| 記号 | 意味 | SSDSE-B-2026 での具体例 |
|---|---|---|
| $Y \in \{0, 1\}$ | 真のクラスラベル | 「総人口 100 万超」= 1, それ以外 = 0 |
| $\hat{p}(X)$ | モデルが出す陽性スコア(確率) | ロジスティック回帰の predict_proba |
| $t$ | 判定閾値(0 → 1 に動かす) | 陽性と予測する確率の境界 |
| $\mathrm{TPR}(t)$ | 真陽性率(再現率) | 「100 万超」県のうち、 スコア > t と判定された割合 |
| $\mathrm{FPR}(t)$ | 偽陽性率 | 「100 万以下」県のうち、 誤って スコア > t になった割合 |
| $X_+, X_-$ | 陽性・陰性の特徴量 | 陽性都道府県 vs 陰性都道府県 |
| $\mathrm{AUC}$ | 曲線下面積 | 「ランダム陽性陰性ペアでスコアが正しく順序付く確率」 |
まず、 ROC 曲線を「手で作る」プロセスを体感します。 47 都道府県を「総人口 100 万超 = 陽性」とラベル付けすると、 2023 年データでは陽性が 37 件、 陰性が 10 件です。 単純な予測指標として「日本人人口」を使い、 これを閾値で切るだけで ROC を作れます。
| 閾値(日本人人口) | TP | FN | FP | TN | TPR | FPR |
|---|---|---|---|---|---|---|
| 0(全て陽性予測) | 37 | 0 | 10 | 0 | 1.000 | 1.000 |
| 500,000 | 37 | 0 | 10 | 0 | 1.000 | 1.000 |
| 800,000 | 37 | 0 | 3 | 7 | 1.000 | 0.300 |
| 1,000,000 | 36 | 1 | 0 | 10 | 0.973 | 0.000 |
| 1,500,000 | 24 | 13 | 0 | 10 | 0.649 | 0.000 |
| 2,000,000 | 15 | 22 | 0 | 10 | 0.405 | 0.000 |
| 3,000,000 | 10 | 27 | 0 | 10 | 0.270 | 0.000 |
| +∞(全て陰性予測) | 0 | 37 | 0 | 10 | 0.000 | 0.000 |
このケースでは「日本人人口」と「総人口」が極めて高相関(r ≈ 0.999)なので、 閾値 100 万付近で TPR ≈ 0.97, FPR = 0.000 という 左上隅 に近い曲線が描けます(スコア順位で見れば完全分離で AUC = 1.0)。 つまり AUC ≈ 1.0 の理想的な分類です。 ROC 曲線が「左上隅に張り付くほど良い」というのは、 こうした例で実感できます。
🎯 このコードでやること:SSDSE-B-2026 (2023 年) の 47 都道府県を「総人口 100 万超 = 陽性」とラベル付けし、 「日本人人口(A1102)」と「出生数(A4101)」を特徴量にロジスティック回帰を学習。 sklearn.metrics.roc_curve / roc_auc_score で ROC と AUC を計算する。
📥 入力データ:SSDSE-B-2026.csv (cp932 エンコード、 2 行目を skip)
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_curve, roc_auc_score from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) # 陽性ラベル: 総人口 100 万超 y = (d['A1101'] > 1_000_000).astype(int).values # 特徴量: 日本人人口 (A1102) と 出生数 (A4101) X = d[['A1102','A4101']].values X = StandardScaler().fit_transform(X) model = LogisticRegression().fit(X, y) proba = model.predict_proba(X)[:, 1] fpr, tpr, th = roc_curve(y, proba) auc = roc_auc_score(y, proba) print(f'陽性数 (人口100万超) : {y.sum()}') print(f'陰性数 (人口100万以下): {(1-y).sum()}') print(f'AUC : {auc:.4f}') print(f'\nROC 曲線の代表点:') for i in [0, len(fpr)//4, len(fpr)//2, 3*len(fpr)//4, -1]: print(f' 閾値={th[i]:.3f} -> FPR={fpr[i]:.3f}, TPR={tpr[i]:.3f}') |
📤 実行結果:
💬 結果の読み方:AUC = 1.0000 で完全分離。 これは「日本人人口」と「総人口」が単調に対応するため当然の結果。 ROC は左上隅 (0, 1) を通る理想形になる。 ただし汎化性能の指標としては overfitting に注意(同じデータで学習・評価しているため)。 本来は学習用と評価用を分けるべき。
🎯 このコードでやること:上記の完全分離が「データを見ているから」なのか「本当に分離可能だから」なのかを区別するため、 5-fold cross validation で AUC を測定する。
📥 入力データ:前コードの X, y
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score, StratifiedKFold from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 1_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A1102','A4101']].values) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0) auc_cv = cross_val_score(LogisticRegression(), X, y, cv=cv, scoring='roc_auc') print(f'CV AUC 各 fold: {auc_cv.round(4)}') print(f'CV AUC 平均 : {auc_cv.mean():.4f}') print(f'CV AUC 標準偏差: {auc_cv.std():.4f}') |
📤 実行結果:
💬 結果の読み方:CV 平均 AUC = 1.00 で全 fold とも完全分離、 標準偏差も 0.000。 汎化性能も極めて高く、 ロジスティック回帰でも本タスクは安定に解ける。 これは「総人口」(およびそれと高相関な日本人人口・出生数)が線形分離可能な綺麗な構造を持つため。
🎯 このコードでやること:人口 300 万超(東京・神奈川・大阪等の少数)を陽性とし、 不均衡データ環境で ROC と PR を比較。 不均衡時には PR の方が情報量が多いことを実証する。
📥 入力データ:SSDSE-B-2026 全 47 県、 ラベルを「総人口 300 万超」(陽性 10 件 / 陰性 37 件)に変更。
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, average_precision_score from sklearn.metrics import precision_recall_curve, roc_curve from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) # 不均衡ラベル: 人口 300 万超 y = (d['A1101'] > 3_000_000).astype(int).values print(f'陽性数: {y.sum()}, 陰性数: {(1-y).sum()}, 陽性率: {y.mean():.3f}') # 特徴量: 出生数のみ (意図的に弱い指標) X = StandardScaler().fit_transform(d[['A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] auc = roc_auc_score(y, proba) ap = average_precision_score(y, proba) # PR-AUC print(f'\nROC-AUC: {auc:.4f}') print(f'PR-AUC : {ap:.4f} (ベースライン = 陽性率 {y.mean():.3f})') # 上位 N 件抽出時の Precision order = np.argsort(proba)[::-1] for n in [3, 5, 8, 10, 15]: sel = order[:n] p = y[sel].sum() / n print(f' 上位{n}件選択時 Precision={p:.3f}') |
📤 実行結果:
💬 結果の読み方:SSDSE-B-2026(2023)の実測では、 出生数 A4101 だけで「人口 300 万超の 10 県」を完全に分離でき、 ROC-AUC・PR-AUC ともに 1.0000(PR-AUC のベースライン=陽性率 0.213)。 上位 10 件までは Precision=1.000、 11 件目以降で初めて陰性が混じる(上位 15 件で 0.667=10/15)。 このデータでは 出生数 が強力な予測子であるため、 両指標に差が出なかった。
📝 より正確な分析:本コードの狙いは「不均衡データで ROC-AUC が楽観的に高く出る一方、 PR-AUC はベースライン(陽性率)を基準にするため実用価値を見抜きやすい」という両者の乖離の実証でした。 しかし現行の SSDSE-B-2026 実データでは 出生数 と人口 300 万超ラベルがほぼ完全に線形分離するため、 ROC-AUC=PR-AUC=1.0000 となり乖離が現れません(=予測が易しすぎる)。 乖離を体験したい場合は、 (a) より弱い単一特徴量に替える、 (b) 陽性をさらに稀(例: 人口 800 万超=3〜4 県)にする、 (c) 特徴量にノイズを加える、 のいずれかで「ROC は高いのに PR は伸びない」状況を作れます。 教育上の主張(不均衡では PR-AUC を併読せよ)は一般論として有効ですが、 この具体データは反例に近い「易しい」ケースである点に注意してください。
🎯 このコードでやること:ROC 曲線上で最適閾値を選ぶ。 (a) Youden 指数 J = TPR - FPR を最大化、 (b) F1 スコアを最大化、 の 2 つの基準を比較する。
📥 入力データ:コード 3 の y, proba
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_curve, f1_score, confusion_matrix from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 3_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] fpr, tpr, th = roc_curve(y, proba) # Youden 指数 J = TPR - FPR youden = tpr - fpr idx_y = np.argmax(youden) print(f'Youden 最適閾値: {th[idx_y]:.4f}') print(f' TPR={tpr[idx_y]:.3f}, FPR={fpr[idx_y]:.3f}, J={youden[idx_y]:.3f}') # F1 最大化 f1s = [] for t in th[1:]: # 閾値 +∞ を除く pred = (proba >= t).astype(int) f1s.append(f1_score(y, pred, zero_division=0)) f1s = np.array(f1s) idx_f = np.argmax(f1s) print(f'\nF1 最適閾値 : {th[1:][idx_f]:.4f}') print(f' F1={f1s[idx_f]:.3f}') # 各閾値で混同行列を表示 for t, name in [(th[idx_y], 'Youden'), (th[1:][idx_f], 'F1')]: pred = (proba >= t).astype(int) cm = confusion_matrix(y, pred) print(f'\n{name} 閾値 t={t:.3f} の混同行列:') print(f' TN={cm[0,0]:2d} FP={cm[0,1]:2d}') print(f' FN={cm[1,0]:2d} TP={cm[1,1]:2d}') |
📤 実行結果:
💬 結果の読み方:SSDSE-B-2026(2023)の実測では 出生数 が人口 300 万超を完全分離するため、 Youden 指数・F1 とも同じ閾値 t=0.231 で最大化され、 いずれも偽陽性 0・見逃し 0(J=1.000, F1=1.000)。 完全分離のケースでは 2 基準が一致します。
📝 より正確な分析:本コードは本来「Youden は見逃しを嫌い偽陽性を 1 件許容、 F1 は無駄打ちを嫌い陽性を 1 件見逃す」という基準ごとの閾値の食い違いを示す教材でした。 しかし現行実データでは完全分離が起きるため両基準が一致し、 食い違いは観測されません。 食い違いを再現するには、 特徴量を弱めるか陽性クラスをより稀にして ROC 曲線が左上隅を通らない(=偽陽性と見逃しがトレードオフになる)状況を作ってください。 業務コストに応じて Youden(見逃し重視)と F1(精度重視)を使い分ける、 という枠組み自体は不変です。
🎯 このコードでやること:SSDSE-B-2026 の都道府県を「人口規模」で 3 クラス(小: <100 万、 中: 100-300 万、 大: ≥300 万)に分け、 多クラス分類で AUC を計算する。 macro/micro/weighted 平均を比較する。
📥 入力データ:SSDSE-B-2026.csv
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score from sklearn.preprocessing import StandardScaler, label_binarize df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) # 3 クラスラベル def to_class(p): if p < 1_000_000: return 0 if p < 3_000_000: return 1 return 2 # ブラウザ版は 32bit 環境なので、int64 のままだと label_binarize が # 安全にキャストできず落ちる。Python の list を渡して型を任せる。 y = d['A1101'].apply(to_class).tolist() print(f'クラス分布: {np.bincount(y)}') # 特徴量 X = StandardScaler().fit_transform(d[['A1102','A4101','A4103']].values) proba = LogisticRegression().fit(X, y).predict_proba(X) y_bin = label_binarize(y, classes=[0, 1, 2]) # macro AUC: 各クラス AUC の単純平均 auc_macro = roc_auc_score(y_bin, proba, average='macro', multi_class='ovr') # micro AUC: クラスを区別せず全予測を統合 auc_micro = roc_auc_score(y_bin, proba, average='micro', multi_class='ovr') # weighted AUC: クラスサイズで重み付け auc_w = roc_auc_score(y_bin, proba, average='weighted', multi_class='ovr') print(f'\nmacro AUC: {auc_macro:.4f}') print(f'micro AUC: {auc_micro:.4f}') print(f'weighted AUC: {auc_w:.4f}') # クラス毎 AUC for c in range(3): auc_c = roc_auc_score(y_bin[:, c], proba[:, c]) print(f' クラス{c} AUC: {auc_c:.4f}') |
📤 実行結果:
💬 結果の読み方:クラス 2(大)は完全分離 AUC=1.0、 クラス 0(小)0.970・クラス 1(中)0.969 と中小クラスがやや難しい(境界が接する)。 macro=0.980 は各クラス AUC の単純平均、 weighted=0.976 はサンプル数で重み付け、 micro=0.965 は全 one-vs-rest 予測をプールした平均で、 この例では最も低く出る。 3 種の平均は必ずしも一致せず、 クラス不均衡なら macro、 全予測を等価に見たいなら micro と、 目的に応じて選ぶ。
scikit-posthocs 等で計算可能。
業務的に「FPR ≤ 0.1 の範囲のみが意味を持つ」場合、 部分 AUC(pAUC)を使います。
$$ \mathrm{pAUC}(t_0) = \int_0^{t_0} \mathrm{TPR}(\mathrm{FPR}^{-1}(u))\, du $$
| 記号 | 意味 |
|---|---|
| $t_0$ | FPR の許容上限(例:0.1 = 偽陽性 10% まで) |
| $\mathrm{pAUC}(t_0)$ | FPR が $[0, t_0]$ の範囲での AUC |
| 正規化 pAUC | $\mathrm{pAUC}(t_0) / t_0$ で範囲 [0.5, 1] に揃える |
🎯 このコードでやること:FPR ≤ 0.1 の範囲のみで AUC を計算し、 全 AUC との差を確認する。
📥 入力データ:コード 3 の y, proba
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_curve, roc_auc_score from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 3_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] fpr, tpr, _ = roc_curve(y, proba) # 全 AUC auc_full = roc_auc_score(y, proba) # 部分 AUC (FPR <= 0.1) mask = fpr <= 0.1 # 区間内の台形積分 pauc = np.trapz(tpr[mask], fpr[mask]) pauc_norm = pauc / 0.1 # 正規化 print(f'全 AUC : {auc_full:.4f}') print(f'部分 AUC (FPR<=0.1): {pauc:.4f}') print(f'正規化部分 AUC : {pauc_norm:.4f}') print(f'\n→ FPR<=0.1 の範囲内では TPR を平均 {pauc_norm:.3f} 達成') |
📤 実行結果:
💬 結果の読み方:全 AUC は 1.0000(完全分離)。 このとき ROC 曲線は (0,0)→(0,1)→(1,1) と左上隅を通り、 FPR≤0.1 の区間に含まれる点はすべて FPR=0 に重なる。 台形公式は FPR 方向の幅(dx)で面積を測るため、 幅 0 の区間では部分 AUC が 0.0000 と計算される。 これは「性能が低い」のではなく、 完全分類器では部分 AUC の台形近似が退化するという計算上のアーティファクトである。
📝 より正確な分析:本コードの狙いは「全 AUC が高くても、 厳しい運用制約(FPR≤10%)下では拾える真陽性が限られる」ことを部分 AUC で可視化する点にありました。 しかし SSDSE-B-2026(2023)実データでは 出生数 が人口 300 万超を完全分離するため全 AUC=1.0 となり、 部分 AUC は上記の退化により 0.0000 になります(TPR が低いのではなく、 台形の底辺が 0 のため)。 部分 AUC が意味を持つのは ROC が (0,1) 隅を通らず、 低 FPR 域で TPR が徐々に立ち上がる不完全な分類器の場合です。 退化を避けたい場合は、 fpr に低 FPR 側の格子点を線形補間してから積分するか、 より弱い特徴量・稀な陽性クラスで ROC を非自明にしてください。 「許容偽陽性率を起点に部分 AUC を測る」という実務的枠組み自体は有効です。
| 指標 | 定義 | ROC との関係 | 使いどころ |
|---|---|---|---|
| 感度 (TPR) | TP / (TP+FN) | ROC の縦軸 | 見逃し回避が重要 |
| 特異度 (TNR) | TN / (TN+FP) | 1 − FPR | 誤検出回避が重要 |
| 適合率 (Precision) | TP / (TP+FP) | PR 曲線の縦軸 | 陽性予測の品質重視 |
| F1 スコア | 2·P·R / (P+R) | 特定閾値での要約 | 不均衡データの単一指標 |
| AUC-ROC | ROC 曲線下面積 | 順位の妥当性 | 閾値非依存の総合指標 |
| AUC-PR (AP) | PR 曲線下面積 | 不均衡で頑健 | 陽性が稀なケース |
| Brier スコア | $\frac{1}{n}\sum(\hat{p}-y)^2$ | 校正性能も含む | 確率予測の校正評価 |
sklearn.metrics.roc_curve、 sklearn.metrics.roc_auc_score、 sklearn.metrics.RocCurveDisplay のドキュメントここでは ROC 曲線が実社会でどのように使われ、 どんな落とし穴が現場で生じるかを、 3 つの典型ケースで詳述します。
| 要素 | 内容 |
|---|---|
| 陽性 | がん患者 |
| 陰性 | 非がん患者 |
| 陽性率 | 0.5〜2%(極端な不均衡) |
| FP のコスト | 不要な精密検査・患者の不安 |
| FN のコスト | がん見逃し(致命的) |
| 運用閾値 | FPR を犠牲にして TPR を 0.99+ にする |
| 評価指標 | pAUC (FPR ≤ 0.1)、 sensitivity at fixed specificity |
| 要素 | 内容 |
|---|---|
| 陽性 | 債務不履行(デフォルト)顧客 |
| 陰性 | 正常返済顧客 |
| 陽性率 | 2〜5% |
| FP のコスト | 機会損失(融資せず利益を逃す) |
| FN のコスト | 貸倒損失(融資金額の数倍) |
| 運用閾値 | 期待利益 = (1-FP)·利息 - FN·貸倒額 を最大化 |
| 評価指標 | KS 統計量 = max(TPR - FPR)、 Gini = 2·AUC - 1 |
| 要素 | 内容 |
|---|---|
| 陽性 | DM に反応して購入する顧客 |
| 陰性 | 反応しない顧客 |
| 陽性率 | 1〜3% |
| FP のコスト | DM 発送費(1 通あたり 100 円程度) |
| FN のコスト | 機会損失(潜在購入額の数倍) |
| 運用閾値 | 「上位 10% に DM 送付」のような予算制約 |
| 評価指標 | Lift Chart、 Top-K Precision、 Cumulative Gains |
🎯 このコードでやること:SSDSE-B-2026 を題材に、 「人口大の県」を陽性とみなしてマーケティングシナリオを擬似的に作成。 上位 K% の Lift を計算する。
📥 入力データ:SSDSE-B-2026.csv (2023)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 2_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101','A1102']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] # スコア降順でソートし、 上位 K% の精度・Lift を計算 order = np.argsort(proba)[::-1] y_sorted = y[order] base_rate = y.mean() print(f'ベースライン (陽性率): {base_rate:.3f}') print(f'\n{"上位%":<8}{"件数":<8}{"陽性数":<8}{"Precision":<12}{"Lift":<8}') for pct in [0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0]: k = max(1, int(len(y) * pct)) p = y_sorted[:k].mean() lift = p / base_rate if base_rate > 0 else float('inf') print(f'{pct*100:<8.0f}{k:<8}{y_sorted[:k].sum():<8}{p:<12.3f}{lift:<8.2f}') |
📤 実行結果:
💬 結果の読み方:上位 30% (14 件) を選ぶと Precision = 1.0、 Lift = 2.94(ランダム送付の約 2.9 倍効率)。 マーケでは「予算で 30% しか DM を送れない」状況下、 モデルを使うことで反応率を約 3 倍にできる、 と経営層に説明できる。 これは ROC AUC を単独で見るより遥かにビジネス文脈で伝わる。
🎯 このコードでやること:AUC の点推定だけでなく、 Bootstrap リサンプリングで 95% 信頼区間を算出。 小サンプル時の不確実性を可視化する。
📥 入力データ:SSDSE-B-2026 (2023)
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 2_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] # 点推定 auc_point = roc_auc_score(y, proba) print(f'点推定 AUC: {auc_point:.4f}') # Bootstrap (1000 回) rng = np.random.default_rng(0) aucs = [] n = len(y) for _ in range(1000): idx = rng.choice(n, size=n, replace=True) if y[idx].sum() in (0, n): # 全陽性 or 全陰性は AUC 定義不可 continue aucs.append(roc_auc_score(y[idx], proba[idx])) aucs = np.array(aucs) ci_low, ci_high = np.percentile(aucs, [2.5, 97.5]) print(f'Bootstrap 平均: {aucs.mean():.4f}') print(f'95% CI : [{ci_low:.4f}, {ci_high:.4f}]') print(f'幅 : {ci_high - ci_low:.4f}') print(f'\n→ 小サンプル (n=47) のため CI 幅が広く、 AUC の不確実性大') |
📤 実行結果:
💬 結果の読み方:点推定 0.986 だが、 95% CI は [0.953, 1.000]。 サンプルが 47 件と小さいため、 AUC のばらつきが目立つ。 実務では 必ず CI を併記 し、 「AUC = 0.986 (95% CI: 0.953-1.000)」のような形で報告する。 単一値だけ報告するのは統計的に不誠実。
🎯 このコードでやること:ロジスティック回帰、 ランダムフォレスト、 勾配ブースティングの 3 モデルで AUC を比較。 同じデータ上で公平に評価する。
📥 入力データ:SSDSE-B-2026 (2023)
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 2_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A1102','A4101','A4103']].values) models = { 'LogisticRegression': LogisticRegression(), 'RandomForest ': RandomForestClassifier(n_estimators=100, random_state=0), 'GradientBoosting ': GradientBoostingClassifier(n_estimators=100, random_state=0), } cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0) print(f'{"モデル":<22}{"平均 AUC":<12}{"標準偏差":<12}{"各 fold":<35}') for name, m in models.items(): aucs = cross_val_score(m, X, y, cv=cv, scoring='roc_auc') print(f'{name:<22}{aucs.mean():<12.4f}{aucs.std():<12.4f}{aucs.round(3).tolist()}') |
📤 実行結果:
💬 結果の読み方:RF が平均 1.000 で最良、 LR と GB が 0.967 で並ぶ。 各モデルとも 1 つの fold(境界例の県を含む)でのみ崩れ、 GB は最悪 fold で 0.833 まで低下。 47 件のような小データでは fold 構成しだいで AUC が大きく振れるため、 単一の平均値でなく fold ごとのばらつき(標準偏差)まで見て安定性を評価する のが要点。 このタスクはもともと線形分離しやすく、 どのモデルも高い AUC を出す。
$$ \mathrm{AUC} = \frac{1}{n_+ n_-} \sum_{i: y_i=1} \sum_{j: y_j=0} \mathbb{1}[\hat{p}_i > \hat{p}_j] $$
これは Mann-Whitney U 統計量と等価です。 ここで $n_+, n_-$ はそれぞれ陽性数・陰性数、 $\hat{p}_i$ は予測スコア。 同点(タイ)が存在する場合は 0.5 を加える慣習があります。
$$ \mathrm{AUC}_{\text{tied}} = \frac{1}{n_+ n_-} \sum_{i,j} \left( \mathbb{1}[\hat{p}_i > \hat{p}_j] + 0.5 \cdot \mathbb{1}[\hat{p}_i = \hat{p}_j] \right) $$
scikit-learn の roc_auc_score はこの「タイ込み」式を使うため、 離散スコア(決定木の確率など)でも安定して動作します。
| 年代 | 事項 |
|---|---|
| 1940 年代 | 第二次大戦のレーダー信号検出で「Receiver Operating Characteristic」が誕生 |
| 1960 年代 | 心理物理学(信号検出理論)に応用 |
| 1970-80 年代 | 医療診断(特に放射線科)で標準評価法に |
| 1990 年代 | 機械学習・データマイニング分野で普及 |
| 2000 年代 | 不均衡データ問題で PR 曲線が台頭、 ROC との使い分けが議論 |
| 2010 年代 | 深層学習でも標準指標として定着 |
| 2020 年代 | 公平性評価(fairness)と ROC の関係が新たな研究テーマに |
ROC 曲線を「自分の手で描く」「自分のデータに適用する」流れを、 失敗パターンも含めて段階的に解説します。
ROC を計算するには、 以下 2 つが必要です:
pos_label 引数で対応可能だが、 デフォルトは 1 を陽性とする。
ROC を描く前に、 まず 陽性と陰性のスコア分布 をヒストグラムで重ね描きしてください。 ここで「分離が見えない」なら ROC を見ても情報は少ない。
🎯 このコードでやること:陽性・陰性のスコア分布をヒストグラムで重ね描きし、 ROC との対応を理解する。
📥 入力データ:コード 9 までの y, proba
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 | import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 2_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] # 分布サマリ print(f'陽性スコア統計: 平均 {proba[y==1].mean():.3f}, 標準偏差 {proba[y==1].std():.3f}') print(f'陰性スコア統計: 平均 {proba[y==0].mean():.3f}, 標準偏差 {proba[y==0].std():.3f}') print(f'\n陽性スコアの分位点 (Q1, median, Q3):') print(f' {np.percentile(proba[y==1], [25, 50, 75]).round(3)}') print(f'陰性スコアの分位点 (Q1, median, Q3):') print(f' {np.percentile(proba[y==0], [25, 50, 75]).round(3)}') # 分布の重なり判定 overlap_lo = max(proba[y==0].min(), proba[y==1].min()) overlap_hi = min(proba[y==0].max(), proba[y==1].max()) if overlap_lo < overlap_hi: n_overlap = ((proba >= overlap_lo) & (proba <= overlap_hi)).sum() print(f'\n重なり区間 [{overlap_lo:.3f}, {overlap_hi:.3f}] に {n_overlap}/{len(y)} 件') else: print(f'\n完全分離 (重なり区間なし) → AUC = 1.0') |
📤 実行結果:
💬 結果の読み方:陽性平均 0.677、 陰性平均 0.167 と分離良好。 重なり区間 [0.252, 0.299] に 6 件(境界付近の県)。 ヒストグラムを描けば、 陰性は左に固まり陽性は右に広く散る山が見える。 AUC = 0.986(人口 200 万超・出生数モデル)はこの分離度合いの数値表現。
ROC 曲線上で「業務的最適点」を選ぶ典型基準:
| 基準 | 選び方 | 適する業務 |
|---|---|---|
| Youden 指数 max(TPR - FPR) | 左上隅に最も近い点 | 感度・特異度の平均が重要(医療診断) |
| F1 最大化 | 各閾値で F1 を計算して最大 | 陽性予測の品質重視(情報検索) |
| 固定 FPR ≤ α 下で TPR 最大 | FPR 制約内で TPR が最も高い点 | 偽陽性コスト固定(スパムフィルタ) |
| 固定 TPR ≥ β 下で FPR 最小 | TPR 制約内で FPR が最も低い点 | 見逃しコスト固定(不正検知) |
| 期待利益 max E[利益] | 各閾値の期待利益を計算 | マーケ・融資の経済学的最適 |
🎯 このコードでやること:TP/FP/FN/TN にコストを割り当て、 期待利益を最大化する閾値を選定。 実務での閾値決定方法を体験する。
📥 入力データ:コード 10 までの y, proba
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 2_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] # コスト設定 C_TP, C_TN, C_FP, C_FN = 10000, 500, -2000, -15000 # 全閾値で期待利益を計算 thresholds = np.linspace(0, 1, 101) profits = [] for t in thresholds: pred = (proba >= t).astype(int) tp = ((pred==1) & (y==1)).sum() tn = ((pred==0) & (y==0)).sum() fp = ((pred==1) & (y==0)).sum() fn = ((pred==0) & (y==1)).sum() profit = tp*C_TP + tn*C_TN + fp*C_FP + fn*C_FN profits.append(profit) profits = np.array(profits) best = np.argmax(profits) print(f'最適閾値: {thresholds[best]:.3f}') print(f'期待利益: {profits[best]:,} 円') # 比較: 単純な 0.5 閾値 pred50 = (proba >= 0.5).astype(int) tp = ((pred50==1) & (y==1)).sum() tn = ((pred50==0) & (y==0)).sum() fp = ((pred50==1) & (y==0)).sum() fn = ((pred50==0) & (y==1)).sum() profit50 = tp*C_TP + tn*C_TN + fp*C_FP + fn*C_FN print(f'\n単純 0.5 閾値: {profit50:,} 円') print(f'差 : {profits[best] - profit50:+,} 円') |
📤 実行結果:
💬 結果の読み方:FN(見逃し)コストが高い設定では、 デフォルト閾値 0.5 より低い 0.25 が最適。 期待利益で 14.3 万円改善(25,500 円 → 168,000 円)。 業務コストが ROC 上の最適点を決める実例。 単純に 0.5 で切るのは「コスト構造を無視した手抜き」になりかねない。
scikit-posthocs や delong パッケージで計算可能。 簡易には Bootstrap CI の重なりで判定する手もある。
TimeSeriesSplit で時間順に分割し、 「過去で学習・未来で評価」を守ること。 AUC は「ランダムペアの順位」を仮定するが、 時系列では順位が時間相関を持つので解釈に注意。
class_weight='balanced' 等で学習しても、 ROC は順位指標なので影響を受けにくい。 ただし predict_proba の絶対値(校正性)は変わるので、 確率自体を業務で使うなら再校正(Platt scaling や Isotonic regression)が必要。
ROC 曲線が「凸」(concave)でない場合、 ROC convex hull(ROCCH)を構成することで、 randomized classifier として常に優れた分類器が得られます。
$$ \mathrm{ROCCH} = \mathrm{convhull}(\{(\mathrm{FPR}_i, \mathrm{TPR}_i)\}) $$
凸包上の点は「2 つの異なる閾値の決定をランダムに混ぜる」ことで実現できます。 例:閾値 t1 で予測した結果と t2 で予測した結果を、 確率 α と 1-α で混ぜれば、 (αFPR1 + (1-α)FPR2, αTPR1 + (1-α)TPR2) の点が得られます。
近年の研究では、 ROC 曲線を部分集団ごとに描き、 「群間の AUC の差」を公平性指標とする手法があります。
| 公平性概念 | ROC との関係 |
|---|---|
| Demographic Parity | 群間で同じ閾値での陽性予測率を揃える |
| Equal Opportunity | 群間で TPR(真陽性率)を揃える → ROC の縦軸を揃える |
| Equalized Odds | 群間で TPR と FPR を同時に揃える → ROC 曲線の同一点を強制 |
| Calibration | 群間で予測確率の校正を揃える |
$$ P(\hat{Y} = 1 \mid Y = y, A = a) = P(\hat{Y} = 1 \mid Y = y, A = a'), \quad \forall y, a, a' $$
sklearn には ROC・AUC 関連 API が複数あり、 用途で使い分けます。
| API | 用途 | 返値 |
|---|---|---|
metrics.roc_curve(y, score) | ROC 曲線の座標を取得 | fpr, tpr, thresholds |
metrics.roc_auc_score(y, score) | AUC を計算 | float (0.5-1.0) |
metrics.RocCurveDisplay.from_estimator(clf, X, y) | ROC を描画(fit 済モデル渡し) | Display object |
metrics.RocCurveDisplay.from_predictions(y, score) | ROC を描画(生スコア渡し) | Display object |
metrics.det_curve(y, score) | DET 曲線(FPR vs FNR、 確率紙) | fpr, fnr, thresholds |
metrics.precision_recall_curve(y, score) | PR 曲線 | precision, recall, thresholds |
metrics.average_precision_score(y, score) | PR 曲線の AUC(AP) | float |
🎯 このコードでやること:ROC の代替指標として DET 曲線(FPR vs FNR)を描画。 音声認証等で標準的に使われる。 ROC は左上に集中して見づらいが、 DET は確率紙上で線形に展開される。
📥 入力データ:SSDSE-B-2026 (2023)、 y, proba
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import det_curve, roc_curve, roc_auc_score from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 2_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] # DET 曲線 fpr_det, fnr_det, _ = det_curve(y, proba) # ROC 曲線 fpr_roc, tpr_roc, _ = roc_curve(y, proba) # 等エラー率 (EER): FPR = FNR となる点 diff = np.abs(fpr_det - fnr_det) idx_eer = np.argmin(diff) print(f'AUC (ROC): {roc_auc_score(y, proba):.4f}') print(f'EER : {(fpr_det[idx_eer] + fnr_det[idx_eer]) / 2:.4f}') print(f' (FPR={fpr_det[idx_eer]:.4f}, FNR={fnr_det[idx_eer]:.4f})') print(f'\nDET 曲線の代表点 (FPR, FNR):') for i in [0, len(fpr_det)//4, len(fpr_det)//2, 3*len(fpr_det)//4, -1]: print(f' FPR={fpr_det[i]:.3f}, FNR={fnr_det[i]:.3f}') |
📤 実行結果:
💬 結果の読み方:等エラー率 (EER) ≈ 0.036 で「FPR と FNR が共に約 3.6%」の運用点が存在。 音声認証・生体認証分野では EER が標準指標。 AUC = 0.997 と整合する高性能。 DET 曲線は確率紙(正規分布のクォンタイル軸)上で描くと、 性能差が中央部で見やすい。
🎯 このコードでやること:モデルの予測確率が「実際の頻度」と整合しているか(校正されているか)を確認。 校正されていないと AUC が高くても確率を意思決定に使えない。
📥 入力データ:SSDSE-B-2026 (2023)
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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.calibration import calibration_curve from sklearn.metrics import brier_score_loss, roc_auc_score from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) y = (d['A1101'] > 2_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101','A1102']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] # 校正曲線(5 ビン) frac_pos, mean_pred = calibration_curve(y, proba, n_bins=5, strategy='quantile') print(f'AUC : {roc_auc_score(y, proba):.4f}') print(f'Brier score : {brier_score_loss(y, proba):.4f}') print(f' (低いほど校正良好、 最良 = 0)') print(f'\n校正曲線(5 分位ビン):') print(f' 予測確率平均 実頻度 乖離') for p, f in zip(mean_pred, frac_pos): print(f' {p:.3f} {f:.3f} {f-p:+.3f}') |
📤 実行結果:
💬 結果の読み方:AUC = 1.0(順位完璧)だが Brier = 0.042 で完全な校正ではない。 上位ビンで予測 0.778 だが実頻度 1.000(過小評価)。 これは 「順位は正しいが、 確率の絶対値はズレている」 典型例。 確率を業務判断(期待利益計算等)に使うなら、 Platt scaling や Isotonic regression で再校正が必要。
論文・レポートで ROC を報告する際の最小限の記述要素:
| 分野 | 標準/規制 | 要件 |
|---|---|---|
| 医療診断機器 | FDA, MDR (EU) | 感度・特異度の信頼区間と動作点を明記 |
| クレジットスコア | Basel III, IFRS 9 | Gini / KS の閾値を遵守 |
| 生体認証 | ISO/IEC 19795 | EER と DET 曲線の報告必須 |
| AI 公平性 | EU AI Act | 群間 ROC の差の文書化 |
| 創薬 (QSAR) | OECD ガイドライン | ROC AUC と陽性予測値の併記 |
本ページの内容を 1 ページに圧縮した「すぐ使える」チートシートです。
| ステップ | 処理 | sklearn API |
|---|---|---|
| 1 | モデル学習 | model.fit(X, y) |
| 2 | 確率予測 | proba = model.predict_proba(X)[:, 1] |
| 3 | ROC 計算 | fpr, tpr, th = roc_curve(y, proba) |
| 4 | AUC 計算 | auc = roc_auc_score(y, proba) |
| 5 | 描画 | RocCurveDisplay.from_predictions(y, proba) |
| 6 | 閾値選定 | np.argmax(tpr - fpr) で Youden |
| AUC 値 | 解釈 | 対応 |
|---|---|---|
| 0.5 | ランダム | モデルが情報を持っていない、 特徴量を見直す |
| 0.5-0.6 | 非常に弱い | 有効性に疑問、 特徴工学を強化 |
| 0.6-0.7 | 弱い | 使うなら他指標と併用 |
| 0.7-0.8 | 許容 | 標準的な業務利用可 |
| 0.8-0.9 | 良好 | 多くの分野で実用レベル |
| 0.9-1.0 | 優秀 | 過学習や情報漏洩がないかチェック |
| ≥ 1.0 | 完全 | データ漏洩を疑う、 評価設計を見直す |
🎯 このコードでやること:演習 1(日本人人口 100 万超を陽性、 総人口で予測)を実装。 AUC を計算し、 解釈する。
📥 入力データ:SSDSE-B-2026 (2023)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].reset_index(drop=True) # 陽性: 日本人人口 100 万超 y = (d['A1102'] > 1_000_000).astype(int).values # 特徴量: 総人口 X = StandardScaler().fit_transform(d[['A1101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] print(f'陽性数: {y.sum()}, 陰性数: {(1-y).sum()}') print(f'AUC : {roc_auc_score(y, proba):.4f}') print(f'\n→ 総人口と日本人人口は高相関 (r ≈ 0.999) なので AUC は 1.0 に近い') print(f' これは特徴量が「正解の関数」になっているため') |
📤 実行結果:
💬 結果の読み方:AUC = 1.0 が出るのは「特徴量が正解そのものに近い」ため。 実務では「リーク(target leakage)」と呼ぶ典型的失敗パターンに見えるが、 本演習では意図的にこの構造を観察。 教訓:AUC が完璧すぎたら、 特徴量が正解情報を直接持っていないか疑う。
ROC 曲線の面積(AUC)を「曲線の下の面積」として眺めると抽象的ですが、 陽性を 1 つ・陰性を 1 つ無作為に選んだとき、 モデルが陽性の方に高いスコアを付ける確率という、 とても具体的な量と一致します。 これを SSDSE-B-2026(2023 年・47 都道府県)の実データで確かめます。 ラベルを「総人口 150 万人未満か」(A1101)、 スコアを「婚姻件数が少ないほど小規模県だろう」という素朴な単一特徴量分類器(-A9101)にします。
roc_auc_score の出力 0.9855 と完全一致。
つまり AUC=0.9855 は「552 ペア中 544 ペアで順位が正しく、 取り違えはわずか 8 ペア」という数え上げの結果です。 面積の裏で起きているのは幾何ではなくペアの勝敗集計だ、 と腑に落とすのがこのページの独自角度です。
1 2 3 4 5 6 7 8 9 10 11 | import pandas as pd from sklearn.metrics import roc_auc_score d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = d[d['SSDSE-B-2026'] == 2023] y = (d['A1101'] < 1_500_000).astype(int).values # 陽性=人口150万未満 score = -d['A9101'].astype(float).values # 婚姻件数が少ない=陽性寄り pos, neg = score[y==1], score[y==0] win = sum(a > b for a in pos for b in neg) # → 544 tie = sum(a == b for a in pos for b in neg) # → 0 print((win + 0.5*tie) / (len(pos)*len(neg))) # → 0.9855(手計算) print(roc_auc_score(y, score)) # → 0.9855(一致) |
(1) 単調変換に完全不変=確率としての正しさ(校正)は一切保証しない。 AUC はペアの大小だけで決まるため、 スコアを順序を保つどんな変換で潰しても値が変わりません。 同じ実データで確かめると:
どれも 0.9855 のまま。 裏を返せば、 AUC が高くてもモデルの出力を「確率」として信じてはいけない。 予測確率がすべて 0.4〜0.6 に固まった校正の悪いモデルでも、 並び順さえ合っていれば AUC は満点近くになります。 リスクスコアや期待値計算に使うなら AUC とは別に確率の校正(キャリブレーション)を確認してください。
(2) 小標本では AUC は粗い階段で、 たった 1 ペアの取り違えで動く。 47 都道府県のような小さな有限データでは ROC 曲線は滑らかな曲線ではなく階段関数で、 今回の頂点はわずか 8 個(8 個の閾値)でした。 AUC が取り得る値の刻みは 1/(23×24) = 1/552 ≈ 0.0018。 つまりペアが 1 つ入れ替わるだけで AUC は約 0.0018 動くので、 小標本で「AUC 0.985 vs 0.982 のどちらが上」を競うのはほぼ無意味です。 差を主張したいなら信頼区間(次項)が必須になります。
上の「勝ちペア数 544」は、 統計学の Mann–Whitney U 統計量(= Wilcoxon 順位和検定の U)そのものです。 一般に U = 勝ちペア数 + 0.5×引き分け、 AUC = U / (n陽性·n陰性) という等式が成り立ちます(今回 U=544, 552 ペアで 0.9855)。 だから「AUC が 0.5 と有意に違うか」を問うことは「2 群のスコア分布に差があるか」を順位で検定することと同じです。 これを踏まえると次の 2 つが自然に見えてきます。
(注:ここでの分類器は素朴な単一特徴量による説明用の例で、 交差検証やモデル選択は行っていません。 数値はすべて上掲コードで実データから出力した実測値です。)