本記事はQrunchからの転載です。


Pandasのgroupbyについては雰囲気でやっていたところがありますので、ちょっと真面目に使い方を調べてみました。使っているPandasのバージョンは1.0.1です。

以下では次のようなDataFrameを使用します。

df = pd.DataFrame({"名字": ["田中", "山田", "上田", "田中", "田中"], 
                            "年齢": [10, 20, 30, 40, 50], 
                            "出身": ["北海道", "東京", None, "沖縄", "北海道"]})
 名字年齢出身
0田中10北海道
1山田20東京
2上田30 
3田中40沖縄
4田中50北海道

Pandasのgroupby#

PandasのgroupbyはSQLにおけるgroupbyと似たような働きになります。つまるところ、主に集計に使われます。

例えば名字という列をキーとしてgroupbyするときには次のようにします。

df.groupby("名字")

ただしこれだけでは全く意味がありません。 以下ではgroupbyをしたあとにどう利用することができるかを示します。

グループ毎にDataFrameを取り出す#

forを使う#

forを使ってグループ毎にDataFrameとしてデータを取り出せます。

for name, grouped_df in df.groupby("名字"):
    print(f"名字:{name}")
    print(grouped_df)

名字:上田

 名字年齢出身
2上田30

名字:山田

 名字年齢出身
1山田20東京

名字:田中

 名字年齢出身
0田中10北海道
3田中40沖縄
4田中50北海道

get_groupを使う#

get_groupを使えば1つのグループを指定することもできます。

df.groupby("名字").get_group("田中")
 名字年齢出身
0田中10北海道
3田中40沖縄
4田中50北海道

groupbyを用いた集計#

describe#

グループごとに統計量を色々計算できます。

df.groupby("名字")["年齢"].describe()
名字countmeanstdmin25%50%75%max
上田130nan3030303030
山田120nan2020202020
田中333.333320.81671025404550

グループの個数のカウント#

グループごとに行数が数えられます。

df.groupby("名字").size()
名字0
上田1
山田1
田中3

次のcountも似たような感じで行数を数えます。ただし、欠損値はカウントされませんので、次のcountの結果とsizeの結果は異なります。

df.groupby("名字")["出身"].count()
名字出身
上田0
山田1
田中3

演算#

#

グループごとに和を計算します。

df.groupby("名字")["年齢"].sum()
名字年齢
上田30
山田20
田中100

平均#

グループごとに平均を計算します。

df.groupby("名字")["年齢"].mean()
名字年齢
上田30
山田20
田中33.3333

特定のデータ取得#

グループの先頭を取得します。

df.groupby("名字")["年齢"].first()
名字年齢
上田30
山田20
田中10

次のnthでもグループの先頭を取得できます。

df.groupby("名字")["年齢"].nth(0)
名字年齢
上田30
山田20
田中10

nthの引数を1にするとグループの2番目のデータが取得できます。 dfでは名字が"田中"のケースのみが複数行存在しますので、“田中"の2番目だけが取得されます。

df.groupby("名字")["年齢"].nth(1)
名字年齢
田中40

関数を渡して計算#

次のように任意の関数をaggregateに渡すことで、好きなように集計できます。

df.groupby("名字")["年齢"].aggregate(np.sum)
名字年齢
上田30
山田20
田中100

aggregateに渡す関数はもちろんlambda式でもOKです。

df.groupby("名字")["年齢"].aggregate(lambda vals: sum(vals))
名字年齢
上田30
山田20
田中100

ちなみにこんな感じで複数個の関数を渡すこともできます。

df.groupby("名字")["年齢"].aggregate([np.mean, np.sum, lambda vals: sum(vals)])
名字meansum<lambda_0>
上田303030
山田202020
田中33.3333100100

集計結果に名前をつけることもできます。以下ではmax_ageという名前の集計結果が得られます。

df.groupby("名字")["年齢"].aggregate(max_age=pd.NamedAgg(column="年齢", aggfunc=np.max))
名字max_age
上田30
山田20
田中50

transformでグループの集計結果と各行の値から計算#

transformを使えばグループの集計結果と行の値を組み合わせた値を計算できます。

例1#

以下のようにして、行の値とグループの平均との差を計算できます。lambda式のxがグループの各行であり、x.mean()でグループの平均を計算しています。

df.groupby("名字")["年齢"].transform(lambda x: (x - x.mean())
 年齢
0-23.3333
10
20
36.66667
416.6667

例2#

グループの平均よりも大きいかどうかをあらわすbooleanが得られます。

df.groupby("名字")["年齢"].transform(lambda x: (x > x.mean())
 年齢
00
10
20
31
41

ブロードキャストの例#

transformに渡す関数がスカラ値を返せば、同じグループのなかで同じ値をもつことになります。

df.groupby("名字")["年齢"].transform(lambda x: x.mean())
 年齢
033.3333
120
230
333.3333
433.3333

前後の値を使う計算#

rolling#

移動平均の計算などで使うrollingはグループ単位でおこなえます。

df.groupby("名字")["年齢"].rolling(window=2).mean()
 年齢
(‘上田’, 2)nan
(‘山田’, 1)nan
(‘田中’, 0)nan
(‘田中’, 3)25
(‘田中’, 4)45

cumsum#

累積和もグループ単位でおこなえます。

df.groupby("名字")["年齢"].cumsum()
 年齢
010
120
230
350
4100

累積和は以下のようにしても計算できます。

df.groupby("名字").expanding().sum()
 年齢
(‘上田’, 2)30
(‘山田’, 1)20
(‘田中’, 0)10
(‘田中’, 3)50
(‘田中’, 4)100

applyでグループ毎に好き勝手に処理する#

applyを使えば、グループ単位のDataFrameを好きに処理したあとにconcatできます。 以下の例を参照ください。

def f(df):
    return pd.DataFrame({"name": df["名字"], "age": df["年齢"] - df["年齢"].mean()})

df.groupby("名字").apply(lambda x: f(x))
 nameage
0田中-23.3333
1山田0
2上田0
3田中6.66667
4田中16.6667

グループをfilteringする#

filterを使えば、条件を満たすグループのみを残すような処理が可能です。

例1#

グループ内の個数が1より大きいグループだけを残す。

df.groupby("名字")["年齢"].filter(lambda x: len(x) > 1)
 年齢
010
340
450

例2#

グループの値の最大値が40未満であるグループだけを残す。

df.groupby("名字")["年齢"].filter(lambda x: x.max() < 40)
 年齢
120
230

例3#

グループに北海道出身の人が含まれるグループだけを残す。

df.groupby("名字").filter(lambda x: "北海道" in x["出身"].tolist()
 名字年齢出身
0田中10北海道
3田中40沖縄
4田中50北海道