Pandas の groupby 関数についての深い理解

順序

最近パンダを勉強しています。データを処理するときに、データの特定のフィールドをグループ化して分析する必要があることがよくあります。そのためには、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 はマッピング、関数、ラベル、ラベルのリストのいずれかになります。使用されるラベルは次のとおりです。つまり、次のようにすることもできます以下のように書きます

  1. ラベルリスト
grouped = df.groupby(['category'])
  1. マッピング方法は、果物野菜を大きなグループに分割する
    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
============================== 
  1. このモードの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 を継承していることがわかり、継承関係は図のようになります。

SelectionMixin
+aggregation()
+agg()
BaseGroupBy
+ int axis
+ ops.BaseGrouper grouper
+ bool group_keys
+groups()
GroupBy
+ ops.BaseGrouper grouper
+ bool as_index
+apply()
DataFrameGroupBy
+apply()
+transform()
+filter()
SeriesGroupBy
+apply()
+transform()
+filter()

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对象
__iter__() メソッド BaseGroupBy のデバッグの詳細

三、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 の紹介

おすすめ

転載: blog.csdn.net/u013481793/article/details/127158683