【Pandas】groupby集計入門|分割・適用・結合とagg/transform/filterを徹底解説
groupby集計はデータ分析の心臓部です。地域別の売上確認、カテゴリ別の平均価格の計算、月別の注文数の集計など、これらはすべて「分割して、計算して、結合する」に帰着します。Pandasのgroupbyは、古典的なsplit-apply-combine(分割・適用・結合)パラダイムを実装しています。本レッスンでは、Mermaid図を使ってそのプロセスを完全に理解し、agg / transform / filterという3つの重要な操作をマスターします。
⚠️ 注意: 以下のコードを実行するには、ローカルのPython環境が必要です。
1. 学習内容
- ❶ groupbyの仕組み(split-apply-combine)
- ❷ 集計関数(sum/mean/count/max/min)
- ❸ aggによる複数集計とカスタム関数
- ❹ transformによるグループ内変換
- ❺ filterによるグループレベルのフィルタリング
2. アリスがカフェの売上をカテゴリ別に集計する
(1) 課題:手動ループは面倒
アリスはドリンクカテゴリ別に売上を集計したいと考えています。ループを手書きすると次のようになります。
PYTHON
import pandas as pd
df = pd.DataFrame({
'drink': ['Latte', 'Americano', 'Mocha', 'Latte', 'Espresso',
'Americano', 'Latte', 'Mocha', 'Espresso', 'Latte'],
'category': ['Hot', 'Hot', 'Hot', 'Hot', 'Hot',
'Iced', 'Iced', 'Iced', 'Iced', 'Iced'],
'sales': [320, 280, 350, 310, 150, 200, 180, 160, 90, 120],
'quantity': [64, 56, 50, 62, 30, 40, 36, 22, 18, 24]
})
# 手動ループ — 面倒でミスが起きやすい
for cat in df['category'].unique():
subset = df[df['category'] == cat]
print(f"{cat}: sales={subset['sales'].sum()}, qty={subset['quantity'].sum()}")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
(2) 解決策:groupbyなら1行で完了
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:groupbyの基本(難易度 ⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'drink': ['Latte', 'Americano', 'Mocha', 'Latte', 'Espresso',
'Americano', 'Latte', 'Mocha', 'Espresso', 'Latte'],
'category': ['Hot', 'Hot', 'Hot', 'Hot', 'Hot',
'Iced', 'Iced', 'Iced', 'Iced', 'Iced'],
'sales': [320, 280, 350, 310, 150, 200, 180, 160, 90, 120],
'quantity': [64, 56, 50, 62, 30, 40, 36, 22, 18, 24]
})
# 1行でループ全体を置き換えられる!
result = df.groupby('category')['sales'].sum()
print(result)
# category
# Hot 1410
# Iced 750
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
3. split-apply-combineの原理
(1) グループ化プロセスのMermaid図
graph TB
subgraph Split["① Split — カテゴリで分割"]
S1["Hot: Latte 320<br>Americano 280<br>Mocha 350<br>Latte 310<br>Espresso 150"]
S2["Iced: Americano 200<br>Latte 180<br>Mocha 160<br>Espresso 90<br>Latte 120"]
end
subgraph Apply["② Apply — 各グループを集計"]
A1["Hot → 320+280+350<br>+310+150 = 1410"]
A2["Iced → 200+180+160<br>+90+120 = 750"]
end
subgraph Combine["③ Combine — 結果を結合"]
C1["category | sales<br>Hot | 1410<br>Iced | 750"]
end
Split --> Apply --> Combine
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
(2) GroupByオブジェクトは遅延評価される
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:GroupByの遅延評価(難易度 ⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['Hot', 'Hot', 'Iced', 'Iced'],
'sales': [320, 280, 200, 180]
})
# groupbyはGroupByオブジェクトを返す — まだ計算は行われない
grouped = df.groupby('category')
print(type(grouped)) # <class 'pandas.core.groupby.DataFrameGroupBy'>
# 集計関数を呼び出した時点で計算が実行される
print(grouped['sales'].sum())
# グループを確認する
print(grouped.groups) # {'Hot': [0, 1], 'Iced': [2, 3]}
print(grouped.ngroups) # 2
# グループをイテレートする(ほとんど使わない)
for name, group in grouped:
print(f"Group: {name}")
print(group)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
4. よく使う集計関数
(1) 組み込みの集計関数
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:組み込みの集計関数(難易度 ⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['Electronics', 'Electronics', 'Clothing', 'Clothing', 'Home', 'Home'],
'product': ['Laptop', 'Phone', 'Shirt', 'Pants', 'Lamp', 'Chair'],
'price': [999, 699, 45, 65, 30, 120],
'stock': [50, 120, 200, 180, 300, 80],
'rating': [4.7, 4.5, 4.2, 4.0, 3.8, 4.3]
})
# 列ごとに単一の集計
print(df.groupby('category')['price'].mean())
# Electronics 849.0
# Clothing 55.0
# Home 75.0
# 複数の列に同じ集計を適用
print(df.groupby('category')[['price', 'stock']].sum())
# よく使う集計メソッド:
# .sum() .mean() .median() .min() .max()
# .count() .size() .std() .var() .first() .last()
# .nunique() .idxmax() .idxmin()
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
(2) sizeとcountの違い
| メソッド | 計算内容 | NaNの扱い |
|---|---|---|
| size | 各グループの行数(NaNを含む) | NaNを含む |
| count | 各グループのNaN以外の値の数 | NaNを除外 |
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:sizeとcountの違い(難易度 ⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'category': ['A', 'A', 'B', 'B', 'A'],
'value': [1, np.nan, 3, 4, 5]
})
print(df.groupby('category').size())
# category
# A 3 ← 3行(NaNの行を含む)
# B 2
print(df.groupby('category').count())
# value
# category
# A 2 ← NaN以外の値は2つ
# B 2
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
5. aggによる複数集計
(1) 複数の集計関数
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:aggによる複数集計(難易度 ⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['Electronics', 'Electronics', 'Clothing', 'Clothing', 'Home', 'Home'],
'product': ['Laptop', 'Phone', 'Shirt', 'Pants', 'Lamp', 'Chair'],
'price': [999, 699, 45, 65, 30, 120],
'stock': [50, 120, 200, 180, 300, 80],
'rating': [4.7, 4.5, 4.2, 4.0, 3.8, 4.3]
})
# 1つの列に複数の集計を適用
print(df.groupby('category')['price'].agg(['mean', 'min', 'max']))
# mean min max
# category
# Clothing 55.0 45 65
# Electronics 849.0 699 999
# Home 75.0 30 120
# 列ごとに異なる集計を適用
print(df.groupby('category').agg({
'price': ['mean', 'max'],
'stock': 'sum',
'rating': 'mean'
}))
# 名前付き集計(より分かりやすい列名になる)
result = df.groupby('category').agg(
avg_price=('price', 'mean'),
max_price=('price', 'max'),
total_stock=('stock', 'sum'),
avg_rating=('rating', 'mean')
)
print(result)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
(2) カスタム集計関数
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:カスタム集計(難易度 ⭐⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['A', 'A', 'A', 'B', 'B', 'B'],
'value': [10, 20, 30, 100, 200, 300]
})
# カスタム関数:レンジ(最大値 - 最小値)
def value_range(series):
return series.max() - series.min()
result = df.groupby('category')['value'].agg(['mean', value_range])
print(result)
# mean value_range
# category
# A 20.0 20
# B 200.0 200
# agg内でのラムダ式(可読性は下がるが簡潔)
result2 = df.groupby('category')['value'].agg([
('mean', 'mean'),
('range', lambda x: x.max() - x.min()),
('cv', lambda x: x.std() / x.mean()) # 変動係数
])
print(result2)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
6. transformによるグループ内変換
(1) transformは元の形状を保持する
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:transformによるグループ内標準化(難易度 ⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'student': ['Alice', 'Alice', 'Bob', 'Bob', 'Charlie', 'Charlie'],
'subject': ['Math', 'English', 'Math', 'English', 'Math', 'English'],
'score': [85, 90, 92, 88, 78, 82]
})
# 学生ごとのZスコア正規化
df['z_score'] = df.groupby('student')['score'].transform(
lambda x: (x - x.mean()) / x.std()
)
print(df)
# 欠損値をグループ平均で埋める(全体平均ではなく)
df2 = pd.DataFrame({
'category': ['A', 'A', 'A', 'B', 'B'],
'value': [10, 20, None, 100, None]
})
df2['value_filled'] = df2.groupby('category')['value'].transform(
lambda x: x.fillna(x.mean())
)
print(df2)
# グループ内の順位付け
df['rank'] = df.groupby('student')['score'].transform('rank', method='min')
print(df[['student', 'subject', 'score', 'rank']])
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
(2) transformとaggの比較
| 特徴 | agg | transform |
|---|---|---|
| 返り値の形状 | グループごとに1行 | 元のDataFrameと同じ長さ |
| 用途 | サマリーレポート | 元のテーブルへの書き戻し |
| 代表的な操作 | sum/mean/count | 標準化/欠損補完/順位付け |
| ブロードキャスト | ❌ | ✅ 自動的に元の行へブロードキャスト |
7. filterによるグループレベルのフィルタリング
(1) 条件によるグループの保持・除外
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:filterによるグループのフィルタリング(難易度 ⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['Electronics', 'Electronics', 'Electronics',
'Books', 'Books',
'Clothing', 'Clothing', 'Clothing', 'Clothing'],
'product': ['Laptop', 'Phone', 'Tablet', 'Novel', 'Textbook',
'Shirt', 'Pants', 'Jacket', 'Socks'],
'sales': [5000, 3000, 2000, 200, 300, 800, 600, 400, 100]
})
# 売上合計が1000を超えるグループだけを保持
big_categories = df.groupby('category').filter(lambda x: x['sales'].sum() > 1000)
print(big_categories)
# ElectronicsとClothingが保持され、Booksは除外される(200+300=500 < 1000)
# 項目数が3つ以上のグループを保持
large_groups = df.groupby('category').filter(lambda x: len(x) >= 3)
print(large_groups['category'].unique()) # ['Electronics', 'Clothing']
# 売上が4000を超える商品を含むグループを保持
has_top_seller = df.groupby('category').filter(lambda x: x['sales'].max() > 4000)
print(has_top_seller['category'].unique()) # ['Electronics']
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
8. 複数列でのグループ化とas_index
(1) 複数列でのグループ化
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:複数列groupbyの集計(難易度 ⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'region': ['North', 'North', 'North', 'South', 'South', 'South'],
'category': ['Electronics', 'Clothing', 'Electronics',
'Electronics', 'Clothing', 'Clothing'],
'product': ['Laptop', 'Shirt', 'Phone', 'Tablet', 'Pants', 'Jacket'],
'sales': [5000, 800, 3000, 2000, 600, 400]
})
# 複数の列でグループ化
result = df.groupby(['region', 'category'])['sales'].sum()
print(result)
# region category
# North Clothing 800
# Electronics 8000
# South Clothing 1000
# Electronics 2000
# as_index=False → グループ列を通常の列として保持
result2 = df.groupby(['region', 'category'], as_index=False)['sales'].sum()
print(result2)
# region category sales
# 0 North Clothing 800
# 1 North Electronics 8000
# 2 South Clothing 1000
# 3 South Electronics 2000
# sort=False → 最初に出現した順序を保持
result3 = df.groupby(['region', 'category'], sort=False)['sales'].sum()
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
9. 総合例:ECサイトの多次元グループ分析
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
:多次元groupby分析(難易度 ⭐⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# ============================================
# 総合例:agg/transform/filterを使った
# ECサイトの多次元groupby分析
# ============================================
# 1. 売上データの作成
np.random.seed(42)
df = pd.DataFrame({
'region': np.random.choice(['North', 'South', 'East', 'West'], 100),
'category': np.random.choice(['Electronics', 'Clothing', 'Home', 'Sports'], 100),
'product': [f'Item_{i:03d}' for i in range(100)],
'sales': np.round(np.random.uniform(50, 2000, 100), 2),
'quantity': np.random.randint(1, 50, 100),
'discount': np.random.choice([0, 0.1, 0.2, 0.3], 100)
})
# 2. agg:カテゴリごとの複数指標サマリー
summary = df.groupby('category').agg(
total_sales=('sales', 'sum'),
avg_sales=('sales', 'mean'),
max_sales=('sales', 'max'),
order_count=('sales', 'count'),
avg_quantity=('quantity', 'mean')
).round(2)
print("=== カテゴリ別サマリー ===")
print(summary)
# 3. 複数列でのグループ化
region_cat = df.groupby(['region', 'category'], as_index=False).agg(
total_sales=('sales', 'sum'),
avg_discount=('discount', 'mean')
).round(3)
print("\n=== 地域 × カテゴリ ===")
print(region_cat.head(8))
# 4. transform:グループ内の順位付け
df['sales_rank_in_region'] = df.groupby('region')['sales'].transform(
'rank', method='min', ascending=False
)
df['sales_pct_in_category'] = df.groupby('category')['sales'].transform(
lambda x: (x / x.sum() * 100).round(1)
)
print("\n=== North地域の上位3件 ===")
print(df[df['region'] == 'North'].nsmallest(3, 'sales_rank_in_region')
[['product', 'sales', 'sales_rank_in_region']])
# 5. filter:注文数が20件を超えるカテゴリを保持
big_cats = df.groupby('category').filter(lambda x: len(x) > 20)
print(f"\n=== 大規模カテゴリ ===")
print(big_cats['category'].value_counts())
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して試してください。実際の値はpandasのバージョンによって若干異なる場合があります。
❓ よくある質問
Q groupbyは何を返しますか?
A groupbyはGroupByオブジェクト(遅延評価)を返します。すぐには計算は行われません。split-apply-combineのプロセスは、集計関数(sum/mean/aggなど)を呼び出した時点で初めて実行されます。この遅延設計により、中間変数なしで複数の操作をチェーンできます。
Q aggとapplyの違いは何ですか?
A aggは各列に集計関数を適用し、グループごとに1行を返します。applyは各グループに任意の関数を適用し、より柔軟な結果(DataFrameを返すことも可能)を返します。単純な集計にはagg(高速で明示的)を使い、複雑なグループ内計算にはapplyを使ってください。aggは名前付き集計(name=(col, func))もサポートしており、列名をよりきれいにできます。
Q transformは形状を保持しますか?
A はい。transformは元のDataFrameと同じ長さの結果を返し、各グループの結果を元の行へブロードキャストします。代表的な用途は、グループ内標準化(zスコア)、グループ平均による欠損値の補完、グループ内の順位付けです。結果の長さがグループのサイズと一致しない場合はエラーが発生します。
Q filterは行を削除しますか、それともグループを削除しますか?
A filterはグループ全体を削除します。グループの集計結果が条件を満たさない場合、そのグループのすべての行が除外されます。グループ内の個別の行をフィルタリングするわけではありません!これはブールインデックスとは異なります。ブールインデックスは行レベルの条件でフィルタリングしますが、filterはグループレベルの条件で動作します。
Q as_index=Falseは何をしますか?
A デフォルトでは、groupbyはグループ化に使う列をインデックス(通常の列ではない)にします。as_index=Falseを使うと、グループ列を通常の列として保持でき、MultiIndexを持つSeriesではなく、クリーンなDataFrameが得られます。後でグループ列を操作する必要がある場合(mergeなど)は、as_index=Falseの方が便利です。
Q 複数列groupbyの結果はどう読みますか?
A 複数列でのグループ化はMultiIndex(階層インデックス)を生成します。特定のグループを選択するには
result.loc[('North', 'Electronics')]を使うか、as_index=Falseを使って階層インデックス自体を避けることができます。reset_index()を使えば、階層インデックスを通常の列にフラット化することもできます。Q groupbyのsortは処理を遅くしますか?
A sort=True(デフォルト)はグループ化キーをソートするため、わずかなオーバーヘッドが発生します。カーディナリティの高いグループ化キーを持つ大規模なDataFrameでは、sort=Falseで高速化できます。ただし、ソートされた結果の方が読みやすいです。推奨:小規模データセットではsort=True(可読性優先)、超大規模データセットではsort=False(性能優先)を使ってください。
📖 まとめ
- groupbyの核心はsplit-apply-combine(分割・適用・結合)です。分割して、計算して、結合します。
- GroupByオブジェクトは遅延評価されます。集計関数が呼び出された時点で初めて計算が行われます。
- 組み込みの集計関数:sum/mean/count/size/min/max/std/nuniqueなど。
- aggは複数集計や列ごとの異なる集計をサポートします。名前付き集計の構文が最もクリーンです。
- transformは同じ長さの結果を返すため、標準化/欠損補完/順位付けに最適です。
- filterはグループレベルの条件で選択し、グループ全体を保持または除外します。
- 複数列でのグループ化はMultiIndexを生成します。階層インデックスを避けるにはas_index=Falseを使います。
📝 練習問題
- 基礎(難易度 ⭐):売上のDataFrame(region/category/sales)を作成してください。groupby + sum/mean/countを使って3種類の異なる集計を行い、結果を比較してください。
- 応用(難易度 ⭐⭐):学生の成績表を作成してください。aggを使って各科目のmean/max/minを計算し、transformを使って学生ごとのzスコア正規化成績を算出してください。
- 挑戦(難易度 ⭐⭐⭐):ECの注文データ50行(region/category/sales/quantity/discount)をシミュレートしてください。次のパイプラインを完成させてください。aggによる複数指標サマリー → 複数列groupby(region+category) → transformによるグループ内順位付け → filterで大規模グループを保持 → 総合レポートの作成。



