目次
順序
最近パンダを勉強しています。データを処理するときに、データの特定のフィールドをグループ化して分析する必要があることがよくあります。そのためには、groupby 関数を使用する必要があります。この記事では詳細に記録します。
パンダ バージョン 1.4.3
Pandas の groupby 関数は、まず対象のフィールドに従って DataFrame または Series を分割し、同じ属性をグループに分割してから、分割されたグループに対して対応する変換操作を実行し、最後に要約変換後に各グループの結果を返します。
1. 基本的な使い方
デモンストレーションのために最初にいくつかのデータを初期化します
import pandas as pd
df = pd.DataFrame({
'name': ['香蕉', '菠菜', '糯米', '糙米', '丝瓜', '冬瓜', '柑橘', '苹果', '橄榄油'],
'category': ['水果', '蔬菜', '米面', '米面', '蔬菜', '蔬菜', '水果', '水果', '粮油'],
'price': [3.5, 6, 2.8, 9, 3, 2.5, 3.2, 8, 18],
'count': [2, 1, 3, 6, 4, 8, 5, 3, 2]
})
カテゴリ別にグループ化する
grouped = df.groupby('category')
print(type(grouped))
print(grouped)
出力結果
<class 'pandas.core.groupby.generic.DataFrameGroupBy'>
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x127112df0>
グループ化の種類はDataFrameGroupByで、直接出力してみますが、printはメモリアドレスなのであまり直感的ではありませんが、それを表示する関数です(書き方の原理は後ほど紹介します)
def view_group(the_pd_group):
for name, group in the_pd_group:
print(f'group name: {
name}')
print('-' * 30)
print(group)
print('=' * 30, '\n')
view_group(grouped)
出力結果
group name: 水果
------------------------------
name category price count
0 香蕉 水果 3.5 2
6 柑橘 水果 3.2 5
7 苹果 水果 8.0 3
==============================
group name: 米面
------------------------------
name category price count
2 糯米 米面 2.8 3
3 糙米 米面 9.0 6
==============================
group name: 粮油
------------------------------
name category price count
8 橄榄油 粮油 18.0 2
==============================
group name: 蔬菜
------------------------------
name category price count
1 菠菜 蔬菜 6.0 1
4 丝瓜 蔬菜 3.0 4
5 冬瓜 蔬菜 2.5 8
==============================
2. パラメータのソースコードの解析
次に、ソースコード内のDataFrame の groupby を定義するメソッドを確認します。
def groupby(
self,
by=None,
axis: Axis = 0,
level: Level | None = None,
as_index: bool = True,
sort: bool = True,
group_keys: bool = True,
squeeze: bool | lib.NoDefault = no_default,
observed: bool = False,
dropna: bool = True,
) -> DataFrameGroupBy:
pass
シリーズのグループ化
def groupby(
self,
by=None,
axis=0,
level=None,
as_index: bool = True,
sort: bool = True,
group_keys: bool = True,
squeeze: bool | lib.NoDefault = no_default,
observed: bool = False,
dropna: bool = True,
) -> SeriesGroupBy:
pass
Series の groupby 関数の動作は DataFrame の動作と似ていますが、この記事では例として DataFrame のみを使用します。
入参
に
基本的な使い方の書き方を思い出しましょう
grouped = df.groupby('category')
ここを通りましたカテゴリーこれは最初のパラメータ by であり、グループ化の基準を示します。公式ドキュメントによると、 by はマッピング、関数、ラベル、ラベルのリストのいずれかになります。使用されるラベルは次のとおりです。つまり、次のようにすることもできます以下のように書きます
- ラベルリスト
grouped = df.groupby(['category'])
- マッピング方法は、果物と野菜を大きなグループに分割する
DataFrame のインデックスに従ってマッピングする必要があります。野菜と果物、ビーフンと穀物と油は大きく分けられます。米粉、穀物、油
category_dict = {
'水果': '蔬菜水果', '蔬菜': '蔬菜水果', '米面': '米面粮油', '粮油': '米面粮油'}
the_map = {
}
for i in range(len(df.index)):
the_map[i] = category_dict[df.iloc[i]['category']]
grouped = df.groupby(the_map)
view_group(grouped)
出力は次のとおりです
group name: 米面粮油
------------------------------
name category price count
2 糯米 米面 2.8 3
3 糙米 米面 9.0 6
8 橄榄油 粮油 18.0 2
==============================
group name: 蔬菜水果
------------------------------
name category price count
0 香蕉 水果 3.5 2
1 菠菜 蔬菜 6.0 1
4 丝瓜 蔬菜 3.0 4
5 冬瓜 蔬菜 2.5 8
6 柑橘 水果 3.2 5
7 苹果 水果 8.0 3
==============================
- このモードのfunctionでは
、カスタム関数の入力パラメーターは DataFrame のインデックスでもあり、出力結果はマッピングの例と同じになります。
category_dict = {
'水果': '蔬菜水果', '蔬菜': '蔬菜水果', '米面': '米面粮油', '粮油': '米面粮油'}
def to_big_category(the_idx):
return category_dict[df.iloc[the_idx]['category']]
grouped = df.groupby(to_big_category)
view_group(grouped)
軸
axis は、グループ化のセグメンテーションの基準として使用される軸を示します。 0 - Index
と同等、行ごとに分割することを意味します、デフォルト1 - columnsと同等、列ごとに分割することを意味します
列ごとに分割する例を次に示します。
def group_columns(column_name: str):
if column_name in ['name', 'category']:
return 'Group 1'
else:
return 'Group 2'
# 等价写法 grouped = df.head(3).groupby(group_columns, axis='columns')
grouped = df.head(3).groupby(group_columns, axis=1)
view_group(grouped)
出力は次のとおりです
group name: Group 1
------------------------------
name category
0 香蕉 水果
1 菠菜 蔬菜
2 糯米 米面
==============================
group name: Group 2
------------------------------
price count
0 3.5 2
1 6.0 1
2 2.8 3
==============================
表を縦に切るのと同じで、左半分がグループ1、右半分がグループ2になります。
レベル
軸が MultiIndex (階層構造) の場合、特定のレベルでグループ化されます。ここでのレベルは 0 から始まる int 型です。0 は最初の層を意味します。
MultiIndex を使用して別のテスト データ セットを構築する
the_arrays = [['A', 'A', 'A', 'B', 'A', 'A', 'A', 'B', 'A', 'A'],
['蔬菜水果', '蔬菜水果', '米面粮油', '休闲食品', '米面粮油', '蔬菜水果', '蔬菜水果', '休闲食品', '蔬菜水果', '米面粮油'],
['水果', '蔬菜', '米面', '糖果', '米面', '蔬菜', '蔬菜', '饼干', '水果', '粮油']]
the_index = pd.MultiIndex.from_arrays(arrays=the_arrays, names=['one ', 'two', 'three'])
df_2 = pd.DataFrame(data=[3.5, 6, 2.8, 4, 9, 3, 2.5, 3.2, 8, 18], index=the_index, columns=['price'])
print(df_2)
出力は次のとおりです
price
one two three
A 蔬菜水果 水果 3.5
蔬菜 6.0
米面粮油 米面 2.8
B 休闲食品 糖果 4.0
A 米面粮油 米面 9.0
蔬菜水果 蔬菜 3.0
蔬菜 2.5
B 休闲食品 饼干 3.2
A 蔬菜水果 水果 8.0
米面粮油 粮油 18.0
1. 階層 3 ごとにグループ化する
grouped = df_2.groupby(level=2)
view_group(grouped)
出力は次のとおりです
group name: 水果
------------------------------
price
one two three
A 蔬菜水果 水果 3.5
水果 8.0
==============================
group name: 米面
------------------------------
price
one two three
A 米面粮油 米面 2.8
米面 9.0
==============================
group name: 粮油
------------------------------
price
one two three
A 米面粮油 粮油 18.0
==============================
group name: 糖果
------------------------------
price
one two three
B 休闲食品 糖果 4.0
==============================
group name: 蔬菜
------------------------------
price
one two three
A 蔬菜水果 蔬菜 6.0
蔬菜 3.0
蔬菜 2.5
==============================
group name: 饼干
------------------------------
price
one two three
B 休闲食品 饼干 3.2
==============================
計6グループ
2. 1層目と2層目でグループ化する
grouped = df_2.groupby(level=[0, 1])
view_group(grouped)
出力は次のとおりです
group name: ('A', '米面粮油')
------------------------------
price
one two three
A 米面粮油 米面 2.8
米面 9.0
粮油 18.0
==============================
group name: ('A', '蔬菜水果')
------------------------------
price
one two three
A 蔬菜水果 水果 3.5
蔬菜 6.0
蔬菜 3.0
蔬菜 2.5
水果 8.0
==============================
group name: ('B', '休闲食品')
------------------------------
price
one two three
B 休闲食品 糖果 4.0
饼干 3.2
==============================
合計 3 つのグループがあり、グループ名がタプルになっていることがわかります。
as_index
bool 型の場合、デフォルト値は True です。集約された出力の場合、返されたオブジェクトにはグループ名によってインデックスが付けられます。
grouped = self.df.groupby('category', as_index=True)
print(grouped.sum())
as_index がTrueの場合の出力は次のとおりです。
price count
category
水果 14.7 10
米面 11.8 9
粮油 18.0 2
蔬菜 11.5 13
grouped = self.df.groupby('category', as_index=False)
print(grouped.sum())
as_index is Falseの出力は次のとおりです。これは SQL の groupby 出力スタイルに似ています。
category price count
0 水果 14.7 10
1 米面 11.8 9
2 粮油 18.0 2
3 蔬菜 11.5 13
選別
bool 型。デフォルトはTrueです。グループ名をソートするかどうか、自動ソートをオフにするとパフォーマンスが向上する可能性があります。注: グループ名を並べ替えても、グループ内の順序には影響しません。
グループキー
Bool 型、デフォルトはTrueです。 True
の場合、apply を呼び出すときに、グループ化されたキーをインデックスに追加します。
絞る
バージョン 1.1.0 は廃止されました、説明はありません
観察された
bool 型、デフォルト値は False です
。Categoricals のグルーパーにのみ適用されます。 True
の場合、カテゴリカル グループの観測値のみを表示します。False の場合、カテゴリカル グループのすべての値を表示します。
どろどろの
bool 型、デフォルト値は True、バージョン 1.1.0 の新しいパラメータです。True
の場合、グループ化されたキーに NA 値が含まれている場合、NA 値は行 (axis=0)/列 ( axis=1) Falseの場合、NA 値もグループ化キーとみなされ処理されません
戻り値
DateFrame の grpuby 関数、戻り値の型はDataFrameGroupBy、 Series の groupby 関数、戻り値の型はSeriesGroupBy
ソース コードを確認したところ、いずれも BaseGroupBy を継承していることがわかり、継承関係は図のようになります。
BaseGroupBy类中有一个grouper属性,是ops.BaseGrouper类型,但BaseGroupBy类没有__init__方法,因此进入GroupBy类,该类重写了父类的grouper属性,在__init__方法中调用了grouper.py的get_grouper,下面是抽取出来的伪代码
groupby.py文件
class GroupBy(BaseGroupBy[NDFrameT]):
grouper: ops.BaseGrouper
def __init__(self, ...):
# ...
if grouper is None:
from pandas.core.groupby.grouper import get_grouper
grouper, exclusions, obj = get_grouper(...)
grouper.py文件
def get_grouper(...) -> tuple[ops.BaseGrouper, frozenset[Hashable], NDFrameT]:
# ...
# create the internals grouper
grouper = ops.BaseGrouper(
group_axis, groupings, sort=sort, mutated=mutated, dropna=dropna
)
return grouper, frozenset(exclusions), obj
class Grouping:
"""
obj : DataFrame or Series
"""
def __init__(
self,
index: Index,
grouper=None,
obj: NDFrame | None = None,
level=None,
sort: bool = True,
observed: bool = False,
in_axis: bool = False,
dropna: bool = True,
):
pass
ops.py文件
class BaseGrouper:
"""
This is an internal Grouper class, which actually holds
the generated groups
......
"""
def __init__(self, axis: Index, groupings: Sequence[grouper.Grouping], ...):
# ...
self._groupings: list[grouper.Grouping] = list(groupings)
@property
def groupings(self) -> list[grouper.Grouping]:
return self._groupings
BaseGrouper中包含了最终生成的分组信息,是一个list,其中的元素类型为grouper.Grouping,每个分组对应一个Grouping,而Grouping中的obj对象为分组后的DataFrame或者Series
在第一部分写了一个函数来展示groupby返回的对象,这里再来探究一下原理,对于可迭代对象,会实现__iter__()方法,先定位到BaseGroupBy的对应方法
class BaseGroupBy:
grouper: ops.BaseGrouper
@final
def __iter__(self) -> Iterator[tuple[Hashable, NDFrameT]]:
return self.grouper.get_iterator(self._selected_obj, axis=self.axis)
接下来进入BaseGrouper类中
class BaseGrouper:
def get_iterator(
self, data: NDFrameT, axis: int = 0
) -> Iterator[tuple[Hashable, NDFrameT]]:
splitter = self._get_splitter(data, axis=axis)
keys = self.group_keys_seq
for key, group in zip(keys, splitter):
yield key, group.__finalize__(data, method="groupby")
Debug模式进入group.finalize()方法,发现返回的确实是DataFrame对象
三、4大函数
有了上面的基础,接下来再看groupby之后的处理函数,就简单多了
agg
聚合操作是groupby后最常见的操作,常用来做数据分析
比如,要查看不同category分组的最大值,以下三种写法都可以实现,并且grouped.aggregate和grouped.agg完全等价,因为在SelectionMixin类中有这样的定义:agg = aggregate
但是要聚合多个字短时,就只能用aggregate或者agg了,比如要获取不同category分组下price最大,count最小的记录
还可以结合numpy里的聚合函数
import numpy as np
grouped.agg({
'price': np.max, 'count': np.min})
常见的聚合函数如下
聚合函数 | 功能 |
---|---|
max | 最大值 |
mean | 平均值 |
median | 中位数 |
min | 最小值 |
sum | 求和 |
std | 标准差 |
var | 方差 |
count | 计数 |
其中,count在numpy中对应的调用方式为np.size
transform
现在需要新增一列price_mean,展示每个分组的平均价格
transform函数刚好可以实现这个功能,在指定分组上产生一个与原df相同索引的DataFrame,返回与原对象有相同索引且已填充了转换后的值的DataFrame,然后可以把转换结果新增到原来的DataFrame上
示例代码如下
grouped = df.groupby('category', sort=False)
df['price_mean'] = grouped['price'].transform('mean')
print(df)
输出结果如下
apply
现在需要获取各个分组下价格最高的数据,调用apply可以实现这个功能,apply可以传入任意自定义的函数,实现复杂的数据操作
from pandas import DataFrame
grouped = df.groupby('category', as_index=False, sort=False)
def get_max_one(the_df: DataFrame):
sort_df = the_df.sort_values(by='price', ascending=True)
return sort_df.iloc[-1, :]
max_price_df = grouped.apply(get_max_one)
max_price_df
输出结果如下
filter
filter函数可以对分组后数据做进一步筛选,该函数在每一个分组内,根据筛选函数排除不满足条件的数据并返回一个新的DataFrame
假设现在要把平均价格低于4的分组排除掉,根据transform小节的数据,会把蔬菜分类过滤掉
grouped = df.groupby('category', as_index=False, sort=False)
filtered = grouped.filter(lambda sub_df: sub_df['price'].mean() > 4)
print(filtered)
输出结果如下
四、总结
groupby のプロセスは、groupby のフィールドに従って、元のデータフレーム/シリーズをいくつかのグループ化されたデータフレーム/シリーズに分割することであり、グループの数と同じ数のグループ化されたデータフレーム/シリーズが存在します。したがって、groupby の後の一連の操作 (agg、apply など) は、サブ DataFrame/Series 操作に基づいています。これを理解すると、Pandas の groupby 操作の主原理が理解できます。
5. 参考資料
Pandas 公式 Web サイトの pandas.DateFrame.groupby の紹介
Pandas 公式 Web サイトの pandas.Series.groupby の紹介