Pandasを利用したデータ分析において、特定のグループごとに集計を行う「groupby」は非常に頻繁に活用される機能です。
しかし、単に集計値を計算するだけでなく、その集計結果に基づいて特定の条件を満たすグループのみを抽出したいという場面も少なくありません。
例えば、「売上の合計が100万円以上の店舗データだけを残したい」といった処理がこれに該当します。
このようなニーズを効率的に満たしてくれるのが、Pandasのgroupbyオブジェクトに用意されているfilterメソッドです。
本記事では、このfilterメソッドの基本的な使い方から応用的なフィルタリング手法までを詳しく解説します。
Pandasのgroupbyにおけるフィルタリングの基本
Pandasのgroupbyは、データを特定のキーに基づいて分割し、各グループに対して計算を適用するプロセス(Split-Apply-Combine)を容易にします。
通常、sum()やmean()などの集計関数を使用すると、結果はグループごとの集計値に縮約されます。
しかし、実際の分析実務では、特定の条件を満たしたグループに属する元の行データをそのまま保持したい場合があります。
このような「グループ単位の判定結果に基づいて元の行を抽出する」という操作を実現するのがfilter()メソッドです。
filterメソッドの基本的な動作原理
filter()メソッドは、引数として関数(通常はラムダ式)を受け取ります。
この関数は各グループ(DataFrameまたはSeriesのサブセット)に対して適用され、その結果がTrueであればそのグループのすべての行が保持され、Falseであれば削除されます。
結果として得られるデータは、元のDataFrameと同じカラム構造を持つサブセットとなります。
実践:filterメソッドを用いた具体的なデータ抽出例
具体的なサンプルデータを用いて、filter()メソッドの記述方法を確認していきましょう。
まずは、商品の販売ログを模したデータを作成します。
import pandas as pd
# サンプルデータの作成
df = pd.DataFrame({
'store': ['A', 'A', 'A', 'B', 'B', 'C', 'C', 'C'],
'item': ['Apple', 'Orange', 'Banana', 'Apple', 'Banana', 'Apple', 'Orange', 'Banana'],
'sales': [100, 150, 200, 50, 80, 300, 400, 500]
})
print(df)
store item sales
0 A Apple 100
1 A Orange 150
2 A Banana 200
3 B Apple 50
4 B Banana 80
5 C Apple 300
6 C Orange 400
7 C Banana 500
例1:グループ内の平均値によるフィルタリング
次に、店舗(store)ごとの平均売上が150を超える店舗のデータだけを抽出してみます。
# 平均売上が150を超えるグループ(店舗)のみを抽出
filtered_df = df.groupby('store').filter(lambda x: x['sales'].mean() > 150)
print(filtered_df)
store item sales
0 A Apple 100
1 A Orange 150
2 A Banana 200
5 C Apple 300
6 C Orange 400
7 C Banana 500
店舗Bの平均売上は65であるため、店舗Bに紐付く行がすべて除外されていることがわかります。
このように、グループ全体の統計量に基づいて行を絞り込めるのが最大の特徴です。
例2:データ件数(サイズ)によるフィルタリング
次に、取引件数(データの行数)が3件以上の店舗のみを抽出する例を見てみましょう。
# データ件数が3件以上のグループを抽出
size_filtered_df = df.groupby('store').filter(lambda x: len(x) >= 3)
print(size_filtered_df)
store item sales
0 A Apple 100
1 A Orange 150
2 A Banana 200
5 C Apple 300
6 C Orange 400
7 C Banana 500
この手法は、データが十分に集まっているカテゴリのみを分析対象としたい場合に非常に便利です。
応用:複数条件や外部変数の利用
filterメソッド内では、より複雑なロジックを組み込むことも可能です。
ラムダ式内での複雑な条件判定
「合計売上が500以上」かつ「1つの商品で300以上の売上がある」という条件でグループをフィルタリングしてみましょう。
# 複数条件を組み合わせたフィルタリング
complex_filtered = df.groupby('store').filter(lambda x: (x['sales'].sum() >= 500) and (x['sales'].max() >= 300))
print(complex_filtered)
store item sales
5 C Apple 300
6 C Orange 400
7 C Banana 500
店舗Aは合計売上は450であり条件を満たさず、店舗Cのみが抽出されました。
定義済み関数の利用
ラムダ式が長くなりすぎる場合は、あらかじめ関数を定義しておくことでコードの可読性を高めることができます。
def my_filter(group):
# 特定の商品が含まれているか、かつ平均売上が高いか
has_orange = 'Orange' in group['item'].values
high_average = group['sales'].mean() > 100
return has_orange and high_average
custom_filtered = df.groupby('store').filter(my_filter)
print(custom_filtered)
このように関数を分離することで、デバッグが容易になり、複雑なビジネスロジックにも対応しやすくなります。
filterメソッドと他の手法の比較
groupbyを活用したデータ抽出には、他にもtransform()やapply()、あるいはブールインデックスを用いた方法があります。
それぞれの違いを理解し、適切な場面で使い分けることが重要です。
| 手法 | 主な用途 | 返り値の形式 |
|---|---|---|
| filter | 条件を満たすグループの行をそのまま抽出 | 元のDataFrameの一部 |
| transform | グループ統計量を各行に展開し、計算に利用 | 元のDataFrameと同じ長さのSeries/DataFrame |
| apply | グループごとに自由な処理を行い、再構築 | 柔軟(集計値や変形されたDataFrameなど) |
filter vs transform
transform()を使用した場合も、フィルタリング自体は可能です。
例えば、df[df.groupby('store')['sales'].transform('mean') > 150]と記述すれば、filterメソッドと同じ結果が得られます。
しかし、「グループ全体を一つの単位として評価する」という意図を明確にする上では、filter()の方がコードの意図が伝わりやすくなります。
パフォーマンスとエラー回避のポイント
大規模なデータセットに対してgroupby.filterを適用する際には、いくつか注意すべき点があります。
1. 処理速度の考慮
filter()メソッド内で複雑なPythonの関数を実行すると、データのグループ数が多い場合に処理が遅くなる可能性があります。
可能な限り、x['sales'].mean()のように、Pandas内部で最適化されたベクトル化演算を使用するようにしましょう。
2. メモリ使用量
filter()は条件に一致するデータを新しくコピーして生成するため、メモリが限られている環境では注意が必要です。
不要になった元のDataFrameを削除する、あるいはインプレースに近い操作が必要な場合は別の検討が必要になるかもしれません。
3. NaN(欠損値)の扱い
グループ内に欠損値が含まれている場合、集計結果がNaNになることがあります。
ラムダ式の結果が不確定にならないよう、dropna()やfillna()を適切に組み合わせて使用することをお勧めします。
実務での活用シーン:異常値グループの除外
最後に、より実務に近い例として、ログデータから「極端にデータが少ないユーザー」を除外するケースを考えます。
# ユーザーごとの行動ログ
logs = pd.DataFrame({
'user_id': [1, 1, 1, 2, 3, 3, 4, 4, 4, 4],
'action': ['login', 'view', 'logout', 'login', 'login', 'view', 'login', 'view', 'click', 'logout']
})
# 行動回数が3回以上の活発なユーザーのみを抽出
active_users = logs.groupby('user_id').filter(lambda x: len(x) >= 3)
print(active_users)
このように、分析のノイズとなる少量のデータを一括で排除する際に、groupby.filterは無類の強さを発揮します。
まとめ
Pandasのgroupby().filter()は、特定のグループ単位で条件判定を行い、該当する元のデータを抽出するための非常に強力なメソッドです。
「集計値を求めるだけではなく、集計結果に基づいて行を絞り込みたい」という状況において、直感的かつ簡潔なコードを記述できます。
ラムダ式やカスタム関数を組み合わせることで、複雑な条件にも柔軟に対応可能です。
ぜひ日々のデータ分析業務に取り入れ、より高度なデータ加工を実現してください。
