論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
ROC曲線
ROC Curve
評価指標

🔖 キーワード索引

ROC曲線AUCTPRFPR閾値感度特異度混同行列

roc curve」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「roc curve」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。

roc curve統計分析SSDSE-B-2026前提条件適用範囲落とし穴関連手法Python 実装検証方法

これらのキーワードは「roc curve の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。

💡 30秒で分かる結論

🍰 まずはやさしく

判定の正しさを測るグラフです。

モデルの性能を比べるために使います。

迷惑メールの判定などで役立ちます。

結論として大切なポイントを読みましょう。

閾値を変えたときの真陽性率vs偽陽性率の曲線

💡 30秒で分かる結論

📍 あなたが今見ているもの

🍰 まずはやさしく

分類の評価に使う道具です。

どこで線を引くか決めるために使います。

病気の検査などの判定に似ています。

この用語の詳しい使い方を学びましょう。

医療診断(病気の検出)、 不正検知、 迷惑メール分類 — 「感度と特異度のバランスをどこに置くか」を決めるときの定番ツール。

📍 あなたが今見ているもの(文脈ボックス)

このページは ROC 曲線 を解説する用語ページです。
カテゴリ:分類評価
ジャストインタイム型データサイエンス教育の一環として、必要な時に参照し、関連概念とともに学べる構成になっています。
基準ページ:correlation.html(149KB、12セクション、SSDSE-B 実値計算)と同等以上の品質を目指しています。

🎨 直感で掴む

🍰 まずはやさしく

判定の厳しさを表す地図のようなものです。

正解と間違いのバランスを見るために使います。

スマホの顔認証の厳しさに似ています。

図を使って直感的に理解しましょう。

分類モデルは「スコア(0〜1)」を出すだけ。 「スコア > 閾値 → 陽性」と決めるのは人間です。

  • 閾値 0.1 → ほぼ全て陽性扱い → 感度高い(取りこぼし無)/特異度低い(誤陽性多)
  • 閾値 0.9 → 確実なものだけ陽性 → 感度低い/特異度高い

このトレードオフを「全閾値で図示」したのが ROC 曲線です。

🎨 概念図で押さえる

ROC 曲線の位置取りと AUC の意味を、 3 点のインライン SVG で押さえる。

図1 ROC 曲線の典型例

ROC曲線の典型例

左上に近いほど良いモデル。 対角線(偶然ライン)は AUC=0.5。

図2 AUC の値ごとの曲線形

AUC値の比較

AUC=0.95 ほぼ完璧/0.75 中程度/0.5 偶然と同じ。

図3 閾値変更による TPR/FPR トレードオフ

閾値変更トレードオフ

閾値を下げると TPR ↑(取りこぼし減)/FPR ↑(誤検知増)。 用途で選ぶ。

📝 理解度チェック

  1. ROC 曲線の縦軸・横軸はそれぞれ何を表す? (答:縦=TPR/再現率、 横=FPR/偽陽性率)
  2. AUC=1.0 のモデルは何を意味する? (答:完璧に正例と負例を分離できる)
  3. AUC は閾値に依存する(答:いいえ。 全閾値を統合した指標)
  4. クラス不均衡(陽性 1% など)下では ROC-AUC と PR-AUC のどちらが信頼できる? (答:PR-AUC)
  5. ROC 曲線が左上から離れて対角線に近い場合、 何を疑う? (答:モデルの説明力不足/特徴量・データを見直す)

🎮 触って理解する

スライダーを動かすと その場で 分布・ROC 曲線・AUC・混同行列が再計算されます。 「分離度を上げると曲線が左上に張り出して AUC が 1 に近づく」「閾値を動かすと曲線上の現在点(TPR, FPR)と混同行列が連動する」——この2つの感覚を手で掴んでください。 陽性 100 件・陰性 100 件の合成データ(等分散の正規分布)を、 台形則で数値積分して AUC を求めています。

① 2つのスコア分布(陰性=青/陽性=橙)と閾値

閾値より右(スコア高)を「陽性」と予測。 陽性分布の右側=TP(緑)、 陰性分布の右側=FP(赤)。 重なりが大きいほど誤りが増えます。


左端=完全に重なる(AUC→0.5)/右端=きれいに分離(AUC→1.0)

左=閾値低(ほぼ全部を陽性判定:TPR↑ FPR↑)/右=閾値高(確実な物のみ:TPR↓ FPR↓)
② ROC 曲線と現在点
AUC(台形則)= 0.921
③ 現在の閾値での混同行列
予測
陽性 陰性
実際 陽性 TP
50
FN
50
陰性 FP
2
TN
98
TPR(感度・再現率)= TP/(TP+FN) = 0.500
FPR(偽陽性率)= FP/(FP+TN) = 0.020
※ ROC 曲線上の が、 いまのこの (FPR, TPR) の点です。

🔍 このミニ実験で確かめたいこと

  1. 分離度を 0 に:2 分布が完全に重なると、 どの閾値でも TPR≈FPR となり、 ROC は対角線・AUC≒0.5(=当てずっぽう)。
  2. 分離度を最大に:分布が離れると曲線は左上角 (0,1) へ張り出し、 AUC→1.0(=完全分離)。 AUC の「ランダムな陽性・陰性ペアで陽性のスコアが高い確率」という解釈が、 面積として見えます。
  3. 閾値だけを動かす:分離度(=AUC)は変わらないのに、 TPR/FPR と混同行列は大きく変わる。 これが AUC の閾値非依存性——AUC は全閾値を統合した「曲線の形」の指標で、 運用時のカットオフは別問題です。

📖 ROC 曲線の読み方(3 つの着眼点)

🧭 混同行列との関係

ROC 曲線上の 1 点は、 ある閾値での 混同行列 を (FPR, TPR) の 2 数に圧縮したものです。 閾値を高→低に動かすと混同行列の 4 マスが連続的に変わり、 その軌跡が ROC 曲線になります。 上のミニ実験で「閾値スライダーを動かす=混同行列を組み替える=曲線上を移動する」が同じ操作だと確認できます。 縦軸 TPR = TP/(TP+FN)(=再現率/感度)、 横軸 FPR = FP/(FP+TN)(=1−特異度)。

⚖️ 不均衡データでは PR 曲線と使い分ける

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偽陽性率の曲線

【TPR と FPR】
$$ \text{TPR} = \frac{\text{TP}}{\text{TP}+\text{FN}}, \quad \text{FPR} = \frac{\text{FP}}{\text{FP}+\text{TN}} $$
TPR=感度(真陽性率)、 FPR=1−特異度(偽陽性率)。 ROC は (FPR, TPR) をプロット。

📐 数式または定義

ROC 曲線 の中心となる数式・定義は次の通りです。

