📚 背景知識(読んでから問題へ)
| 用語 | 直感的な意味 |
|---|---|
| groupby統計量 | 同じキー(顧客IDなど)でグループ化し、グループ内の平均・合計・件数・標準偏差などを計算したもの |
| 差分特徴量(Deviation) | 個々の値からグループの代表値(平均など)を引き算した値。絶対的なズレの大きさを表す |
| 比率特徴量(Ratio) | 個々の値をグループの代表値で割り算した値。金額の絶対的な大小に影響されにくい |
transform() | groupby().transform()は集約結果を元の行数のまま返す。mergeせずに各行へ集約値を貼り付けられる |
| キー粒度(granularity) | 「誰について集約するか」=顧客単位・カテゴリ単位・店舗単位など。同じデータでも複数粒度で作れる |
🧩 Aggregation特徴量を作る3つのテクニック
📐 agg()
count / mean / std / max / nunique
顧客・カテゴリ単位でデータを縮約し、行動の「型」を数値化する。
🔁 transform()
merge不要で1行合成
集約結果を各行にそのまま割り当て、行レベルの相対特徴量の材料にする。
➖➗ 差分・比率
amount - mean / amount ÷ mean
「普段通りか、逸脱しているか」という文脈をモデルに直接渡す。
🎯 問題
あるECサイトの注文明細データ(1行=1注文)があります。顧客ごとの「普段の購買行動」を特徴量として取り出し、後続のモデル(不正注文検知・離脱予測など)に渡せる形に整えるタスクです。
import numpy as np import pandas as pd np.random.seed(42) n_orders = 3000 n_customers = 400 customer_id = np.random.choice( [f"cust_{i:04d}" for i in range(n_customers)], n_orders ) category = np.random.choice( ["食品", "家電", "衣料", "書籍", "日用品"], n_orders, p=[0.35, 0.15, 0.20, 0.10, 0.20] ) order_date = pd.to_datetime("2026-01-01") + pd.to_timedelta( np.random.randint(0, 180, n_orders), unit="D" ) amount_yen = (np.random.gamma(shape=2.0, scale=1500, size=n_orders) + 300).round(-1) orders = pd.DataFrame({ "order_id": np.arange(n_orders), "customer_id": customer_id, "category": category, "order_date": order_date, "amount_yen": amount_yen, })
| カラム名 | 意味 | 型 |
|---|---|---|
| order_id | 注文ID(1行1注文) | int |
| customer_id | 顧客ID(400人が繰り返し登場) | category |
| category | 購入カテゴリ(食品/家電/衣料/書籍/日用品) | category |
| order_date | 注文日 | datetime |
| amount_yen | 注文金額(円) | float |
タスク
customer_id単位でgroupby().agg()し、注文回数・平均額・標準偏差・最大額・カテゴリ数を持つcustomer_aggを作るtransform()で各注文行に顧客平均額を付与し、差分特徴量と比率特徴量を作成するcategory単位でも比率特徴量を作り、「顧客内での立ち位置」と「カテゴリ内での立ち位置」を両方確認するamount_stdがNaNになる理由と対処法を、Train/Testのリークにも触れながら説明する⚖️ 差分特徴量 と 比率特徴量 はどう違うか
「平均より+1000円」という差分は、顧客ごとの金銭感覚の違いを無視してしまう。比率にすると、平均額のレベルが違っても相対的な立ち位置を揃えて比較できる。
📊 集約の「粒度」を変えると特徴量が広がる
同じ「集約→比率」というテクニックでも、groupbyのキーを変えるだけで意味の異なる特徴量が量産できる。
1つの注文行から作れる相対特徴量(例)
💡 ヒント
「注文額そのもの」は行ごとにバラバラだが、「その顧客にとって普段よりどれだけ高いか/安いか」という相対的な情報に変換すると、モデルが学習しやすい特徴量になる。今日はその変換を、①グループごとの代表値を求める(agg)、②その代表値を元の行数のまま各行に貼り付ける(transform)、③引き算・割り算で相対値に変換する、という3ステップで行う。
orders.groupby("customer_id")["amount_yen"].agg(["count","mean","std","max"])のようにリストで複数統計量を渡せる- 列名を分かりやすくしたい場合は
agg(order_count=("amount_yen","count"), ...)のnamed aggregationが便利 nunique()はユニーク数を数える関数。groupby("customer_id")["category"].nunique()で購買の多様性が分かるtransform()はagg()と違い元の行数と同じ行数を返すので、そのまま新しい列として代入できる- 差分は引き算、比率は割り算。今回
amount_yenは常に正の値なのでゼロ割りは起きない
# タスク2: 顧客単位の集約 customer_agg = orders.groupby("customer_id").agg( order_count=("amount_yen", "count"), amount_mean=("amount_yen", "mean"), amount_std=("amount_yen", "std"), amount_max=("amount_yen", "max"), category_nunique=("category", "nunique"), ).reset_index() # タスク3: transformで行ごとに顧客平均を付与 → 差分・比率 orders["cust_amount_mean"] = orders.groupby("customer_id")["amount_yen"].transform(___) orders["amount_diff_from_cust_mean"] = orders["amount_yen"] - orders[___] orders["amount_ratio_to_cust_mean"] = orders["amount_yen"] / orders[___] # タスク4: カテゴリ単位も同様に orders["cat_amount_mean"] = orders.groupby(___)["amount_yen"].transform("mean") orders["amount_ratio_to_cat_mean"] = orders["amount_yen"] / orders[___] # タスク5: 注文1回だけの顧客のstdを確認 customer_agg[customer_agg["order_count"] == 1][["customer_id", "order_count", "amount_std"]].head()
✅ 模範解答
import numpy as np import pandas as pd # ====== データ準備 ====== np.random.seed(42) n_orders = 3000 n_customers = 400 customer_id = np.random.choice( [f"cust_{i:04d}" for i in range(n_customers)], n_orders ) category = np.random.choice( ["食品", "家電", "衣料", "書籍", "日用品"], n_orders, p=[0.35, 0.15, 0.20, 0.10, 0.20] ) order_date = pd.to_datetime("2026-01-01") + pd.to_timedelta( np.random.randint(0, 180, n_orders), unit="D" ) amount_yen = (np.random.gamma(shape=2.0, scale=1500, size=n_orders) + 300).round(-1) orders = pd.DataFrame({ "order_id": np.arange(n_orders), "customer_id": customer_id, "category": category, "order_date": order_date, "amount_yen": amount_yen, }) # ====== タスク2: 顧客単位の集約特徴量(customer_agg) ====== customer_agg = orders.groupby("customer_id").agg( order_count=("amount_yen", "count"), amount_mean=("amount_yen", "mean"), amount_std=("amount_yen", "std"), amount_max=("amount_yen", "max"), category_nunique=("category", "nunique"), ).reset_index() print("--- タスク2: customer_agg ---") print(customer_agg.head()) print(f"顧客数: {customer_agg.shape[0]}") # ====== タスク3: transformで顧客平均を付与 → 差分・比率特徴量 ====== orders["cust_amount_mean"] = orders.groupby("customer_id")["amount_yen"].transform("mean") orders["amount_diff_from_cust_mean"] = orders["amount_yen"] - orders["cust_amount_mean"] orders["amount_ratio_to_cust_mean"] = orders["amount_yen"] / orders["cust_amount_mean"] # ====== タスク4: カテゴリ単位の比率特徴量 ====== orders["cat_amount_mean"] = orders.groupby("category")["amount_yen"].transform("mean") orders["amount_ratio_to_cat_mean"] = orders["amount_yen"] / orders["cat_amount_mean"] print("\n--- タスク3・4: 行ごとの相対特徴量 ---") cols = ["customer_id", "category", "amount_yen", "cust_amount_mean", "amount_diff_from_cust_mean", "amount_ratio_to_cust_mean", "cat_amount_mean", "amount_ratio_to_cat_mean"] print(orders[cols].head(8).round(2)) # ====== タスク5: 注文1回だけの顧客のstdを確認 ====== single_order_customers = customer_agg[customer_agg["order_count"] == 1] print(f"\n--- タスク5: 注文1回だけの顧客数: {len(single_order_customers)} ---") print(single_order_customers[["customer_id", "order_count", "amount_std"]].head()) # 欠損への対処例: 「新規/少数注文フラグ」を立てた上でstdは0埋め customer_agg["is_single_order"] = (customer_agg["order_count"] == 1).astype(int) customer_agg["amount_std_filled"] = customer_agg["amount_std"].fillna(0)
amount_yenだけを見ているモデルはこの違いを区別できないが、顧客ごとの平均との差分・比率に変換すれば、同じ数値の裏にある文脈をモデルに直接渡せる。人間が無意識にやっている「あの人にしては珍しいね」という判断を、数値として機械学習モデルに教える作業だと言える。stdは「バラつき」の指標であり、データが2件以上ないと計算できない(1件だけでは「平均からのズレ」を評価する対象が無い)。対処法は①fillna(0)(バラつきが無い=安定とみなす)、②fillna(全体平均std)(過度に安定扱いしない保守的処置)、③is_single_orderのような欠損理由自体をフラグ化した列の3つが定番。また、Testに含まれる顧客の注文がTrain期間の後にしか無い場合、Trainの集約統計だけをTestに適用することが重要で、これを怠ると未来の情報がリークしCVスコアだけ不自然に良くなる典型的な失敗パターンになる。🪜 Step-by-Step 解説
1agg()で「グループの代表値」を求める
customer_agg = orders.groupby("customer_id").agg( order_count=("amount_yen", "count"), amount_mean=("amount_yen", "mean"), amount_std=("amount_yen", "std"), amount_max=("amount_yen", "max"), category_nunique=("category", "nunique"), ).reset_index()
agg(新列名=(元の列, 関数名))というnamed aggregationを使うと集約後の列名が最初から分かりやすくなる(単に.agg(["mean","std"])だとMultiIndexの列名になり扱いづらい)。countは「行数」、mean/std/maxは「金額の分布の形」、nuniqueは「購買の多様性」を表す、それぞれ異なる切り口の特徴量。2transform()で集約結果を「元の行数のまま」貼り付ける
orders["cust_amount_mean"] = orders.groupby("customer_id")["amount_yen"].transform("mean")
agg()は「顧客数(400行)」に縮約された結果を返すが、transform()は入力と同じ行数(3000行)を保ったまま、各行に「その行が属するグループの統計量」を割り当てる。これによりcustomer_aggを作ってmerge()する2ステップを踏まずに、1行で集約値を貼り付けられる。3差分(引き算)と比率(割り算)で「相対値」に変換する
orders["amount_diff_from_cust_mean"] = orders["amount_yen"] - orders["cust_amount_mean"] orders["amount_ratio_to_cust_mean"] = orders["amount_yen"] / orders["cust_amount_mean"]
4集約の「キー(粒度)」を変えると別の切り口の特徴量になる
orders["cat_amount_mean"] = orders.groupby("category")["amount_yen"].transform("mean") orders["amount_ratio_to_cat_mean"] = orders["amount_yen"] / orders["cat_amount_mean"]
groupbyのキーをcustomer_idからcategoryに変えるだけで、「顧客の中での立ち位置」から「カテゴリの中での立ち位置」というまったく別の意味を持つ特徴量が作れる。「この注文はこの顧客にとっては普段通りだが、家電カテゴリとしては非常に高額」というように、1つの行に複数粒度の“文脈”を同時に持たせるのがAggregation特徴量の本領。5集約件数が少ないグループの扱いを決める
customer_agg["is_single_order"] = (customer_agg["order_count"] == 1).astype(int) customer_agg["amount_std_filled"] = customer_agg["amount_std"].fillna(0)
stdのNaNをそのままモデルに渡すと、線形モデルではエラーになったり、GBDT系でも「欠損=特別な意味」として過度に学習してしまうことがある。単純に0埋めするのではなく、「なぜ欠損したか(=件数が少なく信頼度が低い)」という情報自体をis_single_orderのような列に切り出しておくことで、モデルが「この統計量は参考程度」と学習しやすくなる。🧮 数学・統計の補足(文系向け)
NaNになる。数式を見たら
std = √( (1/(n-1)) × Σ(xᵢ - x̄)² )
分母に「n-1」があるため、n=1のとき分母が0になり計算不能(0で割ることになる)になる。これが「1件だけのグループのstdがNaNになる」理由を数式で説明したもの。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 画像認識(CV) / ☑ 時系列(Time Series)
- 不正検知系コンペ(例: IEEE-CIS Fraud Detection): カード番号・デバイスID単位で「過去の平均取引額との比率」「同一デバイスからの取引回数」などのAggregation特徴量が上位解法で必ずと言っていいほど使われる
- 需要予測・購買予測系コンペ(例: Instacart Market Basket Analysis): 「そのユーザーがそのカテゴリを買う頻度」「カート内の平均商品数との比較」など、ユーザー粒度・商品粒度・カテゴリ粒度で何十〜何百もの集約特徴量を作り込むのが定番戦略
- 与信・金融系コンペ(例: Home Credit Default Risk): 顧客ごとの過去の申込・返済履歴をAggregationし、「過去の平均返済額に対して今回の申込額はどれくらい大きいか」といった比率特徴量が重要な予測因子になる
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
agg()の結果をそのまま元のDataFrameに代入しようとしてエラーになる | agg()はグループ数に縮約された行数を返すことを知らない | 元の行数のまま各行に貼り付けたい場合はtransform()を使う。agg()は集約テーブル(マスタ)を作るときに使う |
| Test期間のデータも含めて集約統計を計算してしまう | Train/Testをまとめて前処理してしまう習慣がある | 未来の情報が漏れる(リーク)。集約統計は原則Trainのみから計算し、その値をTestに同じ値として適用する |
amount_stdのNaNを無条件にモデルに投げてしまう | 欠損値の扱いを後回しにする | GBDT系はNaNをある程度扱えるが線形モデルではエラーになる。件数が少ないグループの統計量は信頼度が低いことも念頭に置く |
| 差分特徴量と比率特徴量のどちらか一方しか作らない | 「似たようなものだから片方で十分」と判断してしまう | スケールの違いに敏感かどうかでモデルへの効き方が変わるため、両方作ってモデルに選ばせるのが定石 |
| 集約のキー(粒度)を1種類しか試さない | 「顧客単位で集約すればOK」と決め打ちしてしまう | 顧客単位・カテゴリ単位・店舗単位・時間帯単位など複数粒度で繰り返すことで特徴量のバリエーションが大きく広がる |
🚀 次のステップ
- 発展:
groupby(["customer_id", "category"])のように複数キーの組み合わせで集約し、「この顧客はこのカテゴリで普段どれくらい使うか」というより粒度の細かい特徴量を作ってみる - 次回予告: Day 099「Rolling・Lag特徴量」 — 時系列データへの応用。今日の「グループ全体の平均」から一歩進み、「直近N件だけの平均」「1つ前の注文額」など時間の前後関係を考慮した集約に挑戦します
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: