EDAと前処理の裏にある統計 ― describe・欠損・分布・相関を読み直す
公開ノート前半のEDAと前処理を、統計検定3級の知識の延長として読み直します。describeが見ているもの、欠損の型と補完が生むバイアス、カテゴリ変数のエンコーディングとリーク、スケーリングの要否を、House PricesとSOLNAで確認します。
by Akisame
df.describe() や欠損補完は、統計的に何をしているのでしょうか。結論から言えば、EDAは分布を見る記述統計であり、前処理は分布を加工する操作です。そしてどちらも、やり方を一つ間違えると下流のモデルへ静かにバイアスを注入します。この回では、公開ノート前半のEDAと前処理を統計検定3級の知識の延長として読み直し、写経では飛ばしてしまう「このセルは何を見ているのか」を言語化しながら、リークの入口を塞いでいきます。
公開データは House Prices(回帰)を使い、実務への移植は過去の受託案件をもとに設計した架空のD2Cブランド SOLNA で確認します。他者の公開Notebookは貼らず、構造を言葉で解剖し、要点だけ自前の最小再実装で示します。
この回の到達目標
- 記述統計(平均・分散・分位点)を「数値」ではなく「分布の形」として読めるようになる。
- 欠損の3つの型(MCAR / MAR / MNAR)を当てはめ、補完がどの統計量を動かすか説明できる。
- カテゴリ変数のエンコーディングで最も事故りやすいリークを、交差検証の中で塞げるようになる。
- スケーリングが要るモデルと要らないモデルを「なぜ」で区別できる。
前提
前回(第1回)で、典型的な表形式ノートを「読み込み→EDA→前処理→交差検証→モデル→評価→重要度→提出」の8ブロックに分解しました。本稿はその2〜3ブロック目、EDAと前処理を縦に掘ります。pandasで read_csv と集計ができ、平均・分散・割合・相関といった3級レベルの記述統計を計算した経験があることを前提にします。
describe・分位点・分布 ― 3級の記述統計を「形」で見る
df.describe() が一行で返すのは、3級で習った要約統計量そのものです。件数、平均、標準偏差、最小・最大、そして四分位点。写経ではここを「とりあえず眺める」で済ませがちですが、解読の段階では一つずつ分布の形に翻訳します。平均 $\mu$ と中央値が大きくずれていれば分布は歪んでいます。標準偏差が平均に対して大きければ、ばらつきが効いていて単純な平均値での要約は危ういと分かります。
House Prices の目的変数 SalePrice は典型的な右に裾の長い分布です。安い物件に山があり、高額物件が右へ長く伸びるため、平均は中央値より高く出ます。この形のまま二乗誤差で線形回帰にかけると、高額側の大きな残差にモデルが引きずられます。そこで対数変換 $y' = \log(1+y)$ を当て、裾を縮めて分布を対称に近づけます。これは「精度を上げる小技」ではなく、誤差の前提(残差が概ね等分散・対称)を満たしにいく操作だと理解しておくと、ほかのデータでも判断がぶれません。
import numpy as np
import pandas as pd
train = pd.read_csv("train.csv")
# describe は 3級の要約統計量の束。数値ではなく「形」を読む
print(train["SalePrice"].describe())
# 歪度を数値で確認(正なら右に裾が長い)
print("skew:", train["SalePrice"].skew()) # House Prices では正に大きい
# 対数変換で裾を縮める。0 を含みうる量は log1p を使う
train["SalePrice_log"] = np.log1p(train["SalePrice"])
print("skew(after log1p):", train["SalePrice_log"].skew()) # 0 に近づく欠損の型(MCAR/MAR/MNAR)と補完が動かすもの
欠損補完は「埋めれば動く」ので写経で最も無造作に通過する箇所です。しかし「なぜ欠けたか」によって、補完が正しいか有害かが変わります。欠損には3つの型があります。
- MCAR(完全にランダムな欠損):欠けた理由が観測値とも欠損値とも無関係。単純な補完でバイアスはほぼ入りません。
- MAR(観測値に依存する欠損):欠けやすさが、ほかの観測できている列で説明できる。条件付きの補完(グループ別の中央値など)が筋になります。
- MNAR(欠損値そのものに依存する欠損):欠けやすさが、まさにその欠けた値に依存する。たとえば高所得ほど年収を書かない、など。これは補完では原理的に救えず、「欠けていること自体」を特徴量(欠損フラグ)にするのが現実解です。
平均補完は手軽ですが、その列の分散を必ず縮めます。同じ値を大量に詰めるので、ばらつきが過小評価され、相関も歪みます。中央値補完は外れ値に強い代わり、やはり分布に人工的な山を作ります。どれを選ぶかは、上のどの型に当てはまるかで決めるべきで、「とりあえず平均」は型を確認しない限り賭けです。
カテゴリのエンコーディングとリーク
数値しか食べないモデルにカテゴリ変数を渡すには、数値へ変換(エンコード)します。代表的な3つは、目的に対する安全度が違います。
one-hot / label
one-hotエンコーディングは、カテゴリの各水準を0/1の列に展開します。順序の無いカテゴリ(地域、流入チャネルなど)に素直で、リークも起きにくい安全な既定値です。難点は水準数が多いと列が爆発することです。labelエンコーディング(各水準に整数を振る)は列を増やしませんが、整数に勝手な大小関係を持ち込みます。線形モデルや距離ベースの手法では「3は1より大きい」と誤読されるため、本来は順序のあるカテゴリに限るべきです。決定木系は分割で扱うため、labelでも比較的頑健です。
target encoding とリーク
target encodingは、各水準を「その水準における目的変数の平均」に置き換えます。表現力が高く効きやすい反面、最も事故りやすい手法です。素朴に全データで水準ごとの平均を計算してから各行に振り直すと、各行の目的変数が自分自身のエンコード値に混入します。これがリークです。検証スコアは華やかに上がりますが、本番では再現しません。
回避の原則は単純で、エンコードの平均を「その行が属さない別のfoldだけ」で計算することです。交差検証の内側でエンコーダを学習し、検証foldには他foldで作った値を適用します。下はSOLNAの customers(acquisition_channel を churned_flag でエンコード)でリークを再現し、fold内算出で塞ぐ最小構成です。
import numpy as np
import pandas as pd
from sklearn.model_selection import KFold
# SOLNA: customers の一部。channel を churn 率でエンコードしたい
df = customers[["acquisition_channel", "churned_flag"]].copy()
# --- やってはいけない例:全データで平均→自分の目的変数が漏れる ---
leaky_map = df.groupby("acquisition_channel")["churned_flag"].mean()
df["te_leaky"] = df["acquisition_channel"].map(leaky_map) # リーク
# --- 正しい例:自分が属さない fold だけで平均を作る ---
df["te_safe"] = np.nan
kf = KFold(n_splits=5, shuffle=True, random_state=42) # seed は再現性のため固定
global_mean = df["churned_flag"].mean()
for tr_idx, va_idx in kf.split(df):
means = df.iloc[tr_idx].groupby("acquisition_channel")["churned_flag"].mean()
df.loc[df.index[va_idx], "te_safe"] = (
df.iloc[va_idx]["acquisition_channel"].map(means)
)
df["te_safe"] = df["te_safe"].fillna(global_mean) # 学習側に無い水準は全体平均で代替件数の少ない水準は平均が暴れます。3級の感覚どおり、サンプルが少ない群の平均は分散が大きく信用できません。そこで全体平均へ縮める平滑化を入れます。
ここで $n_c$ は水準 $c$ の件数、$\bar{y}_c$ はその水準の目的平均、$\bar{y}$ は全体平均、$m$ は縮め具合を決める強さです。$n_c$ が小さいほど全体平均 $\bar{y}$ に寄り、十分なデータがある水準ほど自前の平均を信じます。少数群を全体平均へ引き戻すという、ベイズ的な縮約の素朴版です。
スケーリングは木系に不要・線形/距離に必要、の理由
「とりあえず標準化」も写経の定番ですが、要否はモデル族で決まります。決定木とその集合(ランダムフォレスト、勾配ブースティング)は、各特徴量の「しきい値で分ける」操作しかしません。単調変換でしきい値も一緒にずれるだけなので、スケーリングしても分割結果は本質的に変わらず、不要です。
一方、線形モデルや距離・勾配を使う手法(線形/ロジスティック回帰、k近傍法、SVM、ニューラルネット)は、特徴量の数値の大きさがそのまま効きます。単位の大きい列が距離や勾配を支配し、正則化の罰則も列ごとに不公平になります。だからこれらでは標準化(平均0・分散1)や正規化が要ります。「効くから」ではなく「その手法が数値の尺度に依存するか」で判断するのが解読の作法です。なおスケーリングのための平均・分散も、欠損補完と同様に訓練側だけで決めます。
相関行列の罠(非線形・多重共線性の予告)
EDAの締めに相関行列のヒートマップを描くのも定番です。ただしピアソン相関が拾うのは線形の関係だけです。U字や飽和のような非線形な強い関係は、相関係数では0近くに見えることがあります。「相関が低い=無関係」と読むのは早合点で、散布図で形を確認する一手間が要ります。
もう一つの罠が多重共線性です。互いに強く相関する説明変数が複数あると、線形モデルでは係数が不安定になり、符号が反転することさえあります。これは「どの変数が効いているか」の解釈を壊します。検知と対処(VIFや正則化)は第5回で正則化と一緒に扱うので、ここでは「相関の高い列が並んでいたら後で要注意」という伏線に留めます。
SOLNAへ ― 未来情報・新カテゴリという実務だけの落とし穴
ここまでの操作は、House Pricesのような固定・クリーンな公開データなら素直に通ります。実務データで初めて現れる崩れが二つあります。コンペで効いた手順がそのままでは実務で壊れる、その典型です。
一つ目は未来情報の混入です。SOLNAでChurnを予測するなら、特徴量は「予測したい時点まで」の情報だけで作らねばなりません。orders を顧客単位に素朴に集計すると、予測時点より後の注文まで巻き込み、未来から答えを覗いた特徴量になります。基準日(as-of)を切り、その日までの行動だけで集計するのが鉄則です。
-- SOLNA: 基準日 ref_date 時点で使える特徴量だけを集計する(未来情報を入れない)
-- 例: 予測時点の直近90日の注文挙動。order_at > ref_date は決して含めない
SELECT
c.customer_id,
c.acquisition_channel,
COUNTIF(o.order_at > DATE_SUB(ref.ref_date, INTERVAL 90 DAY)
AND o.order_at <= ref.ref_date) AS orders_90d,
SUM(IF(o.order_at <= ref.ref_date, o.amount, 0)) AS amount_to_date
FROM customers AS c
CROSS JOIN (SELECT DATE '2026-03-01' AS ref_date) AS ref
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.order_at <= ref.ref_date -- ここで未来を遮断する
GROUP BY c.customer_id, c.acquisition_channel, ref.ref_date二つ目は新カテゴリです。学習時に存在しなかった acquisition_channel や product_id が、本番では平気で現れます。target encodingの写像にその水準が無く、エンコード値が欠損して落ちる、というのが定番の事故です。先ほどのコードで未知水準を全体平均へ寄せたのは、この備えでもあります。コンペデータは水準が固定なので、この問題はそもそも起きません。固定された世界で完成させた前処理を、動く世界へ持ち込むと壊れる――この非対称こそ、公開コードを「解読して移植する」ときに毎回点検すべき箇所です。
なお、最近獲得したばかりの顧客はまだ解約する時間が経っておらず、churned_flag が将来反転しうる未成熟コホートです。target encodingの平均も、こうした観測途中のラベルで作ると過小評価に傾きます。基準日と観測窓を固定して扱うのが安全です。
対応書籍と読みどころ
- 門脇ら『Kaggleで勝つデータ分析の技術』:本連載の実装のバックボーン。特に前処理・特徴量とtarget encodingの章は、本稿で触れたリークと平滑化を具体例で確認できます。
- 東京大学教養学部統計学教室『統計学入門』(赤本):describeで見た要約統計量・分位点・分布の歪みは、3級からの地続きでこの一冊が土台になります。
PRMLへの一歩
本稿で前提にした「分布を見て、その形に合わせて加工する」という発想は、PRML 2章(確率分布)の入口です。対数変換の動機にある正規分布、カテゴリ変数の背後にあるカテゴリ分布——前処理の判断は、暗黙にこれらの分布を仮定しています。2章を開くとき、本稿の各操作が「どの分布を想定した手当てだったか」で読み返すと、式が操作とつながって見えてきます。
章末チェックリスト
- 目的変数の分布を見て、対数変換などの要否を判断できる
- 欠損の型(MCAR/MAR/MNAR)を当てはめ、補完が動かす統計量を言える
- target encoding を交差検証の内側(fold内)で算出している
- 補完・スケーリングの統計量を訓練側だけで決めている
- スケーリングの要否を、モデル族(木系か線形/距離か)で説明できる
- 相関の低さを「無関係」と早合点していない
- SOLNA移植時に、未来情報の遮断(as-of)と新カテゴリの備えを点検した
ここまでは「分布をどう読み、どう加工するか」の話でした。次回は線形回帰を、LinearRegression().fit() の一行から最小二乗・正規方程式まで作り直し、線形代数の最初の壁を越えます。前処理した特徴量が、いよいよモデルの中でどう使われるのか——その手前の数学に入ります。
なお、こうして作った特徴量や予測を「何のために測り、どの意思決定を動かすのか」という上流の判断は、対になる記述・予測編で扱っています。実装(HOW)と判断(WHAT/WHY)を行き来しながら読むと、前処理の一手一手が現場の数字に効く理由が見えてきます。