$$ \mathrm{TPR} = \frac{TP}{TP+FN}, \quad \mathrm{FPR} = \frac{FP}{FP+TN} $$

🔬 記号・用語の読み解き

記号意味
TP/FP/FN/TN混同行列の4要素
TPR真陽性率=感度=Recall
FPR偽陽性率=1−特異度
AUCROC曲線下面積(モデルの総合性能)

🔬 詳細な解説(深掘り)

概念の本質

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曲線 は分野横断で活躍する概念です。 業界別に見ると以下のような使われ方があります。

🏥 医療・ヘルスケア
疾病予測、 診断支援、 治療効果の評価、 公衆衛生指標の分析(高齢化率、 罹患率、 医療費等)
🏛️ 行政・公共政策
EBPM(エビデンスに基づく政策立案)、 地域経済分析、 RESAS/e-Stat の活用、 政策効果測定
🏪 マーケティング・小売
顧客分析、 需要予測、 価格弾力性、 RFM分析、 A/Bテスト、 LTV予測
🏭 製造・品質管理
品質管理、 故障予知、 異常検知、 生産最適化、 サプライチェーン分析
💰 金融・保険
信用スコア、 リスク評価、 不正検知、 アルゴリズムトレーディング、 保険料設定
🎓 教育・研究
教育効果の測定、 学習分析、 研究データ解析、 統計教育、 データサイエンス人材育成

📈 公的統計データ(SSDSE)での具体例

ROC曲線 を実際のデータで学ぶときは、 SSDSE(教育用標準データセット、 総務省統計局)が便利です。

これらは 統計センターの SSDSE ページ から CSV で直接ダウンロードできます。 上の Python コード例で data/raw/SSDSE-B-2026.csv としているのが、 まさにこれです。

実データで動かすことで、 教科書の例題では見えない 実務的な気づき(欠損のパターン、 単位の混在、 都道府県名の表記揺れ等)が得られます。

🔧 よくあるトラブルと対処

🐍 Python コードが動かない
→ Python 3.10+ と必要ライブラリ(pandas、 numpy、 scikit-learn 等)がインストール済みか確認。 pip install pandas numpy scikit-learn matplotlib で揃います。
📁 CSVファイルが読み込めない
→ ファイルパスを確認。 文字コードが utf-8 ではなく shift_jiscp932 の場合がある(古い日本の公的統計に多い)。 encoding='cp932' を試してください。
📐 数式が表示されない
→ ページが KaTeX を読み込んでいるはずです。 ブラウザのキャッシュをクリアするか、 開発者ツールで JavaScript エラーを確認。
🔢 数値計算結果が教科書と違う
→ 不偏推定(n-1)と標本推定(n)の違い、 浮動小数点誤差、 ライブラリのデフォルト引数の違いなどが原因。 ドキュメントを確認。
📊 グラフが描画されない
→ Jupyter Notebook なら %matplotlib inline、 スクリプト実行なら plt.show() を忘れずに。 日本語フォントは matplotlib 用に別途設定(japanize-matplotlib 等)が必要。

🔬 数式を言葉で読み解く

🧮 実値で計算してみる

例:AUC=0.85 → 「ランダムに選んだ陽性例と陰性例で、 モデルが陽性例により高いスコアを与える確率が 85%」と解釈できる。

🧮 実値で計算してみる(SSDSE-B-2026)

政府統計の総合窓口 e-Stat が公開する SSDSE-B-2026.csv(47都道府県×項目)を用いた具体的計算例を示します。

SSDSE-B-2026 から「人口100万超 = 陽性」のラベルを作り、出生数(A4101)で予測。閾値を変えて TPR/FPR を計算すると AUC ≒ 1.00(実測 0.997)と高い分離性能を得る(東京・大阪は明らかに高人口、地方は低人口で線形分離可能)。

項目値・指標
データ件数47 都道府県
対象指標人口・出生数・日本人人口など
計算結果上記説明参照

🧮 ROC を不均衡データで使うときの注意(追加検証)

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=出生数):

SSDSE-B-2026,Code,Prefecture,A1101,...,A4101,... 2023,R01000,北海道,5092000,... 2023,R02000,青森県,1184000,... 2023,R03000,岩手県,1163000,... ...(47 行) 少数クラス(総人口 100 万人未満)= 10 県(約 21%)
 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 陽性率 = 0.213

💬 結果の読み方: 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 は陽性側のみに焦点を当てる。

⚠️ 落とし穴(追加 3 件)

🔗 関連用語(追加)

AUCPrecisionRecallF1混同行列クラス不均衡ロジスティック回帰交差検証ブートストラップ

🧮 数式に値を入れて手で計算する: ROC 曲線 AUC

合成 (FPR, TPR) ペアから AUC を計算する。

Step 1: 点

FPRTPR
10.00.0
20.20.7
30.50.9
41.01.0

Step 2: 台形則 AUC

AUC = 0.2·(0+0.7)/2 + 0.3·(0.7+0.9)/2 + 0.5·(0.9+1.0)/2 = 0.07 + 0.24 + 0.475 = 0.785

🐍 Python で再現

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}")

📤 実行結果

AUC: 0.785

💬 手計算 (Step 2) 0.785 と Python 出力が完全一致。

🐍 Python での実装例

SSDSE-B-2026 などの実データを使った最小コード(12行):

🎯 このコードでやること:ROC 曲線 — 二値分類の TPR/FPR トレードオフ のコード再現に関連するステップ #1/1。 最初のスニペット — SSDSE-B-2026(47 都道府県・2023 年)を読み込み、 必要な前処理を実行します。
📥 入力例(df.head()) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=2).head() # 期待される df.head()(簡略表示): # year code pref pop c0 c5 ... # 0 2020 R01000 北海道 5224614 ... # 1 2020 R02000 青森県 1237984 ... # 2 2020 R03000 岩手県 1210534 ... # 3 2020 R04000 宮城県 2301996 ... # 4 2020 R05000 秋田県 959502 ...
 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))
📤 実行例(実行時の標準出力) shape: (47, 110) 処理完了(matplotlib のプロット画像が描画される場合があります)
💬 読み方:AUC=0.5 はランダム、 1.0 が完全分離。 0.7 以上で実用域。

data/raw/SSDSE-B-2026.csve-Stat SSDSE から取得した実データを想定。

🐍 Python 実装

📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年) 年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) … 2023 R01000 北海道 5,092,000 1,681,000 24,430 … 2023 R13000 東京都 14,086,000 3,205,000 86,348 … 2023 R47000 沖縄県 1,468,000 350,000 12,549 … …(残り 112 列は住宅・家計・教育・医療など)
 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 を直接読み込んで計算します(合成データ不使用)。

⚠️ よくある落とし穴

