本記事は、Pandas の公式ドキュメントのUser Guide - Group by: split-apply-combineを機械翻訳した後、一部の不自然な文章を手直ししたものである。
誤訳の指摘・代訳案・質問等があればコメント欄や編集リクエストでお願いします。
Pandas公式ドキュメント日本語訳記事一覧
- データの取得と選択
- マルチインデックス・高度な索引
- mergeとjoinとconcatenateとcompare
- テーブルの整形とピボットテーブル
- テキストデータの操作
- 欠損データの操作
- 重複ラベル
- カテゴリデータ
- Group by - 分割・適用・結合
Group by - 分割・適用・結合
group byとは、以下の1つまたは複数のステップを含む処理を指します。
- 分割(Splitting):ある基準に基づいてデータをグループに分ける。
- 適用(Applying):各グループに独立して関数を適用する。
- 結合(Combining):結果を1つのデータ構造にまとめる。
これらのうち、分割のステップが最も簡単です。実際、多くの場面で、データセットをグループに分割し、それらのグループに対して何かを行いたいと思うことがあるでしょう。適用のステップでは、次のようなことをしたいかもしれません。
-
集計(Aggregation):各グループの要約統計量(または統計量)を計算する。例えば、
- グループの合計または平均を計算する。
- グループのサイズやカウントを計算する。
-
変換(Transformation):グループ固有の計算を行い、同様のインデックスを持つオブジェクトを返す。例えば、
- グループ内のデータ(zscore)を標準化する。
- グループ内のNAを各グループに由来する値で埋める。
-
フィルタリング(Filtration):TrueまたはFalseと評価されるグループ単位の計算に従って、いくつかのグループを破棄する。例えば、
- メンバーが数人しかいないグループに属するデータを破棄する。
- グループの合計や平均に基づいてデータをフィルタリングする。
- 上記のいくつかの組み合わせ。GroupByは適用ステップの結果を調べ、上記の2つのカテゴリのどちらにも当てはまらない場合は、適切に組み合わせた結果を返そうとします。
pandasのデータ構造上のオブジェクトインスタンスメソッドは一般的に豊富で表現力があるので、例えばデータフレーム関数を各グループに対して簡単に呼び出したいと思うことがよくあります。GroupByという名前は、SQLベースのツール(または itertools
)を使ったことがある人にはかなり馴染みがあるはずで、その中では次のようなコードを書くことができます。
SELECT Column1, Column2, mean(Column3), sum(Column4)
FROM SomeTable
GROUP BY Column1, Column2
このような操作を、pandasを使って自然かつ簡単に表現できるようにすることを目指しています。ここでは、GroupByの各機能を説明し、簡単な例や使用例を紹介します。
より応用的な操作については、cook bookもご覧ください。
オブジェクトをグループに分割する
pandasのオブジェクトは、そのどの軸でも分割することができます。グループ化の抽象的な定義は、ラベルとグループ名の対応付けを提供することです。GroupByオブジェクトを作成するには(GroupByオブジェクトが何であるかについては後で説明します)、次のようにします。
df = pd.DataFrame(
[
("bird", "Falconiformes", 389.0),
("bird", "Psittaciformes", 24.0),
("mammal", "Carnivora", 80.2),
("mammal", "Primates", np.nan),
("mammal", "Carnivora", 58),
],
index=["falcon", "parrot", "lion", "monkey", "leopard"],
columns=("class", "order", "max_speed"),
)
df
# class order max_speed
# falcon bird Falconiformes 389.0
# parrot bird Psittaciformes 24.0
# lion mammal Carnivora 80.2
# monkey mammal Primates NaN
# leopard mammal Carnivora 58.0
# デフォルトはaxis=0
grouped = df.groupby("class")
grouped = df.groupby("order", axis="columns")
grouped = df.groupby(["class", "order"])
マッピングはさまざまな方法で指定することができます。
- 各軸ラベルで呼び出されるPython関数。
- 選択された軸と同じ長さのリストまたはNumPy配列。
- ラベルからグループ名へのマッピング(
label -> group name
)を提供する辞書またはSeries
。 -
DataFrame
オブジェクトの場合、グループ化に使用される列名またはインデックスレベル名を示す文字列。 -
df.groupby(df['A'])
に対する単なる糖衣構文である、df.groupby('A')
。 - 上記のもののリスト。
グループ化されたオブジェクトを総称してキーと呼びます。例えば、次のようなDataFrame
を考えてみましょう。
groupby
に渡す文字列には、列またはインデックスのレベルを指定することができます。文字列がカラム名とインデックスレベル名の両方に一致する場合はValueError
が発生します。
df = pd.DataFrame(
{
"A": ["foo", "bar", "foo", "bar", "foo", "bar", "foo", "foo"],
"B": ["one", "one", "two", "three", "two", "two", "one", "three"],
"C": np.random.randn(8),
"D": np.random.randn(8),
}
)
df
# A B C D
# 0 foo one 0.469112 -0.861849
# 1 bar one -0.282863 -2.104569
# 2 foo two -1.509059 -0.494929
# 3 bar three -1.135632 1.071804
# 4 foo two 1.212112 0.721555
# 5 bar two -0.173215 -0.706771
# 6 foo one 0.119209 -1.039575
# 7 foo three -1.044236 0.271860
DataFrameの場合、groupby()
を呼び出すとGroupByオブジェクトが得られます。もちろん、A
列かB
列のどちらか、または両方でグループ化することができます。
grouped = df.groupby("A")
grouped = df.groupby(["A", "B"])
マルチインデックスにA
列とB
列がある場合は、指定した列を除いてグループ化することも可能です。
df2 = df.set_index(["A", "B"])
grouped = df2.groupby(level=df2.index.names.difference(["B"]))
grouped.sum()
# C D
# A
# bar -1.591710 -1.739537
# foo -0.752861 -1.402938
これらは、DataFrameをそのインデックス(行)で分割します。また、列単位で分割することも可能です。
def get_letter_type(letter):
if letter.lower() in 'aeiou':
return 'vowel'
else:
return 'consonant'
grouped = df.groupby(get_letter_type, axis=1)
pandasのIndex
オブジェクトは重複した値をサポートしています。重複したラベルを持つインデックスがgroupby操作のグループキーとして使用された場合、同じインデックスの値に対応するすべての値は1つのグループであるとみなされるため、集約関数の出力には一意のインデックス値のみが含まれることになります。
lst = [1, 2, 3, 1, 2, 3]
s = pd.Series([1, 2, 3, 10, 20, 30], lst)
grouped = s.groupby(level=0)
grouped.first()
# 1 1
# 2 2
# 3 3
# dtype: int64
grouped.last()
# 1 10
# 2 20
# 3 30
# dtype: int64
grouped.sum()
# 1 11
# 2 22
# 3 33
# dtype: int64
分割は必要なときまで実行されないことに注意してください。GroupByオブジェクトの作成は、有効なマッピングが渡されたことを確認するだけです。
(最も効率的であることは保証できませんが)複雑なデータ操作の多くはGroupByの操作で表現できます。ラベルマッピング関数を使えば、かなりクリエイティブなことができます。
Groupby ソート
デフォルトでは、グループキーはgroupby操作の間にソートされます。しかし、高速化のためにsort=False
を渡すこともできます。
df2 = pd.DataFrame({"X": ["B", "B", "A", "A"], "Y": [1, 2, 3, 4]})
df2.groupby(["X"]).sum()
# Y
# X
# A 7
# B 3
df2.groupby(["X"], sort=False).sum()
# Y
# X
# B 3
# A 7
groupbyは、各グループの中ではソートされた出現する順序を保持することに注意してください。例えば、以下のgroupby()
によって作成されたグループは、元のDataFrame
に現れた順番になっています。
df3 = pd.DataFrame({"X": ["A", "B", "A", "B"], "Y": [1, 4, 3, 2]})
df3.groupby(["X"]).get_group("A")
# X Y
# 0 A 1
# 2 A 3
df3.groupby(["X"]).get_group("B")
# X Y
# 1 B 4
# 3 B 2
バージョン 1.1.0. から
groupby dropna
デフォルトでは、groupby操作の際NA
値はグループキーから除外されます。ただし、グループキーにNA
値を含めたい場合は、dropna=False
を渡すことで実現できます。
df_list = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]]
df_dropna = pd.DataFrame(df_list, columns=["a", "b", "c"])
df_dropna
# a b c
# 0 1 2.0 3
# 1 1 NaN 4
# 2 2 1.0 3
# 3 1 2.0 2
# デフォルトの`dropna` はTrueに設定されており、キーに含まれるNaNを除外する
df_dropna.groupby(by=["b"], dropna=True).sum()
# a c
# b
# 1.0 2 3
# 2.0 2 5
# キーにNaNを許容するには、`dropna`をFalseに設定する
df_dropna.groupby(by=["b"], dropna=False).sum()
# a c
# b
# 1.0 2 3
# 2.0 2 5
# NaN 1 4
dropna
引数のデフォルトはTrueで、これはNA
をグループキーに含まないことを意味します。
GroupByオブジェクトの属性
groups
属性は、計算されたユニークなグループをキーとし、各グループに属する軸ラベルが対応する値となっている辞書です。上記の例では、次のようになります。
df.groupby("A").groups
# {'bar': [1, 3, 5], 'foo': [0, 2, 4, 6, 7]}
df.groupby(get_letter_type, axis=1).groups
# {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}
Pythonの標準的なlen
関数をGroupByオブジェクトに対して呼び出すと、グループdictの長さが返されるだけなので、大部分は単なる便宜的なものです。
Pythonの標準的なlen関数をGroupByオブジェクトに対して呼び出すと、単に辞書groups
の長さを返すだけなので、大部分は単なる便宜的なものです。
grouped = df.groupby(["A", "B"])
grouped.groups
# {('bar', 'one'): [1], ('bar', 'three'): [3], ('bar', 'two'): [5], ('foo', 'one'): [0, 6], ('foo', 'three'): [7], ('foo', 'two'): [2, 4]}
len(grouped)
# 6
GroupBy
に対するタブ補完では、完全な列名(およびその他の属性)が表示されます。
df
# height weight gender
# 2000-01-01 42.849980 157.500553 male
# 2000-01-02 49.607315 177.340407 male
# 2000-01-03 56.293531 171.524640 male
# 2000-01-04 48.421077 144.251986 female
# 2000-01-05 46.556882 152.526206 male
# 2000-01-06 68.448851 168.272968 female
# 2000-01-07 70.757698 136.431469 male
# 2000-01-08 58.909500 176.499753 female
# 2000-01-09 76.435631 174.094104 female
# 2000-01-10 45.306120 177.540920 male
gb = df.groupby("gender")
gb.<TAB> # noqa: E225, E999
# gb.agg gb.boxplot gb.cummin gb.describe gb.filter gb.get_group gb.height gb.last gb.median gb.ngroups gb.plot gb.rank gb.std gb.transform
# gb.aggregate gb.count gb.cumprod gb.dtype gb.first gb.groups gb.hist gb.max gb.min gb.nth gb.prod gb.resample gb.sum gb.var
# gb.apply gb.cummax gb.cumsum gb.fillna gb.gender gb.head gb.indices gb.mean gb.name gb.ohlc gb.quantile gb.size gb.tail gb.weight
マルチインデックスを介したGroupBy
階層的なインデックスを持つデータでは、階層の1つのレベルでグループ化するのはごく自然なことです。
ここでは、2レベルのMultiIndex
を持つSeriesを作成します。
arrays = [
["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
["one", "two", "one", "two", "one", "two", "one", "two"],
]
index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
s = pd.Series(np.random.randn(8), index=index)
s
# first second
# bar one -0.919854
# two -0.042379
# baz one 1.247642
# two -0.009920
# foo one 0.290213
# two 0.495767
# qux one 0.362949
# two 1.548106
# dtype: float64
s
のレベルのうちの1つでグループ化することができます。
grouped = s.groupby(level=0)
grouped.sum()
# first
# bar -0.962232
# baz 1.237723
# foo 0.785980
# qux 1.911055
# dtype: float64
マルチインデックスに名前(name
属性)が指定されている場合、レベル番号の代わりにそれらを渡すことができます。
s.groupby(level="second").sum()
# second
# one 0.980950
# two 1.991575
# dtype: float64
複数のレベルを用いたグルーピングにも対応しています。
s
# first second third
# bar doo one -1.131345
# two -0.089329
# baz bee one 0.337863
# two -0.945867
# foo bop one -0.932132
# two 1.956030
# qux bop one 0.017587
# two -0.016692
# dtype: float64
s.groupby(level=["first", "second"]).sum()
# first second
# bar doo -1.220674
# baz bee -0.608004
# foo bop 1.023898
# qux bop 0.000895
# dtype: float64
インデックスのレベル名もキーとして与えることができます。
s.groupby(["first", "second"]).sum()
# first second
# bar doo -1.220674
# baz bee -0.608004
# foo bop 1.023898
# qux bop 0.000895
# dtype: float64
sum
関数や集計については後で詳しく説明します。
インデックスレベルと列によるデータフレームのグループ化
データフレームは、列名を文字列で、インデックスレベルをpd.Grouper
オブジェクトで指定することにより、列とインデックスレベルの組み合わせによってグループ化することができます。
arrays = [
["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
["one", "two", "one", "two", "one", "two", "one", "two"],
]
index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
df = pd.DataFrame({"A": [1, 1, 1, 1, 2, 2, 3, 3], "B": np.arange(8)}, index=index)
df
# A B
# first second
# bar one 1 0
# two 1 1
# baz one 1 2
# two 1 3
# foo one 2 4
# two 2 5
# qux one 3 6
# two 3 7
次の例では、dfを2番目のインデックスレベルとA列でグループ化しています。
df.groupby([pd.Grouper(level=1), "A"]).sum()
# B
# second A
# one 1 2
# 2 4
# 3 6
# two 1 4
# 2 5
# 3 7
インデックスレベルは、名前で指定することもできます。
df.groupby([pd.Grouper(level="second"), "A"]).sum()
# B
# second A
# one 1 2
# 2 4
# 3 6
# two 1 4
# 2 5
# 3 7
インデックスレベル名はgroupby
に直接キーとして指定することができる。
df.groupby(["second", "A"]).sum()
# B
# second A
# one 1 2
# 2 4
# 3 6
# two 1 4
# 2 5
# 3 7
GroupByでDataFrameの列を選択する
DataFrameからGroupByオブジェクトを作成した後、各列に対して異なる処理を行いたい場合があります。このとき、DataFrameからカラムを取得するのと同様の[]
を使用すると、次のようになります。
df = pd.DataFrame(
{
"A": ["foo", "bar", "foo", "bar", "foo", "bar", "foo", "foo"],
"B": ["one", "one", "two", "three", "two", "two", "one", "three"],
"C": np.random.randn(8),
"D": np.random.randn(8),
}
)
df
# A B C D
# 0 foo one -0.575247 1.346061
# 1 bar one 0.254161 1.511763
# 2 foo two -1.143704 1.627081
# 3 bar three 0.215897 -0.990582
# 4 foo two 1.193555 -0.441652
# 5 bar two -0.077118 1.211526
# 6 foo one -0.408530 0.268520
# 7 foo three -0.862495 0.024580
grouped = df.groupby(["A"])
grouped_C = grouped["C"]
grouped_D = grouped["D"]
次の書き方は主に代替案のための糖衣構文であり、はるかに冗長です。
df["C"].groupby(df["A"])
# <pandas.core.groupby.generic.SeriesGroupBy object at 0x7fd79e866040>
また、この方法では、渡されたキーから得られる内部グループ化情報を再計算する必要がありません。
グループのイテレート
GroupByオブジェクトがあれば、グループ化されたデータの反復処理は非常に自然で、itertools.groupby()
と似たような機能を持ちます。
grouped = df.groupby('A')
for name, group in grouped:
print(name)
print(group)
# bar
# A B C D
# 1 bar one 0.254161 1.511763
# 3 bar three 0.215897 -0.990582
# 5 bar two -0.077118 1.211526
# foo
# A B C D
# 0 foo one -0.575247 1.346061
# 2 foo two -1.143704 1.627081
# 4 foo two 1.193555 -0.441652
# 6 foo one -0.408530 0.268520
# 7 foo three -0.862495 0.024580
複数のキーでグループ化した場合、グループ名はタプルになります。
for name, group in df.groupby(['A', 'B']):
print(name)
print(group)
# ('bar', 'one')
# A B C D
# 1 bar one 0.254161 1.511763
# ('bar', 'three')
# A B C D
# 3 bar three 0.215897 -0.990582
# ('bar', 'two')
# A B C D
# 5 bar two -0.077118 1.211526
# ('foo', 'one')
# A B C D
# 0 foo one -0.575247 1.346061
# 6 foo one -0.408530 0.268520
# ('foo', 'three')
# A B C D
# 7 foo three -0.862495 0.02458
# ('foo', 'two')
# A B C D
# 2 foo two -1.143704 1.627081
# 4 foo two 1.193555 -0.441652
「グループのイテレーション」もご覧ください。
グループの選択
get_group()
でグループのうちの1つを選択することができます。
grouped.get_group("bar")
# A B C D
# 1 bar one 0.254161 1.511763
# 3 bar three 0.215897 -0.990582
# 5 bar two -0.077118 1.211526
複数の列でグループ化されたオブジェクトの場合は次のようにします。
df.groupby(["A", "B"]).get_group(("bar", "one"))
# A B C D
# 1 bar one 0.254161 1.511763
集計
GroupByオブジェクトが作成されると、グループ化されたデータに対して計算を実行するためのいくつかのメソッドが利用できるようになります。これらの演算は、aggregating API、window API、resample APIと同様です。
わかりやすいのは、aggregate()
または同等のagg()
メソッドによる集計です。
grouped = df.groupby("A")
grouped[["C", "D"]].aggregate(np.sum)
# C D
# A
# bar 0.392940 1.732707
# foo -1.796421 2.824590
grouped = df.groupby(["A", "B"])
grouped.aggregate(np.sum)
# C D
# A B
# bar one 0.254161 1.511763
# three 0.215897 -0.990582
# two -0.077118 1.211526
# foo one -0.983776 1.614581
# three -0.862495 0.024580
# two 0.049851 1.185429
このように、集約後はグループ化された軸に沿った新しいインデックスにグループ名を持ちます。複数のキーがある場合、結果はデフォルトでマルチインデックスになりますが、これはas_index
オプションで変更することが可能です。
grouped = df.groupby(["A", "B"], as_index=False)
grouped.aggregate(np.sum)
# A B C D
# 0 bar one 0.254161 1.511763
# 1 bar three 0.215897 -0.990582
# 2 bar two -0.077118 1.211526
# 3 foo one -0.983776 1.614581
# 4 foo three -0.862495 0.024580
# 5 foo two 0.049851 1.185429
df.groupby("A", as_index=False)[["C", "D"]].sum()
# A C D
# 0 bar 0.392940 1.732707
# 1 foo -1.796421 2.824590
なお、reset_index
データフレーム関数を使用すると、結果のMultiIndex
に列名が格納されるため、同じ結果を得ることができます。
df.groupby(["A", "B"]).sum().reset_index()
# A B C D
# 0 bar one 0.254161 1.511763
# 1 bar three 0.215897 -0.990582
# 2 bar two -0.077118 1.211526
# 3 foo one -0.983776 1.614581
# 4 foo three -0.862495 0.024580
# 5 foo two 0.049851 1.185429
もう一つの簡単な集約の例を挙げると、各グループのサイズ(行数)を計算することができます。これはGroupByのsize
メソッドとして実装されています。インデックスがグループ名で、値が各グループのサイズであるシリーズを返します。
grouped.size()
# A B size
# 0 bar one 1
# 1 bar three 1
# 2 bar two 1
# 3 foo one 2
# 4 foo three 1
# 5 foo two 2
grouped.describe()
# C ... D
# count mean std min ... 25% 50% 75% max
# 0 1.0 0.254161 NaN 0.254161 ... 1.511763 1.511763 1.511763 1.511763
# 1 1.0 0.215897 NaN 0.215897 ... -0.990582 -0.990582 -0.990582 -0.990582
# 2 1.0 -0.077118 NaN -0.077118 ... 1.211526 1.211526 1.211526 1.211526
# 3 2.0 -0.491888 0.117887 -0.575247 ... 0.537905 0.807291 1.076676 1.346061
# 4 1.0 -0.862495 NaN -0.862495 ... 0.024580 0.024580 0.024580 0.024580
# 5 2.0 0.024925 1.652692 -1.143704 ... 0.075531 0.592714 1.109898 1.627081
#
# [6 rows x 16 columns]
他の集計例として、各グループのユニークな値の数を計算できます。これはvalue_counts
関数と似ていますが、ユニークな値だけをカウントする点が異なります。
ll = [['foo', 1], ['foo', 2], ['foo', 2], ['bar', 1], ['bar', 1]]
df4 = pd.DataFrame(ll, columns=["A", "B"])
df4
# A B
# 0 foo 1
# 1 foo 2
# 2 foo 2
# 3 bar 1
# 4 bar 1
df4.groupby("A")["B"].nunique()
# A
# bar 1
# foo 2
# Name: B, dtype: int64
集計関数は、デフォルトのas_index=True
では、集計対象のグループが名前付きカラムであればそのグループを返しません。グループ化されたカラムは、返されるオブジェクトのインデックスとなります。
as_index=False
を渡すと、名前付きカラムの場合、集約しているグループを返します。
集計関数は、返されたオブジェクトの次元を縮小する関数です。一般的な集計関数を以下に列挙します。
Function |
Description |
---|---|
mean() |
グループの平均 |
sum() |
グループの合計 |
size() |
グループのサイズ |
count() |
グループの数 |
std() |
グループの標準偏差 |
var() |
グループの分散 |
sem() |
グループの標準誤差 |
describe() |
グループの要約統計量 |
first() |
グループの最初の値 |
last() |
グループの最後の値 |
nth() |
n番目、またはnがリストの場合はサブセット |
min() |
グループの最少値 |
max() |
グループの最大値 |
上記の集計関数は、欠損値を除外します。Series
をスカラー値に変換する関数はすべて集約関数として動作します。簡単な例としてはdf.groupby('A').agg(lambda ser: 1)
があります。nth()
はリデューサーまたはフィルターとして動作することに注意してください。
複数の関数を同時に適用
グループ化されたSeries
に、集約する関数のリストまたは辞書を渡して、データフレームを出力させることもできます。
grouped = df.groupby("A")
grouped["C"].agg([np.sum, np.mean, np.std])
# sum mean std
# A
# bar 0.392940 0.130980 0.181231
# foo -1.796421 -0.359284 0.912265
グループ化されたDataFrame
に対しては、各列に適用する関数のリストを渡すことができ、階層インデックスを持つ集約結果を生成することができます。
grouped[["C", "D"]].agg([np.sum, np.mean, np.std])
# C D
# sum mean std sum mean std
# A
# bar 0.392940 0.130980 0.181231 1.732707 0.577569 1.366330
# foo -1.796421 -0.359284 0.912265 2.824590 0.564918 0.884785
結果として得られる集計は、関数そのものの名前が付けられます。もし、名前を変更する必要がある場合は、以下のようにSeries
の連鎖操作を追加します。
(
grouped["C"]
.agg([np.sum, np.mean, np.std])
.rename(columns={"sum": "foo", "mean": "bar", "std": "baz"})
)
# foo bar baz
# A
# bar 0.392940 0.130980 0.181231
# foo -1.796421 -0.359284 0.912265
グループ化されたDataFrame
の場合も、同様の方法で名前を変更することができます。
(
grouped[["C", "D"]].agg([np.sum, np.mean, np.std]).rename(
columns={"sum": "foo", "mean": "bar", "std": "baz"}
)
)
Out[86]:
C D
foo bar baz foo bar baz
A
bar 0.392940 0.130980 0.181231 1.732707 0.577569 1.366330
foo -1.796421 -0.359284 0.912265 2.824590 0.564918 0.884785
一般に、出力カラム名は一意であるべきです。同じカラムに同じ関数(あるいは同じ名前の2つの関数)を適用することはできません。
grouped["C"].agg(["sum", "sum"])
# sum sum
# A
# bar 0.392940 0.392940
# foo -1.796421 -1.796421
pandasは複数のラムダ関数を渡すことができます。この場合、pandasは(名前のない)ラムダ関数の名前を加工して、それぞれのラムダの後ろに_<i>
を付加します。
grouped["C"].agg([lambda x: x.max() - x.min(), lambda x: x.median() - x.mean()])
# <lambda_0> <lambda_1>
# A
# bar 0.331279 0.084917
# foo 2.337259 -0.215962
名前付き集計
バージョン 0.25.0 から
出力の列名を制御しながら列固有の集約をサポートするために、pandasはDataFrameGroupBy.agg()
とSeriesGroupBy.agg()
において、「名前付き集計」として知られる特別な構文を受け入れます。
- キーワードは、出力列名です。
- pandasは、引数が何であるかを明確にするために、フィールド
['column', 'aggfunc']
を持つ名前付きタプルpandas.NamedAgg
を提供します。通常通り、集約は呼び出し可能関数か文字列のエイリアスになります。
animals = pd.DataFrame(
{
"kind": ["cat", "dog", "cat", "dog"],
"height": [9.1, 6.0, 9.5, 34.0],
"weight": [7.9, 7.5, 9.9, 198.0],
}
)
animals
# kind height weight
# 0 cat 9.1 7.9
# 1 dog 6.0 7.5
# 2 cat 9.5 9.9
# 3 dog 34.0 198.0
animals.groupby("kind").agg(
min_height=pd.NamedAgg(column="height", aggfunc="min"),
max_height=pd.NamedAgg(column="height", aggfunc="max"),
average_weight=pd.NamedAgg(column="weight", aggfunc=np.mean),
)
# min_height max_height average_weight
# kind
# cat 9.1 9.5 8.90
# dog 6.0 34.0 102.75
pandas.NamedAgg
は単なるnamedtuple
です。プレーンなタプルでも問題ありません。
animals.groupby("kind").agg(
min_height=("height", "min"),
max_height=("height", "max"),
average_weight=("weight", np.mean),
)
# min_height max_height average_weight
# kind
# cat 9.1 9.5 8.90
# dog 6.0 34.0 102.75
出力したいカラム名が有効なPythonキーワードでない場合、辞書を構築し、キーワード引数を展開します。
animals.groupby("kind").agg(
**{
"total weight": pd.NamedAgg(column="weight", aggfunc=sum)
}
)
# total weight
# kind
# cat 17.8
# dog 205.5
追加のキーワード引数は、集約関数に渡されません。(column, aggfunc)
のペアだけが**kwargs
として渡されることになります。もし渡す集約関数が追加の引数を必要とする場合は、functools.partial()
でそれらを部分的に適用してください。
Python 3.5以前のバージョンでは、関数内の**kwargs
の順序は保存されませんでした。これは、出力カラムの順序が一貫していないことを意味します。Python 3.5では、一貫した順序を保証するために、キー(および出力カラム)は常にソートされています。
名前付き集計は、シリーズgroupby集計でも有効です。この場合、列の選択はないので、値は関数だけです。
animals.groupby("kind").height.agg(
min_height="min",
max_height="max",
)
# min_height max_height
# kind
# cat 9.1 9.5
# dog 6.0 34.0
データフレームの列にさまざまな関数を適用する
aggregate
に辞書を渡すことで、DataFrameのカラムに異なる集計を適用することができます。
grouped.agg({"C": np.sum, "D": lambda x: np.std(x, ddof=1)})
# C D
# A
# bar 0.392940 1.366330
# foo -1.796421 0.884785
関数名には、文字列も使用できます。文字列が有効であるためには、GroupByで実装されているか、ディスパッチによって利用可能である必要があります。
grouped.agg({"C": "sum", "D": "std"})
# C D
# A
# bar 0.392940 1.366330
# foo -1.796421 0.884785
Cythonで最適化された集計関数
いくつかの一般的な集約は(現在はsum
・mean
・std
・sem
のみですが)、Cythonの実装が最適化されています。
df.groupby("A")[["C", "D"]].sum()
# C D
# A
# bar 0.392940 1.732707
# foo -1.796421 2.824590
df.groupby(["A", "B"]).mean()
# C D
# A B
# bar one 0.254161 1.511763
# three 0.215897 -0.990582
# two -0.077118 1.211526
# foo one -0.491888 0.807291
# three -0.862495 0.024580
# two 0.024925 0.592714
もちろん、sum
とmean
はpandasオブジェクトに実装されているので、ディスパッチによる特別なバージョン(後述)でなくても上記のコードは動作するはずです。
ユーザー定義関数による集計
ユーザーは、独自の関数を渡して、カスタム集計を行うこともできます。User-Defined Function (UDF)を使って集約する場合、UDFは渡されたSeries
を変形させてはいけません。詳細は「ユーザー定義関数(UDF)を使った変形」を参照してください。
animals.groupby("kind")[["height"]].agg(lambda x: set(x))
# height
# kind
# cat {9.1, 9.5}
# dog {34.0, 6.0}
結果のデータ型は集約関数のものが反映されます。異なるグループの結果が異なるデータ型を持つ場合、共通のデータ型はDataFrame
の構築と同じ方法で決定されます。
animals.groupby("kind")[["height"]].agg(lambda x: x.astype(int).sum())
# height
# kind
# cat 18
# dog 40
変換
transform
メソッドは、グループ化されるものと同じインデックスを持つオブジェクトを返します。transform関数は必ず以下のように動作します。
- グループチャンクと同じサイズか、グループチャンクのサイズにブロードキャストした結果を返す(例えば、スカラー、
grouped.transform(lambda x: x.iloc[-1])
)。 - グループチャンクの列単位で操作します。変換は chunk.apply を使って最初のグループチャンクに適用されます。
- グループチャンクに対してインプレース操作を実行しない。グループチャンクは不変なものとして扱われるべきで、グループチャンクへの変更は予期せぬ結果をもたらす可能性があります。例えば、
fillna
を使用する場合、inplace
はFalse
でなければなりません(grouped.transform(lambda x: x.fillna(inplace=False))
)。 - (オプション) グループチャンク全体を操作します。これがサポートされている場合、2番目のチャンクから始まる高速パスが使用されます。
バージョン1.5.0から非推奨
グループ化されたデータフレームに対して.transform
を使用し、変換関数がデータフレームを返す場合、現在pandasは結果のインデックスを入力のインデックスと整列させることはありません。この動作は非推奨で、整列はpandasの将来のバージョンで実行される予定です。変換関数の結果に.to_numpy()
を適用することで、アライメントを回避することができます。
ユーザー定義関数による集計と同様に、結果のデータ型は変換関数のものが反映されます。異なるグループの結果が異なるデータ型を持つ場合、共通のデータ型はDataFrame
の構築と同じ方法で決定されます。
例えば、各グループ内のデータを標準化したいとします。
index = pd.date_range("10/1/1999", periods=1100)
ts = pd.Series(np.random.normal(0.5, 2, 1100), index)
ts = ts.rolling(window=100, min_periods=100).mean().dropna()
ts.head()
# 2000-01-08 0.779333
# 2000-01-09 0.778852
# 2000-01-10 0.786476
# 2000-01-11 0.782797
# 2000-01-12 0.798110
# Freq: D, dtype: float64
ts.tail()
# 2002-09-30 0.660294
# 2002-10-01 0.631095
# 2002-10-02 0.673601
# 2002-10-03 0.709213
# 2002-10-04 0.719369
# Freq: D, dtype: float64
transformed = ts.groupby(lambda x: x.year).transform(
lambda x: (x - x.mean()) / x.std()
)
その結果、各グループの平均が0、標準偏差が1になっていることが予想されますが、これは簡単に確認できます。
# オリジナルのデータ
grouped = ts.groupby(lambda x: x.year)
grouped.mean()
# 2000 0.442441
# 2001 0.526246
# 2002 0.459365
# dtype: float64
grouped.std()
# 2000 0.131752
# 2001 0.210945
# 2002 0.128753
# dtype: float64
# Transformed Data
grouped_trans = transformed.groupby(lambda x: x.year)
grouped_trans.mean()
# 2000 1.193722e-15
# 2001 1.945476e-15
# 2002 1.272949e-15
# dtype: float64
grouped_trans.std()
# 2000 1.0
# 2001 1.0
# 2002 1.0
# dtype: float64
また、元のデータセットと変換後のデータセットを視覚的に比較することができます。
compare = pd.DataFrame({"Original": ts, "Transformed": transformed})
compare.plot()
# <AxesSubplot:>
出力が低次元である変換関数は、入力配列の形状に合わせてブロードキャストされます。
ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
# 2000-01-08 0.623893
# 2000-01-09 0.623893
# 2000-01-10 0.623893
# 2000-01-11 0.623893
# 2000-01-12 0.623893
# ...
# 2002-09-30 0.558275
# 2002-10-01 0.558275
# 2002-10-02 0.558275
# 2002-10-03 0.558275
# 2002-10-04 0.558275
# Freq: D, Length: 1001, dtype: float64
また、内蔵のメソッドを使用しても同じ出力が得られます。
max_ts = ts.groupby(lambda x: x.year).transform("max")
min_ts = ts.groupby(lambda x: x.year).transform("min")
max_ts - min_ts
# 2000-01-08 0.623893
# 2000-01-09 0.623893
# 2000-01-10 0.623893
# 2000-01-11 0.623893
# 2000-01-12 0.623893
# ...
# 2002-09-30 0.558275
# 2002-10-01 0.558275
# 2002-10-02 0.558275
# 2002-10-03 0.558275
# 2002-10-04 0.558275
# Freq: D, Length: 1001, dtype: float64
もう一つの一般的なデータ変換は、欠損データをグループ平均に置き換えることです。
data_df
# A B C
# 0 1.539708 -1.166480 0.533026
# 1 1.302092 -0.505754 NaN
# 2 -0.371983 1.104803 -0.651520
# 3 -1.309622 1.118697 -1.161657
# 4 -1.924296 0.396437 0.812436
# .. ... ... ...
# 995 -0.093110 0.683847 -0.774753
# 996 -0.185043 1.438572 NaN
# 997 -0.394469 -0.642343 0.011374
# 998 -1.174126 1.857148 NaN
# 999 0.234564 0.517098 0.393534
#
# [1000 rows x 3 columns]
countries = np.array(["US", "UK", "GR", "JP"])
key = countries[np.random.randint(0, 4, 1000)]
grouped = data_df.groupby(key)
# 各グループの非欠損値の数
grouped.count()
# A B C
# GR 209 217 189
# JP 240 255 217
# UK 216 231 193
# US 239 250 217
transformed = grouped.transform(lambda x: x.fillna(x.mean()))
変換後のデータでグループ平均が変化していないこと、変換後のデータに欠損値が含まれていないことを確認することができます。
grouped_trans = transformed.groupby(key)
grouped.mean() # オリジナルのグループ平均
# A B C
# GR -0.098371 -0.015420 0.068053
# JP 0.069025 0.023100 -0.077324
# UK 0.034069 -0.052580 -0.116525
# US 0.058664 -0.020399 0.028603
grouped_trans.mean() # 変換後もグループ平均は変化していない
# A B C
# GR -0.098371 -0.015420 0.068053
# JP 0.069025 0.023100 -0.077324
# UK 0.034069 -0.052580 -0.116525
# US 0.058664 -0.020399 0.028603
grouped.count() # オリジナルは欠損値をいくらか含む
# A B C
# GR 209 217 189
# JP 240 255 217
# UK 216 231 193
# US 239 250 217
grouped_trans.count() # 変換後の数
# A B C
# GR 228 228 228
# JP 267 267 267
# UK 247 247 247
# US 258 258 258
grouped_trans.size() # 非欠損値の数がグループのサイズと一致していることを確認
# GR 228
# JP 267
# UK 247
# US 258
# dtype: int64
いくつかの関数は、GroupByオブジェクトに適用されると自動的に入力を変換しますが、元のオブジェクトと同じ形状のオブジェクトを返します。as_index=False
を渡しても、これらの変換メソッドには影響しません。
例:fillna
・ffill
・bfill
・shift
など
grouped.ffill()
# A B C
# 0 1.539708 -1.166480 0.533026
# 1 1.302092 -0.505754 0.533026
# 2 -0.371983 1.104803 -0.651520
# 3 -1.309622 1.118697 -1.161657
# 4 -1.924296 0.396437 0.812436
# .. ... ... ...
# 995 -0.093110 0.683847 -0.774753
# 996 -0.185043 1.438572 -0.774753
# 997 -0.394469 -0.642343 0.011374
# 998 -1.174126 1.857148 -0.774753
# 999 0.234564 0.517098 0.393534
#
# [1000 rows x 3 columns]
窓掛けとリサンプリング操作
groupbyに対するメソッドとして、resample()
・expanding()
・rolling()
を使用することが可能です。
以下の例では、A列のグループをもとに、B列のサンプルに対してrolling()
メソッドを適用しています。
df_re = pd.DataFrame({"A": [1] * 10 + [5] * 10, "B": np.arange(20)})
df_re
# A B
# 0 1 0
# 1 1 1
# 2 1 2
# 3 1 3
# 4 1 4
# .. .. ..
# 15 5 15
# 16 5 16
# 17 5 17
# 18 5 18
# 19 5 19
#
# [20 rows x 2 columns]
df_re.groupby("A").rolling(4).B.mean()
# A
# 1 0 NaN
# 1 NaN
# 2 NaN
# 3 1.5
# 4 2.5
# ...
# 5 15 13.5
# 16 14.5
# 17 15.5
# 18 16.5
# 19 17.5
# Name: B, Length: 20, dtype: float64
expanding()
メソッドは、各特定グループのメンバー全員について、与えられた演算(例ではsum()
)を累積します。
df_re.groupby("A").expanding().sum()
# B
# A
# 1 0 0.0
# 1 1.0
# 2 3.0
# 3 6.0
# 4 10.0
# ... ...
# 5 15 75.0
# 16 91.0
# 17 108.0
# 18 126.0
# 19 145.0
#
# [20 rows x 1 columns]
例えば、resample()
メソッドを使ってデータフレームの各グループの日次頻度を取得し、ffill()
メソッドで欠損値を補完したいとします。
df_re = pd.DataFrame(
{
"date": pd.date_range(start="2016-01-01", periods=4, freq="W"),
"group": [1, 1, 2, 2],
"val": [5, 6, 7, 8],
}
).set_index("date")
df_re
# group val
# date
# 2016-01-03 1 5
# 2016-01-10 1 6
# 2016-01-17 2 7
# 2016-01-24 2 8
df_re.groupby("group").resample("1D").ffill()
# group val
# group date
# 1 2016-01-03 1 5
# 2016-01-04 1 5
# 2016-01-05 1 5
# 2016-01-06 1 5
# 2016-01-07 1 5
# ... ... ...
# 2 2016-01-20 2 7
# 2016-01-21 2 7
# 2016-01-22 2 7
# 2016-01-23 2 7
# 2016-01-24 2 8
#
# [16 rows x 2 columns]
フィルタリング
filter
メソッドは、元のオブジェクトの部分集合を返します。例えば、グループ和が2より大きいグループに属する要素だけを取り出したいとします。
sf = pd.Series([1, 1, 2, 3, 3, 3])
sf.groupby(sf).filter(lambda x: x.sum() > 2)
# 3 3
# 4 3
# 5 3
# dtype: int64
filter
の引数は、グループ全体に適用され、True
またはFalse
を返す関数である必要があります。
もう一つの便利な操作は、メンバーが数人しかいないグループに属する要素をフィルタリングすることです。
dff = pd.DataFrame({"A": np.arange(8), "B": list("aabbbbcc")})
dff.groupby("B").filter(lambda x: len(x) > 2)
# A B
# 2 2 b
# 3 3 b
# 4 4 b
# 5 5 b
また、問題のあるグループを削除する代わりに、フィルタを通過しないグループをNaNで埋めた類似のインデックスを持つオブジェクトを返すこともできる。
dff.groupby("B").filter(lambda x: len(x) > 2, dropna=False)
# A B
# 0 NaN NaN
# 1 NaN NaN
# 2 2.0 b
# 3 3.0 b
# 4 4.0 b
# 5 5.0 b
# 6 NaN NaN
# 7 NaN NaN
複数のカラムを持つDataFrameの場合、フィルタはカラムを明示的にフィルタ基準として指定する必要があります。
dff["C"] = np.arange(8)
dff.groupby("B").filter(lambda x: len(x["C"]) > 2)
# A B C
# 2 2 b 2
# 3 3 b 3
# 4 4 b 4
# 5 5 b 5
groupbyオブジェクトに適用されるいくつかの関数は、入力に対してフィルターとして働き、インデックスを変更せずに、オリジナルの縮小された形状を返します(そして、グループを排除する可能性があります)。as_index=False
を渡すと、これらの変換メソッドには影響しません。
例:head
・tail
dff.groupby("B").head(2)
# A B C
# 0 0 a 0
# 1 1 a 1
# 2 2 b 2
# 3 3 b 3
# 6 6 c 6
# 7 7 c 7
インスタンスメソッドへのディスパッチ
集計や変換を行う際に、各データグループに対してインスタンスメソッドを呼び出したい場合があります。これはラムダ関数を渡すことで簡単に実現できます。
grouped = df.groupby("A")
grouped.agg(lambda x: x.std())
# C D
# A
# bar 0.181231 1.366330
# foo 0.912265 0.884785
しかし、これはかなり冗長で、追加の引数を渡す必要がある場合、整頓できないことがあります。メタプログラミングのちょっとした工夫で、GroupByにはグループへのメソッド呼び出しを「ディスパッチ」する機能があります。
grouped.std()
# C D
# A
# bar 0.181231 1.366330
# foo 0.912265 0.884785
ここで実際に起こっているのは、関数のラッパーが生成されていることです。呼び出されると、渡された引数を受け取り、それぞれのグループ(上の例ではstd
関数)に対して任意の引数を持つ関数を呼び出します。その結果は、agg
やtransform
のようなスタイルで結合されます(実際には、次に述べるように、apply
を使って結合を推測します)。これによって、いくつかの操作がかなり簡潔に実行されるようになります。
tsdf = pd.DataFrame(
np.random.randn(1000, 3),
index=pd.date_range("1/1/2000", periods=1000),
columns=["A", "B", "C"],
)
tsdf.iloc[::2] = np.nan
grouped = tsdf.groupby(lambda x: x.year)
grouped.fillna(method="pad")
# A B C
# 2000-01-01 NaN NaN NaN
# 2000-01-02 -0.353501 -0.080957 -0.876864
# 2000-01-03 -0.353501 -0.080957 -0.876864
# 2000-01-04 0.050976 0.044273 -0.559849
# 2000-01-05 0.050976 0.044273 -0.559849
# ... ... ... ...
# 2002-09-22 0.005011 0.053897 -1.026922
# 2002-09-23 0.005011 0.053897 -1.026922
# 2002-09-24 -0.456542 -1.849051 1.559856
# 2002-09-25 -0.456542 -1.849051 1.559856
# 2002-09-26 1.123162 0.354660 1.128135
#
# [1000 rows x 3 columns]
この例では、時系列のコレクションを1年単位で切り分け、そのグループに対して個別にfillnaを呼び出します。
nlargest
とnsmallest
メソッドはSeries
スタイルのgroupbyで動作します。
s = pd.Series([9, 8, 7, 5, 19, 1, 4.2, 3.3])
g = pd.Series(list("abababab"))
gb = s.groupby(g)
gb.nlargest(3)
# a 4 19.0
# 0 9.0
# 2 7.0
# b 1 8.0
# 3 5.0
# 7 3.3
# dtype: float64
gb.nsmallest(3)
# a 6 4.2
# 2 7.0
# 0 9.0
# b 5 1.0
# 7 3.3
# 3 5.0
# dtype: float64
柔軟なapply
グループ化されたデータに対する操作の中には、集計や変換のカテゴリーに当てはまらないものがあるかもしれません。また、GroupByに結果の結合方法を推測させたい場合もあります。このような場合は、apply
関数を使用します。この関数は、多くの標準的なユースケースでaggregate
とtransform
の両方の代わりを務めます。しかし、apply
は、いくつかの例外的なユースケースを扱うことができます。
apply
は、渡されるものによって、reducer、transformer、filterのいずれかの関数として動作することができます。渡された関数とグループ化するものに依存することがあります。したがって、グループ化されたカラムは、インデックスの設定と同様に、出力に含まれるかもしれません。
df
# A B C D
# 0 foo one -0.575247 1.346061
# 1 bar one 0.254161 1.511763
# 2 foo two -1.143704 1.627081
# 3 bar three 0.215897 -0.990582
# 4 foo two 1.193555 -0.441652
# 5 bar two -0.077118 1.211526
# 6 foo one -0.408530 0.268520
# 7 foo three -0.862495 0.024580
grouped = df.groupby("A")
# .describe()を呼び出すことも可能
grouped["C"].apply(lambda x: x.describe())
# A
# bar count 3.000000
# mean 0.130980
# std 0.181231
# min -0.077118
# 25% 0.069390
# ...
# foo min -1.143704
# 25% -0.862495
# 50% -0.575247
# 75% -0.408530
# max 1.193555
# Name: C, Length: 16, dtype: float64
また、返される結果の次元が変わることもあります。
grouped = df.groupby('A')['C']
def f(group):
return pd.DataFrame({'original': group,
'demeaned': group - group.mean()})
シリーズにapply
すると、適用した関数の戻り値(それ自体がシリーズ)を操作し、場合によってはその結果をデータフレームにアップキャストすることができます。
def f(x):
return pd.Series([x, x ** 2], index=["x", "x^2"])
s = pd.Series(np.random.rand(5))
s
# 0 0.321438
# 1 0.493496
# 2 0.139505
# 3 0.910103
# 4 0.194158
# dtype: float64
s.apply(f)
# x x^2
# 0 0.321438 0.103323
# 1 0.493496 0.243538
# 2 0.139505 0.019462
# 3 0.910103 0.828287
# 4 0.194158 0.037697
group_keys
によるグループ化された列の配置の制御
groupby()
の呼び出し時にgroup_keys=True
が指定された場合、同じようなインデックスを持つ出力を返すapply
に渡された関数は、グループのキーが結果のインデックスに追加されます。以前のバージョンのpandasでは、適用された関数からの結果が入力と異なるインデックスを持つ場合にのみ、グループキーが追加されました。group_keys
が指定されない場合、グループキーは同じインデックスを持つ出力には追加されません。将来的には、この挙動は常にgroup_keys
を尊重するように変更される予定です(デフォルトはTrue
です)。
バージョン1.5.0で変更
グループ化されたカラムがインデックスに含まれるかどうかを制御するには、group_keys
という引数を使用します。以下の2つを比較してください。
df.groupby("A", group_keys=True).apply(lambda x: x)
# A B C D
# A
# bar 1 bar one 0.254161 1.511763
# 3 bar three 0.215897 -0.990582
# 5 bar two -0.077118 1.211526
# foo 0 foo one -0.575247 1.346061
# 2 foo two -1.143704 1.627081
# 4 foo two 1.193555 -0.441652
# 6 foo one -0.408530 0.268520
# 7 foo three -0.862495 0.024580
df.groupby("A", group_keys=False).apply(lambda x: x)
# A B C D
# 0 foo one -0.575247 1.346061
# 1 bar one 0.254161 1.511763
# 2 foo two -1.143704 1.627081
# 3 bar three 0.215897 -0.990582
# 4 foo two 1.193555 -0.441652
# 5 bar two -0.077118 1.211526
# 6 foo one -0.408530 0.268520
# 7 foo three -0.862495 0.024580
ユーザー定義関数による集計と同様に、結果のデータ型は適用した関数のものが反映されます。異なるグループの結果が異なるデータ型を持つ場合、DataFrame
の構築と同じ方法で共通のデータ型が決定されます。
Numba高速化ルーチン
バージョン1.1から
Numbaがオプションの依存関係としてインストールされている場合、transform
およびaggregate
メソッドはengine='numba'
とengine_kwargs
引数をサポートします。引数の一般的な使用方法とパフォーマンスに関する考察は、「Numbaによるパフォーマンスの向上」を参照してください。
関数の書式は、各グループに属するデータがvalues
に渡され、グループのインデックスがindex
に渡されるように、正確にvalue, index
で始まる必要があります。
engine='numba'
を使用した場合、内部では「フォールバック」動作は行われません。グループデータとグループインデックスはNumPyの配列としてJITされたユーザー定義関数に渡され、代替実行は試されません。
その他の便利な機能
「余計な」カラムの自動排除
もう一度、先ほどのデータフレームの例を考えてみましょう。
df
# A B C D
# 0 foo one -0.575247 1.346061
# 1 bar one 0.254161 1.511763
# 2 foo two -1.143704 1.627081
# 3 bar three 0.215897 -0.990582
# 4 foo two 1.193555 -0.441652
# 5 bar two -0.077118 1.211526
# 6 foo one -0.408530 0.268520
# 7 foo three -0.862495 0.024580
A
列でグループ化した標準偏差を計算したいとします。しかし、B列のデータはどうでもいいという問題がある。これを「余計な」列と呼ぶ。numeric_only=True
を指定することにより、余計な列を避けることができる。
df.groupby("A").std(numeric_only=True)
# C D
# A
# bar 0.181231 1.366330
# foo 0.912265 0.884785
df.groupby('A').colname.std()
はdf.groupby('A').std().colname
よりも効率が良いので、集計関数の結果が一つの列(ここではcolname
)に対してのみ興味がある場合、集計関数の適用前にフィルタリングすることができることに注意しましょう。
オブジェクト列は、Decimal
オブジェクトのような数値を含む場合、「余計な」列とみなされます。これらは、groupbyにおいて、自動的に集約関数から除外されます。
もし、十進浮動小数列やオブジェクト列を他の邪魔にならないデータ型と一緒に集約したい場合は、明示的にそうする必要があります。
不要なカラムの自動削除は非推奨であり、pandasの将来のバージョンで廃止される予定です。操作できない列が含まれる場合、pandasは代わりにエラーを発生させます。これを避けるには、操作したい列を選択するか、numeric_only=True
を指定してください。
from decimal import Decimal
df_dec = pd.DataFrame(
{
"id": [1, 2, 1, 2],
"int_column": [1, 2, 3, 4],
"dec_column": [
Decimal("0.50"),
Decimal("0.15"),
Decimal("0.25"),
Decimal("0.40"),
],
}
)
# 十進浮動小数の列は、それ自体で明示的に合計することができます。
df_dec.groupby(["id"])[["dec_column"]].sum()
# dec_column
# id
# 1 0.75
# 2 0.55
# しかし、標準的なデータ型と組み合わせることができない場合、それらは除外されます。
df_dec.groupby(["id"])[["int_column", "dec_column"]].sum()
# int_column
# id
# 1 4
# 2 6
# .agg関数を使用して、標準的なデータ型と「余計な」データ型を同時に集計します。
df_dec.groupby(["id"]).agg({"int_column": "sum", "dec_column": "sum"})
# int_column dec_column
# id
# 1 4 0.75
# 2 6 0.55
カテゴリカル値の(未)観測の取り扱い
(単一のグルーパーとして、あるいは複数のグルーパーの一部として)Categorical
グルーパーを使う場合、observed
キーワードは、可能なすべてのグルーパーの値のデカルト積を返すか(observed=False
)、あるいはデータに見られるグルーパーの値のみを返すか(observed=True
)を制御します。
すべての値を表示する。
pd.Series([1, 1, 1]).groupby(
pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=False
).count()
# a 3
# b 0
# dtype: int64
データに見られる値のみを表示する。
pd.Series([1, 1, 1]).groupby(
pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=True
).count()
a 3
dtype: int64
返されるグループ化されたもののdtype
は、常にグループ化されたすべてのカテゴリを含みます。
s = (
pd.Series([1, 1, 1])
.groupby(pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=False)
.count()
)
s.index.dtype
# CategoricalDtype(categories=['a', 'b'], ordered=False)
NA および NaT のグループ処理
グループ化キーにNaNまたはNaTの値がある場合、これらは自動的に除外されます。言い換えれば、「NAグループ」や「NaTグループ」は決して存在しないことになります。これはpandasの古いバージョンではそうではありませんでしたが、ユーザーは一般的には常にNAグループを破棄していました(そしてそれをサポートすることは実装上の頭痛の種でした)。
順序付き因子によるグループ化
pandas のCategorical
クラスのインスタンスとして表されるカテゴリ変数は、グループのキーとして使用することができます。その場合、レベルの順序は保持されます。
data = pd.Series(np.random.randn(100))
factor = pd.qcut(data, [0, 0.25, 0.5, 0.75, 1.0])
data.groupby(factor).mean()
# (-2.645, -0.523] -1.362896
# (-0.523, 0.0296] -0.260266
# (0.0296, 0.654] 0.361802
# (0.654, 2.21] 1.073801
# dtype: float64
グルーパー仕様でグループ化
適切にグループ化するためには、もう少しデータを指定する必要があるかもしれません。pd.Grouper
を使えば、このローカルな制御を行うことができます。
import datetime
df = pd.DataFrame(
{
"Branch": "A A A A A A A B".split(),
"Buyer": "Carl Mark Carl Carl Joe Joe Joe Carl".split(),
"Quantity": [1, 3, 5, 1, 8, 1, 9, 3],
"Date": [
datetime.datetime(2013, 1, 1, 13, 0),
datetime.datetime(2013, 1, 1, 13, 5),
datetime.datetime(2013, 10, 1, 20, 0),
datetime.datetime(2013, 10, 2, 10, 0),
datetime.datetime(2013, 10, 1, 20, 0),
datetime.datetime(2013, 10, 2, 10, 0),
datetime.datetime(2013, 12, 2, 12, 0),
datetime.datetime(2013, 12, 2, 14, 0),
],
}
)
df
# Branch Buyer Quantity Date
# 0 A Carl 1 2013-01-01 13:00:00
# 1 A Mark 3 2013-01-01 13:05:00
# 2 A Carl 5 2013-10-01 20:00:00
# 3 A Carl 1 2013-10-02 10:00:00
# 4 A Joe 8 2013-10-01 20:00:00
# 5 A Joe 1 2013-10-02 10:00:00
# 6 A Joe 9 2013-12-02 12:00:00
# 7 B Carl 3 2013-12-02 14:00:00
特定の列を任意の周期でGroupbyします。これはリサンプリングのようなものです。
df.groupby([pd.Grouper(freq="1M", key="Date"), "Buyer"])[["Quantity"]].sum()
# Quantity
# Date Buyer
# 2013-01-31 Carl 1
# Mark 3
# 2013-10-31 Carl 6
# Joe 9
# 2013-12-31 Carl 3
# Joe 9
グループ化の可能性がある名前付きインデックスと列があるという曖昧な仕様になっています。
df = df.set_index("Date")
df["Date"] = df.index + pd.offsets.MonthEnd(2)
df.groupby([pd.Grouper(freq="6M", key="Date"), "Buyer"])[["Quantity"]].sum()
# Quantity
# Date Buyer
# 2013-02-28 Carl 1
# Mark 3
# 2014-02-28 Carl 9
# Joe 18
df.groupby([pd.Grouper(freq="6M", level="Date"), "Buyer"])[["Quantity"]].sum()
# Quantity
# Date Buyer
# 2013-01-31 Carl 1
# Mark 3
# 2014-01-31 Carl 9
# Joe 18
各グループの最初の行を取得する
データフレームやシリーズと同じように、groupbyでもheadやtailを呼び出すことができます。
df = pd.DataFrame([[1, 2], [1, 4], [5, 6]], columns=["A", "B"])
df
# A B
# 0 1 2
# 1 1 4
# 2 5 6
g = df.groupby("A")
g.head(1)
# A B
# 0 1 2
# 2 5 6
g.tail(1)
# A B
# 1 1 4
# 2 5 6
これは、各グループの最初または最後のn行を表示します。
各グループのn番目の行を取得する
DataFrameやSeriesからn番目の項目を選択するには、nth()
を使用します。これは削減メソッドで、nに整数を渡すとグループごとに1行(または0行)を返します。
df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])
g = df.groupby("A")
g.nth(0)
# B
# A
# 1 NaN
# 5 6.0
g.nth(-1)
# B
# A
# 1 4.0
# 5 6.0
g.nth(1)
# B
# A
# 1 4.0
もし、n番目のnullでない項目を選択したい場合は、dropna
引数を使用してください。データフレームの場合、dropnaに渡すのと同じように、'any'
か'all'
のどちらかを指定する必要があります。
# nth(0) は g.first() に等しい
g.nth(0, dropna="any")
Out[202]:
B
A
1 4.0
5 6.0
g.first()
Out[203]:
B
A
1 4.0
5 6.0
# nth(-1) は g.last() に等しい
g.nth(-1, dropna="any") # NaNs denote group exhausted when using dropna
Out[204]:
B
A
1 4.0
5 6.0
g.last()
Out[205]:
B
A
1 4.0
5 6.0
g.B.nth(0, dropna="all")
Out[206]:
A
1 4.0
5 6.0
Name: B, dtype: float64
他のメソッドと同様に、as_index=False
を渡すと、フィルタリングが行われ、グループ化された行が返されます。
df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])
g = df.groupby("A", as_index=False)
g.nth(0)
# A B
# 0 1 NaN
# 2 5 6.0
g.nth(-1)
# A B
# 1 1 4.0
# 2 5 6.0
また、複数のn番目の値をint型のリストとして指定することで、各グループから複数行を選択することができます。
business_dates = pd.date_range(start="4/1/2014", end="6/30/2014", freq="B")
df = pd.DataFrame(1, index=business_dates, columns=["a", "b"])
# 各月の1日・4日・最終日を取得
df.groupby([df.index.year, df.index.month]).nth([0, 3, -1])
# a b
# 2014 4 1 1
# 4 1 1
# 4 1 1
# 5 1 1
# 5 1 1
# 5 1 1
# 6 1 1
# 6 1 1
# 6 1 1
グループ項目の数え上げ
グループ内の各行の表示順を確認するには、cumcount
メソッドを使用します。
dfg = pd.DataFrame(list("aaabba"), columns=["A"])
dfg
# A
# 0 a
# 1 a
# 2 a
# 3 b
# 4 b
# 5 a
dfg.groupby("A").cumcount()
# 0 0
# 1 1
# 2 2
# 3 0
# 4 1
# 5 3
# dtype: int64
dfg.groupby("A").cumcount(ascending=False)
# 0 3
# 1 2
# 2 1
# 3 1
# 4 0
# 5 0
# dtype: int64
グループの数え上げ
(cumcount
で与えられるグループ内の行の順序とは反対に)グループの順序を見るには、ngroup()
を使用することができます。
グループに与えられた番号は、groupbyオブジェクトを反復処理したときにグループが見える順番と一致していることを知っておいてください。
dfg = pd.DataFrame(list("aaabba"), columns=["A"])
dfg
# A
# 0 a
# 1 a
# 2 a
# 3 b
# 4 b
# 5 a
dfg.groupby("A").ngroup()
# 0 0
# 1 0
# 2 0
# 3 1
# 4 1
# 5 0
# dtype: int64
dfg.groupby("A").ngroup(ascending=False)
# 0 1
# 1 1
# 2 1
# 3 0
# 4 0
# 5 1
# dtype: int64
プロット
groupbyは、いくつかのプロット方法とも連動しています。例えば、データフレームのいくつかの特徴量がグループによって異なる可能性があるとします。次の場合、グループが「B」である列1の値は、平均して3高いということです。
np.random.seed(1234)
df = pd.DataFrame(np.random.randn(50, 2))
df["g"] = np.random.choice(["A", "B"], size=50)
df.loc[df["g"] == "B", 1] += 3
これを箱ひげ図で簡単に視覚化することができます。
df.groupby("g").boxplot()
# A AxesSubplot(0.1,0.15;0.363636x0.75)
# B AxesSubplot(0.536364,0.15;0.363636x0.75)
# dtype: object
boxplot
の結果は、グループ化列g
の値(「A」と「B」)をキーとする辞書になります。結果の辞書の値は、boxplot
のreturn_type
キーワードで制御することができます。詳しくは、可視化のドキュメントを参照してください。
歴史的な理由により、 df.groupby("g").boxplot()
はdf.boxplot(by="g")
と同等ではありません。説明についてはこちらを参照してください。
関数呼び出しのパイプ化
DataFrame
やSeries
が提供する機能と同様に、GroupBy
オブジェクトを受け取る関数は、pipe
メソッドを使用して連結することができ、よりすっきりとした読みやすい構文にすることができます。.pipe
の一般的な説明は、こちらをご覧ください。
.groupby
と.pipe
の組み合わせは、GroupByオブジェクトを再利用する必要がある場合によく役に立ちます。
例として、stores、products、revenue、quantity soldのカラムを持つDataFrameがあるとします。店舗ごと、商品ごとの価格(つまり、収益/数量)をグループ単位で計算したいと思います。これを多段階で行うこともできますが、パイプで表現することでコードを読みやすくすることができます。まず、データをセットします。
n = 1000
df = pd.DataFrame(
{
"Store": np.random.choice(["Store_1", "Store_2"], n),
"Product": np.random.choice(["Product_1", "Product_2"], n),
"Revenue": (np.random.random(n) * 50 + 10).round(2),
"Quantity": np.random.randint(1, 10, size=n),
}
)
df.head(2)
# Store Product Revenue Quantity
# 0 Store_2 Product_1 26.12 1
# 1 Store_2 Product_1 28.86 1
そして、店舗・商品ごとの価格を求めるのは、次のように単純です。
(
df.groupby(["Store", "Product"])
.pipe(lambda grp: grp.Revenue.sum() / grp.Quantity.sum())
.unstack()
.round(2)
)
# Product Product_1 Product_2
# Store
# Store_1 6.82 7.05
# Store_2 6.30 6.64
また、グループ化されたオブジェクトを任意の関数に受け渡す場合などにも、パイプは表現力を発揮します。
def mean(groupby):
return groupby.mean()
df.groupby(["Store", "Product"]).pipe(mean)
# Revenue Quantity
# Store Product
# Store_1 Product_1 34.622727 5.075758
# Product_2 35.482815 5.029630
# Store_2 Product_1 32.972837 5.237589
# Product_2 34.684360 5.224000
ここでmean
はGroupByオブジェクトを受け取り、StoreとProductの組み合わせについてRevenueとQuantity列の平均をそれぞれ求めます。mean
関数は、GroupByオブジェクトを受け取る任意の関数です。.pipe
は、GroupByオブジェクトをパラメータとして指定した関数に渡します。
例
係数による再グループ化
DataFrameのカラムをその合計によって再グループ化し、集約されたものを合計します。
df = pd.DataFrame({"a": [1, 0, 0], "b": [0, 1, 0], "c": [1, 0, 0], "d": [2, 3, 4]})
df
# a b c d
# 0 1 0 1 2
# 1 0 1 0 3
# 2 0 0 0 4
df.groupby(df.sum(), axis=1).sum()
# 1 9
# 0 2 2
# 1 1 3
# 2 0 4
複数列のラベルエンコード
ngroup()
を使用することで、(reshaping APIでさらに説明されているように) factorize()
と同様の方法でグループに関する情報を抽出することができますが、これは、混合型および異なるソースの複数の列に対して自然に適用されます。これは、グループ行の間の関係がその内容よりも重要である場合、または整数エンコーディングのみを受け付けるアルゴリズムへの入力として、処理におけるカテゴリ的な中間段階として有用である可能性があります。(pandasにおける完全なカテゴリカルデータのサポートについての詳細は、カテゴリカルの説明とAPI documentationを参照してください)。
dfg = pd.DataFrame({"A": [1, 1, 2, 3, 2], "B": list("aaaba")})
dfg
# A B
# 0 1 a
# 1 1 a
# 2 2 a
# 3 3 b
# 4 2 a
dfg.groupby(["A", "B"]).ngroup()
# 0 0
# 1 0
# 2 1
# 3 2
# 4 1
# dtype: int64
dfg.groupby(["A", [0, 0, 0, 1, 1]]).ngroup()
# 0 0
# 1 0
# 2 1
# 3 3
# 4 2
# dtype: int64
データを「リサンプリング」するためのインデクサによるGroupby
リサンプリング(再標本化)は、すでに存在する観測データ、あるいはデータを生成するモデルから新しい仮想的なサンプル(リサンプル)を生成する。これらの新しいサンプルは、既存のサンプルと類似しています。
データタイムに依存しないインデックスに対応するためにリサンプリングを行うには、以下の手順を利用することができます。
以下の例では、df.index // 5はバイナリ配列を返し、groupby操作で何が選択されるかを決定するために使用されています。
以下の例では、サンプルをより少ないサンプルに集約するダウンサンプリングの方法を示しています。ここでは、df.index // 5を用いて、サンプルをビン単位で集約しています。**std()**関数を適用することで、多くのサンプルに含まれる情報を、その標準偏差である小さな値のサブセットに集約し、サンプル数を減らしているのです。
df = pd.DataFrame(np.random.randn(10, 2))
df
# 0 1
# 0 -0.793893 0.321153
# 1 0.342250 1.618906
# 2 -0.975807 1.918201
# 3 -0.810847 -1.405919
# 4 -1.977759 0.461659
# 5 0.730057 -1.316938
# 6 -0.751328 0.528290
# 7 -0.257759 -1.081009
# 8 0.505895 -1.701948
# 9 -1.006349 0.020208
df.index // 5
# Int64Index([0, 0, 0, 0, 0, 1, 1, 1, 1, 1], dtype='int64')
df.groupby(df.index // 5).std()
# 0 1
# 0 0.823647 1.312912
# 1 0.760109 0.942941
名前を伝搬させるシリーズを返す
DataFrameの列をグループ化し、一連のメトリクスを計算し、名前を付けたシリーズを返します。シリーズの名前は、列インデックスの名前として使用されます。これは、列インデックス名が挿入された列の名前として使用されるスタッキングなどのリシェイプ操作と組み合わせて使用すると特に便利です。
df = pd.DataFrame(
{
"a": [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2],
"b": [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1],
"c": [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
"d": [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
}
)
def compute_metrics(x):
result = {"b_sum": x["b"].sum(), "c_mean": x["c"].mean()}
return pd.Series(result, name="metrics")
result = df.groupby("a").apply(compute_metrics)
result
# metrics b_sum c_mean
# a
# 0 2.0 0.5
# 1 2.0 0.5
# 2 2.0 0.5
result.stack()
# a metrics
# 0 b_sum 2.0
# c_mean 0.5
# 1 b_sum 2.0
# c_mean 0.5
# 2 b_sum 2.0
# c_mean 0.5
# dtype: float64