pandasで、ある特定の列の値に応じてグループ化(集計・集約)し、特定の列の値ごとに最初の行(もしくは最後の行)を求めたいときの話。
ある特定の列の値に応じてグループ化するにはgroupby関数を使う。 pandasのgroupby関数の返り値はGroupByオブジェクトというやつになる。(正確にはDataFrameGroupByまたはSeriesGroupByオブジェクト)GroupByオブジェクトに対して、やりたい操作に対応する関数を適用すれば結果が求まる。平均のmeanや合計のsum、最大値のmaxなどが有名だろう。
で、「グループごとの最初のレコードが欲しいんだけど、そういう関数はあるのだろうか」と思い、 "pandas groupby first"で検索したら、普通に出てきた。firstという関数があるので、それを使えばよい。
first関数の説明ページはこちら。
pandas.core.groupby.GroupBy.first — pandas 0.25.1 documentation
pandas.core.groupby.GroupBy.first
Compute first of group values.
これだけ。説明はいたって単純だ。
また、StackOverflowの質問を色々見ていたら、firstの他にnth、headを使っても同様のことができると書いてあったので、合わせて実験してみよう。
特に、NaNを含むときには挙動が複雑になり、思わぬ落とし穴にはまる可能性もあるので、詳述している。
基本的な例:グループごとに最初の行を選択する(first、nth、head)
以下、出力された結果は# ---
の後に記載している。
import pandas as pd <200b> df = pd.DataFrame({ 'name' : ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Fred', 'George', 'Helen', 'Ian', 'John'], 'class' : ['A', 'A', 'B', 'B', 'A', 'C', 'A', 'B', 'C', 'C'], 'English' : list(range(0, 100, 10)), 'Math' : list(range(100, 0, -10)) }) df # --- name class English Math 0 Alice A 0 100 1 Bob A 10 90 2 Charlie B 20 80 3 David B 30 70 4 Eve A 40 60 5 Fred C 50 50 6 George A 60 40 7 Helen B 70 30 8 Ian C 80 20 9 John C 90 10
df.groupby("class") # --- <pandas.core.groupby.generic.DataFrameGroupBy object at 0x03DB2950>
groupbyの結果はDataFrameGroupBy オブジェクトであった。これに対して各関数を適用し、結果を見てみよう。
df.groupby("class").first() # --- name English Math class A Alice 0 100 B Charlie 20 80 C Fred 50 50
もとのデータと見比べて欲しい。各Classの最初のデータだけが抽出されたことが分かる。
df.groupby("class").nth(0) # --- name English Math class A Alice 0 100 B Charlie 20 80 C Fred 50 50
n番目の行を表示するための関数なので、n番目→n-th→nth関数という名前になったようだ。
最初の行を表示したいので、引数に0を指定する。結果はfirstの時と全く同じだ。
df.groupby("class").head(1) # --- name class English Math 0 Alice A 0 100 2 Charlie B 20 80 5 Fred C 50 50
head関数は「グループごとに最初のn行を表示する」メソッドである。今回は最初の1行だけが欲しいので引数に1を入れている。
first、nthの結果では、groupbyに用いていたclass列が行名に移動している。それに対して、headの結果ではclass列は元のままで、元のindexが行名に使われている。
基本的な例:グループごとに最後の行を選択する(last、nth、tail)
正反対の操作、つまりグループごとに最後の行を選択する操作の例である。 先ほどとほぼ同じ話なので、簡潔に述べる。
- firstの反対がlast
- nthは関数名は変わらない。最後の要素を指定するために引数に-1を入れる
- headの反対がtail
df.groupby("class").last() # --- name English Math class A George 60 40 B Helen 70 30 C John 90 10
df.groupby("class").nth(-1) # --- name English Math class A George 60 40 B Helen 70 30 C John 90 10
df.groupby("class").tail(1) # --- name class English Math 6 George A 60 40 7 Helen B 70 30 9 John C 90 10
NaNを含むときの挙動
よく似た挙動をするこれらの関数だが、データにNaNが入っていた場合がなかなか厄介なようだ。
最初の行の方だけしか確認していないが……最後の行についても同様だろう。
まずNaNが混じったデータを作ります。
df = pd.DataFrame({ 'name' : ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Fred', 'George', 'Helen', 'Ian', 'John'], 'class' : ['A', 'A', 'B', 'B', 'A', 'C', 'A', 'B', 'C', 'C'], 'English' : list(range(0, 100, 10)), 'Math' : list(range(100, 0, -10)) }) import numpy as np df.loc[0, "name"] = np.nan df.loc[0, "English"] = np.nan df.loc[0, "Math"] = np.nan df.loc[1, "Math"] = np.nan df.loc[5, "English"] = np.nan df # --- name class English Math 0 NaN A NaN NaN 1 Bob A 10.0 NaN 2 Charlie B 20.0 80.0 3 David B 30.0 70.0 4 Eve A 40.0 60.0 5 Fred C NaN 50.0 6 George A 60.0 40.0 7 Helen B 70.0 30.0 8 Ian C 80.0 20.0 9 John C 90.0 10.0
単純なほうから述べる。
head
まずheadは、単純に先頭からN行を抜き出す動作であり、NaNであろうがなかろうが関係ない。 pandas.core.groupby.GroupBy.head — pandas 0.25.2 documentation
df.groupby("class").head(1) # --- name class English Math 0 NaN A NaN NaN 2 Charlie B 20.0 80.0 5 Fred C NaN 50.0
nth
次にnthについて。
この関数にはdropnaパラメータがある。dropnaにはNone、'any'、'all'を指定できる。デフォルトはNoneだ。
pandas.core.groupby.GroupBy.nth — pandas 0.25.2 documentation
dropna=NoneはNaNかどうかを気にせずに数えて、n番目の行を抜き出す。
df.groupby("class").nth(0) # --- name English Math class A NaN NaN NaN B Charlie 20.0 80.0 C Fred NaN 50.0
dropna='any'は、どれか1つの列でもNaNがあったらその行はノーカウントとする。
下の例だとclass AとCの結果が変わっている。
df.groupby("class").nth(0, dropna="any") # --- name English Math class A Eve 40.0 60.0 B Charlie 20.0 80.0 C Ian 80.0 20.0
dropna='all'は、全ての列がNaNであったらその行はノーカウントとする。
df.groupby("class").nth(0, dropna="all") # --- name English Math class A NaN NaN NaN B Charlie 20.0 80.0 C Fred NaN 50.0
あれれ。全ての列がNaNの行が選択されている。公式ドキュメントは
If dropna, will take the nth non-null row, dropna is either ‘all’ or ‘any’; this is equivalent to calling dropna(how=dropna) before the groupby.
なので、groupnbyに使った列(class)もNaNの場合に限ってノーカウント、ということか?よく分からない。
first
最後にfirstだ。この関数はかなり奇妙な動作をしていて、落とし穴になるので要注意だ。
「列ごとに、1番目から見ていって、最初にNaNでない値を表示する」という動きになる。列ごとに全く独立であることに注意。
といってもわかりづらいので、具体例を出そう。
df.groupby("class").first() # --- name English Math class A Bob 10.0 60.0 B Charlie 20.0 80.0 C Fred 80.0 50.0
Class Cの場合、name列とMath列は1行目(5、Fred)にNaNでないデータがあるのでそれを使う。
一方、Englishの列は1行目がNaNなので、その次のデータ(8、Ian)を使う。
結果として、別々の行から継ぎはぎしたデータが返ってくるのだ!
Class Aでも同様だ。
pandas開発のGitHubの中で、関連するissueは以下。バグなのか仕様なのか分からないが、最初の報告は2014年と、ずいぶん昔からこの挙動のようだ。
BUG: groupby.first/last with nans · Issue #8427 · pandas-dev/pandas · GitHub
このStackOverflowの質問経由で、firstがNaNの時の挙動を知った。 python - pandas: how do I select first row in each GROUP BY group? - Stack Overflow
groupbyをした後に使える関数一覧は、公式ドキュメントのここに書いてある。
GroupBy — pandas 0.25.1 documentation
その中でもよく使う関数は、公式ドキュメントのGroupByのユーザーズガイドに書いてある。
Group By: split-apply-combine — pandas 0.25.2 documentation
firstに関連する質問。 python - Pandas dataframe get first row of each group - Stack Overflow
GitHubの中で、上述したfirstの挙動に関するissue一覧。
BUG: groupby.first/last with nans · Issue #8427 · pandas-dev/pandas · GitHubこれは先ほども記載したもの
groupby().first() skips NaN values · Issue #6732 · pandas-dev/pandas · GitHub
まだ調べればいろいろありそうだけど、書き続けて疲れたのでこの辺で。
それでは。