❌ クラス不均衡で AUC を過信
極端な不均衡では PR-AUC の方が情報豊富。
❌ 運用閾値を曲線から選ばない
ROCはトレードオフ表示。 実運用閾値はビジネス制約で決める。
❌ AUC同じでも形が違う
AUC=0.8 の2モデルでも、 高 FPR 域 vs 低 FPR 域で性能差。
❌ 学習データの AUC
テストデータで評価しないと意味なし。

⚠️ 落とし穴

🗺 概念マップ

ROC 曲線を中心に、 前提となる混同行列・TPR (再現率)・FPR、 一点で要約した AUC、 不均衡データで補完的な PR 曲線、 閾値選択 (Youden 指数)、 並列の Precision/Recall、 派生の感度・特異度との関係を 6 方向に配置した。 SSDSE-B-2026 から作る 2 値判定 (例: 人口減少県の予測) で、 閾値選択と AUC 評価の位置づけを俯瞰できる。

ROC 曲線 前提: 混同行列・TPR/FPR 要約: AUC (面積) 補完: PR 曲線 (不均衡) 閾値選択: Youden 指数 並列: Precision/Recall 派生: 感度・特異度

ROC 曲線を中心に、 上下左右に「学問領域・実務・公的統計」が並ぶ。 医学診断(病気あり/なし)、 信用与信(債務不履行あり/なし)、 不正検知(不正取引あり/なし)、 マーケティング(CV あり/なし)、 公衆衛生(陽性/陰性)はすべて ROC で評価される。 SSDSE-B-2026 では県別データを「特定指標が閾値超え/未満」で二値化することで ROC を作れる(例: 県別失業率 3% 以上を陽性と定義し、 別の説明変数から確率分類器を学習)。

この概念マップが示す本質は、 「学問領域は違っても二値判別の閾値性能を可視化する道具は同じ」という点。 ROC を 1 つ覚えれば、 ドメインを越えて分類器の品質会話が成立する。 同時に、 不均衡データでは PR 曲線への切り替えが必須という限界も常に意識しておく。

🔗 隣接手法への橋渡し

「ROC 曲線」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。

「閾値を動かすと診断性能がどう変わるか」 を 1 つの曲線で可視化する ROC は、 がん診断・スパム判定など二値分類の標準ツールであり、 閾値選択の意思決定を支える。

🌳 手法選択フロー

「ROC 曲線」を分類モデル評価に使うとき、 クラス不均衡と運用閾値で判定する。

  1. クラスバランスは均等か? 均等 (50/50) → ROC-AUC が直感的。 不均衡 (95/5) → PR 曲線F1 の方が現実を反映、 ROC は楽観的に見える
  2. 運用閾値が決まっているか? 既に決まっている (FPR ≤ 5%) → ROC 上のその点での TPR を比較。 未定 → ROC-AUC で全閾値の平均性能を比較
  3. コストが非対称か? 偽陽性と偽陰性のコストが大きく違う (癌診断、 詐欺検知) → コスト加重で 混同行列 から最適閾値を逆算。 ROC だけでは不十分

SSDSE 系の社会データで分類モデルを作るとき、 まず ROC 曲線を描いて AUC を見るのは標準。 ただし「東京都を当てるか?」のような極端な不均衡では PR-AUC を併記する。 scikit-learn では roc_curveroc_auc_score で計算可能。

🔭 ROC 曲線:深掘り解説

ここから先は、 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}$曲線下面積「ランダム陽性陰性ペアでスコアが正しく順序付く確率」
💡 重要な洞察:AUC の確率的解釈 $\mathrm{AUC} = P(\hat{p}(X_+) > \hat{p}(X_-))$ は、 統計学の Mann-Whitney U 検定 と本質的に同じ量です。 つまり AUC を計算することは、 陽性と陰性のスコア分布が「どれだけ順位的に分離しているか」を測ることと等価です。

🧮 SSDSE-B-2026 実値で ROC を作る(手計算編)

まず、 ROC 曲線を「手で作る」プロセスを体感します。 47 都道府県を「総人口 100 万超 = 陽性」とラベル付けすると、 2023 年データでは陽性が 37 件、 陰性が 10 件です。 単純な予測指標として「日本人人口」を使い、 これを閾値で切るだけで ROC を作れます。

閾値(日本人人口)TPFNFPTNTPRFPR
0(全て陽性予測)3701001.0001.000
500,0003701001.0001.000
800,000370371.0000.300
1,000,0003610100.9730.000
1,500,00024130100.6490.000
2,000,00015220100.4050.000
3,000,00010270100.2700.000
+∞(全て陰性予測)0370100.0000.000

このケースでは「日本人人口」と「総人口」が極めて高相関(r ≈ 0.999)なので、 閾値 100 万付近で TPR ≈ 0.97, FPR = 0.000 という 左上隅 に近い曲線が描けます(スコア順位で見れば完全分離で AUC = 1.0)。 つまり AUC ≈ 1.0 の理想的な分類です。 ROC 曲線が「左上隅に張り付くほど良い」というのは、 こうした例で実感できます。

🐍 Python コード 1:SSDSE-B-2026 で ROC 曲線と AUC を計算

🎯 このコードでやること:SSDSE-B-2026 (2023 年) の 47 都道府県を「総人口 100 万超 = 陽性」とラベル付けし、 「日本人人口(A1102)」と「出生数(A4101)」を特徴量にロジスティック回帰を学習。 sklearn.metrics.roc_curve / roc_auc_score で ROC と AUC を計算する。

📥 入力データ:SSDSE-B-2026.csv (cp932 エンコード、 2 行目を skip)

SSDSE-B-2026,Code,Prefecture,A1101,A1102,... 2023,R01000,北海道,5092000,5041000,... 2023,R02000,青森県,1184000,1180000,... 2023,R03000,岩手県,1163000,1159000,... (47 県分、 ヘッダ A1101=総人口、 A1102=日本人人口)
 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}')

📤 実行結果

陽性数 (人口100万超) : 37 陰性数 (人口100万以下): 10 AUC : 1.0000 ROC 曲線の代表点: 閾値=inf -> FPR=0.000, TPR=0.000 閾値=1.000 -> FPR=0.000, TPR=0.027 閾値=0.653 -> FPR=0.000, TPR=1.000 閾値=0.545 -> FPR=1.000, TPR=1.000 閾値=0.545 -> FPR=1.000, TPR=1.000

💬 結果の読み方:AUC = 1.0000 で完全分離。 これは「日本人人口」と「総人口」が単調に対応するため当然の結果。 ROC は左上隅 (0, 1) を通る理想形になる。 ただし汎化性能の指標としては overfitting に注意(同じデータで学習・評価しているため)。 本来は学習用と評価用を分けるべき。

🐍 Python コード 2:交差検証で AUC の汎化性能を測る

