Python pandasのgroupby完全解説|業務で使える実装パターン集

Python

Python pandasのgroupby完全解説|業務で使える実装パターン集

データ分析業務において、pandas の groupby は最頻出のメソッドの一つです。しかし、基本的な使い方だけでは実務で求められる複雑な集計に対応できません。本記事では、実際のビジネスシーンで役立つ groupby のパターンを、実装コード付きで詳しく解説します。

groupbyの基本的な考え方

groupby は、データフレームを特定のカラムで分類し、各グループに対して同じ処理を適用するメソッドです。SQL の GROUP BY と同じ概念ですが、pandas ではより柔軟に複数の集計を同時に実行できます。

基本的な流れは「分割 → 適用 → 結合」の3ステップです。

import pandas as pd
import numpy as np

# サンプルデータの作成
df = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=10),
    'department': ['営業', '営業', 'IT', 'IT', '営業', '企画', 'IT', '企画', '営業', '企画'],
    'sales': [150000, 200000, 50000, 75000, 180000, 120000, 80000, 95000, 210000, 110000],
    'employee_id': [1, 2, 3, 4, 1, 5, 3, 5, 2, 4]
})

# 基本的なgroupby
print(df.groupby('department')['sales'].sum())

実務で頻出するユースケース

ユースケース1:複数カラムでの集計(営業成績管理)

営業部門では、部門別・従業員別の売上を同時に確認する必要があります。複数のカラムで groupby を実行し、複数の集計関数を適用するパターンです。

import pandas as pd
import numpy as np

# より実務的なデータセット
sales_data = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=100),
    'department': np.random.choice(['営業', 'IT', '企画'], 100),
    'employee_id': np.random.choice([1, 2, 3, 4, 5], 100),
    'sales': np.random.randint(50000, 300000, 100),
    'costs': np.random.randint(10000, 100000, 100)
})

# 複数カラムでのgroupby
result = sales_data.groupby(['department', 'employee_id']).agg({
    'sales': ['sum', 'mean', 'count'],
    'costs': 'sum'
}).round(2)

print(result)

ユースケース2:日単位での累積売上追跡

マネージメント層は、各日付での売上合計と累積売上を確認します。このような時系列データの集計は業務で非常に重要です。

import pandas as pd

# 日付ごとの集計
daily_sales = sales_data.groupby('date').agg({
    'sales': 'sum',
    'employee_id': 'count'  # 取引件数
}).rename(columns={'employee_id': 'transaction_count'})

# 累積売上の追加
daily_sales['cumulative_sales'] = daily_sales['sales'].cumsum()

print(daily_sales.head(10))

実装コード:実務パターン集

パターン1:条件付き集計

特定条件下での売上のみを集計したい場合があります。

import pandas as pd

# 条件付き集計:50万円以上の売上のみ対象
high_value_sales = sales_data[sales_data['sales'] >= 150000].groupby('department')['sales'].agg(['sum', 'count'])
print(high_value_sales)

# または別の方法:apply を使った複雑な条件
def calc_high_value_ratio(group):
    high_value = (group['sales'] >= 150000).sum()
    total = len(group)
    return high_value / total if total > 0 else 0

ratio_result = sales_data.groupby('department').apply(calc_high_value_ratio)
print(ratio_result)

パターン2:複数の集計結果を別々のカラムに展開

実務では、集計結果を見やすい形に加工する必要があります。

import pandas as pd

# agg と rename を組み合わせた実装
result = sales_data.groupby('department').agg({
    'sales': ['sum', 'mean', 'std'],
    'costs': 'sum'
})

# マルチレベルインデックスをフラット化
result.columns = ['_'.join(col).strip() for col in result.columns.values]
result = result.reset_index()

print(result)

パターン3:グループ内での順位付け

各部門内で従業員を売上ランク付けするシーンは日常茶飯事です。

import pandas as pd

# グループ内ランク付け
sales_data['dept_rank'] = sales_data.groupby('department')['sales'].rank(ascending=False, method='dense')

