agg()で関数を用いて集約する方法

agg()では、集約方法として関数を指定することも可能です。そのため、指定する関数次第で柔軟な集約処理が行えます。

# 指定した列でグループ化
grouped = df.groupby(列名)
# 指定した関数の処理を使って集約
grouped.agg(関数)

集約方法として指定する関数は、「引数でグループごとに各列のSeriesを受け取り、戻り値で集約結果を返す」 ような処理を定義します。

具体的な例を見てみましょう。次のような、試験結果の点数と学習時間のデータについて考えます。


生徒ID
クラス点数学習時間(分)
ST0011-A60232
ST0021-A87345
ST0031-B66180
ST0041-A7222
ST0051-B74120
ST0061-B58215

各クラスごとに、各列の「最大値と最小値の差」を計算したい場合、次のように引数で各列のSeriesを受け取る関数を作成します。

def calc_range(sr):
    # 最大値と最小値の差を計算する関数    
    # 引数srには、グループごとに各列のSeriesが渡される
    return sr.max() - sr.min()

# 列「クラス」でグループ化
grouped = df.groupby("クラス")
# 各グループをcalc_range()関数を使って集約
grouped.agg(calc_range)

クラス
点数学習時間(分)
1-A27323
1-B1695

この処理では、calc_range()は次の4回呼び出されます。

  • グループ1-Aの列点数 を引数srで受け取り、計算する(87点 – 60点 = 27点
  • グループ1-Bの列点数 を引数srで受け取り、計算する(74点 – 58点 = 16点
  • グループ1-Aの列学習時間(分) を引数srで受け取り、計算する(345分 – 22分 = 323分
  • グループ1-Bの列学習時間(分) を引数srで受け取り、計算する(215分 – 120分 = 95分

最終的に、上記の4つの結果が結合された1つのDataFrameが返却されます。

SeriesGroupByのagg()で関数を適用

SeriesGroupByのagg()でも、集約方法として関数を指定できます。指定する関数は、DataFrameGroupBy同様、「引数でグループごとにSeriesを受け取り、戻り値で集約結果を返す」ようにします。

先ほどの具体例で使ったcalc_range()を、SeriesGroupByを使って列点数だけに適用する場合、次のような結果になります。

# グループごとに、列「点数」を集約
# SeriesGroupByを使って適用
grouped["点数"].agg(calc_range)
クラス
1-A    27
1-B    16
Name: 点数, dtype: int64

演習

import pandas as pd

# データの読み込み
df = pd.read_csv("dataset/score_study_time.csv", index_col="生徒ID")
# 先頭5行を確認
df.head()
クラス点数学習時間(分)
生徒ID
ST0011-A48.0226
ST0021-A0.024
ST0031-B80.0271
ST0041-ANaN45
ST0051-A68.0271

今回はクラスごとに集約を行いたいので、列クラスでグループ化します。

# 列「クラス」でグループ化
grouped = df.groupby("クラス")

(1)列点数と列学習時間(分)の最大値と最小値の差

まず、各グループの列点数と列学習時間(分)について「最大値と最小値の差」を計算してみましょう。

引数で列(Series)を受け取って、そのSeries内の要素の「最大値と最小値の差」を計算し、戻り値で計算結果を返す関数calc_range()を定義します。この関数をagg()に渡して実行すると、グループごとに各列の計算結果が得られることがわかります。

def calc_range(sr):
    # 最大値と最小値の差を計算する関数
    # 引数srには、グループごとに各列のSeriesが渡される
    return sr.max() - sr.min()


# グループごとに、各列の最大値と最小値の差を求める
agg_df_1 = grouped.agg(calc_range)
agg_df_1

点数	学習時間(分)
クラス		
1-A	98.0	357
1-B	25.0	142

どのような処理が行われているかイメージを掴むために、先ほど定義した関数内でprint()を使って変数を出力してみましょう。次のコードを実行すると、列数 * グループ数の分だけcalc_range_debug()が呼ばれており、それぞれ各グループの各列に相当するSeriesが引数で渡されていることがわかります。

def calc_range_debug(sr):
    # 最大値と最小値の差を計算する関数
    # デバッグ用に処理過程を表示
    print(f"列名: {sr.name}")  # 列名を表示
    print()
    print(sr)  # シリーズの中身を表示
    print()  # 空行
    print(f"最大値: {sr.max()}, 最小値: {sr.min()}, 差: {sr.max() - sr.min()}")
    print("========================")
    return sr.max() - sr.min()


# グループごとに、各列の最大値と最小値の差を求める
agg_df_2 = grouped.agg(calc_range_debug)
agg_df_2
列名: 点数

ST001    48.0
ST002     0.0
ST004     NaN
ST005    68.0
ST007    49.0
ST011    98.0
ST012    84.0
ST017    81.0
ST019    78.0
ST020    90.0
Name: 点数, dtype: float64

最大値: 98.0, 最小値: 0.0, 差: 98.0
========================
列名: 点数

ST003    80.0
ST006    58.0
ST008    79.0
ST009    75.0
ST010     NaN
ST013    58.0
ST014    83.0
ST015    62.0
ST016    69.0
ST018     NaN
Name: 点数, dtype: float64

最大値: 83.0, 最小値: 58.0, 差: 25.0
========================
列名: 学習時間(分)

ST001    226
ST002     24
ST004     45
ST005    271
ST007    236
ST011    381
ST012    286
ST017    355
ST019    326
ST020    301
Name: 学習時間(分), dtype: int64

最大値: 381, 最小値: 24, 差: 357
========================
列名: 学習時間(分)

ST003    271
ST006    215
ST008    334
ST009    256
ST010    229
ST013    192
ST014    286
ST015    220
ST016    222
ST018    224
Name: 学習時間(分), dtype: int64

最大値: 334, 最小値: 192, 差: 142
点数学習時間(分)
クラス
1-A98.0357
1-B25.0142

前問では、集約方法を示す文字列をリスト形式で指定することで、複数の集約値を一度に計算しました。集約方法として関数を指定する場合も、同様の使い方ができます。実行結果の列名には、関数名(今回の場合は calc_range)が使われます。

# グループごとに各列の最大値、最小値、最大値と最小値の差を計算
agg_df_3 = grouped.agg(["max", "min", calc_range])
agg_df_3
点数学習時間(分)
maxmincalc_rangemaxmincalc_range
クラス
1-A98.00.098.038124357
1-B83.058.025.0334192142

(2)列点数が40点未満の生徒の数

別の例として、40点未満の生徒数を求める例を見てみましょう。

先ほどと同様に、「列(Series)を受け取って集約処理を行う関数」 を定義して指定します。「Series < 40」とすると、40点未満の要素はTrue、それ以外の要素はFalseのSeriesを取得できます。これをsum()で合計すると、True1False0として計算されるため、「Trueの個数」つまり「40点未満のデータの個数」が得られます。

この処理の内容を書いた関数count_40point()を定義し、agg()で列点数に適用しましょう。

def count_40point(sr):
    # 40点未満のデータの個数を計算
    # 1. sr < 40 で、40未満の要素はTrue、それ以外の要素はFalseとなるSeriesが得られる
    # 2. sum()は合計を計算する(Trueは1、Falseは0として計算される)
    return (sr < 40).sum()

# グループごとに、列「点数」が40点未満の生徒の数を求める
agg_sr = grouped["点数"].agg(count_40point)
agg_sr
クラス
1-A    1
1-B    0
Name: 点数, dtype: int64

上記の実行結果から、40点未満の生徒はグループ1-Aでは1人、グループ1-Bでは0人ということがわかりました。

実際のデータと照らし合わせてみましょう。元のデータから列点数が40点未満の行を選択すると、確かにクラス1-Aに1人該当者がいることがわかります。また、クラス1-Bには該当者がいないこともわかります。

# 40点未満の行を選択
df[df["点数"] < 40]

クラス
点数学習時間(分)
生徒ID
ST0021-A0.024

なお、列ごとに集約方法を変える場合は、前問で学んだように{列名: 集約方法のリスト}の辞書形式で指定します。次のコードでは、(1)で使った「最大値」「最小値」「最大値と最小値の差(calc_range())」に加え、列点数だけにcount_40_point()による集約を追加しています。

# 列ごとに異なる集約方法を指定
agg_df_4 = grouped.agg(
    {  # 40点未満のデータの個数をカウントする集約関数を列「点数」に追加
        "点数": ["max", "min", calc_range, count_40point],
        "学習時間(分)": ["max", "min", calc_range],
    }
)
agg_df_4
点数学習時間(分)
maxmincalc_rangecount_40pointmaxmincalc_range
クラス
1-A98.00.098.0138124357
1-B83.058.025.00334192142

補足: 引数がある関数の適用

写経では、次のような「40点未満のデータの個数を数える関数」を定義しました。

def count_40point(sr):
    # 40点未満のデータの個数を計算
    return (sr < 40).sum()

# グループごとに、列「点数」が40点未満の生徒の数を求める
agg_sr = grouped["点数"].agg(count_40point)

では、次のように閾値を外から指定可能な関数を適用したい場合、どうすればよいでしょうか。

def count_less_threshold(sr, threshold):
    # 引数thresholdの値未満のデータの個数を数える関数
    return (sr < threshold).sum()

上記のthresholdのように追加の引数を指定したい場合は、下記のようにagg()呼び出し時にキーワード引数で指定します。

import pandas as pd

# 試験結果のデータを読み込み
df = pd.read_csv("dataset/score_study_time.csv", index_col="生徒ID")
# 列「クラス」でグループ化
grouped = df.groupby("クラス")
# 閾値を40に指定
agg_sr = grouped["点数"].agg(count_less_threshold, threshold=40)
agg_sr
クラス
1-A    1
1-B    0
Name: 点数, dtype: int64

次のように、引数に渡す値を変えることで閾値を変更できます。

# 閾値を60に指定
agg_sr = grouped["点数"].agg(count_less_threshold, threshold=60)
agg_sr
クラス
1-A    3
1-B    2
Name: 点数, dtype: int64

位置引数で指定したい場合は、次のように第2引数にタプルを使って指定します。

grouped[列名].agg(関数, (第2引数に渡す値, 第3引数に渡す値, ... , 第N引数に渡す値)) 

たとえば、先ほどのcount_less_threshold()の第2引数(threshold)に60を指定する場合、次のようになります。

# 位置引数で指定する場合
grouped["点数"].agg(count_less_threshold, (60,))

パート「データの加工」のクエスト「列や行に関数を適用しよう」で学んだapply()と似ていますね。

また、列ごとに集約方法を変える場合は、次のようにラムダ式と組み合わせて使います。ラムダ式とは、1つの式からなる小さな名前のない関数のことで、lambda 引数の並び: 式 のように書けます。

# 列「点数」では、40未満のデータの個数を計算
# 列「学習時間」では、200未満のデータの個数を計算
agg_df = grouped.agg(
    {
        "点数": lambda sr: count_less_threshold(sr, 40),
        "学習時間(分)": lambda sr: count_less_threshold(sr, 200),
    }
)
agg_df
クラス点数学習時間(分)
1-A12
1-B01

コメント

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