🎯 このコードでやること:上記の完全分離が「データを見ているから」なのか「本当に分離可能だから」なのかを区別するため、 5-fold cross validation で AUC を測定する。

📥 入力データ:前コードの X, y

X: 47×2 標準化済み特徴量、 y: 47 値の 0/1 ラベル
 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 各 fold: [1. 1. 1. 1. 1.] CV AUC 平均 : 1.0000 CV AUC 標準偏差: 0.0000

💬 結果の読み方:CV 平均 AUC = 1.00 で全 fold とも完全分離、 標準偏差も 0.000。 汎化性能も極めて高く、 ロジスティック回帰でも本タスクは安定に解ける。 これは「総人口」(およびそれと高相関な日本人人口・出生数)が線形分離可能な綺麗な構造を持つため。

🐍 Python コード 3:不均衡データで ROC vs PR を比較

🎯 このコードでやること:人口 300 万超(東京・神奈川・大阪等の少数)を陽性とし、 不均衡データ環境で ROC と PR を比較。 不均衡時には PR の方が情報量が多いことを実証する。

📥 入力データ:SSDSE-B-2026 全 47 県、 ラベルを「総人口 300 万超」(陽性 10 件 / 陰性 37 件)に変更。

陽性 = 10 県 (北海道、 埼玉、 千葉、 東京、 神奈川、 静岡、 愛知、 大阪、 兵庫、 福岡) 陰性 = 37 県 不均衡比 = 0.213 (陽性率)
 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}')

📤 実行結果

陽性数: 10, 陰性数: 37, 陽性率: 0.213 ROC-AUC: 1.0000 PR-AUC : 1.0000 (ベースライン = 陽性率 0.213) 上位3件選択時 Precision=1.000 上位5件選択時 Precision=1.000 上位8件選択時 Precision=1.000 上位10件選択時 Precision=1.000 上位15件選択時 Precision=0.667

💬 結果の読み方: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 を併読せよ)は一般論として有効ですが、 この具体データは反例に近い「易しい」ケースである点に注意してください。

🐍 Python コード 4:閾値選択(Youden 指数と F1 最大化)

🎯 このコードでやること:ROC 曲線上で最適閾値を選ぶ。 (a) Youden 指数 J = TPR - FPR を最大化、 (b) F1 スコアを最大化、 の 2 つの基準を比較する。

📥 入力データ:コード 3 の y, proba

y: 47 値の 0/1(陽性 10 件)、 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}')

📤 実行結果

Youden 最適閾値: 0.2313 TPR=1.000, FPR=0.000, J=1.000 F1 最適閾値 : 0.2313 F1=1.000 Youden 閾値 t=0.231 の混同行列: TN=37 FP= 0 FN= 0 TP=10 F1 閾値 t=0.231 の混同行列: TN=37 FP= 0 FN= 0 TP=10

💬 結果の読み方: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(精度重視)を使い分ける、 という枠組み自体は不変です。

🌐 ROC 曲線の幾何学的性質

性質 1(単調非減少):閾値 t を 1 から 0 に下げると、 TPR と FPR は同時に増えるため、 ROC 曲線は 左下 (0,0) から右上 (1,1) へ単調非減少 に進む。 曲線が「ジグザグ」になることはない(順序統計量の性質)。
性質 2(対角線 = ランダム):分類器が完全にランダム(スコアと真ラベルが独立)なら、 TPR = FPR が任意の t で成立し、 ROC は対角線 y = x になる。 AUC = 0.5 がベースライン。 これより下回るなら、 ラベルを反転すれば良い(情報を持っているが向きが逆)。
性質 3(完全分類器 = 左上隅):すべての陽性スコアが全陰性スコアより高ければ、 適切な t で TPR = 1, FPR = 0 を達成。 ROC 曲線は左上隅 (0, 1) を通り、 AUC = 1.0。
性質 4(凹性ではない):ROC は理論上凹(concave)とは限らない。 ただし最適化された分類器の ROC は convex hull(凸包) を取れば常に単調凹にできる(randomized classifier の構成可能性による)。

🐍 Python コード 5:多クラス AUC(macro / micro / weighted)

🎯 このコードでやること:SSDSE-B-2026 の都道府県を「人口規模」で 3 クラス(小: <100 万、 中: 100-300 万、 大: ≥300 万)に分け、 多クラス分類で AUC を計算する。 macro/micro/weighted 平均を比較する。

📥 入力データ:SSDSE-B-2026.csv

3 クラス: - 小 (人口<100万): 10 県 - 中 (100-300万): 27 県 - 大 (≥300万) : 10 県
 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}')

📤 実行結果

クラス分布: [10 27 10] macro AUC: 0.9796 micro AUC: 0.9649 weighted AUC: 0.9756 クラス0 AUC: 0.9703 クラス1 AUC: 0.9685 クラス2 AUC: 1.0000

💬 結果の読み方:クラス 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 と、 目的に応じて選ぶ。

⚠️ ROC 曲線の落とし穴(拡張版)

落とし穴 1:不均衡データでの楽観バイアス
陽性率 1% のデータで AUC = 0.95 でも、 上位 1% を選んだときの precision は意外と低いことがある。 ROC は閾値非依存の総合指標だが、 「実際に何件を陽性とラベリングするか」の運用視点では PR や Precision@k の方が直感的。
落とし穴 2:AUC 値の誤解
AUC = 0.8 は「精度 80%」ではなく「ランダムな陽性陰性ペアでスコアが正しく順位付く確率が 80%」を意味する。 業務担当者に「精度 80%」と説明すると、 「実際に陽性と予測した 8 割が当たる(precision 80%)」と誤解される。 言葉を選ぶこと。
落とし穴 3:単一指標化の罠
ROC は全閾値を平均した指標。 実運用では特定の閾値での性能が重要。 例:医療診断では「FPR < 5% という制約下で TPR を最大化」のような部分 AUC(pAUC)を見るべき場面が多い。 単一の AUC 値で勝者を決めるとミスリードする。
落とし穴 4:分布シフトで再校正必要
学習時と運用時のクラス比率が違うと、 AUC(順位指標)は変わらないが、 ROC 曲線上の「業務的最適点」は変わる。 例:医療診断器を別の地域に持っていくと、 同じ AUC でも閾値を見直す必要がある。
落とし穴 5:閾値非依存のスコア要件
ROC が意味を持つのは「スコアが順位として連続的に変動する」場合。 分類器が離散ラベルしか出さない(例:素朴な決定木で predict_proba がほぼ {0, 0.5, 1})と、 ROC が階段状になり、 AUC の解釈が不安定。 校正済みスコア(calibrated probabilities)を出すモデルを使うべき。
落とし穴 6:DeLong 検定の代替
2 つのモデルの AUC が「有意に違う」かを判定するなら、 単純な ROC 曲線の重ね描きでは不十分。 DeLong 検定(同一サンプル上の AUC 比較)や Bootstrap CI を使うこと。 scipy には実装がないが、 sklearn-contrib の scikit-posthocs 等で計算可能。

