公開ノートブックを「解剖」する ― 典型ノートを8ブロックに分解する
典型的な表形式ノートブックを「読み込み→EDA→前処理→交差検証→モデル→評価→重要度→提出」の8ブロックに分解し、各セルがどの問いに答えているかを地図にします。Titanicを例に、写経で見落とす「セルの意味」と、コピペが事故る瞬間を解剖します。
by Akisame
公開ノートのセルは、なぜその順で、何のために並んでいるのか。ほとんどの表形式(tabular)ノートは8つのブロックに分解でき、各ブロックは特定の問いにだけ答えています。この記事では、Titanicの典型ノートを上から下まで解剖し、写経では飛ばしがちな「そのセルの意味」を言語化します。
扱うのはテクニックではなく、読み方の作法です。コードを一行ずつ追う前に「いまどのブロックにいて、何の問いに答えているか」が見えていれば、写経は解読に変わります。そして同じ骨格を、後半で過去の受託案件をもとに設計した架空企業SOLNAの解約(Churn)予測に移し替えます。公開コンペのデータでは見えない実務の崩れ(リーク・分布シフト)が、移し替えた瞬間にどこで顔を出すかも併せて示します。
典型ノートの解剖図(8ブロックの役割)
題材が分類でも回帰でも、tabularノートの骨格はほぼ同じです。上から下へ、8つの区画が並んでいます。
[1] 読み込み train / test を読む・形と欠損を見る
│
[2] EDA 分布・欠損・関係を観察する(まだ加工しない)
│
[3] 前処理 欠損補完・エンコード・特徴量作成
│
[4] 交差検証(CV) 汎化性能を訓練データの外で見積もる「枠」
│
[5] モデル fit で重み(パラメータ)を決める
│
[6] 評価 指標で良し悪しを測る
│
[7] 重要度 どの特徴が効いたかを見る
│
[8] 提出 test を予測して submit する写経でつまずくのは、この区画の境目が見えていないからです。たとえば前処理([3])と交差検証([4])は、コード上は数行で隣接しますが、役割はまったく別です。前者はデータを加工する操作、後者は加工と学習を「訓練の外から検証する」ための入れ物で、両者の順序を取り違えると後述のリークが起きます。
各ブロックが答えている問い(「何を/なぜ」)
各ブロックを「この問いに答える区画」として言い換えると、ノート全体が一枚の問答表になります。
| ブロック | 答えている問い | 取り違えると起きること |
|---|---|---|
| [1] 読み込み | データは何行・何列・どんな型か。欠損はどこか | 型・欠損を見ずに進み、後段で静かに壊れる |
| [2] EDA | 目的変数と特徴はどんな分布・関係か | 加工方針を勘で決める |
| [3] 前処理 | 欠損とカテゴリをどう数値に直すか | 未来情報・テスト情報を混ぜる(リーク) |
| [4] CV | 汎化性能をどう訓練の外で測るか | 課題と合わない切り方で楽観値が出る |
| [5] モデル | どの仮説族で重みを決めるか | 評価設計より先にモデルに凝る |
| [6] 評価 | どの指標で意思決定を支えるか | 指標が現場の判断と無関係 |
| [7] 重要度 | どの特徴が予測に効いたか | 重要度を因果と取り違える |
| [8] 提出 | 予測を最終的な出力にどう変えるか | 実務では「提出」が存在しない |
この表の右列が、本連載で繰り返し戻ってくる注意点です。とくに[3][4][6][7]の取り違えは、コンペでは目立たなくても実務で事故ります。理由は単純で、コンペのデータは固定・クリーンですが、実務のデータは動き、欠け、汚れているからです。
写経と解読の違い ― コピペが事故る3つの瞬間
「コピペで動いた」と「中身が分かった」は別物です。動くコードがそのまま実務で壊れる瞬間を、代表的な3つに絞って先に名前を付けておきます。後続回(第2・5・7回)でそれぞれ詳しく開きます。
前処理をCVの外でやる
最も多い事故です。欠損補完やスケーリング、target encoding を「分割する前に」全データでやってしまうと、検証用に取り分けたはずのデータの情報が、訓練側の統計量に漏れます。検証スコアは本来より良く見え、本番で裏切られます。原則は一つで、前処理は分割の内側(fold ごと)に置く、です。
検証の切り方が課題と合っていない
ランダムなKFoldは、行が互いに独立で時間構造が無いときにだけ素直に機能します。顧客のように時間軸を持つデータや、同一ユーザーが複数行に現れるデータでは、ランダムに切ると未来や同一個体が訓練と検証にまたがり、楽観的なスコアが出ます。課題の構造(時系列・グループ・不均衡)に検証の切り方を合わせる必要があります。
指標がビジネスと無関係
accuracy が高いモデルが、現場の判断を一つも良くしないことは普通に起こります。とくに不均衡データでは、多数派に全部張るだけで accuracy は高く出ます。精度が高いことと役に立つことは別で、何を最適化しているのかを毎回問う必要があります。
pandas/numpyの最小限(DataFrame・Series・ベクトル化)
コードを読むのに要ることばだけに絞ります。網羅は目的ではありません。DataFrame は表、Series はその1列、そして for ループを書かずに列ごと一括で計算する「ベクトル化」が、pandas/numpy の基本姿勢です。
import numpy as np
import pandas as pd
train = pd.read_csv("train.csv") # 891行・生存ラベルあり
test = pd.read_csv("test.csv") # 418行・ラベルなし(提出対象)
# ブロック[1]の問い: 何行・何列・型・欠損はどこか
train.shape # (891, 12)
train.info() # 型と欠損を俯瞰する
train["Survived"].mean() # 生存率 およそ 0.38(不均衡を確認)
# ベクトル化: 行ごとのループを書かず、列同士の演算で一括計算する
train["FamilySize"] = train["SibSp"] + train["Parch"] + 1train["Survived"].mean() の一行は、ただの平均ではなく「正例がどれくらい少ないか」という不均衡の確認です。ここを見ているかどうかで、後段の指標選び([6])が変わります。写経では数字が出て終わりますが、解読ではこの数字が次のブロックの判断材料になります。
ここで前処理の事故を、最小の対比で見せておきます。欠損補完を「全データでやってから分割する」のがリーク、「分割してから訓練側だけで作る」のが正解です。
# NG: 全データで統計量を作ってから分割する(典型的なリーク)
age_mean = df["Age"].mean() # train と test が混ざった平均
df["Age"] = df["Age"].fillna(age_mean)
X_tr, X_va = split(df) # この時点で X_va の情報が X_tr に漏れている
# OK: 分割してから、訓練側だけで統計量を作る
X_tr, X_va = split(df)
age_mean = X_tr["Age"].mean() # 訓練側のみで算出
X_tr["Age"] = X_tr["Age"].fillna(age_mean)
X_va["Age"] = X_va["Age"].fillna(age_mean) # 同じ値を検証側へ「適用」する「動いた」と「分かった」を分ける問いの立て方
各セルに対して、読者が自分に投げるべき定型質問があります。次の4つを習慣にすると、写経が解読に変わります。
- このセルは8ブロックのどこに属し、どの問いに答えているか。
- この操作が使っている情報は、本番の推論時にも手に入るか(手に入らないなら未来情報=リーク)。
- この前処理は分割の内側にあるか、外側にあるか。
- この出力(スコア・指標・重要度)は、最終的にどの意思決定を動かすのか。
4つ目は特に効きます。出力が何の判断も動かさないなら、そのセルは保守コストだけの負債です。コンペなら submit という出口がありますが、実務には submit がありません。出口が「誰に何をするか」に変わることを、最後のブロックで確認します。
SOLNAへ ― このノート構造をChurn予測に写すと何を差し替えるか
ここから「読む素材」から「移す素材」へ移ります。SOLNAはD2Cスキンケアの定期購入(サブスク)ブランドで、customers・orders・sessions などのテーブルを持ちます。Titanicの8ブロックは、ほぼそのまま解約予測の骨格になりますが、3か所を必ず差し替えます。
まず、特徴量は「基準日(as_of)までの情報だけ」で作ります。これは前処理の事故を、SQLの時点で塞ぐためです。
-- ブロック[1][3]に相当: as_of までの情報だけで特徴量を作る
-- 未来情報の混入(リーク)を、結合条件の時点で防ぐ
WITH features AS (
SELECT
c.customer_id,
c.acquisition_channel,
c.region,
c.membership_tier,
COUNT(o.order_id) AS n_orders, -- as_of までの注文数
SUM(o.amount) AS revenue_to_date, -- as_of までの売上
MAX(o.order_at) AS last_order_at
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
AND o.order_at < @as_of -- ここが「時間の壁」
GROUP BY 1, 2, 3, 4
)
SELECT * FROM features;次に、CVの切り方とモデル後段を差し替えます。Titanicでは行が独立でしたが、SOLNAの顧客は時間軸を持つので、ランダムKFoldは時間リークを起こします。骨格をコメント主体で示します。
# SOLNA版・8ブロックの骨格(実行より読解を優先)
# [2] EDA : churned_flag の割合とチャネル別の偏りを見る(不均衡の確認)
# [3] 前処理 : acquisition_channel などのカテゴリは CV の内側でエンコードする
# [4] CV : 顧客は時間構造を持つ → ランダムKFoldでなく時系列で切る
from sklearn.model_selection import TimeSeriesSplit
cv = TimeSeriesSplit(n_splits=5) # 過去で学習し未来で検証する向きに固定
# [5] モデル : まずは素朴な分類器から(GBDTは第6回)
# [6] 評価 : accuracy ではなく、不均衡に強い指標+業務閾値で測る
# [7] 重要度 : 「効いた特徴」を見る。ただし重要度は因果ではない(後述)
# [8] 出力 : 「提出」ではなく「誰に解約防止施策を打つか」へ出口を差し替える差し替えの三点を言い換えると、(1) 特徴量を as_of で時間方向に閉じる、(2) 検証を時系列で切る、(3) 出口を submit から「打つ/打たない」の意思決定に変える、です。さらにSOLNAでは、学習時のチャネル構成と本番のチャネル構成がずれる分布シフトや、過去に無かった新カテゴリ(新規チャネル)が本番で現れる問題も起きます。コンペで効いたものが実務で効くとは限らない理由の大半は、この「固定 vs 動く」の非対称にあります。
最後に、重要度([7])について一点だけ釘を刺します。解約予測で「最終ログインからの日数」が重要だったとして、それは予測に効く変数であって、そこに介入すれば解約が減る変数とは限りません。重要度が高いことと因果があることは別です。誰に打てば効くのか、という問いは予測ではなく因果の問題で、本連載の範囲外(因果編の主題)になります。
PRMLへの一歩
各ブロックは、この先のどこかでPRMLの章につながります。今回は索引だけ置きます。詳細な読書地図は第9回でお伝えします。
| ブロック | 本連載での深掘り | PRMLの対応 |
|---|---|---|
| 前処理(分布の前提) | 第2回 | 2章(確率分布) |
| モデル=回帰 | 第3回 | 3章(線形回帰モデル) |
| モデル=分類・最尤 | 第4回 | 4章・1.2(線形識別・最尤) |
| CV・過学習・正則化 | 第5回 | 1.1・3.1-3.3 |
| 評価・指標 | 第7回 | 1.5(決定理論) |
対応書籍としては、tabularノートのことばと全体像を一冊で掴むなら門脇ら『Kaggleで勝つデータ分析の技術』が実装のバックボーンになります。最初のノートを動かす入口を補いたい場合のみ、Kaggleスタートブックを任意で。
章末チェックリスト
- 手元のノートを8ブロックに分解できる
- 各ブロックの「問い」を一文で言える
- 前処理が分割の内側にあるか(リークしていないか)点検した
- 検証の切り方が課題の構造(時系列・グループ)に合っているか確認した
- 各出力が、最終的にどの意思決定を動かすか言える
ここまでは「公開ノートをどう読み、どう自社データへ移すか」の解剖でした。その出力を「何を測り、どの判断を動かすか」という決裁の言葉に翻訳する側は、対になる記述・予測編で扱っています。なお「予測に効く変数 ≠ 因果のある変数」の深掘りは因果編の主題です。次回は、このノートの前半(EDAと前処理)を、統計検定3級の記述統計の延長として読み直します。