404 Not Found

404 Not Found


nginx

【Pandas】groupby集計入門|分割・適用・結合とagg/transform/filterを徹底解説

groupby集計はデータ分析の心臓部です。地域別の売上確認、カテゴリ別の平均価格の計算、月別の注文数の集計など、これらはすべて「分割して、計算して、結合する」に帰着します。Pandasのgroupbyは、古典的なsplit-apply-combine(分割・適用・結合)パラダイムを実装しています。本レッスンでは、Mermaid図を使ってそのプロセスを完全に理解し、agg / transform / filterという3つの重要な操作をマスターします。

⚠️ 注意: 以下のコードを実行するには、ローカルのPython環境が必要です。

1. 学習内容



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図

100%
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(性能優先)を使ってください。

📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):売上のDataFrame(region/category/sales)を作成してください。groupby + sum/mean/countを使って3種類の異なる集計を行い、結果を比較してください。
  2. 応用(難易度 ⭐⭐):学生の成績表を作成してください。aggを使って各科目のmean/max/minを計算し、transformを使って学生ごとのzスコア正規化成績を算出してください。
  3. 挑戦(難易度 ⭐⭐⭐):ECの注文データ50行(region/category/sales/quantity/discount)をシミュレートしてください。次のパイプラインを完成させてください。aggによる複数指標サマリー → 複数列groupby(region+category) → transformによるグループ内順位付け → filterで大規模グループを保持 → 総合レポートの作成。

← 前へ:データ変換 · 次へ:マージと結合 →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%