# グループ内で売上がトップのレコードを抽出
top_per_dept = sales_data.loc[sales_data.groupby('department')['sales'].idxmax()]

print(top_per_dept[['date', 'department', 'employee_id', 'sales']])

パターン4:前月比・前年比計算

ビジネスでは時系列での成長率追跡が欠かせません。

import pandas as pd

#月別売上データの作成
sales_data['year_month'] = sales_data['date'].dt.to_period('M')

monthly_sales = sales_data.groupby('year_month')['sales'].sum().reset_index()
monthly_sales.columns = ['year_month', 'sales']

# 前月比を計算
monthly_sales['prev_month_sales'] = monthly_sales['sales'].shift(1)
monthly_sales['mom_growth_rate'] = (
    (monthly_sales['sales'] - monthly_sales['prev_month_sales']) / 
    monthly_sales['prev_month_sales'] * 100
).round(2)

print(monthly_sales)

パターン5:複数グループでの条件判定

各グループが特定条件を満たしているかを判定するケースです。

import pandas as pd

# 各部門の平均売上を計算
dept_avg_sales = sales_data.groupby('department')['sales'].mean()

# 部門平均以上の従業員を抽出
sales_data['above_dept_average'] = sales_data.apply(
    lambda row: row['sales'] >= dept_avg_sales[row['department']], 
    axis=1
)

above_average = sales_data[sales_data['above_dept_average']].groupby('department').size()
print(above_average)

よくある応用パターン

応用パターン1:groupby + merge による部門情報の追加

集計結果に部門マスタ情報を追加する実務パターンです。

import pandas as pd

# 部門マスタ
dept_master = pd.DataFrame({
    'department': ['営業', 'IT', '企画'],
    'dept_code': ['SALES', 'IT', 'PLAN'],
    'manager': ['田中', '佐藤', '鈴木']
})

# 集計結果
agg_result = sales_data.groupby('department').agg({
    'sales': 'sum',
    'employee_id': 'nunique'
}).reset_index()

# マージして情報追加
final_result = agg_result.merge(dept_master, on='department', how='left')
print(final_result)

応用パターン2:transform による各レコードへのグループ統計値追加

元のデータフレームのサイズを保ったまま、グループ統計を追加します。

import pandas as pd

# グループ平均を各行に追加
sales_data['dept_avg_sales'] = sales_data.groupby('department')['sales'].transform('mean')

# 部門内での売上割合を計算
sales_data['sales_ratio_in_dept'] = (
    sales_data['sales'] / 
    sales_data.groupby('department')['sales'].transform('sum') * 100
).round(2)

print(sales_data[['date', 'department', 'sales', 'dept_avg_sales', 'sales_ratio_in_dept']].head(10))

応用パターン3:groupby + filter で特定条件のグループを絞り込み

特定条件を満たすグループのみを抽出するパターンです。

import pandas as pd

# 売上合計が100万円以上の部門のみを抽出
large_depts = sales_data.groupby('department').filter(
    lambda x: x['sales'].sum() >= 1000000
)

