メカとら / tidyverseで学ぶ人事データ分析 記事一覧 運営者について お問い合わせ

第18章: 退職予測の評価 — Accuracy 92%が意味しないこと

第18章: 包括的なモデル性能評価をマスターする
⚠️ クラス不均衡 🎯 Recallと閾値設計 📊 退職予測

yardstickパッケージでモデル性能を多角的に評価。回帰・分類問題の主要指標からカスタムメトリックまで、実用的な評価手法を完全身につける。

Accuracy 92% に、一度きちんと騙された

退職予測モデルを組んで、最初にロジスティック回帰を走らせたときの精度が92%でした。 いいモデルができたと思いました。冷静に考えると、その会社の年間退職率は約8%です。 つまり全員に「辞めない」と答えるだけで、自動的に92%になる。モデルは何も学習していないのと同じでした。

00_the_trap.R
# 全員「在職」と予測するだけのモデル pred_naive <- rep("在職", nrow(test_data)) mean(pred_naive == test_data$turnover) #> 0.92 退職率8%の職場なら、これだけで92%になる

この章はyardstickの関数カタログではありません。不均衡なデータで、何を見て、どこで判断を止めるかを書きます。 退職予測は人事データで最も事故が起きやすい題材なので、それを例に進めます。

混合行列を見ると、一瞬で分かる

Accuracyだけ眺めていると見えないものが、混合行列を出すと即座に見えます。私の場合はこうでした。

最初のモデルの混合行列
予測:在職 予測:退職 実際:在職 920 0 実際:退職 80 0 ← 退職者を全員見逃している

退職者80人を、ひとりも捕まえていませんでした。Accuracyは92%です。 この2つの数字が同居できてしまうのが、クラス不均衡のある分類問題です。

yardstickでの最初の落とし穴:どちらが「陽性」か

yardstickは因子の第1水準を陽性(イベント)として扱います。 退職を「1」、在職を「0」としたまま factor() に通すと、水準はアルファベット順で 0, 1 になり、 在職のほうが陽性として計算されます。RecallもPrecisionも、意図と逆の数字が返ります。

水準の順番を明示するか、event_level = "second" を渡してください。ここを間違えたまま 「Recallが高いモデルができた」と報告すると、後で全部やり直しになります。

01_yardstick_setup.R
library(tidymodels) # 退職を第1水準に置く。ここを間違えると全指標が逆になる preds <- tibble( truth = factor(test_data$turnover, levels = c("退職", "在職")), .pred_退職 = pred_probs ) |> mutate( .pred_class = factor(if_else(.pred_退職 >= 0.5, "退職", "在職"), levels = c("退職", "在職")) ) preds |> conf_mat(truth, .pred_class) # クラス指標はまとめて出す class_metrics <- metric_set(accuracy, sensitivity, specificity, precision, f_meas) preds |> class_metrics(truth = truth, estimate = .pred_class) # 確率ベースの指標は別枠 preds |> roc_auc(truth, .pred_退職) preds |> pr_auc(truth, .pred_退職)

不均衡データではPR曲線も見る

ROC曲線は陰性クラスが圧倒的に多いと楽観的に見えます。退職率8%のようなデータでは、 pr_curve() / pr_auc() を併せて確認してください。 autoplot() にそのまま渡せます。

何を主軸に置くか — 見逃しのコストで決まる

指標選びに一般解はありません。どちらの間違いが高くつくかで決まります。 退職予測でいちばん避けたいのは見逃しです。辞める人を「大丈夫」と判断してしまうと、打つ手がなくなります。 逆に「リスクあり」と出した人が辞めなかった場合のコストは、面談を1回多くやることです。

コストが非対称なので、主軸は Recall(退職者のうち何割を事前に捉えられたか)になります。 Accuracyは、この場面では見なくていい数字です。報告資料から外しました。

Recall(再現率)
sensitivity()
辞めた人のうち、事前に拾えた割合。退職予測の主軸
Precision(適合率)
precision()
アラートを出した人のうち、実際に辞めた割合。運用負荷に直結
AUC-ROC
roc_auc()
閾値に依存しない順位付け性能。モデル同士の比較に使う
Accuracy
accuracy()
不均衡データでは意味を持たない。この題材では使わない

閾値は、統計ではなく人員から逆算する

モデルが出すのは0〜1の確率です。どこで線を引くかは分析者が決めます。 デフォルトの0.5を下げればRecallは上がりますが、上げすぎると全員が「リスクあり」になります。 Recall 100%のモデルは作れます。使えないだけです。

ここで効いたのは、統計的な最適化ではありませんでした。 「何人までなら対応できるか」を先に人事チームと決めて、そこから閾値を逆算するやり方です。 月に20人まで面談を積めるなら、アラートが20人に収まる閾値を選ぶ。それだけです。

なので閾値を振るときは、RecallとPrecisionだけでなくアラート件数を必ず列に入れます。 この列がないと会話が始まりません。

