予測モデルと評価指標 ― 「当たる」と「役に立つ」は違う
初回CVや解約を予測するモデルの作り方と、その評価。AUCだけ見て喜ぶ前に、較正・不均衡・ビジネス指標との接続を押さえます。「予測に効く変数」と「因果のある変数」は別物だ、という伏線も置きます。
by Shin
「AUCが0.85出ました」は、良い予測モデルでしょうか。答えは「それだけでは分かりません」です。AUCが高くても、出力した確率が信用できず(「確率70%」と出た相手のうち実際に成果が出るのは7割、という対応が崩れている=較正されていない)、その出力がどの意思決定も動かさないなら、モデルは保守コストだけの負債になります。この回では過去の受託案件をもとに設計した架空のD2C企業SOLNAのデータで初回購入と解約の確率を予測しながら、リークを避けて学習し、確率を較正して閾値を決め、評価指標をビジネスの数字に翻訳するまでを通します。
本連載「データが許す範囲で考える」の予測段にあたる回です。第1回で「相関 ≠ 因果」を直感として押さえました。この回の最後では、もう一つの混同——「予測 ≠ 因果」——への伏線を置いて、次回(因果の言語)へ渡します。
この回で身につくこと
- データリークを避けて、本番でも崩れない予測モデルを組める。
- 評価指標を「順位づけの質」「確率の質」「不均衡への頑健さ」の三つに読み分けられる。
- 確率を較正し、ビジネス制約(通知の本数・配信枠)や期待利益から閾値を決められる。
- 評価指標をビジネス指標(売上・配信コスト)に翻訳できる。
- 「予測に効く変数」と「因果のある変数」が別物だと説明できる(次回への橋)。
前提(ここまでで身についていること)
- 第1回: 不確実性を区間で語ること、相関と因果は別だという直感。
- 第2回: 真の成果(媒体CVではなく実購入・LTV)から逆算すること、そして接合率(spine coverage=広告接触から成果まで一本で辿れる割合)が、これから作る手法の適用範囲の上限を決めること。
この回ではその接合率が、そのまま「予測モデルが学習に使える実弾の量」になります。辿れない成果は、教師データにならないからです。
予測とは何をしているのか
予測モデルがやっているのは、既知の特徴から未知の結果を推測する写像を、過去データから学ぶことです。結果が「買う・買わない」「解約する・しない」のような二択なら分類、「LTVは何円か」のような連続値なら回帰、と最小限に区別すれば足ります。
マーケ実務で効くのは多くが、確率を出す分類です。確率さえあれば、閾値を引いて「この人には通知する/しない」と行動を変えられます。逆に言えば、確率の質と閾値の引き方が、モデルが役に立つかどうかを決めます。
ここで先に一点だけ釘を刺します。予測は「相関の高度な利用」です。なぜそうなるのか(因果)は問わず、過去に一緒に動いた特徴を手がかりにするだけです。だから後で述べるとおり、予測に効く変数が、介入して効く変数とは限りません。
SOLNAでは二つの問いを扱います。ひとつは「匿名の訪問者が30日以内に初回購入するか」、もうひとつは「定期会員が次サイクルで解約するか」です。以降は主に前者(初回CV予測)を縦糸にし、解約予測は不均衡と較正の論点で参照します。
データリークという最大の事故
予測モデルで最も多く、最も静かに進む事故がデータリークです。リークとは、予測したい瞬間にはまだ手に入らない情報を、学習データに混ぜてしまうことです。検証では高精度が出るのに本番で崩れる典型原因がこれです。SOLNAの文脈で三つに分けます。
第一に、目的変数リーク(target leakage)。結果が出た後にしか確定しない特徴を使ってしまう型です。解約予測で customers.ltv_to_date(累積LTV・将来の購入を含む)を使う、初回CV予測で orders.amount(購入してはじめて存在する)を使う、といった例が該当します。答えの一部を特徴に混ぜているので、検証スコアは見かけ上きれいに出ます。
第二に、時間リーク。データをランダムに分割すると、未来のレコードが学習側に、過去のレコードが検証側に混ざり、未来から過去へ情報が漏れます。対策は単純で、初回接触の日付で時系列に分割し、過去で学習・直近で検証することです。
第三に、結合リーク。sessions.customer_id は会員識別後(多くは初回購入後)に埋まる列です。匿名訪問のCV予測で customer_id 由来の特徴を使うと、購入したという事実を見てから購入を予測していることになります。
リークと地続きの落とし穴が、第2回でも出た未成熟コホートです。直近に来た訪問者は「まだ買っていない」だけで「買わない」と決まったわけではありません。結果窓(ここでは30日)を完全に観測できるコホートだけを、教師データに残します。さらに、計測方式の不連続にも注意します。GA4のタグ実装を変えた日を跨ぐと、セッション系の特徴量に段差が入ります。学習期間とスコア期間で計測方式が違えば、モデルは段差を学んでしまいます。
次のSQLは、この三つを避けた学習テーブルの最小構成です。特徴量は「初回接触から7日」の窓だけ、ラベルは「30日以内の初回購入」、そして結果窓を観測しきれない未成熟コホートを除外します。
-- 初回購入予測の学習テーブル(リーク防止+観測窓でラベル付け)
-- 単位: 匿名訪問者 ga_client_id(会員識別の前)
-- 特徴量窓: 初回接触から7日間の行動のみ(未来の行動を混ぜない)
-- 結果窓 : 初回接触から30日以内に初回購入があったか
-- 未成熟コホート除外: 結果窓を完全に観測できる訪問者だけを残す
DECLARE data_cutoff DATE DEFAULT CURRENT_DATE();
WITH first_seen AS ( -- 各訪問者の初回接触時刻
SELECT ga_client_id, MIN(session_start) AS first_seen_at
FROM `solna.sessions`
GROUP BY ga_client_id
),
features AS ( -- 特徴量は「初回接触〜+7日」の窓だけ
SELECT
s.ga_client_id,
COUNT(*) AS sessions_7d,
SUM(s.pageviews) AS pageviews_7d,
SUM(s.product_views) AS product_views_7d,
COUNTIF(s.engaged) AS engaged_sessions_7d,
-- 初回流入チャネル/LPは「最初のセッション」の値(HAVING MIN で抽出)
ANY_VALUE(s.channel HAVING MIN s.session_start) AS first_channel,
ANY_VALUE(s.landing_page HAVING MIN s.session_start) AS first_landing
FROM `solna.sessions` s
JOIN first_seen f USING (ga_client_id)
WHERE s.session_start BETWEEN f.first_seen_at
AND TIMESTAMP_ADD(f.first_seen_at, INTERVAL 7 DAY)
GROUP BY s.ga_client_id
),
label AS ( -- ラベルは「初回接触〜+30日」の初回購入有無
-- ラベルは結果情報を使ってよい(それが教師)。ただし amount 等の購入後“数値”は使わず、有無のみ。
SELECT
f.ga_client_id,
MAX(IF(o.order_type = 'first'
AND o.order_at <= TIMESTAMP_ADD(f.first_seen_at, INTERVAL 30 DAY),
1, 0)) AS y_first_cv
FROM first_seen f
-- ga_client_id → customer_id の紐付け(接合できた訪問者のみが学習対象=接合率の制約)
LEFT JOIN `solna.sessions` sj
ON sj.ga_client_id = f.ga_client_id AND sj.customer_id IS NOT NULL
LEFT JOIN `solna.orders` o
ON o.customer_id = sj.customer_id
GROUP BY f.ga_client_id
)
SELECT feat.*, f.first_seen_at, lab.y_first_cv
FROM features feat
JOIN label lab USING (ga_client_id)
JOIN first_seen f USING (ga_client_id)
-- 未成熟コホート除外: 結果窓(+30日)が data_cutoff を超える訪問者は「まだ分からない」ので外す
WHERE DATE(TIMESTAMP_ADD(f.first_seen_at, INTERVAL 30 DAY)) <= data_cutoff;つまずきやすいのは、ラベル側で結果情報を使うのは正しい(それが教師)一方、特徴量側に結果情報が一滴でも混ざるとリークになる、という非対称です。features は行動列だけを使い、label だけが未来を見ています。
評価指標の地図
評価指標は数が多くて混乱しがちですが、「何を測りたいのか」で三つに整理すると迷いません。
| 問い | 主な指標 | 性質 |
|---|---|---|
| 順位づけは正しいか | AUC-ROC | 陽性を陰性より高く並べられる確率。不均衡と較正の両方に鈍感 |
| 確率は信用できるか | 対数損失(log loss)・Brier | 確率の質を測る。較正が悪いと悪化する(proper scoring rule) |
| まれな陽性を拾えるか | PR-AUC(適合率-再現率) | 陽性が少ないとき、ROCより実態を映す |
最初に避けたいのは、精度(正解率)に頼ることです。解約率が5%の状況で「全員継続する」と予測すれば、正解率は95%になります。数字は立派でも、解約者を一人も捕まえていないので意思決定には無価値です。不均衡なデータで精度を眺めるのは、ほぼ常に誤りです。
適合率(precision)と再現率(recall)は閾値で動くトレードオフです。だから単一の数字で語らず、AUC-ROCやPR曲線のように「閾値を動かしたときの全体像」で見ます。順位づけの質はAUC、まれな陽性を拾う力はPR-AUC、そして次節で扱う確率の質は対数損失、と役割を分けて読みます。
確率の較正と閾値設計
較正(calibration)とは、 と言ったグループの実際の陽性率が約30%になっている、という性質です。較正されていれば確率を額面どおり意思決定に使えますが、LightGBMのようなツリー系や、後述する不均衡対策を施したあとの確率は、しばしば過信・過小に歪みます。AUCが良くても較正は悪い、ということが普通に起きます。AUCは順位しか見ないので、確率の歪みを検知できないからです。
確認には較正曲線(reliability diagram)を使います。予測確率をビンに分け、各ビンの実際の陽性率を縦軸、予測確率の平均を横軸に取り、対角線からのズレを見ます。直し方は、Plattスケーリング(ロジスティックで写像し直す)か、isotonic回帰(単調変換で合わせ込む)です。scikit-learn なら CalibratedClassifierCV で既存推定器をくるむのが手早い方法ですが、較正は必ず学習データと分けた検証データで行います。較正までモデルが見たデータで合わせると、それ自体がリークになります。
較正された確率がそろって、はじめて閾値を意思決定として引けます。引き方は二通りです。
ひとつはビジネス制約からの逆算です。「LINE通知は月に配信枠の上位5%まで」のように容量が決まっているなら、確率の上位5%点を閾値にします。もうひとつは期待利益からの設計です。通知1本が当たれば利益 value、外せば不快コスト cost が出ると置くと、期待利益が正になる最小確率は次式で、これが閾値になります。
SOLNAでこの閾値が動かすのは、通知頻度・クーポン付与対象・CRM配信対象です。これが「役に立つ」の正体で、較正が崩れていると通知が鳴りすぎる/鳴らなさすぎるのどちらかになり、配信負荷の設計が崩れます。なお、対になる取引件数が少ないB2Bのケース(記述・予測編)では、同じ予測が動かすのは営業の架電優先順位でした。手法は同じでも、出力が接続する意思決定は業態で変わります。
不均衡データの扱い
初回CVも解約も、陽性はまれです。ここで素朴にオーバーサンプリングやSMOTEで陽性を水増しすると、見かけの精度やAUCは上がっても、確率分布が歪んで較正が壊れます。確率を意思決定に使う以上、これは本末転倒です。
実務的な優先順位はこうです。まずは再標本化せず、閾値設計と較正で対処する。足りなければ class_weight や LightGBM の scale_pos_weight で損失を重み付けする(再標本化より歪みは小さいものの、較正は必ず後段で確認・補正する)。それでも陽性が決定的に足りないときに限り再標本化を使い、その場合も必ず再較正します。評価はAUC単独でなくPR-AUCと対数損失を併用し、「精度が上がった」に騙されないことが肝心です。
GLMからツリー系へ
いきなりLightGBMに行かず、ロジスティック回帰(GLM・ロジットリンク)をベースラインに置くことを勧めます。係数が解釈でき、しかもリークの検出器になるからです。ある特徴の係数が不自然に巨大なら、それは目的変数リークを疑う合図です。緑本(久保『データ解析のための統計モデリング入門』)で扱うGLMの素養——リンク関数・線形予測子・最尤——が、ここで地に足のついた土台になります。
非線形性や特徴間の交互作用が効くなら、LightGBMに進みます。精度は上がりやすい一方、確率は要較正です。そして解釈は森下『機械学習を解釈する技術』のSHAP等で行えますが、ここに重要な注意があります。SHAPが示すのは「その特徴の“予測への”寄与」であって、因果効果ではありません。この区別が、次節と次回への直結点です。
import pandas as pd
import statsmodels.formula.api as smf
import lightgbm as lgb
from sklearn.metrics import roc_auc_score, average_precision_score, log_loss
# df は Block1 の SQL 出力(特徴量・first_seen_at・y_first_cv)を読み込んだもの
# 重要: ランダム分割は時間リークを生む。初回接触日で時系列に分ける。
cutoff = df["first_seen_at"].quantile(0.8) # 過去80%で学習・直近20%で検証
train = df[df["first_seen_at"] < cutoff].copy()
valid = df[df["first_seen_at"] >= cutoff].copy()
num = ["sessions_7d", "pageviews_7d", "product_views_7d", "engaged_sessions_7d"]
cat = ["first_channel", "first_landing"]
# --- ベースライン: ロジスティック回帰(GLM)。係数が解釈でき、リーク検出器にもなる ---
glm = smf.logit(
"y_first_cv ~ sessions_7d + pageviews_7d + product_views_7d + C(first_channel)",
data=train,
).fit(disp=False)
p_glm = glm.predict(valid)
# --- 本命: LightGBM(非線形・交互作用を拾う)---
for c in cat:
train[c] = train[c].astype("category")
valid[c] = valid[c].astype("category")
dtrain = lgb.Dataset(train[num + cat], label=train["y_first_cv"])
params = {
"objective": "binary",
"metric": ["auc", "binary_logloss"],
# 不均衡対策は SMOTE 等の再標本化より、まず損失の重み付けで(較正は後段で補正)
"scale_pos_weight": (train["y_first_cv"] == 0).sum() / (train["y_first_cv"] == 1).sum(),
"learning_rate": 0.05,
}
model = lgb.train(params, dtrain, num_boost_round=300)
p_lgb = model.predict(valid[num + cat])
for name, p in [("GLM", p_glm), ("LightGBM", p_lgb)]:
print(name,
"AUC", round(roc_auc_score(valid["y_first_cv"], p), 3),
"PR-AUC", round(average_precision_score(valid["y_first_cv"], p), 3),
"logloss", round(log_loss(valid["y_first_cv"], p), 3))最後に、較正と閾値の意思決定までを通します。AUCを眺めて終わらず、確率を直し、ビジネスの数字に落とすところまでが一連です。
import numpy as np
from sklearn.isotonic import IsotonicRegression
from sklearn.calibration import calibration_curve
# --- 較正: 検証データを二分し、前半で較正器を学習・後半で評価(学習データは見ない)---
n = len(valid)
cal_mask = np.arange(n) < n // 2
eval_mask = np.arange(n) >= n // 2
iso = IsotonicRegression(out_of_bounds="clip") # 単調変換で確率を合わせ込む
iso.fit(p_lgb[cal_mask], valid["y_first_cv"].values[cal_mask])
p_cal = iso.predict(p_lgb[eval_mask])
y_eval = valid["y_first_cv"].values[eval_mask]
# --- 較正曲線: 各ビンの「実際の陽性率」が対角線(=予測確率)に近いほど良い ---
frac_pos, mean_pred = calibration_curve(y_eval, p_cal, n_bins=10)
# --- 閾値の意思決定(2通り)---
# (A) ビジネス制約: 配信枠が上位5%まで → 上位5%点を閾値に
threshold_capacity = np.quantile(p_cal, 0.95)
# (B) 期待利益: 当たれば利益 value、外せば不快コスト cost
value, cost = 1200, 50 # 円。実数はビジネスで置き換える
threshold_value = cost / (value + cost) # 期待利益が正になる最小確率
target = p_cal >= threshold_capacity # 通知対象フラグ
print("配信対象率", round(float(target.mean()), 3),
"対象内の実CV率", round(float(y_eval[target].mean()), 3))最終行の「対象内の実CV率」が、配信全体のCV率より十分高ければ、モデルは現場の動きを変えられる、と言えます。逆にここが全体平均と変わらないなら、AUCが何であれそのモデルは意思決定を動かしていません。
予測 ≠ 因果(次回への予告)
ここまでで「当てる」「役に立つ」を分け、後者を閾値と較正で詰めました。最後に、もう一段先の混同に伏線を置きます。予測に効く変数は、介入して効く変数とは限りません。
例えば「来訪頻度が高い人は買いやすい」は、予測には強く効きます。しかし「無理に来訪させれば買う」とは言えません。来訪頻度は購入意欲の結果(表れ)でもあるからです。同じく、スコアが高い相手が、クーポンを打てば効く相手だとは限りません。すでに買う気の人にクーポンを配れば、効果ゼロで原資だけ失います。「誰に打てば効くのか」は予測ではなく因果(uplift)の問いで、これは第6回と因果編の主題です。
だから本連載は予測の次に、いきなり高度な手法へ飛ばず、「因果の言語」(潜在結果・DAG)を据える第4回へ進みます。
対応書籍と読みどころ
- 評価指標入門(ホクソエム): 回帰・分類・ランキング・推薦の評価指標を体系立てて整理し、指標とビジネスKPIのギャップを正面から扱う一冊。この回の「評価指標の地図」の元になっています。較正や不均衡の扱いを腰を据えて学ぶ起点に。
- 森下『機械学習を解釈する技術』: PFI・PD・ICE・SHAPで予測モデルの説明力を補う実践書。本回で触れた「SHAPは予測への寄与であって因果効果ではない」という線引きを意識しながら読むと、次回以降の因果編との接続がきれいになります。
- 久保『データ解析のための統計モデリング入門』(緑本) のGLM部: リンク関数・線形予測子・最尤というGLMの考え方は、ロジスティック回帰を「ただの分類器」でなく統計モデルとして読む土台になります。階層ベイズ部は第7回で本配置。
フレームとの接続
この回は成熟度のステップ(記述 → 予測 → 因果 → 自動化)の2つ目、予測の段階にあたります。第2回で測った接合率が、そのまま予測の学習に使える実弾の上限を画定します。意思決定接続は、SOLNA(取引件数の多いD2C)では通知頻度・クーポン対象・CRM配信対象、対になる取引件数が少ないB2B(記述・予測編)では営業の架電優先順位でした。
非対称も一つ。取引件数の多いSOLNAは最終成果(初回購入)の陽性ラベルが十分に集まるので、成果そのものを目的変数にできます。一方、取引件数の少ないB2Bは成果ラベルが少なく、先行指標(商談化・有望リード)を目的変数に置いて件数の壁を越えます。この差は業態名ではなく「量」という構造から来ています。
章末チェックリスト
- 各特徴量に「予測の“その瞬間”に手に入るか」を1列ずつ問うたか
- ランダム分割ではなく、時系列分割(過去で学習・直近で検証)にしたか
- 結果窓を完全に観測できるコホートだけでラベル付けしたか(未成熟コホート除外)
- 計測方式が変わった日を学習・スコア期間で跨いでいないか
- AUCだけでなく、対数損失とPR-AUCも併せて見たか
- 確率を較正し、較正曲線で対角線からのズレを確認したか(較正は検証データで)
- 閾値をビジネス制約か期待利益から決めたか
- その出力がどの意思決定(通知・クーポン・配信対象)を動かすか言えるか
- 「予測に効く=介入して効く」と混同していないか
ここまでは「どう当て、どう役立てるか(実装)」の話でした。自社のデータで予測が成立するか、何を目的変数に据えるべきか、という判断は、対になる記述・予測編で扱っています。そして次回・第4回は「効いたと言うための言語」(潜在結果・DAG・バックドア基準)へ進み、この回で置いた「予測 ≠ 因果」の伏線を回収します。