Pandas GroupBy

GroupBy 是 Pandas 中非常強大的功能,可以將資料按照某個欄位分組,然後對每個群組進行聚合計算。這類似於 SQL 的 GROUP BY 語法。

GroupBy 的概念

GroupBy 操作包含三個步驟:

  1. Split(分割):根據某個條件將資料分成多個群組
  2. Apply(應用):對每個群組套用函數(如 sum、mean)
  3. Combine(合併):將結果合併成新的資料結構

基本用法

import pandas as pd

df = pd.DataFrame({
    'city': ['Taipei', 'Tokyo', 'Taipei', 'Tokyo', 'Seoul'],
    'year': [2023, 2023, 2024, 2024, 2024],
    'sales': [100, 200, 150, 250, 180]
})

# 按 city 分組,計算 sales 總和
result = df.groupby('city')['sales'].sum()
print(result)
city
Seoul     180
Taipei    250
Tokyo     450
Name: sales, dtype: int64

多欄位分組

# 按 city 和 year 分組
result = df.groupby(['city', 'year'])['sales'].sum()
print(result)
city    year
Seoul   2024    180
Taipei  2023    100
        2024    150
Tokyo   2023    200
        2024    250
Name: sales, dtype: int64
# 轉換成 DataFrame
result = df.groupby(['city', 'year'])['sales'].sum().reset_index()

常見聚合方法

grouped = df.groupby('city')['sales']

grouped.sum()      # 總和
grouped.mean()     # 平均
grouped.median()   # 中位數
grouped.min()      # 最小值
grouped.max()      # 最大值
grouped.count()    # 計數
grouped.std()      # 標準差
grouped.var()      # 變異數
grouped.first()    # 第一個值
grouped.last()     # 最後一個值

對多個欄位聚合

df = pd.DataFrame({
    'city': ['Taipei', 'Tokyo', 'Taipei', 'Tokyo'],
    'sales': [100, 200, 150, 250],
    'profit': [10, 20, 15, 25]
})

# 對多個欄位計算
result = df.groupby('city')[['sales', 'profit']].sum()
print(result)
        sales  profit
city                 
Taipei    250      25
Tokyo     450      45

agg():多種聚合

使用 agg() 可以同時計算多種統計值:

# 對同一欄位計算多種統計
result = df.groupby('city')['sales'].agg(['sum', 'mean', 'max'])
print(result)
        sum   mean  max
city                   
Taipei  250  125.0  150
Tokyo   450  225.0  250

對不同欄位用不同聚合

result = df.groupby('city').agg({
    'sales': 'sum',
    'profit': 'mean'
})
print(result)

自訂聚合名稱

result = df.groupby('city').agg(
    total_sales=('sales', 'sum'),
    avg_profit=('profit', 'mean'),
    max_sales=('sales', 'max')
)
print(result)

自訂聚合函數

# 使用 lambda
result = df.groupby('city')['sales'].agg(lambda x: x.max() - x.min())

# 使用自訂函數
def range_func(x):
    return x.max() - x.min()

result = df.groupby('city')['sales'].agg(range_func)

取得 GroupBy 物件

grouped = df.groupby('city')

# 查看群組
print(grouped.groups)
# {'Taipei': [0, 2], 'Tokyo': [1, 3]}

# 取得特定群組
print(grouped.get_group('Taipei'))

# 遍歷群組
for name, group in grouped:
    print(f'Group: {name}')
    print(group)
    print()

群組數量

# 每個群組的筆數
result = df.groupby('city').size()

# 或使用 count(會排除 NaN)
result = df.groupby('city').count()

# 群組的數量
print(df.groupby('city').ngroups)  # 2

transform()

transform() 會回傳與原始資料相同長度的結果:

df = pd.DataFrame({
    'city': ['Taipei', 'Tokyo', 'Taipei', 'Tokyo'],
    'sales': [100, 200, 150, 250]
})

# 計算每個城市的平均,並對應回每一列
df['city_avg'] = df.groupby('city')['sales'].transform('mean')
print(df)
     city  sales  city_avg
0  Taipei    100     125.0
1   Tokyo    200     225.0
2  Taipei    150     125.0
3   Tokyo    250     225.0

transform 的應用

# 計算與群組平均的差距
df['diff_from_avg'] = df['sales'] - df.groupby('city')['sales'].transform('mean')

# 群組內的百分比
df['pct_of_city'] = df['sales'] / df.groupby('city')['sales'].transform('sum')

# 群組內的排名
df['rank_in_city'] = df.groupby('city')['sales'].transform('rank')

filter()

根據群組條件過濾:

df = pd.DataFrame({
    'city': ['Taipei', 'Tokyo', 'Seoul', 'Tokyo', 'Taipei'],
    'sales': [100, 200, 50, 150, 120]
})

# 只保留 sales 總和大於 200 的城市
result = df.groupby('city').filter(lambda x: x['sales'].sum() > 200)
print(result)

常見應用

計算佔比

df = pd.DataFrame({
    'category': ['A', 'A', 'B', 'B'],
    'value': [10, 20, 30, 40]
})

# 計算每個類別的佔比
df['category_total'] = df.groupby('category')['value'].transform('sum')
df['pct'] = df['value'] / df['category_total']

填補缺失值

# 用群組的平均值填補缺失
df['value'] = df.groupby('category')['value'].transform(lambda x: x.fillna(x.mean()))

取每組的前 N 名

df = pd.DataFrame({
    'city': ['Taipei', 'Taipei', 'Taipei', 'Tokyo', 'Tokyo'],
    'sales': [100, 200, 150, 300, 250]
})

# 每個城市 sales 最高的 2 筆
top2 = df.groupby('city').apply(lambda x: x.nlargest(2, 'sales')).reset_index(drop=True)