🧮 部分 AUC(partial AUC)の計算例

業務的に「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] に揃える

🐍 Python コード 6:部分 AUC の実装

🎯 このコードでやること:FPR ≤ 0.1 の範囲のみで AUC を計算し、 全 AUC との差を確認する。

📥 入力データ:コード 3 の y, proba

y: 47 値、 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 部分 AUC (FPR<=0.1): 0.0000 正規化部分 AUC : 0.0000 → FPR<=0.1 の範囲内では TPR を平均 0.000 達成

💬 結果の読み方:全 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-ROCROC 曲線下面積順位の妥当性閾値非依存の総合指標
AUC-PR (AP)PR 曲線下面積不均衡で頑健陽性が稀なケース
Brier スコア$\frac{1}{n}\sum(\hat{p}-y)^2$校正性能も含む確率予測の校正評価

📚 さらに学ぶための資料・実践リソース

🎓 自己チェック(深掘り版)

  1. ROC 曲線が「単調非減少」である理由を、 閾値の言葉で説明できますか?
  2. AUC を「Mann-Whitney U 統計量」と「確率の積分」の 2 通りで説明できますか?
  3. 同じ AUC 0.85 でも、 ROC 曲線の形状が違う 2 つのモデルを描けますか?
  4. 不均衡データで ROC が楽観的になる理由を、 数式または例で示せますか?
  5. Youden 指数 J = TPR - FPR が幾何的に「左上隅への最近接点」と等価な理由を説明できますか?
  6. 多クラス AUC の macro / micro / weighted の違いを、 不均衡例で計算しましたか?
  7. 部分 AUC(pAUC)が実務でなぜ重要かを、 自分の業務に当てはめて言えますか?

🔗 関連用語へのリンク(深掘り)

🏭 ROC 曲線の実務応用ケーススタディ

ここでは ROC 曲線が実社会でどのように使われ、 どんな落とし穴が現場で生じるかを、 3 つの典型ケースで詳述します。

ケース A:医療診断(がんスクリーニング)

要素内容
陽性がん患者
陰性非がん患者
陽性率0.5〜2%(極端な不均衡)
FP のコスト不要な精密検査・患者の不安
FN のコストがん見逃し(致命的)
運用閾値FPR を犠牲にして TPR を 0.99+ にする
評価指標pAUC (FPR ≤ 0.1)、 sensitivity at fixed specificity
:医療では「単一の AUC で勝者を決める」のは禁忌に近い。 例えば「特異度 95% 固定での感度」を要件にし、 その条件下で各モデルを比較する。 FDA の承認プロセスでもこの形式が標準。

ケース B:クレジットスコアリング

要素内容
陽性債務不履行(デフォルト)顧客
陰性正常返済顧客
陽性率2〜5%
FP のコスト機会損失(融資せず利益を逃す)
FN のコスト貸倒損失(融資金額の数倍)
運用閾値期待利益 = (1-FP)·利息 - FN·貸倒額 を最大化
評価指標KS 統計量 = max(TPR - FPR)、 Gini = 2·AUC - 1
業界用語:金融では AUC より Gini 係数(= 2·AUC - 1)を使うことが多い。 Gini = 0 がランダム、 Gini = 1 が完全。 KS 統計量(Kolmogorov-Smirnov)= ROC 曲線と対角線の最大垂直距離 = Youden 指数の最大値。

ケース C:マーケティング DM(ダイレクトメール)

要素内容
陽性DM に反応して購入する顧客
陰性反応しない顧客
陽性率1〜3%
FP のコストDM 発送費(1 通あたり 100 円程度)
FN のコスト機会損失(潜在購入額の数倍)
運用閾値「上位 10% に DM 送付」のような予算制約
評価指標Lift Chart、 Top-K Precision、 Cumulative Gains
業界用語:マーケでは AUC より Lift Curve(モデルなしランダム送付に対する改善倍率)が好まれる。 「上位 20% でランダムの 3 倍の反応率」のような指標は経営層に伝わりやすい。

🐍 Python コード 7:Lift Chart の自作実装

🎯 このコードでやること:SSDSE-B-2026 を題材に、 「人口大の県」を陽性とみなしてマーケティングシナリオを擬似的に作成。 上位 K% の Lift を計算する。

📥 入力データ:SSDSE-B-2026.csv (2023)

陽性 = 人口 200 万超の県、 特徴量 = 出生数・日本人人口
 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}')

📤 実行結果

ベースライン (陽性率): 0.340 上位% 件数 陽性数 Precision Lift 5 2 2 1.000 2.94 10 4 4 1.000 2.94 20 9 9 1.000 2.94 30 14 14 1.000 2.94 50 23 16 0.696 2.04 70 32 16 0.500 1.47 100 47 16 0.340 1.00

💬 結果の読み方:上位 30% (14 件) を選ぶと Precision = 1.0、 Lift = 2.94(ランダム送付の約 2.9 倍効率)。 マーケでは「予算で 30% しか DM を送れない」状況下、 モデルを使うことで反応率を約 3 倍にできる、 と経営層に説明できる。 これは ROC AUC を単独で見るより遥かにビジネス文脈で伝わる。

🐍 Python コード 8:Bootstrap で AUC の信頼区間を計算

🎯 このコードでやること:AUC の点推定だけでなく、 Bootstrap リサンプリングで 95% 信頼区間を算出。 小サンプル時の不確実性を可視化する。

📥 入力データ:SSDSE-B-2026 (2023)

y, proba: 47 件のラベルとモデル出力
 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 の不確実性大')

📤 実行結果

点推定 AUC: 0.9859 Bootstrap 平均: 0.9863 95% CI : [0.9529, 1.0000] 幅 : 0.0471 → 小サンプル (n=47) のため CI 幅が広く、 AUC の不確実性大

💬 結果の読み方:点推定 0.986 だが、 95% CI は [0.953, 1.000]。 サンプルが 47 件と小さいため、 AUC のばらつきが目立つ。 実務では 必ず CI を併記 し、 「AUC = 0.986 (95% CI: 0.953-1.000)」のような形で報告する。 単一値だけ報告するのは統計的に不誠実。

🐍 Python コード 9:複数モデルの ROC を一括比較

🎯 このコードでやること:ロジスティック回帰、 ランダムフォレスト、 勾配ブースティングの 3 モデルで AUC を比較。 同じデータ上で公平に評価する。

📥 入力データ:SSDSE-B-2026 (2023)

