別名・略称:適合率、 PPV(陽性的中率)
Precision は 陽性予測のうち実際に陽性だった割合。 「陽性と予測した結果の正確さ」を測ります。 偽陽性のコストが高い場面で重要。
🍰 まずはやさしく
予測の当たり率のことです。
間違いを減らすために使います。
迷惑メールの判定などが例です。
まずは結論を短く確認しましょう。
Precision(適合率)(Precision):陽性予測のうち実際に陽性だった割合
🍰 まずはやさしく
間違いを防ぐための指標です。
正しく判定できているか測ります。
大事なメールを消さない時に使います。
どんな場面で使うかを見ていきましょう。
🍰 まずはやさしく
的中させる力のようなものです。
予測の正確さを直感的に知るためです。
おすすめの商品が合うか似ています。
具体例を使って仕組みを考えましょう。
| 指標 | 計算 | 観点 | 例 |
|---|---|---|---|
| Precision | TP/(TP+FP) | 予測の当たり率 | 推薦・スパム検出 |
| Recall | TP/(TP+FN) | 見逃さない率 | がん検診・地震警報 |
「迷惑メール」予測モデルが 100 件を迷惑と判定。 そのうち 85 件が本当に迷惑、 15 件は正常メール。
47 都道府県を 出生数(A4101)などの指標からロジスティック回帰で「人口の多い県」(この例では真の該当を 23 県と仮定)を予測した場面を考える。 確率閾値 0.5 で陽性判定した結果、 仮に 陽性予測 25 県中 22 県が真の該当県 だったとする(以下は数式の使い方を示すための仮の数値):
| 指標 | SSDSE-B-2026 数値 | 読み方 |
|---|---|---|
| TP / FP / FN / TN | 22 / 3 / 1 / 21 | 陽性予測 25 中 22 当たり、 真陽性 23 中 22 当て |
| Precision | 22 / (22+3) = 0.880 | 「該当と予測した県」の 88% が実際に該当 |
| Recall | 22 / (22+1) = 0.957 | 実際の該当 23 県の 95.7% を取りこぼさず検出 |
| F1 | 2·0.88·0.957/(0.88+0.957) = 0.917 | 調和平均 |
意思決定への含意: もし「該当県のみを補助対象から外す」用途なら Precision 重視 (3 県を誤って除外しないため閾値を高く)、 「該当県をすべて要観察リストに入れる」用途なら Recall 重視 (1 県も漏らさないため閾値を低く)。 用途次第で Precision 0.88 を 0.96 まで上げる代わりに Recall を 0.78 まで下げる、 という設計判断が起こる。
🍰 まずはやさしく
計算で出す正確さの数値です。
客観的に性能を比べるために使います。
スマホのアプリ開発などで役立ちます。
数式を使って定義を確認しましょう。
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。 これを「転入者数(日本人移動者・A5101)」だけで予測する単変量分類器の PR 曲線を描く。
🎯 このコードでやること: SSDSE-B-2026 で「総人口 500 万超」を転入者数(A5101)から予測する分類器を作り、 sklearn.metrics.precision_recall_curve と average_precision_score で PR-AUC を算出。 閾値ごとの Precision 推移も観察。
📥 入力データ: SSDSE-B-2026 (data/raw/SSDSE-B-2026.csv) 総人口 A1101・転入者数 A5101 列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd from sklearn.metrics import precision_recall_curve, average_precision_score # 2023 年の 47 都道府県だけ抽出(1 行目の日本語見出しは skiprows=[1] で除外) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023] y_true = (d['A1101'] >= 5_000_000).astype(int) # 総人口 500 万人超 = 陽性 y_score = d['A5101'] # 転入者数(日本人移動者)をスコア化 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 (30_000, 50_000, 60_000): p = precision[(thresholds <= t).sum() - 1] print(f"閾値 転入者数={t:,} のとき Precision = {p:.3f}") |
📤 実行結果:
💬 結果の読み方: PR-AUC=0.98 はベースライン 0.191 を遥かに超え、 転入者数は総人口 500 万超の優秀な単変量予測子。 閾値を厳しく(転入者数 6 万人超)すると Precision=0.889 まで上がるが Recall は犠牲(転入の少ない北海道を取りこぼす)。 閾値をゆるめる(3 万人超)と Precision=0.600 まで下がり、 予測陽性の 4 割が外れる。 業務要件で閾値選択が変わる典型例。
Precision (適合率) は「分類器が陽性と予測したもののうち、 本当に陽性だった割合」を表す。 ここでは 3 つの概念図で、 Precision が PR 曲線・閾値・スコア分布のどこに位置するかを視覚的に整理する。
SSDSE-B-2026(2023 年)で「総人口 > 500 万人」を陽性として、 「転入者数(A5101) > 5 万人」を予測に使ったときの Precision を計算します(数値は実データ)。
| 都道府県 | 人口(千) | 転入者数(千) | 真陽性? | 予測陽性? | 分類 |
|---|---|---|---|---|---|
| 東京都 | 14086 | 407 | Y | Y | TP |
| 神奈川県 | 9229 | 211 | Y | Y | TP |
| 大阪府 | 8763 | 160 | Y | Y | TP |
| 愛知県 | 7477 | 105 | Y | Y | TP |
| 北海道 | 5092 | 47 | Y | N | FN |
| 静岡県 | 3555 | 49 | N | N | TN |
47 件全体で計算すると TP=8, FP=1, FN=1 → Precision = 8/(8+1) = 0.889(Recall も 8/9 = 0.889)。 転入の少ない北海道は FN、 京都府が唯一の FP になります。 閾値をさらに下げれば FP が増え、 Precision は下がります。
合成データで Precision を計算する。
1 2 3 4 5 6 7 | tp, fp, fn = 80, 20, 10 P = tp/(tp+fp) R = tp/(tp+fn) F1 = 2*P*R/(P+R) print(f"Precision: {P}") print(f"Recall: {R:.3f}") print(f"F1: {F1:.3f}") |
💬 手計算 (Step 2) と Python 出力が完全一致。
混同行列の TP / FP / FN / TN をスライダーで動かす(または行列のマスをタップ/ドラッグする)と、 Precision = TP / (TP + FP) がリアルタイムで再計算されます。 Precision は「陽性と予測したもの(左の橙枠の列)のうち、実際に陽性だった割合」。 FP(誤って陽性と判定)を増やすと Precision が下がる様子を、緑の帯が縮む動きで体感してください。
橙の破線枠 = 「陽性と予測した列」(TP + FP)。この中の緑の割合が Precision。マスをタップすると +1。
Precision は「打率」に似ています。「陽性だ」と宣言した回数(TP + FP)を分母に、そのうち本当に当たった回数(TP)が分子。 だから 陰性側(FN・TN)をいくら動かしても Precision は 1 ミリも変わりません(右のマスを動かして確認してみてください)。 Precision を上げたければ「自信のあるものだけを陽性と宣言」= FP を減らせばよい。
この 2×2 は単一の閾値でのスナップショットにすぎません。閾値を 0→1 に動かすと (Recall, Precision) の点が動き、それを結んだのが PR 曲線、その下の面積が PR-AUC。 基礎となる 混同行列、対をなす Recall のページも併せて読むと、指標の使い分けが立体的に見えてきます。
SSDSE-B-2026(47 都道府県・2023 年)の実データを使った最小コード:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # SSDSE-B-2026 で Precision を計算 import pandas as pd from sklearn.metrics import precision_score, recall_score, f1_score, classification_report # skiprows=[1] で 1 行目の日本語見出しを除外し、Code 行(A1101 等)を列名にする df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年の 47 都道府県 y_true = (df['A1101'].astype(float) >= 5_000_000).astype(int) # 総人口 500 万人超 y_pred = (df['A5101'].astype(float) >= 50_000).astype(int) # 転入者数 5 万人超で予測 p = precision_score(y_true, y_pred) r = recall_score(y_true, y_pred) f = f1_score(y_true, y_pred) print(f'Precision = {p:.3f}') # -> 0.889 print(f'Recall = {r:.3f}') # -> 0.889 print(f'F1 = {f:.3f}') # -> 0.889 print(classification_report(y_true, y_pred, digits=3)) |
Precision(適合率)の周辺概念をテーマ別ツリーで整理:
分類モデル評価指標 ├── Recall(再現率)/ Accuracy / Specificity (並列) ├── 【Precision(適合率)= TP / (TP + FP)】 ← ここ │ ├── F1 / F-β スコア (Precision と Recall の調和平均) │ ├── PR-AUC / Average Precision (閾値全域での平均) │ └── Macro / Micro / Weighted Precision / Precision@k └── 混同行列 / ROC-AUC / MCC (関連指標)
この階層構造を頭に入れておくと、 学習や論文読みで「自分が今どこにいるか」を見失わずに済みます。
「Precision(適合率)」を確実にマスターするには、 次の順序で進むのが効率的です:
焦らず、 1 段ずつ確実に。 7 ステップを 1 周すれば、 単に「知っている」から「使える」レベルに到達できます。
適合率 (Precision) は陽性予測のうち真の陽性の割合で、 再現率・F1・accuracy と組み合わせて分類性能を評価する。
SSDSE-B-2026 を用いた演習では、 「適合率」 を中核に据えて上記の上流・並列・下流の手法を実データで連結する経験を積むと、 単独の手法暗記より実務的応用力が身につく。
「Precision(適合率)」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
適合率だけを上げるのは簡単で、 確信のある 1 件だけを陽性にすれば 100% になる。 再現率とセットで見るか、 F1・AP のように両方を含む指標にする。
適合率は「予測陽性のうち、 本当に陽性だった割合」を測る指標で、 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 年データで 14 県)を 47 県のラベルとし、 「出生数」で予測した場合の混同行列を実値で示します。
| 閾値(出生数) | 予測陽性 | TP | FP | FN | TN | Precision | Recall |
|---|---|---|---|---|---|---|---|
| 5,000 | 47 | 14 | 33 | 0 | 0 | 0.298 | 1.000 |
| 10,000 | 27 | 14 | 13 | 0 | 20 | 0.519 | 1.000 |
| 13,000 | 17 | 14 | 3 | 0 | 30 | 0.824 | 1.000 |
| 15,000 | 14 | 14 | 0 | 0 | 33 | 1.000 | 1.000 |
| 20,000 | 10 | 10 | 0 | 4 | 33 | 1.000 | 0.714 |
| 30,000 | 6 | 6 | 0 | 8 | 33 | 1.000 | 0.429 |
| 50,000 | 3 | 3 | 0 | 11 | 33 | 1.000 | 0.214 |
この表から見えるのは 「閾値を厳しくすると Precision は上がるが Recall は下がる」 典型的トレードオフ。 閾値 15,000 で両方 1.0 を達成しているのは、 出生数が人口とほぼ完全相関しているため。
🎯 このコードでやること: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 だが Recall=0.69(TP=11・FN=5)。 出生数と総人口は高相関だが完全分離ではなく、 5 件を取りこぼす。 「同じデータで学習・評価」しているため過学習の可能性もあり、 実務では 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.976 と高精度な PR 曲線。 閾値が低いと予測陽性が多いので Precision が低い(0.340 ≈ ベースライン陽性率)が、 閾値を上げると Precision が上昇。 実務では「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 で完全無能。 戦略 B は Accuracy が 0.91 でさらに高いだけでなく、 Precision = 1.0 で「陽性予測 = 完全正解」、 Recall = 0.56 で「真陽性の 56% を捕捉」。 不均衡データでは 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.812)。 閾値を 0.95 まで上げると Precision は 1.0 を維持するが Recall は 0.312 まで低下(陽性 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.730 が micro/weighted を引き下げる一方、 小・大クラスの Precision = 1.000 が macro を押し上げる。 macro は全クラス対等、 micro/weighted は多数クラス依存。 少数クラスを軽視したくないなら macro、 全体性能なら micro/weighted を使う。
| 要素 | 内容 |
|---|---|
| 陽性 | がん患者 |
| 陽性率 | 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 で Precision@14 = 0.929、 k=20 で 0.800(陽性 16 件をすべて含むが偽陽性が混じる)。 検索エンジンで「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 が最大(Precision 重視)、 閾値 0.20 で F2 = 0.889 が最大(Recall 重視)、 閾値 0.30 で F1 = 0.897 が最大。 業務での β 選択 = 業務コストの非対称性を反映。 スパムフィルタなら 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}') |
📤 実行結果:
💬 結果の読み方:このタスクは完全分離可能なため、 すべての β で同じ閾値 0.26 が最適。 不完全分離のタスクでは β ごとに異なる閾値が選ばれる。 実務で β を選ぶ際は、 「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.8)は実 Precision がほぼ予測通り(乖離 +0.02)。 中間ビン(0.4-0.6 で乖離 +0.52 など)や低確率ビンで乖離が大きい(境界例の不確実性・件数の少なさ)。 一般に、 確率を業務閾値として直接使うなら、 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}: 予測陽性なし') |
📤 実行結果:
💬 結果の読み方:閾値 0.3 で Precision = 1.0(TP=14)、 95% CI は [0.785, 1.000](幅 0.215)。 サンプルが少ないため点推定 1.0 でも真の Precision は 0.79-1.00 と幅広い。 必ず CI を併記し、 「Precision = 1.00 (95% CI: 0.79-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}') |
📤 実行結果:
💬 結果の読み方:日本人人口 100 万超(36 県)を予測する例。 総人口と日本人人口は強相関だが完全一致ではなく、 低閾値では Precision=0.77、 閾値 0.7 で Precision=1.0(Recall=0.72)。 教訓:陽性ラベルと特徴量が極めて近い情報を持つと閾値調整で容易に高 Precision に届く。 実務では特徴量と陽性ラベルに 強い相関がある場合は 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.87%。 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}') |
📤 実行結果:
💬 結果の読み方:本タスクは完全分離可能なため、 すべてのシナリオで 最適閾値 0.18・損失 0 を達成。 完全分離では閾値に幅があり、 コスト非対称でも同じ点が選ばれてしまう。 実務では完全分離不可能なケースが多く、 その場合はコスト構造が閾値を大きく動かす。
| 業界 | 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)なら Precision = 1.0、 陽性率が下がると Precision も下がる(0.191 で 0.900)。 これは 「同じ予測器でも、 運用環境の陽性率が低いと真陽性予測の品質が下がる」 という現実を示す。 医療検査機を別地域に持っていく、 季節性のあるマーケに使う、 などで発生。 対策:定期的な再校正と 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 "有意差なし"}') |
📤 実行結果:
💬 結果の読み方:一方のみ正解のケースが 0 対 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.14 と順位が大きく乱れる(高齢化県で死亡数が多く人口順位と一致しないため)。 検索エンジンでは MAP が標準指標、 推薦システムでは Precision@k や nDCG が重視される。
本ページは Precision(適合率) を以下の構成で深掘りしました:
このページの他ブロックは主に「閾値を動かすと Precision が動く」話でした。 ここでは角度を変え、 閾値も分類器もそのまま固定したのに、 対象集団の陽性割合(基準率 π)が変わるだけで Precision が上下するという、 実務で最も誤解されやすい性質を実データで確認します。 対をなす Recall はこの影響を全く受けません。
Precision とは「陽性と予測されたという証拠を見た後で、 本当に陽性である確率」= 事後確率 P(真陽性 | 予測陽性) です。 ベイズの式で分解すると、 分類器の性能(尤度)だけでなく 事前確率=基準率 π が入り込みます。
ここで Recall=TPR は π を含まない(陽性だけを分母にする量なので、 陰性が何個いようと不変)。 一方 Precision は分母に FPR·(1−π) を持つため、 陰性が相対的に増える(π が下がる)ほど偽陽性が効いて下がる。 これが「Recall は動かないのに Precision だけ動く」の正体です。
SSDSE-B-2026(2023 年・47 都道府県)で、 「総人口 500 万人超」を陽性、 「転入者数(A5101)≥ 5 万人」で予測する分類器を作ると(本ページ上部の実値計算と同じ設定)、 混同行列は実測で TP=8 / FP=1 / FN=1 / TN=37。 ここから測った値は次のとおりで、 いずれも実データの測定値です。
次に、 この分類器の TPR・FPR はそのまま固定し(実測値)、 適用先集団の基準率 π だけを差し替えたときの Precision を上の式で計算します。 π の値のうち 0.191 以外は「もし有病率がこうだったら」という架空の基準率です(TPR/FPR は実測、 π のみ仮想)。
| 基準率 π(陽性割合) | Recall(=TPR・不変) | Precision | 読み方 |
|---|---|---|---|
| 0.01(架空・激レア) | 0.889 | 0.254 | 陽性予測の 3/4 が外れ |
| 0.05(架空) | 0.889 | 0.640 | まだ 4 割が誤検出 |
| 0.10(架空) | 0.889 | 0.790 | 実用ライン付近 |
| 0.191(実測・47 県) | 0.889 | 0.889 | 実データの基準点 |
| 0.30(架空) | 0.889 | 0.935 | 陽性が多いほど当たりやすい |
| 0.50(架空・均衡) | 0.889 | 0.971 | 均衡データでは高く見える |
結論: Recall 列は 0.889 で一定なのに、 Precision は π=0.01 で 0.254、 π=0.50 で 0.971 と 約 4 倍も動く。 実務での帰結は重大です — 50:50 に整えたテストセットで Precision=0.97 と報告したモデルを、 陽性 1% の本番に載せると Precision は 0.25 に崩壊する(Recall は不変なので「性能は変わっていないはず」と錯覚しやすい)。 これが「基準率フォールバシー」。 論文・レポートで Precision を書くときは 評価データの π を必ず併記し、 本番の π と一致させるのが鉄則です。
上の式は、 ある π で測った Precision を別の π へ「翻訳」できることも意味します。 まず実測 Precision と Recall から尤度比 FPR/TPR を復元し、 目標の π を代入すれば本番想定の Precision が予測できる(prior shift 補正)。 医療統計ではこの Precision がそのまま PPV(陽性的中率)で、 「検査の感度・特異度は同じでも、 有病率の低い集団で陽性なら実は多くが偽陽性」という健診の基本原則そのものです。 だからこそ不均衡データでは、 π に無関係な ROC 曲線/AUC ではなく、 π を織り込む PR 曲線・PR-AUC で評価し、 特異度(=1−FPR)を併記するのが定石になります。