別名・略称:PR 曲線、 PR-AUC、 Average Precision(AP)
Precision-Recall 曲線は 分類器の閾値を 0→1 に動かしたときの Precision と Recall の軌跡。 「クラス不均衡データでの分類器評価」に最適で、 ROC 曲線より厳しく・公平に性能を判定できます。 曲線下面積(PR-AUC = Average Precision = AP)が単一の総合指標。
🍰 まずはやさしく
正解を当てる力の成績表のようなものです。
予測の正確さをまとめて評価するために使います。
スマホの検索で正しい結果が出るか測る例です。
この章ではPR曲線の基本について読みます。
Precision-Recall 曲線(PR 曲線):閾値を動かして得られる Precision と Recall の軌跡。 不均衡分類の標準評価。
🍰 まずはやさしく
モデルの総合的な実力を示すグラフです。
設定を変えても変わらない性能を知るために使います。
部活のメンバー選びで基準を変える時に似ています。
ここではPR曲線が役立つ場面について読みます。
🍰 まずはやさしく
正解率と再現率を線で結んだ図です。
基準を動かした時の変化を見るために使います。
買い物で予算を変えて商品を探す感覚に似ています。
ここではグラフの読み方について読みます。
分類モデルがスコア(確率)を出力するとき、 閾値 t を 1.0 → 0.0 にゆっくり下げていきます。 各 t で「陽性予測」が増えていき、 Precision と Recall が変動します。 横軸 Recall、 縦軸 Precision としてプロットしたのが PR 曲線です。 理想は右上隅 (R=1, P=1)、 最悪は陽性率 π の水平線(ランダム分類器)。
| 指標 | 縦軸 | 横軸 | ベースライン | 不均衡耐性 |
|---|---|---|---|---|
| PR 曲線 | Precision | Recall | 陽性率 π(変動) | 高い(陽性側に焦点) |
| ROC 曲線 | TPR (=Recall) | FPR | 0.5(一定) | 低い(陰性が多いと楽観) |
PR 曲線下の面積を Average Precision(AP) と呼びます。 数式的には「Recall が 0 → 1 に動くときの Precision の平均」。 AP=1.0 で完璧、 AP=π(陽性率)でランダム分類と同等。 物体検出では複数クラスの AP を平均した mAP(mean Average Precision) が標準。
本用語に関連する代表的な可視化を 3 点示す。