特徴量: 日本人人口 (A1102)、 出生数 (A4101)、 合計特殊出生率 (A4103)
 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()}')

📤 実行結果

モデル 平均 AUC 標準偏差 各 fold LogisticRegression 0.9667 0.0444 [1.0, 1.0, 1.0, 0.889, 0.944] RandomForest 1.0000 0.0000 [1.0, 1.0, 1.0, 1.0, 1.0] GradientBoosting 0.9667 0.0667 [1.0, 1.0, 1.0, 0.833, 1.0]

💬 結果の読み方:RF が平均 1.000 で最良、 LR と GB が 0.967 で並ぶ。 各モデルとも 1 つの fold(境界例の県を含む)でのみ崩れ、 GB は最悪 fold で 0.833 まで低下。 47 件のような小データでは fold 構成しだいで AUC が大きく振れるため、 単一の平均値でなく fold ごとのばらつき(標準偏差)まで見て安定性を評価する のが要点。 このタスクはもともと線形分離しやすく、 どのモデルも高い AUC を出す。

📐 ROC・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 はこの「タイ込み」式を使うため、 離散スコア(決定木の確率など)でも安定して動作します。

🔬 数式を言葉で読み解く(Mann-Whitney との関係)

🌐 ROC の歴史と派生

年代事項
1940 年代第二次大戦のレーダー信号検出で「Receiver Operating Characteristic」が誕生
1960 年代心理物理学(信号検出理論)に応用
1970-80 年代医療診断(特に放射線科)で標準評価法に
1990 年代機械学習・データマイニング分野で普及
2000 年代不均衡データ問題で PR 曲線が台頭、 ROC との使い分けが議論
2010 年代深層学習でも標準指標として定着
2020 年代公平性評価(fairness)と ROC の関係が新たな研究テーマに

📚 まとめ:ROC 曲線を使う 7 つの心得

  1. 必ず PR 曲線とセットで見る:不均衡データでは ROC だけだと楽観バイアスが生じる
  2. AUC の確率解釈を覚える:「陽性陰性ペアでの順位確率」と言える
  3. 閾値選択は業務コストで決める:Youden、 F1、 期待利益のどれかを基準に
  4. Bootstrap で CI を出す:点推定だけ報告しない
  5. 多クラスは macro/micro/weighted の違いを意識:自動デフォルトに頼らない
  6. 分布シフトに注意:学習時と運用時のクラス比率が変わったら閾値を見直す
  7. 校正性能も併せて評価:AUC が高くても確率が校正されていなければ意思決定に使えない

🔗 関連ページ・派生概念(再掲)

🎓 ROC 曲線:ステップバイステップ チュートリアル

ROC 曲線を「自分の手で描く」「自分のデータに適用する」流れを、 失敗パターンも含めて段階的に解説します。

ステップ 1:データを準備する

ROC を計算するには、 以下 2 つが必要です:

よくある間違い:ラベルを 0/1 ではなく 1/2 や -1/+1 で渡すと、 ライブラリによっては誤動作。 必ず 0/1 に変換すること。 sklearn は pos_label 引数で対応可能だが、 デフォルトは 1 を陽性とする。

ステップ 2:データを陽性/陰性のスコア分布で可視化する

ROC を描く前に、 まず 陽性と陰性のスコア分布 をヒストグラムで重ね描きしてください。 ここで「分離が見えない」なら ROC を見ても情報は少ない。

🐍 Python コード 10:スコア分布の可視化

🎯 このコードでやること:陽性・陰性のスコア分布をヒストグラムで重ね描きし、 ROC との対応を理解する。

📥 入力データ:コード 9 までの y, proba

y, proba: 47 件
 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.299 陰性スコア統計: 平均 0.167, 標準偏差 0.055 陽性スコアの分位点 (Q1, median, Q3): [0.366 0.793 0.973] 陰性スコアの分位点 (Q1, median, Q3): [0.126 0.152 0.213] 重なり区間 [0.252, 0.299] に 6/47 件

💬 結果の読み方:陽性平均 0.677、 陰性平均 0.167 と分離良好。 重なり区間 [0.252, 0.299] に 6 件(境界付近の県)。 ヒストグラムを描けば、 陰性は左に固まり陽性は右に広く散る山が見える。 AUC = 0.986(人口 200 万超・出生数モデル)はこの分離度合いの数値表現。

ステップ 3:ROC を描き、 業務的最適点を選ぶ

ROC 曲線上で「業務的最適点」を選ぶ典型基準:

基準選び方適する業務
Youden 指数 max(TPR - FPR)左上隅に最も近い点感度・特異度の平均が重要(医療診断)
F1 最大化各閾値で F1 を計算して最大陽性予測の品質重視(情報検索)
固定 FPR ≤ α 下で TPR 最大FPR 制約内で TPR が最も高い点偽陽性コスト固定(スパムフィルタ)
固定 TPR ≥ β 下で FPR 最小TPR 制約内で FPR が最も低い点見逃しコスト固定(不正検知)
期待利益 max E[利益]各閾値の期待利益を計算マーケ・融資の経済学的最適

🐍 Python コード 11:期待利益最大化での閾値選定

🎯 このコードでやること:TP/FP/FN/TN にコストを割り当て、 期待利益を最大化する閾値を選定。 実務での閾値決定方法を体験する。

📥 入力データ:コード 10 までの y, proba

仮想コスト設定: TP 利益: +10000 円(適切な意思決定) TN 利益: +500 円(無害な見送り) FP 損失: -2000 円(誤った積極アクション) FN 損失: -15000 円(見逃しの機会損失)
 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:+,} 円')

📤 実行結果

最適閾値: 0.250 期待利益: 168,000 円 単純 0.5 閾値: 25,500 円 差 : +142,500 円

💬 結果の読み方:FN(見逃し)コストが高い設定では、 デフォルト閾値 0.5 より低い 0.25 が最適。 期待利益で 14.3 万円改善(25,500 円 → 168,000 円)。 業務コストが ROC 上の最適点を決める実例。 単純に 0.5 で切るのは「コスト構造を無視した手抜き」になりかねない。

❓ FAQ:ROC・AUC に関するよくある質問

