agg()では、集約方法として関数を指定することも可能です。そのため、指定する関数次第で柔軟な集約処理が行えます。
# 指定した列でグループ化
grouped = df.groupby(列名)
# 指定した関数の処理を使って集約
grouped.agg(関数)
集約方法として指定する関数は、「引数でグループごとに各列のSeriesを受け取り、戻り値で集約結果を返す」 ような処理を定義します。
具体的な例を見てみましょう。次のような、試験結果の点数と学習時間のデータについて考えます。
生徒ID | クラス | 点数 | 学習時間(分) |
---|---|---|---|
ST001 | 1-A | 60 | 232 |
ST002 | 1-A | 87 | 345 |
ST003 | 1-B | 66 | 180 |
ST004 | 1-A | 72 | 22 |
ST005 | 1-B | 74 | 120 |
ST006 | 1-B | 58 | 215 |
各クラスごとに、各列の「最大値と最小値の差」を計算したい場合、次のように引数で各列のSeriesを受け取る関数を作成します。
def calc_range(sr):
# 最大値と最小値の差を計算する関数
# 引数srには、グループごとに各列のSeriesが渡される
return sr.max() - sr.min()
# 列「クラス」でグループ化
grouped = df.groupby("クラス")
# 各グループをcalc_range()関数を使って集約
grouped.agg(calc_range)
クラス | 点数 | 学習時間(分) |
---|---|---|
1-A | 27 | 323 |
1-B | 16 | 95 |
この処理では、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 | |||
ST001 | 1-A | 48.0 | 226 |
ST002 | 1-A | 0.0 | 24 |
ST003 | 1-B | 80.0 | 271 |
ST004 | 1-A | NaN | 45 |
ST005 | 1-A | 68.0 | 271 |
今回はクラスごとに集約を行いたいので、列クラス
でグループ化します。
# 列「クラス」でグループ化
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-A | 98.0 | 357 |
1-B | 25.0 | 142 |
前問では、集約方法を示す文字列をリスト形式で指定することで、複数の集約値を一度に計算しました。集約方法として関数を指定する場合も、同様の使い方ができます。実行結果の列名には、関数名(今回の場合は calc_range
)が使われます。
# グループごとに各列の最大値、最小値、最大値と最小値の差を計算
agg_df_3 = grouped.agg(["max", "min", calc_range])
agg_df_3
点数 | 学習時間(分) | |||||
---|---|---|---|---|---|---|
max | min | calc_range | max | min | calc_range | |
クラス | ||||||
1-A | 98.0 | 0.0 | 98.0 | 381 | 24 | 357 |
1-B | 83.0 | 58.0 | 25.0 | 334 | 192 | 142 |
(2)列点数
が40点未満の生徒の数
別の例として、40点未満の生徒数を求める例を見てみましょう。
先ほどと同様に、「列(Series)を受け取って集約処理を行う関数」 を定義して指定します。「Series < 40」とすると、40点未満の要素はTrue
、それ以外の要素はFalse
のSeriesを取得できます。これをsum()
で合計すると、True
は1
、False
は0
として計算されるため、「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 | |||
ST002 | 1-A | 0.0 | 24 |
なお、列ごとに集約方法を変える場合は、前問で学んだように{列名: 集約方法のリスト}
の辞書形式で指定します。次のコードでは、(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
点数 | 学習時間(分) | ||||||
---|---|---|---|---|---|---|---|
max | min | calc_range | count_40point | max | min | calc_range | |
クラス | |||||||
1-A | 98.0 | 0.0 | 98.0 | 1 | 381 | 24 | 357 |
1-B | 83.0 | 58.0 | 25.0 | 0 | 334 | 192 | 142 |
補足: 引数がある関数の適用
写経では、次のような「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-A | 1 | 2 |
1-B | 0 | 1 |
コメント