groupby — スプリット-適用-結合パターン
このトピックを修了すると
pandasのgroupby()の内部動作をスプリット-適用-結合パターンとして説明でき、実務でよく使われるグループごとの集計、変換、フィルタリングのパターンを習得します。
グループごとの演算が必要な理由
「全体の平均」はdf["score"].mean()で一行で済みます。しかし、実務上の質問はほとんど次のような形です。
- 部署別の平均年収は?
- 月別の売上合計は?
- カテゴリ別で最も売れた商品は?
「〜別」と付く瞬間、グループごとの演算が必要になります。これがgroupby()の役割です。
スプリット-適用-結合パターン
Hadley Wickhamが2011年に名付けたこのパターンは、データ分析で最も頻繁に繰り返される思考構造です。
元のデータ
┌────────┬──────┬───────┐
│ dept │ name │ score │
├────────┼──────┼───────┤
│ Sales │ Alice│ 85 │
│ Sales │ Bob │ 90 │
│ Dev │ Carol│ 95 │
│ Dev │ Dave │ 88 │
│ HR │ Eve │ 78 │
└────────┴──────┴───────┘
1. SPLIT — 部署ごとに分割
Sales: [85, 90]
Dev: [95, 88]
HR: [78]
2. APPLY — 各グループに関数を適用 (例: 平均)
Sales: 87.5
Dev: 91.5
HR: 78.0
3. COMBINE — 結果を1つに結合
┌────────┬───────┐
│ dept │ score │
├────────┼───────┤
│ Dev │ 91.5 │
│ HR │ 78.0 │
│ Sales │ 87.5 │
└────────┴───────┘pandasでは、この3つのステップが1行で済みます。
df.groupby("dept")["score"].mean()基本的な使い方
import pandas as pd
df = pd.DataFrame({ "dept": ["Sales", "Sales", "Dev", "Dev", "HR"], "name": ["Alice", "Bob", "Carol", "Dave", "Eve"], "score": [85, 90, 95, 88, 78], "salary": [50000, 52000, 70000, 68000, 45000]})
# 部署ごとのスコア平均df.groupby("dept")["score"].mean()# dept# Dev 91.5# HR 78.0# Sales 87.5
# 部署ごとの給与合計df.groupby("dept")["salary"].sum()# dept# Dev 138000# HR 45000# Sales 102000GroupByオブジェクトとは
grouped = df.groupby("dept")print(type(grouped)) # <class 'pandas.core.groupby.DataFrameGroupBy'>groupby()はすぐに計算を実行しません。遅延評価(lazy evaluation) — 何らかの関数(mean、sumなど)を適用するときに初めて計算が実行されます。これは、大量のデータで不要な演算を防ぎます。
グループの内容を確認
for name, group in df.groupby("dept"): print(f"\n--- {name} ---") print(group)
# --- Dev ---# dept name score salary# 2 Dev Carol 95 70000# 3 Dev Dave 88 68000# --- HR ---# ...複数の集計関数
1つのカラム、複数の関数
df.groupby("dept")["score"].agg(["mean", "min", "max", "count"])# mean min max count# dept# Dev 91.5 88 95 2# HR 78.0 78 78 1# Sales 87.5 85 90 2カラムごとに異なる関数
df.groupby("dept").agg( avg_score=("score", "mean"), total_salary=("salary", "sum"), headcount=("name", "count"))# avg_score total_salary headcount# dept# Dev 91.5 138000 2# HR 78.0 45000 1# Sales 87.5 102000 2名前付き集計(カラム名=("元のカラム", "関数"))は、結果の列名まで一度に指定できるため、最もきれいな方法です。
複数のカラムでグループ化
sales = pd.DataFrame({ "region": ["East", "East", "West", "West", "East", "West"], "category": ["A", "B", "A", "B", "A", "A"], "revenue": [100, 200, 150, 250, 120, 180]})
sales.groupby(["region", "category"])["revenue"].sum()# region category# East A 220# B 200# West A 330# B 250リストで複数のカラムを渡すと、**階層的インデックス(MultiIndex)**が生成されます。
# MultiIndexを通常のカラムにするresult = sales.groupby(["region", "category"])["revenue"].sum().reset_index()# region category revenue# 0 East A 220# 1 East B 200# 2 West A 330# 3 West B 250reset_index()は、グループキーをインデックスから通常のカラムに戻します。後続の作業(可視化、保存など)では、この形式の方が便利です。
transform — グループごとの変換
agg()はグループごとに1つの値を返します(5行 → 3行)。transform()は元のものと同じサイズの返します(5行 → 5行)。
# 部署ごとの平均を元のデータに付加df["dept_avg"] = df.groupby("dept")["score"].transform("mean")print(df)# dept name score salary dept_avg# 0 Sales Alice 85 50000 87.5# 1 Sales Bob 90 52000 87.5# 2 Dev Carol 95 70000 91.5# 3 Dev Dave 88 68000 91.5# 4 HR Eve 78 45000 78.0各行に、その部署の平均が付加されます。これで、部署平均とのスコアの差を計算できます。
df["vs_avg"] = df["score"] - df["dept_avg"]# Alice: 85 - 87.5 = -2.5 (部署平均以下)# Carol: 95 - 91.5 = +3.5 (部署平均以上)filter — グループごとのフィルタリング
条件を満たすグループ全体を残すか、削除します。
# 人数が2人以上の部署のみdf.groupby("dept").filter(lambda g: len(g) >= 2)# dept name score salary# 0 Sales Alice 85 50000# 1 Sales Bob 90 52000# 2 Dev Carol 95 70000# 3 Dev Dave 88 68000HR(1人)がまとめて削除されました。個々の行ではなく、グループ単位でフィルタリングするのがfilter()の核心です。
実践パターン — 月別売上分析
orders = pd.DataFrame({ "date": pd.to_datetime([ "2026-01-05", "2026-01-15", "2026-02-03", "2026-02-20", "2026-03-10", "2026-03-25" ]), "product": ["Widget", "Gadget", "Widget", "Gadget", "Widget", "Widget"], "amount": [1200, 800, 1500, 900, 1100, 1300]})
# 月別売上合計monthly = orders.groupby(orders["date"].dt.to_period("M"))["amount"].sum()print(monthly)# date# 2026-01 2000# 2026-02 2400# 2026-03 2400
# 商品ごとの月平均売上orders.groupby("product").agg( total=("amount", "sum"), avg=("amount", "mean"), count=("amount", "count"))# total avg count# Gadget 1700 850.0 2# Widget 5100 1275.0 4日付データから.dt.to_period("M")で月単位のグループを作成することは、非常に頻繁に使用されるパターンです。
主要なまとめ
| メソッド | 動作 | 結果のサイズ |
|---|---|---|
agg() | グループごとの集計 (sum, mean, count...) | グループの数だけ (減少) |
transform() | グループごとの変換 (元のサイズを維持) | 元と同じ |
filter() | 条件を満たすグループ全体を維持 | 元以下 |
groupby()は、pandasで最も強力で、最も頻繁に使用される機能です。スプリット-適用-結合パターンを頭の中で描くことができれば、複雑な集計も「どのカラムで分割し(スプリット)、どのような関数を適用し(適用)、結果をどのような形式で結合するか(結合するか)」という3つのステップに自然に分解できます。