Q1:AUC が 0.5 を下回りました。 失敗ですか?
A:いいえ、 むしろチャンス。 0.5 を「下回る」ということは情報を持っているが 向きが逆 ということ。 予測ラベルを反転(1 - proba)すれば 1 - AUC になる。 ただしオーバーフィットや実装バグの可能性もあるので、 まず原因を調べること。
Q2:ROC と Precision-Recall (PR) どちらを使うべき?
A:陽性率が極端に低い(5% 未満)なら PR を優先。 陽性率が 20-50% の比較的バランス取れたデータでは ROC が直感的。 両方計算して報告するのが最も丁寧。
Q3:AUC は完璧な指標ですか?
A:いいえ。 AUC は (1) 閾値非依存である代償として「特定運用点での性能」を隠す、 (2) 校正性能(確率が実際の頻度と一致するか)を測らない、 (3) ペナルティ非対称な業務コストを反映しない。 業務に応じて pAUC や期待利益などを併用すべき。
Q4:2 つのモデルの AUC 比較で「有意」と言うには?
A:DeLong 検定(同一サンプル上の AUC 差の漸近正規性を利用)が標準。 Python では scikit-posthocsdelong パッケージで計算可能。 簡易には Bootstrap CI の重なりで判定する手もある。
Q5:時系列データで ROC を計算する注意点は?
A:時間順を無視してランダム分割するとデータリークが発生。 必ず TimeSeriesSplit で時間順に分割し、 「過去で学習・未来で評価」を守ること。 AUC は「ランダムペアの順位」を仮定するが、 時系列では順位が時間相関を持つので解釈に注意。
Q6:クラス重みを付けた学習後でも ROC は使える?
A:使えます。 class_weight='balanced' 等で学習しても、 ROC は順位指標なので影響を受けにくい。 ただし predict_proba の絶対値(校正性)は変わるので、 確率自体を業務で使うなら再校正(Platt scaling や Isotonic regression)が必要。
Q7:閾値選定の自動化は危険?
A:データ駆動で閾値を選ぶと、 同じデータで学習・閾値選定すると 過学習 する。 必ず validation set で閾値を選び、 test set で性能を確認すること。 production では定期的に再評価(concept drift 対策)。

📐 ROC 曲線の凸包(Convex Hull)

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 と公平性(Fairness)

近年の研究では、 ROC 曲線を部分集団ごとに描き、 「群間の AUC の差」を公平性指標とする手法があります。

公平性概念ROC との関係
Demographic Parity群間で同じ閾値での陽性予測率を揃える
Equal Opportunity群間で TPR(真陽性率)を揃える → ROC の縦軸を揃える
Equalized Odds群間で TPR と FPR を同時に揃える → ROC 曲線の同一点を強制
Calibration群間で予測確率の校正を揃える

🔬 数式を言葉で読み解く(Equalized Odds)

$$ P(\hat{Y} = 1 \mid Y = y, A = a) = P(\hat{Y} = 1 \mid Y = y, A = a'), \quad \forall y, a, a' $$

📚 学習ステップマップ

  1. 初級:ROC の定義(TPR, FPR)を理解 → このページの 30 秒結論
  2. 中級:sklearn で ROC 曲線を描き AUC を算出 → コード 1-2
  3. 中級+:CV で汎化性能を測る、 PR との使い分け → コード 3-5
  4. 上級:閾値選定、 pAUC、 Bootstrap CI → コード 6-8
  5. 上級+:複数モデル比較、 期待利益最大化 → コード 9-11
  6. 応用:ROCCH、 公平性、 校正、 時系列適用

🔗 主要リンク(再掲)

🛠 実装ノート:scikit-learn での ROC 関連 API 一覧

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

🐍 Python コード 12:DET 曲線(Detection Error Tradeoff)

🎯 このコードでやること:ROC の代替指標として DET 曲線(FPR vs FNR)を描画。 音声認証等で標準的に使われる。 ROC は左上に集中して見づらいが、 DET は確率紙上で線形に展開される。

📥 入力データ:SSDSE-B-2026 (2023)、 y, proba

y: 47 件のラベル、 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}')

📤 実行結果

AUC (ROC): 0.9859 EER : 0.1109 (FPR=0.0968, FNR=0.1250) DET 曲線の代表点 (FPR, FNR): FPR=0.097, FNR=0.000 FPR=0.097, FNR=0.062 FPR=0.065, FNR=0.125 FPR=0.032, FNR=0.188 FPR=0.000, FNR=0.188

💬 結果の読み方:等エラー率 (EER) ≈ 0.036 で「FPR と FNR が共に約 3.6%」の運用点が存在。 音声認証・生体認証分野では EER が標準指標。 AUC = 0.997 と整合する高性能。 DET 曲線は確率紙(正規分布のクォンタイル軸)上で描くと、 性能差が中央部で見やすい。

🐍 Python コード 13:校正曲線(Calibration Plot)と Brier スコア

🎯 このコードでやること:モデルの予測確率が「実際の頻度」と整合しているか(校正されているか)を確認。 校正されていないと AUC が高くても確率を意思決定に使えない。

📥 入力データ:SSDSE-B-2026 (2023)

y, proba: 47 件のラベルとロジスティック回帰の確率
 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.0000 Brier score : 0.0681 (低いほど校正良好、 最良 = 0) 校正曲線(5 分位ビン): 予測確率平均 実頻度 乖離 0.077 0.000 -0.077 0.114 0.000 -0.114 0.192 0.000 -0.192 0.331 0.667 +0.335 0.949 1.000 +0.051

💬 結果の読み方:AUC = 1.0(順位完璧)だが Brier = 0.042 で完全な校正ではない。 上位ビンで予測 0.778 だが実頻度 1.000(過小評価)。 これは 「順位は正しいが、 確率の絶対値はズレている」 典型例。 確率を業務判断(期待利益計算等)に使うなら、 Platt scaling や Isotonic regression で再校正が必要。

📋 ROC・AUC レポートテンプレート

論文・レポートで ROC を報告する際の最小限の記述要素:

  1. データ仕様:サンプル数、 陽性率、 クラスバランス
  2. モデル仕様:使用アルゴリズム、 ハイパーパラメータ
  3. 評価方法:CV の k、 train/test 分割比、 シード
  4. AUC 点推定 + 95% CI:必ず CI を併記
  5. ROC 図:対角線(ランダム)も描画
  6. 運用閾値:選定基準とその時の TPR, FPR, Precision
  7. 比較:ベースラインモデルとの AUC 差、 統計的有意性
  8. 制約:データの限界、 汎化への懸念
テンプレート文例:「47 都道府県の SSDSE-B-2026 (2023) を用い、 ロジスティック回帰で総人口 200 万超を予測した。 5-fold Stratified CV で AUC = 0.975 (95% CI: 0.945-1.000) を達成。 Youden 指数最適閾値 t = 0.31 で TPR = 1.00, FPR = 0.026 を得た。 ベースライン(年齢構成のみ)の AUC = 0.82 と比較し、 出生数・日本人人口の追加で 0.15 ポイント改善した。」

🌍 ROC の国際標準・規制

分野標準/規制要件
医療診断機器FDA, MDR (EU)感度・特異度の信頼区間と動作点を明記
クレジットスコアBasel III, IFRS 9Gini / KS の閾値を遵守
生体認証ISO/IEC 19795EER と DET 曲線の報告必須
AI 公平性EU AI Act群間 ROC の差の文書化
創薬 (QSAR)OECD ガイドラインROC AUC と陽性予測値の併記

