LoginSignup
6

More than 1 year has passed since last update.

Pandasユーザーガイド「Group by - 分割・適用・結合」(公式ドキュメント日本語訳)

Last updated at Posted at 2022-09-21

本記事は、Pandas の公式ドキュメントのUser Guide - Group by: split-apply-combineを機械翻訳した後、一部の不自然な文章を手直ししたものである。

誤訳の指摘・代訳案・質問等があればコメント欄や編集リクエストでお願いします。

Pandas公式ドキュメント日本語訳記事一覧

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 APIwindow APIresample 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で最適化された集計関数

いくつかの一般的な集約は(現在はsummeanstdsemのみですが)、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

もちろん、summeanは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を使用する場合、inplaceFalseでなければなりません(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:>

groupby_transform_plot.png

出力が低次元である変換関数は、入力配列の形状に合わせてブロードキャストされます。

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を渡しても、これらの変換メソッドには影響しません。

例:fillnaffillbfillshiftなど

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を渡すと、これらの変換メソッドには影響しません。

例:headtail

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関数)に対して任意の引数を持つ関数を呼び出します。その結果は、aggtransformのようなスタイルで結合されます(実際には、次に述べるように、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を呼び出します。

nlargestnsmallestメソッドは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関数を使用します。この関数は、多くの標準的なユースケースでaggregatetransformの両方の代わりを務めます。しかし、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です)。

:warning: バージョン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

groupby_boxplot.png

boxplotの結果は、グループ化列gの値(「A」と「B」)をキーとする辞書になります。結果の辞書の値は、boxplotreturn_typeキーワードで制御することができます。詳しくは、可視化のドキュメントを参照してください。

歴史的な理由により、 df.groupby("g").boxplot()df.boxplot(by="g")と同等ではありません。説明についてはこちらを参照してください。

関数呼び出しのパイプ化

DataFrameSeriesが提供する機能と同様に、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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6