分類しきい値 t を動かすと、 正例(陽性)と負例(陰性)のスコア分布の上で「判定境界」が左右に移動します。 境界より右側を「陽性」と予測するので、 境界を動かすと TP / FP / FN / TN の 4 区画が変化し、 precision = TP/(TP+FP) と recall = TP/(TP+FN) がリアルタイムに再計算されます。 しきい値を上げると precision↑・recall↓、 下げるとその逆になる「トレードオフ」を、 実際に指で触って確かめてください。 計算は正規分布の累積分布関数(誤差関数)で厳密に行っています。
| 実際のクラス | ||
| 陽性 | 陰性 | |
| 予測 陽性 |
TP 0 |
FP 0 |
| 予測 陰性 |
FN 0 |
TN 0 |
しきい値を上げると、 「よほど確信のある例だけ」を陽性と予測するようになります。 拾う数が減るので取りこぼし(FN)は増え recall は下がる一方、 拾った例は当たりやすくなり誤報(FP)が減って precision は上がる。 逆にしきい値を下げると「疑わしきは全部陽性」となり、 取りこぼしは減る(recall↑)が誤報が増える(precision↓)。 上のグラフで境界を右端・左端まで動かすと、 recall≈0 で precision が高い点と、 recall≈1 で precision がベースライン π に沈む点の両極が体感できます。
関連ページ:再現率(Recall)、 適合率(Precision)、 F1 スコア、 混同行列、 ROC 曲線、 AUC。 なお「しきい値(threshold)」の独立ページは現時点で用語集内に無いため、 本セクションの解説を参照してください。
🍰 まずはやさしく
計算式で表した性能のルールです。
正確な数値でモデルを比べるために使います。
テストの点数を計算して平均を出す時に似ています。
ここでは具体的な数式について読みます。
Precision は単一値で語ると本質を見失います。 閾値を動かせば Precision は 0 から 1 まで連続的に変化し、 同時に Recall がトレードオフ的に変動する。 PR-AUC(Precision-Recall 曲線下面積)は閾値非依存の総合指標で、 クラス不均衡が大きいケースでは ROC-AUC より頑健に分類器を評価できます。
PR-AUC は $\int_0^1 P(R)\,dR$ で、 Recall を 0→1 に動かしたときの Precision の平均。 ランダム分類器のベースラインは正例率 π そのもの(ROC-AUC のように 0.5 ではない)。 PR-AUC=0.8 でも π=0.7 ならほぼランダム、 π=0.05 なら超優秀という相対評価が必要。
SSDSE-B-2026(2023 年)47 県のうち、 人口 500 万超は 9 県(東京・神奈川・大阪・愛知・埼玉・千葉・兵庫・北海道・福岡)。 正例率 π = 9/47 ≈ 0.191。 これを「大学学生数(E6302)」だけで予測する単変量分類器の PR 曲線を描く。 大都市圏ほど大学生が多いという相関を使う。
🎯 このコードでやること: SSDSE-B-2026 で「人口 500 万超」を大学学生数から予測する分類器を作り、 sklearn.metrics.precision_recall_curve と average_precision_score で PR-AUC を算出。 閾値ごとの Precision 推移も観察。
📥 入力データ: SSDSE-B-2026 (data/raw/SSDSE-B-2026.csv) 総人口 A1101・大学学生数 E6302 列(cp932・skiprows=[1])。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd from sklearn.metrics import precision_recall_curve, 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) y_true = (d['A1101'].astype(float) > 5_000_000).astype(int) # 総人口 500万超 y_score = d['E6302'].astype(float) # 大学学生数 pi = y_true.mean() precision, recall, thresholds = precision_recall_curve(y_true, y_score) pr_auc = average_precision_score(y_true, y_score) print(f"正例率 π(ベースライン)= {pi:.3f}") print(f"PR-AUC = {pr_auc:.4f}") for t in (200_000, 120_000, 70_000): prec = (y_true[y_score >= t] == 1).mean() print(f"閾値 = {t:>7,} のとき Precision = {prec:.3f}") |
📤 実行結果:
💬 結果の読み方: PR-AUC=0.93 はベースライン 0.191 を遥かに超え、 大学学生数は人口 500 万超の優秀な単変量予測子。 閾値を厳しく(学生 20 万人超)すると Precision=1.0 だが Recall は犠牲。 閾値を 12 万人まで緩めると京都府(大学の街だが人口 253 万)が偽陽性に入り Precision=0.80 に低下。 閾値 7 万人では 9 県すべてを捕捉して Recall=1.0、 偽陽性は京都のみで Precision=0.90。 業務要件で閾値選択が変わる典型例。
average_precision_score がこの式を実装。SSDSE-B-2026(2023 年)で「人口 > 500 万人」を陽性(不均衡分類)として、 「大学学生数(E6302)」をスコアにしたときの PR 曲線を 手計算で構築します。 陽性 9 件(東京・神奈川・大阪・愛知・埼玉・千葉・兵庫・北海道・福岡)、 陰性 38 件、 陽性率 π = 9/47 ≈ 0.191。
| 閾値(大学学生数・人) | 予測陽性 | TP | FP | Precision | Recall |
|---|---|---|---|---|---|
| ≥ 300,000 | 1 | 1 | 0 | 1.000 | 0.111 |
| ≥ 150,000 | 4 | 4 | 0 | 1.000 | 0.444 |
| ≥ 120,000 | 5 | 4 | 1 | 0.800 | 0.444 |
| ≥ 100,000 | 9 | 8 | 1 | 0.889 | 0.889 |
| ≥ 70,000 | 10 | 9 | 1 | 0.900 | 1.000 |
| ≥ 50,000 | 11 | 9 | 2 | 0.818 | 1.000 |
| ≥ 0(全件) | 47 | 9 | 38 | 0.191 | 1.000 |
表の (Recall, Precision) を順にプロットすると PR 曲線。 最後の行(閾値 0)で Precision = 0.191(= π)に収束、 これがランダム分類のベースライン。 閾値 12 万人で京都府(大学の街だが人口 253 万)が偽陽性として混入し Precision が 1.0 から 0.80 に落ちる点が、 この予測子の弱点を示している。
AP の手計算: Recall の各増分を、 その時点の Precision で重み付けして足し合わせる。 Recall が 0.111→0.444(増分 0.333)まで Precision=1.0、 その後 0.889 で 0.889、 1.0 で 0.900 と段階的に下がるため AP は 1.0 より少し小さく ≈ 0.93 になる。 後の Python コードで average_precision_score により AP=0.9283 を確認します。
合成データで 4 閾値の PR から AP を計算する。
| 閾値 | P | R |
|---|---|---|
| 0.9 | 1.00 | 0.20 |
| 0.7 | 0.95 | 0.50 |
| 0.5 | 0.80 | 0.80 |
| 0.3 | 0.60 | 1.00 |
1 2 3 4 5 6 | import numpy as np R = np.array([0.20, 0.50, 0.80, 1.00]) P = np.array([1.00, 0.95, 0.80, 0.60]) # 階段近似 AP = (np.diff(np.concatenate([[0], R])) * P).sum() print(f"AP: {AP:.3f}") |
💬 手計算 (Step 2) 0.845 と Python 出力が完全一致。
SSDSE-B-2026(47 都道府県・2023 年)の実データで PR 曲線と AP(PR-AUC)を計算する基本コード:
🎯 このコードでやること:「人口 500 万超」を陽性ラベルとし、 大学学生数をスコアにして PR 曲線を sklearn で描き、 AP を計算する。 不均衡分類(陽性率 ≈ 19%)での標準ワークフロー。
📥 入力データ:SSDSE-B-2026.csv(総人口 A1101、 大学学生数 E6302)
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 | # SSDSE-B-2026 で PR 曲線・AP を計算 import pandas as pd import numpy as np from sklearn.metrics import precision_recall_curve, 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) # 陽性 = 人口 500 万超、 スコア = 大学学生数 y_true = (d['A1101'].astype(float) > 5_000_000).astype(int).values scores = d['E6302'].astype(float).values # PR 曲線(precision, recall, thresholds の 3 配列) precision, recall, thresholds = precision_recall_curve(y_true, scores) # AP = PR-AUC ap = average_precision_score(y_true, scores) pi = y_true.mean() print(f'陽性数 = {y_true.sum()}, 陰性数 = {(1-y_true).sum()}') print(f'陽性率 π (ベースライン) = {pi:.3f}') print(f'AP (PR-AUC) = {ap:.4f}') print(f'\n--- PR 曲線の代表点 ---') print(f'{"閾値":>14} {"Precision":>10} {"Recall":>8}') for idx in [0, len(precision)//4, len(precision)//2, 3*len(precision)//4, -2]: t = thresholds[idx] if idx < len(thresholds) else 0 print(f'{t:>14.0f} {precision[idx]:>10.3f} {recall[idx]:>8.3f}') |
📤 実行結果:
💬 結果の読み方:AP = 0.93 はベースライン 0.19 を遥かに上回り、 大学学生数は人口 500 万超の優秀な単変量予測子。 学生 23 万人以上(東京・大阪)に絞れば Precision = 1.0 だが Recall = 0.11 で 9 件中 1 件しか捕捉できない。 逆に閾値を下げて全 9 県を捕捉すると(学生 7 万人付近)、 京都府だけが偽陽性として残り Precision = 0.90。 業務要件次第で閾値選択が決まる。
Precision と Recall は情報検索 (Information Retrieval) 分野で 1955 年に Allen Kent らが初めて定式化した。 初期は「relevance ratio」「coverage ratio」と呼ばれていたが、 1960 年代に Cyril Cleverdon の Cranfield プロジェクトで現在の Precision/Recall という用語が定着。 1970 年代に C.J. van Rijsbergen が Information Retrieval 教科書 (1979) で F-measure (Fβ-score) を提案、 β=1 が現在の F1 として広まった。 1990 年代の機械学習・データマイニングの隆盛で、 不均衡データセット (信用詐欺検出、 医療診断、 スパムフィルタ) が標準的課題となり、 Precision/Recall が accuracy を補完する指標として定着。 1997 年に Bradley が AUC-ROC を提案し、 2006 年に Davis-Goadrich が「不均衡データでは AUPRC が ROC より informative」と論証した。 2017 年に Lin らが Focal Loss を提案し、 不均衡データでの分類性能を直接最適化する手法が広まった。 2020 年代には fairness-aware Precision/Recall (race/gender/age 別の公平性) が AI 倫理の文脈で議論されるようになった。
情報検索では Mean Average Precision (mAP)、 nDCG@k、 Recall@k が標準で、 検索エンジン・推薦システムの評価指標となっている。 物体検出 (Object Detection) では IoU 閾値別の mAP (PASCAL VOC、 COCO) が業界標準。 自然言語処理では BLEU、 ROUGE、 METEOR が Precision/Recall ベースの生成品質指標として使われる。 医療診断では Sensitivity (= Recall) と Specificity の組合せで ROC 曲線が標準。 セキュリティ分野では False Positive Rate (FPR) と True Positive Rate (TPR) で警報システムの性能を評価する。 不正検出 (fraud detection) では precision-recall trade-off を金額重み付け (cost-sensitive) で評価することが多い。 SSDSE-B-2026 を用いた政策評価では「政策介入が必要な都道府県の予測」のような不均衡分類問題で、 Precision 重視 (限られた予算を確実な対象に集中) か Recall 重視 (見落としを避ける) かの戦略選択が重要。 機械学習公平性研究では、 Demographic Parity、 Equalized Odds、 Calibrated Equality of Opportunity など、 サブグループ別の Precision/Recall 一致を要求する公平性指標が定義されており、 EU AI Act、 NIST AI RMF などで言及されている。
Precision-Recall 曲線の周辺概念をテーマ別ツリーで整理:
分類モデル評価指標 ├── ROC 曲線・ROC-AUC (対比: 不均衡データに弱い) ├── 【Precision-Recall 曲線】 ← ここ │ ├── Average Precision (AP) / 曲線下面積 (PR-AUC) │ ├── F1 / F-β スコア (PR の調和平均) │ └── mAP (mean Average Precision) / Precision@k / R-Precision └── 混同行列 / Recall@k / MCC / Cohen's κ
この階層構造を頭に入れておくと、 学習や論文読みで「自分が今どこにいるか」を見失わずに済みます。
「Precision-Recall 曲線」を確実にマスターするには、 次の順序で進むのが効率的です:
焦らず、 1 段ずつ確実に。 7 ステップを 1 周すれば、 単に「知っている」から「使える」レベルに到達できます。
適合率-再現率は不均衡分類の評価指標で、 F1・PR 曲線・ROC・AUC と一体で運用される。
SSDSE-B-2026 を用いた演習では、 「適合率-再現率」 を中核に据えて上記の上流・並列・下流の手法を実データで連結する経験を積むと、 単独の手法暗記より実務的応用力が身につく。
「適合率と再現率」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
適合率と再現率は閾値を動かすと逆方向に動く。 片方だけを報告するのは、 都合の良い点を選んだのと同じになる。
適合率は「予測陽性のうち、 本当に陽性だった割合」を測る指標で、 false alarm(誤報)の少なさを評価します。 ここから先は、 適合率の数学的本質・実装・落とし穴を相関ページレベルで掘り下げます。
$$ \mathrm{Precision} = \frac{TP}{TP + FP} $$
$$ \mathrm{Recall} = \frac{TP}{TP + FN}, \quad F_1 = \frac{2 \cdot P \cdot R}{P + R} $$
$$ \mathrm{AP} = \sum_{n=1}^{N} (R_n - R_{n-1}) \cdot P_n \;\;(\text{PR 曲線下面積}) $$
| 記号 | 意味 | SSDSE-B-2026 での解釈 |
|---|---|---|
| TP | 真陽性(陽性予測かつ実陽性) | 「人口大」予測 ∧ 実際に人口大 |
| FP | 偽陽性(陽性予測だが実陰性) | 「人口大」予測だが実際は人口小 |
| FN | 偽陰性(陰性予測だが実陽性) | 「人口小」予測だが実際は人口大(見逃し) |
| Precision | 陽性予測の正確さ | 「人口大」予測した県の何%が正解か |
| Recall | 陽性の捕捉率 | 人口大県の何%を捕捉できたか |
| AP | PR 曲線下面積 | 閾値非依存の総合精度 |
陽性ラベル = 「総人口 200 万超」(2023 年データで 16 県)を 47 県のラベルとし、 「出生数(A4101)」で予測した場合の混同行列を実値で示します。
| 閾値(出生数) | 予測陽性 | TP | FP | FN | TN | Precision | Recall |
|---|---|---|---|---|---|---|---|
| 5,000 | 39 | 16 | 23 | 0 | 8 | 0.410 | 1.000 |
| 10,000 | 20 | 16 | 4 | 0 | 27 | 0.800 | 1.000 |
| 13,000 | 13 | 13 | 0 | 3 | 31 | 1.000 | 0.812 |
| 15,000 | 11 | 11 | 0 | 5 | 31 | 1.000 | 0.688 |
| 20,000 | 9 | 9 | 0 | 7 | 31 | 1.000 | 0.562 |
| 30,000 | 8 | 8 | 0 | 8 | 31 | 1.000 | 0.500 |
| 50,000 | 3 | 3 | 0 | 13 | 31 | 1.000 | 0.188 |
この表から見えるのは 「閾値を厳しくすると Precision は上がるが Recall は下がる」 典型的トレードオフ。 閾値 10,000 までは Recall = 1.0 を保ちつつ Precision が 0.41→0.80 と改善し、 13,000 以上では偽陽性が消えて Precision = 1.0 になる代わりに Recall が段階的に落ちる。 出生数が総人口と強く相関しているため、 中間閾値で Precision・Recall のバランス点を選べる。
🎯 このコードでやること:SSDSE-B-2026 (2023) で「総人口 200 万超」を陽性とラベル付け。 ロジスティック回帰で予測し、 sklearn.metrics.precision_score / precision_recall_curve で精度を測る。
📥 入力データ: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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import precision_score, recall_score, 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'] > 2_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101','A1102']].values) model = LogisticRegression().fit(X, y) pred = model.predict(X) cm = confusion_matrix(y, pred) print(f'混同行列:') print(f' TN={cm[0,0]:2d} FP={cm[0,1]:2d}') print(f' FN={cm[1,0]:2d} TP={cm[1,1]:2d}') print(f'\nPrecision: {precision_score(y, pred):.4f}') print(f'Recall : {recall_score(y, pred):.4f}') print(f'F1 : {f1_score(y, pred):.4f}') |
📤 実行結果:
💬 結果の読み方:Precision = 1.0(陽性と予測した 11 県はすべて正解、 偽陽性ゼロ)だが Recall = 0.69 で、 人口 200 万超 16 県のうち 5 県を取りこぼしている。 出生数・日本人人口は総人口と強く相関するものの、 境界付近(人口 200 万前後)の県は線形境界で分離しきれない。 Precision だけを見ると「完璧」に見えるが Recall と F1(0.81)を併読して初めて実態が分かる、 という Precision-Recall トレードオフの好例。 なお「同じデータで学習・評価」しているため、 実務では CV で評価すべき。
🎯 このコードでやること:閾値を 0 → 1 に動かしたときの Precision と Recall の軌跡を取得し、 AP (PR 曲線下面積) を計算。 不均衡データでの評価に使う。
📥 入力データ:コード 1 の y, X
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import precision_recall_curve, average_precision_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] prec, rec, th = precision_recall_curve(y, proba) ap = average_precision_score(y, proba) print(f'AP (PR-AUC): {ap:.4f}') print(f'\nPR 曲線の代表点:') print(f'{"閾値":<10}{"Precision":<12}{"Recall":<10}') for i in [0, len(prec)//4, len(prec)//2, 3*len(prec)//4, -2]: t = th[i] if i < len(th) else 1.0 print(f'{t:<10.3f}{prec[i]:<12.3f}{rec[i]:<10.3f}') |
📤 実行結果:
💬 結果の読み方:AP = 0.98 とほぼ完全な PR 曲線。 低い閾値では予測陽性が多く Precision が低い(0.340 = ベースライン陽性率 π = 16/47)が、 閾値を上げると Precision が単調増加し、 閾値 0.44 付近で Precision = 1.0(Recall 0.69)に達する。 実務では「Precision 80% を保つ最大 Recall」のような形で運用閾値を選定。
🎯 このコードでやること:陽性率が低い設定で、 Accuracy が高くても Precision が低いケースを SSDSE-B-2026 で再現。 「Accuracy だけでは判断できない」ことを示す。
📥 入力データ: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.metrics import accuracy_score, precision_score, recall_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) # 極端な不均衡 y = (d['A1101'] > 5_000_000).astype(int).values print(f'陽性数: {y.sum()}, 陰性数: {(1-y).sum()}, 陽性率: {y.mean():.3f}') # 戦略 A: 全て陰性と予測(怠惰モデル) pred_a = np.zeros_like(y) print(f'\n戦略A (全陰性予測):') print(f' Accuracy : {accuracy_score(y, pred_a):.4f}') print(f' Precision: {precision_score(y, pred_a, zero_division=0):.4f}') print(f' Recall : {recall_score(y, pred_a):.4f}') # 戦略 B: 出生数の上位 5 を陽性予測 threshold = np.percentile(d['A4101'].values, 90) pred_b = (d['A4101'].values >= threshold).astype(int) print(f'\n戦略B (出生数 top 10% を陽性):') print(f' Accuracy : {accuracy_score(y, pred_b):.4f}') print(f' Precision: {precision_score(y, pred_b):.4f}') print(f' Recall : {recall_score(y, pred_b):.4f}') |
📤 実行結果:
💬 結果の読み方:戦略 A は Accuracy 81% と高いように見えるが、 Precision = 0、 Recall = 0 で完全無能(陽性を 1 件も当てていない)。 戦略 B は Precision = 1.0 で「陽性予測 = 完全正解」だが Recall = 0.56 で 9 件中 5 件しか捕捉できない。 Accuracy はどちらも高いのに中身は正反対で、 不均衡データでは Precision・Recall を見ないと評価できない 典型例。
🎯 このコードでやること:閾値を変えると Precision と Recall がどう動くかを表で確認。 業務に応じた最適閾値選定の感覚を養う。
📥 入力データ:SSDSE-B-2026 (2023)、 y = 人口 200 万超
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 sklearn.linear_model import LogisticRegression from sklearn.metrics import precision_score, recall_score, f1_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] print(f'{"閾値":<8}{"予測陽性数":<12}{"Precision":<12}{"Recall":<10}{"F1":<8}') for t in [0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9, 0.95]: pred = (proba >= t).astype(int) n_pos = pred.sum() p = precision_score(y, pred, zero_division=0) r = recall_score(y, pred) f = f1_score(y, pred, zero_division=0) print(f'{t:<8.2f}{n_pos:<12}{p:<12.3f}{r:<10.3f}{f:<8.3f}') |
📤 実行結果:
💬 結果の読み方:閾値 0.30 が F1 最大(0.897)のスイートスポットで、 Precision=1.0・Recall=0.81。 閾値を下げる(0.10)と Recall=1.0 になる代わりに Precision がベースライン 0.340 まで落ち、 上げる(0.95)と Precision=1.0 を保つが Recall は 0.31 まで低下(16 件中 11 件見逃し)。 業務で「予測陽性は確実に対応する」なら高閾値、 「漏れを避けたい」なら低閾値、 「両方バランス」なら F1 最大化。
precision_score(average=...) は macro / micro / weighted / None の 4 種。 不均衡なら macro(クラス平均、 少数クラス重視)、 全体性能重視なら micro(サンプル単位、 多数クラスに引きずられる)、 weighted(クラスサイズ重み付け)。 デフォルトは binary(二値のみ)なので多クラスでは明示必須。
zero_division 引数(0 / 1 / warn)で挙動制御。 デフォルトは warn → 0 を返すが、 業務によっては undefined(NaN)として扱うべき場合も。
| 指標 | 定義 | Precision との関係 |
|---|---|---|
| Recall (Sensitivity) | TP / (TP+FN) | トレードオフ関係 |
| F1 スコア | 2PR / (P+R) | 調和平均、 両方が低いと F1 も低い |
| F-β スコア | (1+β²)PR / (β²P+R) | β>1 なら R 重視、 β<1 なら P 重視 |
| Accuracy | (TP+TN) / Total | 不均衡時は Precision より楽観的 |
| AP (PR-AUC) | PR 曲線下面積 | 閾値非依存の総合 Precision |
| Precision@k | 上位 k 件の Precision | 業務件数制約付き Precision |
| PPV (陽性的中率) | = Precision | 医療用語、 同じもの |
2 値分類を超えて 3 クラス以上のとき、 Precision は「クラスごとの Precision を平均する」必要があります。 平均方式が結果を大きく変えます。
$$ \mathrm{Precision}_c = \frac{TP_c}{TP_c + FP_c} \quad (c = 1, \ldots, C) $$
$$ \mathrm{macro\;Precision} = \frac{1}{C} \sum_{c=1}^{C} \mathrm{Precision}_c $$
$$ \mathrm{micro\;Precision} = \frac{\sum_c TP_c}{\sum_c (TP_c + FP_c)} $$
$$ \mathrm{weighted\;Precision} = \frac{1}{N} \sum_{c=1}^{C} n_c \cdot \mathrm{Precision}_c $$
🎯 このコードでやること:SSDSE-B-2026 の都道府県を人口 3 クラス(小/中/大)に分け、 macro/micro/weighted Precision の差を確認。
📥 入力データ:SSDSE-B-2026 (2023)、 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.metrics import precision_score, classification_report 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) def to_class(p): if p < 1_000_000: return 0 if p < 3_000_000: return 1 return 2 y = d['A1101'].apply(to_class).values X = StandardScaler().fit_transform(d[['A1102','A4101','A4103']].values) pred = LogisticRegression(max_iter=500).fit(X, y).predict(X) print(f'macro Precision: {precision_score(y, pred, average="macro"):.4f}') print(f'micro Precision: {precision_score(y, pred, average="micro"):.4f}') print(f'weighted Precision: {precision_score(y, pred, average="weighted"):.4f}') print(f'\nクラスごと Precision: {precision_score(y, pred, average=None)}') print(f'\n--- classification_report ---') print(classification_report(y, pred, target_names=['小','中','大'])) |
📤 実行結果:
💬 結果の読み方:micro 0.787 < weighted 0.845 < macro 0.910 と、 平均方法で値が大きく変わる。 多数派の中クラス(27 件)の Precision = 0.73 が micro / weighted を引き下げる一方、 小・大クラスは Precision = 1.0 のため全クラス対等の macro が最も高く出る。 ただし小クラスは Recall = 0.10(10 件中 1 件しか当てられない)で、 Precision だけを見ると実態を見誤る典型例。 少数クラスを軽視したくないなら macro、 全体的中率を見たいなら micro を使い、 必ず Recall と併読する。
| 要素 | 内容 |
|---|---|
| 陽性 | がん患者 |
| 陽性率 | 0.5% |
| FP コスト | 不要な精密検査・患者の不安 |
| FN コスト | がん見逃し(致命的) |
| 使う指標 | Recall (=Sensitivity) 優先、 Precision は副指標 |
| 典型要件 | 「Recall 99% を保つ Precision」 |
| 要素 | 内容 |
|---|---|
| 陽性 | スパムメール |
| 陽性率 | 50-80% |
| FP コスト | 正規メールがスパム扱い(重大) |
| FN コスト | スパム見逃し(軽微) |
| 使う指標 | Precision 優先(FP を許容しない) |
| 典型要件 | 「Precision 99.5% を保つ Recall」 |
| 要素 | 内容 |
|---|---|
| 陽性 | クエリに関連する文書 |
| 陽性率 | 非常に低い (0.001%) |
| FP コスト | 無関係結果がトップに(UX 低下) |
| FN コスト | 関連結果を見落とし |
| 使う指標 | Precision@k (k=10, 20 等)、 nDCG |
| 典型要件 | 「Top 10 の Precision を最大化」 |
🎯 このコードでやること:SSDSE-B-2026 でスコア上位 k 件のうち真陽性が何割かを計算。 ランキングシステムの標準評価。
📥 入力データ:SSDSE-B-2026 (2023)、 y = 人口 200 万超
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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] # スコア降順でソート order = np.argsort(proba)[::-1] y_sorted = y[order] print(f'真陽性数: {y.sum()}') print(f'\n{"k":<6}{"Precision@k":<14}{"Recall@k":<12}') for k in [3, 5, 10, 14, 20, 30, 47]: p_k = y_sorted[:k].sum() / k r_k = y_sorted[:k].sum() / y.sum() print(f'{k:<6}{p_k:<14.3f}{r_k:<12.3f}') |
📤 実行結果:
💬 結果の読み方:k=10 まで Precision@k = 1.0(上位 10 位はすべて真陽性)。 k=14 で 0.929(1 件だけ偽陽性が混入)、 k=20 で 0.800(陽性 16 件 + 偽陽性 4 件)。 検索エンジンで「Top 10 表示」なら Precision@10 = 1.0 で完璧。 業務の k に合わせた評価 が肝心。
F1 は Precision と Recall を 1:1 で重み付けする調和平均ですが、 業務によっては片方を重視したいケースがあります。 そこで F-β スコアが使われます。
$$ F_\beta = (1 + \beta^2) \cdot \frac{P \cdot R}{\beta^2 \cdot P + R} $$
| β 値 | 意味 | 使いどころ |
|---|---|---|
| β = 0 | F0 = P(Precision のみ) | Precision のみ重視(理論限界) |
| β = 0.5 | Precision 2 倍重視 | スパムフィルタ、 検索エンジン |
| β = 1 | P と R 同等 | 標準的な分類タスク |
| β = 2 | Recall 2 倍重視 | 医療診断、 不正検知 |
| β → ∞ | F∞ = R(Recall のみ) | Recall のみ重視(理論限界) |
🎯 このコードでやること:同じ予測に対し β=0.5, 1, 2 の F スコアを計算し、 業務によってどう優先順位が変わるか観察。
📥 入力データ: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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import fbeta_score, precision_score, recall_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] print(f'{"閾値":<8}{"Precision":<12}{"Recall":<10}{"F0.5":<10}{"F1":<10}{"F2":<10}') for t in [0.2, 0.3, 0.5, 0.7, 0.8, 0.9]: pred = (proba >= t).astype(int) p = precision_score(y, pred, zero_division=0) r = recall_score(y, pred) f05 = fbeta_score(y, pred, beta=0.5, zero_division=0) f1 = fbeta_score(y, pred, beta=1, zero_division=0) f2 = fbeta_score(y, pred, beta=2, zero_division=0) print(f'{t:<8.2f}{p:<12.3f}{r:<10.3f}{f05:<10.3f}{f1:<10.3f}{f2:<10.3f}') |
📤 実行結果:
💬 結果の読み方:閾値 0.30 で F0.5 = 0.956、 F1 = 0.897 がともに最大(Precision と Recall のバランス点)。 一方 Recall を重視する F2 は閾値 0.20 の 0.889 が最大で、 取りこぼしを嫌う場面ではより低い閾値が選ばれる。 業務での β 選択 = 業務コストの非対称性を反映。 スパムフィルタなら F0.5(Precision 重視)、 がん検診なら F2(Recall 重視)。
🎯 このコードでやること:F1 / F0.5 / F2 のそれぞれを最大化する閾値を自動探索し、 比較する。
📥 入力データ: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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import fbeta_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] thresholds = np.linspace(0.01, 0.99, 99) best_results = {} for beta in [0.5, 1.0, 2.0]: scores = [] for t in thresholds: pred = (proba >= t).astype(int) s = fbeta_score(y, pred, beta=beta, zero_division=0) scores.append(s) scores = np.array(scores) idx = np.argmax(scores) best_results[beta] = (thresholds[idx], scores[idx]) print(f'{"β":<6}{"最適閾値":<14}{"スコア":<10}') for beta, (t, s) in best_results.items(): print(f'{beta:<6.1f}{t:<14.3f}{s:<10.3f}') |
📤 実行結果:
💬 結果の読み方:2 特徴量(出生数・日本人人口)のモデルはこのタスクを完全分離できるため、 すべての β で同じ閾値 0.260 が最適(スコア 1.0)。 不完全分離のタスクでは β ごとに異なる閾値が選ばれる。 実務で β を選ぶ際は、 「FP のコスト ÷ FN のコスト」の比を計算し、 それに応じた β を決める。
Precision は「予測陽性のうち真陽性の割合」ですが、 予測確率が校正されていれば 「予測確率 0.9 の案件群の真陽性率は約 0.9」 となるはず。 校正されていないと、 確率の値そのものが意思決定の根拠にならない。
🎯 このコードでやること:予測確率を 5 ビンに分け、 各ビン内の Precision(実際の陽性率)を測定。 校正性能を確認。
📥 入力データ: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 | 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] # 5 ビンに分割 bins = np.linspace(0, 1, 6) print(f'{"確率ビン":<20}{"件数":<8}{"陽性数":<8}{"実 Precision":<14}{"乖離":<10}') for i in range(5): lo, hi = bins[i], bins[i+1] mask = (proba >= lo) & (proba < hi) if i < 4 else (proba >= lo) & (proba <= hi) n = mask.sum() if n == 0: continue pos = y[mask].sum() p_actual = pos / n p_mean = proba[mask].mean() print(f'[{lo:.2f}, {hi:.2f}){"":<8}{n:<8}{pos:<8}{p_actual:<14.3f}{p_actual-p_mean:+.3f}') |
📤 実行結果:
💬 結果の読み方:低確率ビン(<0.2)と最高確率ビン(≥0.8)は実 Precision が予測とよく一致。 一方、 中間ビン(特に [0.40, 0.60))は実 Precision = 1.0 に対して予測確率の平均が低く、 乖離 +0.515 と大きい(少数の境界例で確率が過小評価)。 一般に、 確率を業務閾値として直接使うなら、 Platt scaling か Isotonic regression で校正すると精度向上。 sklearn の CalibratedClassifierCV が便利。
| ステップ | 処理 | API |
|---|---|---|
| 1 | モデル予測 | pred = model.predict(X) |
| 2 | 確率予測 | proba = model.predict_proba(X)[:, 1] |
| 3 | Precision | precision_score(y, pred) |
| 4 | PR 曲線 | precision_recall_curve(y, proba) |
| 5 | AP | average_precision_score(y, proba) |
| 6 | F1 | f1_score(y, pred) |
| 7 | 分類レポート | classification_report(y, pred) |
| 業務 | 優先指標 | 典型要件 |
|---|---|---|
| 医療スクリーニング | Recall | 「Recall 99% を保つ Precision」 |
| スパムフィルタ | Precision | 「Precision 99.5% を保つ Recall」 |
| 検索エンジン | Precision@k | 「Top 10 の Precision」 |
| 不正検知 | Recall | 「FN を最小化」 |
| レコメンド | Precision@k + Diversity | 「Top 20 で多様な推薦」 |
| マーケティング DM | Precision + Lift | 「上位 K% の Lift」 |
| 創薬スクリーニング | Precision | 「上位候補で実験成功率」 |
statsmodels.stats.proportion.proportion_confint で計算可能。 Bootstrap は 1000 回リサンプリングして 95% 範囲を取る。
🎯 このコードでやること:Precision = TP / (TP + FP) の Wilson 95% CI を計算し、 サンプル数による不確実性を把握。
📥 入力データ: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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import precision_score, confusion_matrix from sklearn.preprocessing import StandardScaler from statsmodels.stats.proportion import proportion_confint 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] # 閾値 0.5 と 0.3 の 2 ケースで比較 for t in [0.3, 0.5, 0.7]: pred = (proba >= t).astype(int) cm = confusion_matrix(y, pred) tp, fp = cm[1,1], cm[0,1] n = tp + fp p = tp / n if n > 0 else 0.0 if n > 0: ci_lo, ci_hi = proportion_confint(tp, n, alpha=0.05, method='wilson') print(f'閾値 {t}: TP={tp}, FP={fp}, Precision={p:.3f} (95% CI: [{ci_lo:.3f}, {ci_hi:.3f}])') else: print(f'閾値 {t}: 予測陽性なし') |
📤 実行結果:
💬 結果の読み方:どの閾値でも点推定は Precision = 1.0 だが、 予測陽性数が減るほど(14→11→9)Wilson 区間の下限は 0.785→0.741→0.701 と下がり、 CI は広がる。 サンプルが少ないと「点推定 1.0」でも真の Precision は 0.70 前後まであり得る。 必ず CI を併記し、 「Precision = 1.00 (95% CI: 0.74-1.00)」のように報告する。
本ページの内容を定着させるため、 自分の手を動かす演習を 7 つ用意。
🎯 このコードでやること:演習 1(世帯数 100 万超を陽性として閾値別に評価)の実装。
📥 入力データ: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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import precision_score, recall_score, f1_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','A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] print(f'陽性数: {y.sum()}, 陰性数: {(1-y).sum()}') print(f'\n{"閾値":<8}{"Precision":<12}{"Recall":<10}{"F1":<8}') for t in [0.3, 0.5, 0.7]: pred = (proba >= t).astype(int) p = precision_score(y, pred, zero_division=0) r = recall_score(y, pred) f = f1_score(y, pred, zero_division=0) print(f'{t:<8.1f}{p:<12.3f}{r:<10.3f}{f:<8.3f}') |
📤 実行結果:
💬 結果の読み方:陽性率が 36/47 ≈ 0.77 と高い(多数派が陽性)ため、 低い閾値では Precision がベースライン 0.766 に張り付き Recall = 1.0。 閾値を 0.7 まで上げると Precision = 1.0 に達するが Recall は 0.72 に低下する。 特徴量(総人口)と陽性ラベル(日本人人口 100 万超)はほぼ同じ情報を持つため分離は容易だが、 このように 特徴量と陽性ラベルに強い相関がある場合は target leakage を疑う。
マルチラベル分類(1 サンプルが複数ラベルを持つ)の Precision は次のように定義されます。
$$ \mathrm{Precision}_{\text{multi-label}} = \frac{1}{N} \sum_{i=1}^{N} \frac{|y_i \cap \hat{y}_i|}{|\hat{y}_i|} $$
Precision はベイズの定理を使うと「事後確率」として解釈できます。 これは医療検査などで頻出する「陽性的中率(PPV)」と同じ概念です。
$$ \mathrm{Precision} = P(Y=1 \mid \hat{Y}=1) = \frac{P(\hat{Y}=1 \mid Y=1) \cdot P(Y=1)}{P(\hat{Y}=1)} = \frac{\mathrm{Sens} \cdot \pi}{\mathrm{Sens} \cdot \pi + (1-\mathrm{Spec}) \cdot (1-\pi)} $$
| 記号 | 意味 |
|---|---|
| $\pi = P(Y=1)$ | 陽性の事前確率(有病率・基底比) |
| $\mathrm{Sens} = P(\hat{Y}=1 \mid Y=1)$ | 感度 = Recall |
| $\mathrm{Spec} = P(\hat{Y}=0 \mid Y=0)$ | 特異度 |
| $1 - \mathrm{Spec}$ | 偽陽性率 (FPR) |
🎯 このコードでやること:陽性率(基底比)を変えると、 同じ感度・特異度でも Precision がどう変化するかをシミュレートする。
📥 入力データ: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 | import pandas as pd import numpy as np # 実際のデータで陽性率の幅を確認 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) print('SSDSE-B-2026 (2023) の陽性率実例:') for thresh, name in [ (5_000_000, '人口 500 万超'), (3_000_000, '人口 300 万超'), (2_000_000, '人口 200 万超'), (1_000_000, '人口 100 万超'), (500_000, '人口 50 万超'), ]: pi = (d['A1101'] > thresh).mean() print(f' {name:<12}: 陽性率 π = {pi:.4f}') # 感度・特異度を固定して陽性率を変える sens = 0.95 spec = 0.95 print(f'\n感度 = {sens}, 特異度 = {spec} を固定、 陽性率を変えると Precision はこう変化:') print(f'{"陽性率π":<12}{"Precision (PPV)":<18}{"FP 数 (1000 人検査時)":<25}') for pi in [0.001, 0.005, 0.01, 0.05, 0.10, 0.30, 0.50]: prec = (sens * pi) / (sens * pi + (1 - spec) * (1 - pi)) fp_per1k = (1 - spec) * (1 - pi) * 1000 print(f'{pi:<12.4f}{prec:<18.4f}{fp_per1k:<25.1f}') |
📤 実行結果:
💬 結果の読み方:感度 95%、 特異度 95% の 「優秀な検査」 でも、 陽性率が 0.1% なら Precision はわずか 1.86%。 1000 件中 50 件の FP が出てくる。 これが「稀少事象検出の根本的難しさ」。 医療で「がん検診を全員に施しても、 陽性者の大半は実は陰性」というのはこの数学。 業務で稀少事象を検出するなら、 事前スクリーニング で陽性率を上げる工夫が必須。
FP コストと FN コストが大きく違うとき、 単純な Precision/Recall 最大化ではなく、 期待損失最小化を目指します。
$$ \min_{f} \mathbb{E}_{(X, Y)} \left[ C_{FP} \cdot \mathbb{1}[f(X)=1, Y=0] + C_{FN} \cdot \mathbb{1}[f(X)=0, Y=1] \right] $$
🎯 このコードでやること:FP コスト、 FN コストを与え、 期待損失を最小化する閾値を選定する。
📥 入力データ: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 34 | 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'] > 3_000_000).astype(int).values X = StandardScaler().fit_transform(d[['A4101']].values) proba = LogisticRegression().fit(X, y).predict_proba(X)[:, 1] # シナリオ A: FP = 1000, FN = 1000 (対等) # シナリオ B: FP = 1000, FN = 10000 (見逃しが重い) # シナリオ C: FP = 10000, FN = 1000 (誤報が重い) scenarios = [ ('A 対等', 1000, 1000), ('B FN 重視', 1000, 10000), ('C FP 重視', 10000, 1000), ] thresholds = np.linspace(0.01, 0.99, 99) print(f'{"シナリオ":<14}{"FP cost":<10}{"FN cost":<10}{"最適閾値":<10}{"期待損失":<10}') for name, cfp, cfn in scenarios: losses = [] for t in thresholds: pred = (proba >= t).astype(int) fp = ((pred==1) & (y==0)).sum() fn = ((pred==0) & (y==1)).sum() loss = cfp * fp + cfn * fn losses.append(loss) losses = np.array(losses) idx = np.argmin(losses) print(f'{name:<14}{cfp:<10}{cfn:<10}{thresholds[idx]:<10.3f}{losses[idx]:<10}') |
📤 実行結果:
💬 結果の読み方:本タスク(人口 300 万超を出生数で予測)は完全分離可能なため、 どのコスト設定でも損失 0 を達成でき、 最適閾値は 0.180 で一致する。 完全分離できる問題ではコスト構造は閾値に影響しないが、 実務では完全分離不可能なケースが多く、 その場合は FP コストと FN コストの比が最適閾値を大きく動かす。
| 業界 | FP コスト | FN コスト | 典型 β |
|---|---|---|---|
| がん検診 | 不要な精密検査 (1万円) | がん見逃し (生命) | β > 5 (Recall 重視) |
| スパムフィルタ | 正規メール紛失 (機会損失) | スパム表示 (軽微) | β < 0.5 (Precision 重視) |
| 不正検知 | 正規取引拒否 (顧客損失) | 不正取引許可 (損害) | β ≈ 2-3 |
| 創薬スクリーニング | 無効化合物の追加実験 | 有望候補の見逃し | β ≈ 0.5 |
| クレジット審査 | 機会損失 (利息) | 貸倒損失 (元本) | β ≈ 0.3 |
| マーケ DM | 送付コスト (100 円) | 機会損失 (購入額) | β ≈ 0.5 |
テストデータの陽性率と本番データの陽性率が違うと、 Precision はずれます。 これを prior shift と呼びます。
🎯 このコードでやること:学習データの陽性率を維持しつつ、 テスト時に陽性率が変わると Precision がどう変化するかをシミュレート。
📥 入力データ: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 34 35 36 | 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) # 学習データを構成 (陽性率 0.3 程度) y_train = (d['A1101'] > 2_000_000).astype(int).values X_train = StandardScaler().fit_transform(d[['A4101']].values) scaler = StandardScaler().fit(d[['A4101']]) model = LogisticRegression().fit(X_train, y_train) # テストデータ (人為的にラベル定義を変えて陽性率変動) print(f'学習時の陽性率: {y_train.mean():.3f}') print(f'\n{"テスト陽性率":<15}{"Precision":<12}{"Recall":<10}{"備考":<30}') for thresh, name in [ (5_000_000, '陽性率低'), (3_000_000, '陽性率中'), (2_000_000, '陽性率高'), ]: y_test = (d['A1101'] > thresh).astype(int).values X_test = scaler.transform(d[['A4101']]) pred = model.predict(X_test) pi = y_test.mean() if pred.sum() > 0: p = ((pred==1) & (y_test==1)).sum() / pred.sum() else: p = 0.0 if y_test.sum() > 0: r = ((pred==1) & (y_test==1)).sum() / y_test.sum() else: r = 0.0 print(f'{pi:<15.3f}{p:<12.3f}{r:<10.3f}{name:<30}') |
📤 実行結果:
💬 結果の読み方:学習時と同じ固定閾値を使っても、 テスト環境の陽性率が学習時(0.340)より低い(0.191)と、 同じ予測でも偽陽性の相対的影響が増えて Precision が 1.0 から 0.90 に低下する。 これは 「同じ予測器でも、 運用環境の陽性率が変わると真陽性予測の品質が変わる」 という prior shift の現実を示す。 医療検査機を別地域に持っていく、 季節性のあるマーケに使う、 などで発生。 対策:定期的な再校正と prior の調整。
本番陽性率 $\pi_{\text{new}}$ が学習時 $\pi_{\text{train}}$ と違うとき、 確率を補正できます。
$$ \hat{p}_{\text{new}}(X) = \frac{\hat{p}(X) \cdot (\pi_{\text{new}} / \pi_{\text{train}})}{\hat{p}(X) \cdot (\pi_{\text{new}} / \pi_{\text{train}}) + (1 - \hat{p}(X)) \cdot ((1-\pi_{\text{new}}) / (1-\pi_{\text{train}}))} $$
Precision と Recall は分野ごとに異なる名前で呼ばれることがあります。 用語の混乱を整理します。
| 分野 | Precision の別名 | Recall の別名 | 備考 |
|---|---|---|---|
| 情報検索 (IR) | Precision | Recall | 1960 年代の文献検索評価に由来 |
| 医療診断 | 陽性的中率 (PPV) | 感度 (Sensitivity) | 米国 CDC ガイドラインで標準 |
| 機械学習 (ML) | Precision | Recall | IR 由来、 最も一般的 |
| 統計学 | 陽性的中率 | 真陽性率 (TPR) | 仮説検定の文脈 |
| 信号検出理論 | (あまり使わない) | Hit Rate | 心理物理学で発祥 |
| レーダー工学 | Detection Probability | Detection Rate | 第二次大戦の用語 |
| 年代 | 事項 |
|---|---|
| 1940s | レーダー信号検出で「真陽性 / 偽陽性」概念が成立 |
| 1950s | 心理物理学で「信号検出理論」が体系化 |
| 1960s | 情報検索 (Cleverdon 1966) で Precision/Recall を正式定義 |
| 1970s | 医療診断分野で「感度・特異度」が広まる |
| 1990s | 機械学習で標準指標として定着 |
| 2000s | F1 スコア (van Rijsbergen 1979) が爆発的に普及 |
| 2010s | 不均衡データ問題で PR-AUC が注目 |
| 2020s | 公平性・校正性能の評価指標として再評価 |
「2 つのモデルの Precision に有意差があるか」を判定するには、 McNemar 検定(同一テストセット上)または Fisher の正確検定(小サンプル)が使われます。
🎯 このコードでやること:ロジスティック回帰とランダムフォレストの予測結果を比較し、 McNemar 検定で「予測の差が有意か」を判定。
📥 入力データ: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 | import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import StandardScaler from statsmodels.stats.contingency_tables import mcnemar 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) pred_lr = LogisticRegression().fit(X, y).predict(X) pred_rf = RandomForestClassifier(n_estimators=50, random_state=0).fit(X, y).predict(X) # McNemar 検定: 一方だけ正解の数を比較 b = ((pred_lr == y) & (pred_rf != y)).sum() # LR 正解、 RF 誤り c = ((pred_lr != y) & (pred_rf == y)).sum() # LR 誤り、 RF 正解 print(f'LR 正解 & RF 誤り: {b}') print(f'LR 誤り & RF 正解: {c}') table = [[0, b], [c, 0]] result = mcnemar(table, exact=True) print(f'\nMcNemar 検定 p 値: {result.pvalue:.4f}') print(f' → p < 0.05 なら有意差ありと判断') print(f' 本ケースは {"有意差あり" if result.pvalue < 0.05 else "有意差なし"}') |
📤 実行結果:
💬 結果の読み方:ランダムフォレストが 5 件、 ロジスティック回帰の誤りを正しく分類しており(逆は 0 件)、 RF がやや優勢に見える。 しかし不一致は片側 5 件のみで McNemar 検定の p 値は 0.0625 > 0.05、 統計的には有意差なし。 実務で「モデル A と B どちらが優秀か」と問われたら、 McNemar 検定で「予測の差が偶然の範囲内か」を判定するのが正攻法。 Accuracy や Precision の数値差を単純比較するだけでは不十分。
実務で Precision を扱う際の「これだけは守る」べき原則を、 失敗事例と共に提示します。
データを受け取ったら、 まず陽性率を計算してください。 陽性率 50% と 1% では、 同じ Precision = 0.6 でも意味が全く違います。
Precision、 Recall、 F1、 AUC、 PR-AUC、 Accuracy をすべて並べて確認。 1 つの指標が好結果でも、 他が悪化していたら要注意。
Precision の点推定だけ報告するのは統計的に不誠実。 Bootstrap か Wilson 区間で 95% CI を併記しましょう。
同じデータで学習と閾値選定すると過学習。 必ず train / validation / test に分割し、 validation で閾値を決定。
本番環境では時間と共にデータ分布が変わる(concept drift)。 月 1 回は Precision を測定し、 閾値の見直しを検討。
検索エンジンや推薦システムは「ランキングタスク」であり、 通常の二値分類とは異なる評価指標が用いられます。
| 指標 | 定義 | 用途 |
|---|---|---|
| Precision@k | 上位 k 件の Precision | 検索結果の最初のページ評価 |
| R-Precision | 真陽性数 R 件で Precision@R | 陽性数によって k を自動調整 |
| MAP (Mean Average Precision) | 各クエリの AP の平均 | マルチクエリ評価 |
| nDCG (normalized DCG) | 順位による重み付き正解率 | 順序情報を重視 |
| MRR (Mean Reciprocal Rank) | 最初の正解の逆数の平均 | 「最初の 1 件」を重視 |
🎯 このコードでやること:SSDSE-B-2026 で人口大の県を「正解」とみなし、 各特徴量で順位付けして MAP と R-Precision を計算。
📥 入力データ: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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import pandas as pd import numpy as np 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 R = y.sum() # 真陽性数 print(f'真陽性数 R = {R}') def ap(y, scores): """Average Precision (PR曲線下面積の近似)""" order = np.argsort(scores)[::-1] y_sorted = y[order] precisions = [] tp = 0 for i, yi in enumerate(y_sorted, 1): if yi == 1: tp += 1 precisions.append(tp / i) return np.mean(precisions) if precisions else 0.0 def r_precision(y, scores, R): order = np.argsort(scores)[::-1] y_sorted = y[order] return y_sorted[:R].mean() features = { '出生数': d['A4101'].values, '日本人人口': d['A1102'].values, '合計特殊出生率': d['A4103'].values, } print(f'\n{"特徴量":<14}{"AP":<10}{"R-Precision":<14}{"Precision@5":<14}') for name, score in features.items(): ap_val = ap(y, score) rp = r_precision(y, score, R) order = np.argsort(score)[::-1] p5 = y[order[:5]].mean() print(f'{name:<14}{ap_val:<10.3f}{rp:<14.3f}{p5:<14.3f}') # MAP (複数クエリの平均、 ここではシミュレート) queries = [] for thresh in [3_000_000, 2_000_000, 1_500_000]: y_q = (d['A1101'] > thresh).astype(int).values queries.append(y_q) print(f'\nMAP (3 クエリ平均):') for name, score in features.items(): aps = [ap(y_q, score) for y_q in queries] print(f' {name}: MAP = {np.mean(aps):.3f}') |
📤 実行結果:
💬 結果の読み方:出生数と日本人人口はほぼ完璧な順位(AP=1.0, R-Precision=1.0)。 一方、 合計特殊出生率は AP=0.141・R-Precision=0.000 と極端に低い。 出生率が高い県(沖縄など)は必ずしも人口が多くないため、 人口規模のランキング特徴量としては役に立たない典型例。 検索エンジンでは MAP が標準指標、 推薦システムでは Precision@k や nDCG が重視される。
本ページは Precision-Recall 曲線 を以下の構成で深掘りしました:
姉妹ページ(precision / recall / roc-curve / f1-score)とは切り口を変え、 ここでは PR-AUC(AP)と ROC-AUC を同一タスク・同一ランキングで並べて比較し、 不均衡データで両者がどれだけ食い違うかを実データで検証します。 すべて SSDSE-B-2026(2023 年・47 都道府県)の実測値です。
ROC 曲線のベースライン(当てずっぽう)は 常に AUC=0.5 で固定です。 一方 PR 曲線のベースラインは 陽性率 π そのもので、 タスクによって上下します。 つまり「PR-AUC=0.5」という同じ数字でも、 π=0.5 のタスクなら「当てずっぽうと同じ」、 π=0.1 のタスクなら「ベースラインの 5 倍」で意味が真逆になります。 PR-AUC は絶対値だけ見ても評価できず、 必ず π と並べて読む——これが ROC との最大の違いです。
「総人口 > 700 万人」を陽性(陽性 5 件=埼玉・東京・神奈川・愛知・大阪、 陰性 42 件、 π=0.106)とし、 「一般病院数(I510120)」をスコアにして順位付けした場合の実測 AUC:
なぜ同じランキングでこれほど差が出るのか。 病院数で上位から並べると:
| 順位 | 県(病院数) | 正解 | Precision | Recall |
|---|---|---|---|---|
| 1 | 東京都(588) | 陽性 | 1.000 | 0.20 |
| 2 | 北海道(464) | 陰性(FP) | 0.500 | 0.20 |
| 3 | 大阪府(463) | 陽性 | 0.667 | 0.40 |
| 4 | 福岡県(390) | 陰性(FP) | 0.500 | 0.40 |
| 5 | 兵庫県(312) | 陰性(FP) | 0.400 | 0.40 |
| 6 | 埼玉県(296) | 陽性 | 0.500 | 0.60 |
| 7 | 神奈川県(289) | 陽性 | 0.571 | 0.80 |
| 8 | 愛知県(278) | 陽性 | 0.625 | 1.00 |
北海道(病院 464=全国 2 位だが人口 509 万で陰性)、 福岡・兵庫(人口 500 万台で 700 万に届かず陰性)が上位に食い込み、 再現率がまだ低い序盤で Precision を 0.5 前後まで削り取ります。 病院数は「面積・医療体制」を色濃く反映し「人口規模」とはズレるためです。 ROC-AUC はこの誤りを 42 件の真陰性(TN)の多さで薄めて 0.95 と楽観的に見せますが、 PR は TN を一切見ないため、 少数の陽性を取り違えるコストがそのまま数字に出て 0.67 に落ちます。 AP を階段近似で手計算しても
0.2×1.000 + 0.2×0.667 + 0.2×0.500 + 0.2×0.571 + 0.2×0.625 = 0.6726
と sklearn の average_precision_score に一致します。 教訓:不均衡タスクで「ROC-AUC が高い=良いモデル」と早合点すると危険。 少数派の検出品質は PR-AUC でしか正しく見えない。
📝 補足:同じ陽性ラベルでも「大学学生数(E6302)」をスコアにすると ROC-AUC=0.9810 / PR-AUC=0.9111(差 0.07)、 「出生数(A4101)」なら両方 1.0000。 スコアの良し悪しによって PR-ROC の乖離幅そのものが変わる点も、 実データで確認できます(いずれも 2023 年実測)。
average_precision_score(階段近似)と auc(recall, precision)(台形補間)は、 特にプロット点が少ない場合に値がずれます。 論文報告では AP を使うのが標準です。