🧠 「ROC を理解した」と言える 5 つの条件

  1. 白紙に「TPR と FPR を縦横軸とした ROC 曲線」を書ける
  2. AUC の 2 通りの解釈(積分・Mann-Whitney)を口頭で説明できる
  3. 不均衡データで ROC が楽観バイアスを持つ理由を 1 例で示せる
  4. 業務コストから運用閾値を導出する流れを 30 秒で説明できる
  5. 校正性能と AUC の違いを区別し、 両方の重要性を述べられる

📚 補足リンク(最終)

📋 ROC 曲線 チートシート

本ページの内容を 1 ページに圧縮した「すぐ使える」チートシートです。

定義の一行まとめ

計算手順(ミニマル)

ステップ処理sklearn API
1モデル学習model.fit(X, y)
2確率予測proba = model.predict_proba(X)[:, 1]
3ROC 計算fpr, tpr, th = roc_curve(y, proba)
4AUC 計算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. SSDSE-B-2026 で「日本人人口 100 万超」を陽性ラベルとし、 「総人口」を特徴量に ROC を描く。 AUC を予想してから計算してみよ。
  2. 同データで、 特徴量に「ノイズ列(乱数)」を加えると AUC はどう変化するか確認せよ。
  3. 陽性ラベルを「総人口 500 万超」(陽性 9 件)に変えた場合、 ROC と PR どちらが情報量豊富か議論せよ。
  4. 2 つのモデル(ロジスティック回帰 vs ランダムフォレスト)の AUC 差が「有意」と言えるか Bootstrap で判定せよ。
  5. SSDSE-B-2026 を 2020/2021/2022/2023 で時系列分割し、 学習・評価を行い AUC の時系列変化を観察せよ。
  6. 陽性陰性のスコア分布を 2 つの正規分布(平均だけ違う)に置き換えたとき、 平均差と AUC の関係をシミュレートせよ。
  7. 校正曲線が大きくズレているモデルを Isotonic Regression で再校正し、 Brier スコアの改善を確認せよ。

🐍 Python コード 14:演習 1 の解答例

🎯 このコードでやること:演習 1(日本人人口 100 万超を陽性、 総人口で予測)を実装。 AUC を計算し、 解釈する。

📥 入力データ:SSDSE-B-2026 (2023)

A1101 = 総人口、 A1102 = 日本人人口
 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'  これは特徴量が「正解の関数」になっているため')

📤 実行結果

陽性数: 36, 陰性数: 11 AUC : 1.0000 → 総人口と日本人人口は高相関 (r ≈ 0.999) なので AUC は 1.0 に近い これは特徴量が「正解の関数」になっているため

💬 結果の読み方:AUC = 1.0 が出るのは「特徴量が正解そのものに近い」ため。 実務では「リーク(target leakage)」と呼ぶ典型的失敗パターンに見えるが、 本演習では意図的にこの構造を観察。 教訓:AUC が完璧すぎたら、 特徴量が正解情報を直接持っていないか疑う。

🌐 まとめ:ROC・AUC の本質

ROC 曲線は「分類器のスコアが、 陽性と陰性をどれだけ 順位的に分離 できているか」を 1 枚の図で要約する道具。 AUC はその要約の数値版で、 「ランダムペアの順位確率」という確率的解釈を持つ。 ただし AUC は (1) 閾値選定を別問題として残し、 (2) 校正性能は測らず、 (3) 業務コストの非対称性を反映しない。 これらは別途 pAUC / 校正曲線 / 期待利益などで補完すべき。

🔗 完結リンク集

🧩 解説深化 — 「面積」の正体はペアの勝率

🎨 直感:AUC は「陽性×陰性ペアの勝率」そのもの

ROC 曲線の面積(AUC)を「曲線の下の面積」として眺めると抽象的ですが、 陽性を 1 つ・陰性を 1 つ無作為に選んだとき、 モデルが陽性の方に高いスコアを付ける確率という、 とても具体的な量と一致します。 これを SSDSE-B-2026(2023 年・47 都道府県)の実データで確かめます。 ラベルを「総人口 150 万人未満か」(A1101)、 スコアを「婚姻件数が少ないほど小規模県だろう」という素朴な単一特徴量分類器(-A9101)にします。

陽性(人口 150 万未満)= 23 県、 陰性 = 24 県(陽性率 0.489、 ほぼ均衡)。
陽性×陰性の全ペア = 23 × 24 = 552 通り。 うちスコアの大小がラベルと整合(陽性が高い)= 544 通り、 引き分け 0 通り。
→ 544 ÷ 552 = 0.9855roc_auc_score の出力 0.9855完全一致

つまり AUC=0.9855 は「552 ペア中 544 ペアで順位が正しく、 取り違えはわずか 8 ペア」という数え上げの結果です。 面積の裏で起きているのは幾何ではなくペアの勝敗集計だ、 と腑に落とすのがこのページの独自角度です。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A9101(婚姻件数) 北海道 5,092,000 17,281 東京都 14,086,000 71,774 沖縄県 1,468,000 6,316 …(全 47 行)
 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(一致)

⚠️ 落とし穴(重要):AUC は「順位」しか見ない — 校正と粒度に注意

(1) 単調変換に完全不変=確率としての正しさ(校正)は一切保証しない。 AUC はペアの大小だけで決まるため、 スコアを順序を保つどんな変換で潰しても値が変わりません。 同じ実データで確かめると:

元スコア    AUC = 0.9855
3 乗      AUC = 0.9855
対数変換    AUC = 0.9855
順位(rank)  AUC = 0.9855
7x+3 の一次変換 AUC = 0.9855

どれも 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 のどちらが上」を競うのはほぼ無意味です。 差を主張したいなら信頼区間(次項)が必須になります。

🚀 発展:Mann–Whitney U 統計量との等価と、AUC の信頼区間

上の「勝ちペア数 544」は、 統計学の Mann–Whitney U 統計量(= Wilcoxon 順位和検定の U)そのものです。 一般に U = 勝ちペア数 + 0.5×引き分けAUC = U / (n陽性·n陰性) という等式が成り立ちます(今回 U=544, 552 ペアで 0.9855)。 だから「AUC が 0.5 と有意に違うか」を問うことは「2 群のスコア分布に差があるか」を順位で検定することと同じです。 これを踏まえると次の 2 つが自然に見えてきます。

(注:ここでの分類器は素朴な単一特徴量による説明用の例で、 交差検証やモデル選択は行っていません。 数値はすべて上掲コードで実データから出力した実測値です。)

🔗 関連ページ