02_threshold_sweep.R
sweep <- map_dfr(seq(0.05, 0.60, by = 0.05), function(t) { p <- preds |> mutate(.pred_class = factor(if_else(.pred_退職 >= t, "退職", "在職"), levels = c("退職", "在職"))) tibble( threshold = t, アラート数 = sum(p$.pred_class == "退職"), # ← この列が交渉の材料になる recall = sensitivity_vec(p$truth, p$.pred_class), precision = precision_vec(p$truth, p$.pred_class) ) }) sweep |> pivot_longer(c(recall, precision), names_to = "metric") |> ggplot(aes(threshold, value, colour = metric)) + geom_line(linewidth = 1.1) + labs(title = "閾値とRecall・Precisionの関係", y = NULL) + theme_minimal()

probably パッケージの threshold_perf() を使えば、この掃引を1行で書けます。 ただ自分で書いたほうがアラート件数のような独自の列を足しやすいので、最初は手で組むのをおすすめします。

閾値は分析者が一人で決めない

ここは技術的な判断ではなく運用の判断です。「Youden's J で最適」と言って持っていっても、 現場が回らなければ使われません。掃引した表を持って行き、対応可能な人数を先に聞く。 そこから閾値が決まります。順番が逆になると、たいてい形骸化します。

SMOTEより先に、閾値を疑う

クラス不均衡への対処というと、SMOTEなどのオーバーサンプリングや、学習時の重み調整が挙がります。 どちらも有効ですが、私は最初の一手として閾値の見直しを選びました。理由は精度ではなく、説明のしやすさです。

「少数クラスの合成データを生成して学習データを水増ししました」と人事チームに説明するのと、 「70%以上をリスクありと呼んでいたのを、55%以上に変えました」と説明するのでは、後者のほうが伝わります。 データを触らずに運用側のつまみだけで調整できる、という点も扱いやすい。

それでも足りない場合に、学習側で重みをかけます。tidymodelsなら step_downsample() / step_upsample()(themis)をレシピに入れるか、 LightGBMなら scale_pos_weight を設定します。

03_class_weight.R
# 陰性/陽性の比をそのまま重みにする(LightGBMの場合) w <- sum(train$turnover == "在職") / sum(train$turnover == "退職") params <- list( objective = "binary", metric = "auc", learning_rate = 0.05, num_leaves = 15, # 小さいデータで大きくすると過学習する min_data_in_leaf = 20, # ここを増やすほうが効く scale_pos_weight = w )

実際に一度過学習させました。データ量が小さいのに num_leaves を大きくしすぎたのが原因です。 葉あたりの最小データ数を増やして正則化をかけたら落ち着きました。 人事データは行数が数千のオーダーになりがちなので、木を深くする方向のチューニングは効きにくいと思っておいたほうがいいです。

最終的な数字を、どう読むか

変数を入れ替え、閾値を調整したあとの数字がこうなりました。

最終モデルの評価
AUC-ROC 0.92 〜 0.93 順位付けの性能 Recall 約 0.78 退職者の約8割を事前に捕捉 Precision 約 0.61 アラートの約6割が実際に退職

冒頭のAccuracy 92%と、このAUC 0.92は数字として近いのに、意味がまったく違います。 前者は何も予測していない状態で、後者は退職者の8割を事前に拾えている状態です。 報告するときは、この違いを先に説明したほうがいい。数字だけ出すと同じに見えます。

Precision 0.61 は「4割は空振り」ということでもあります。これを低いと見るかは運用次第です。 面談を1回多くやるだけのコストなら、見逃しを減らすほうを取る。そういう判断ができるように、 Precisionはアラート件数と一緒に提示します。

評価指標が決まっても、そこは出発点

指標を整えて、閾値を決めて、リストが出ました。この時点でぶつかったのが「で、この人をどうするのか」です。 第22章でサーベイの自由記述について書いたことと、構造はまったく同じでした。 出力はできる。次の一手に繋がらない。

いくつか分かったことがあります。退職を申し出てきた時点では、たいてい転職先が決まっていて手遅れです。 意味があるのは、まだ迷っている段階で声がかかることでした。スコアが教えてくれるのは 「誰が辞めるか」ではなく「いつ声をかけるか」です。

もうひとつ、スコアの届け方も設計が要りました。マネージャーに「Aさんは退職確率73%です」と伝えるのは、 本人との関係に影響します。スコアは人事が持ったまま、「このメンバーと近いうちに1on1の機会を作ってください」 という形に変換して渡す。この一段階を挟むかどうかで、運用の受け入れられ方が変わりました。

モデルを作る側の全過程について

この章は評価指標に絞って書きました。実際にどの変数が効いてどれが効かなかったか—— 残業時間が効かなかった理由、評価の絶対値ではなく差分を使う話、 エンゲージメントサーベイの回答スタイルバイアスをどう除去したか、 ロジスティック回帰からLightGBMに移した判断基準まで含めた全過程は、noteに別途まとめています。

【実務公開】退職予測モデルを作ったら最初に「罠」に落ちた話|ES差分×LightGBMで精度92%に至るまで(有料記事)

この章のまとめ

不均衡データを評価するときの順番

  • Accuracyを最初に捨てる:退職率8%なら全員在職予測で92%出る。指標として機能しない
  • 混合行列を先に見る:見逃しがゼロ件か全件かは、ここでしか分からない
  • 陽性クラスの水準を確認する:yardstickは第1水準を陽性に取る。逆だと全指標が反転する
  • 主軸はコストの非対称性で決める:退職予測では見逃しが高くつくのでRecall
  • 閾値は人員から逆算する:掃引表にアラート件数を入れ、対応可能人数から選ぶ
  • SMOTEより先に閾値:説明しやすさで運用の定着が変わる
  • 小さいデータで木を深くしない:葉あたりの最小データ数を増やすほうが効く

次の章へ

リストが出たあとに必ず聞かれるのが「なぜこの人がリスク高と出たのか」です。 予測の根拠を説明できないモデルは、人事の現場では使われません。 変数重要度とSHAPをどう扱うかは第19章で書きます。

📚 学習におすすめの書籍

📊

Rユーザのためのtidymodels[実践]入門

〜モダンな統計・機械学習モデリングの世界

松村 優哉 ほか

🛒 Amazonで見る
📈

改訂2版 RユーザのためのRStudio[実践]入門

〜tidyverseによるモダンな分析フローの世界

松村 優哉 ほか

🛒 Amazonで見る

※ 当サイトはAmazonアソシエイトプログラムに参加しています