print(f\"対象レコード数: {len(large_depts)}\")
print(large_depts.groupby('department')['sales'].sum())

応用パターン4:複雑な集計を関数で定義

実務では複数の計算を組み合わせた複雑な集計が必要になります。

import pandas as pd

def calculate_sales_metrics(group):
    return pd.Series({
        'total_sales': group['sales'].sum(),
        'avg_sales': group['sales'].mean(),
        'std_sales': group['sales'].std(),
        'max_sales': group['sales'].max(),
        'transaction_count': len(group),
        'avg_cost_ratio': (group['costs'].sum() / group['sales'].sum() * 100).round(2)
    })

metrics = sales_data.groupby('department').apply(calculate_sales_metrics)
print(metrics)

注意点と落とし穴

注意点1:NaN値の扱い

groupby はデフォルトで NaN を除外します。これが予期しない結果を招くことがあります。

import pandas as pd
import numpy as np

# NaN値を含むデータ
df_with_nan = pd.DataFrame({
    'group': ['A', 'A', 'B', 'B', 'A', None],
    'value': [10, 20, 30, 40, 50, 60]
})

# NaN グループは集計から除外される
result = df_with_nan.groupby('group', dropna=False)['value'].sum()
print(result)  # NaN グループも含まれる

注意点2:メモリ効率

大規模データセットで groupby を多用する場合、メモリ使用量に注意が必要です。不要な中間結果は削除しましょう。

import pandas as pd

# 大規模データの集計(メモリ効率的な書き方)
# 不要なカラムを事前に削除
slim_data = sales_data[['department', 'sales']].copy()

# メモリ効率的な集計
result = slim_data.groupby('department')['sales'].agg(['sum', 'mean']).reset_index()

# 不要になったら明示的に削除
del slim_data

注意点3:パフォーマンス

複数の groupby 操作を組み合わせる場合、単一の groupby で複数集計する方が高速です。

import pandas as pd
import time

# 非効率:複数回の groupby
start = time.time()
sum1 = sales_data.groupby('department')['sales'].sum()
mean1 = sales_data.groupby('department')['sales'].mean()
inefficient_time = time.time() - start

# 効率的:単一の groupby で複数集計
start = time.time()
result = sales_data.groupby('department')['sales'].agg(['sum', 'mean'])
efficient_time = time.time() - start

print(f\"非効率: {inefficient_time:.4f}秒\")
print(f\"効率的: {efficient_time:.4f}秒\")

注意点4:カテゴリカルデータの型

groupby 対象カラムのデータ型が正しくないと、予期しない結果になります。

import pandas as pd

# 日付の型を確認・修正
sales_data['date'] = pd.to_datetime(sales_data['date'])

# 部門コードが数値で保存されている場合は文字列に
sales_data['department'] = sales_data['department'].astype('category')

# カテゴリに順序を付ける
sales_data['department'] = sales_data['department'].cat.set_categories(
    ['IT', '企画', '営業'],
    ordered=True
)

result = sales_data.groupby('department', observed=True)['sales'].sum()
print(result)

実践的なビジネス例

売上レポート自動生成スクリプト

上記のパターンを組み合わせた実務的な例です。

import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# 3ヶ月分のサンプルデータ生成
dates = pd.date_range('2024-01-01', periods=200)
sales_data = pd.DataFrame({
    'date': dates,
    'department': np.random.choice(['営業', 'IT', '企画'], 200),
    'employee_id': np.random.choice(range(1, 11), 200),
    'sales': np.random.randint(50000, 300000, 200),
    'costs': np.random.randint(10000, 100000, 200)
})

# 月別レポート作成
sales_data['year_month'] = sales_data['date'].dt.to_period('M')

monthly_report = sales_data.groupby(['year_month', 'department']).agg({
    'sales': ['sum', 'mean'],
    'costs': 'sum',
    'employee_id': 'nunique'
}).round(0)

monthly_report.columns = ['total_sales', 'avg_sales', 'total_costs', 'unique_employees']
monthly_report = monthly_report.reset_index()

# 利益率を追加
monthly_report['profit'] = (monthly_report['total_sales'] - monthly_report['total_costs']).round(0)
monthly_report['profit_margin'] = (
    (monthly_report['profit'] / monthly_report['total_sales'] * 100).round(2)
)

print(monthly_report)

まとめ

pandas の groupby は、データ分析業務の中核を担うメソッドです。本記事で解説した5つの基本パターンと4つの応用パターンを習得することで、ほとんどの実務集計に対応できます。

重要なポイント:

  • 複数カラムでの groupby と agg の組み合わせは最頻出パターン
  • transform と filter で元データの形を保ったまま処理する
  • apply で複雑な計算を実装する際は、関数として定義すると可読性が上がる
  • NaN 値、メモリ、パフォーマンスに注意を払う
  • 時系列データやランク付けなどの典型的なユースケースに対応できる

これらの知識を活用することで、Excel では対応できない大規模・複雑なデータ分析を効率的に実行できるようになります。実務での groupby 習得は、データ分析スキル向上の第一歩です。

タイトルとURLをコピーしました