pandasのDataFrame にはデータを集計するためのgroupby や resampleというメソッドが用意されています。
個々の使い方については他のサイトで解説されていますが、体系的に解説している記事が見つからなかったので、今回はこれを取り上げました。
図を使って説明していますので、興味のあるかたは是非ご一読下さい。
pandasを使った集計の概要
pandasのDataFrameに格納されているデータに対して、groupby や resample というメソッドを使うとことで、グルーピング集計が可能です。
groupby メソッドは任意の数値型、文字列型のカラムに対して集計するのに対し、resample は時系列データに対しての集計を行います。
これらグループ化メソッドを使うとオブジェクトが生成されますので、そのオブジェクトにある集計メソッドを呼び出すことで集計結果を受取ることが出来ます。

集計結果は DataFrame として返って来ますが、階層型インデックス(MultiIndex)になっているため、値の取り出し方には少しだけ注意が必要です。

集計結果から値を取り出すには、通常通り添え字にカラム名と行番号を指定すれば良いのですが、インデックスとなっている元のカラム名は指定できません。
下図は売上年月日と商品名で構成されたMultiIndexの例です。
MultiIndexは1つ以上のカラム名がタプルで構成されており、index[行番号] で取得することが可能です。

このアクセス方法が面倒な場合は、reset_index メソッドを使ってMultiIndexを解除すれば通常のDataFrameと同じアクセスができるようになります。
df.reset_index()
という1行を実行するだけで解除可能です。
ちなみに、MultiIndexを完全に削除したい場合は、引数に drop=Trueを指定します。
df.reset_index(drop= True)
groupby を使った集計
groupby によるグループ化は引数にグルーピング対象のカラム名と、集計対象のカラム名を記述します。
df.groupby(グルーピング対象カラム名)[集計対象カラム名]
複数のカラム名で階層的にグループ化したい場合、リスト形式でカラム名を列挙します。
df.groupby([グルーピング対象カラム名1,・・・])[ [集計対象カラム名1,・・・] ]
集計対象のカラム名を省略すると、全カラムに対して集計が行われます。
df.groupby(グルーピング対象のカラム名)

集計メソッドに組み込み集約関数を使う
集計関数の中には、NumPyやPandasを使っているものもあるので、必要に応じてimport して下さい。ここではよく使われる集計関数を紹介しておきます。
詳細な情報が必要な場合は、pandas公式ドキュメントをご参照ください。
メソッド名 | 内容 |
---|---|
mean() | 平均を求める |
sum() | 合計を求める |
max() | 最大を求める |
min() | 最小を求める |
median() | 中央値を求める |
std() | 標準偏差を求める |
var() | 分散を求める |
count() | 行数を求める |
first() | 先頭の1行を取得 |
last() | 最後の1行を取得 |
cumsum() | 累計和を求める |
では、具体的な例を挙げてみます。
下記はサンプルデータです。この記事に掲載したプログラムを実際に動かしたい場合、メモ帳等でCSVファイルとして保存してお使いください。
1 2 3 4 5 6 7 8 9 10 11 |
売上年月日,商品名,単価,数量 2021-7-1,ケーキ,200,5 2021-7-1,ケーキ,200,3 2021-7-2,ケーキ,200,1 2021-7-2,ケーキ,150,2 2021-7-2,バナナ,100,5 2021-7-2,プリン,150,4 2021-7-3,バナナ,100,5 2021-7-3,プリン,150,6 2021-7-3,バナナ,100,5 2021-7-3,ケーキ,200,2 |
p:ドライブに group_data.csv という名前のCSVがあり、上記データが格納されているという前提で、商品名でグループ化し、それぞれの合計を計算する場合、次の様になります。
1 2 3 4 5 |
import pandas as pd df = pd.read_csv('p:/group_data.csv',encoding='shift-jis',sep=',') df.groupby('商品名').sum() |

商品名だけで集計しているため、インデックス以外のカラム(単価と数量)はそれぞれ合計値が計算されます。

次は、売上年月日と商品名でグループ化してみます。
1 2 3 4 |
import pandas as pd df = pd.read_csv('p:/group_data.csv',encoding='shift-jis',sep=',') df.groupby(['売上年月日','商品名']).sum() |

売上年月日と商品名が index となっていることが分かると思います。
グルーピング集計した結果の行数(=インデックスの個数)を取得する場合は、 size プロパティを使います。
df.index.size
1 2 3 4 5 6 7 8 9 10 11 12 |
import pandas as pd df = pd.read_csv('p:/group_data.csv',encoding='shift-jis',sep=',') res = df.groupby(['売上年月日','商品名']).sum() #sizeで取得したインデックス個数を使ったループ for i in range(res.index.size): print(res.index[i]) #indexを使ったループ for i in res.index: print(i) |
集計メソッドに agg メソッド を使う
agg (Aggregationの略称) は指定したカラムに対して、集計関数を適用するメソッドです。集計したいカラムと集計関数を辞書形式で渡すことで、一度の呼び出しで複数カラムの集計が可能です。
df.groupby(グルーピング対象カラム名).agg({集計対象カラム名1: 集計関数1,・・・})
1 |
df.groupby('商品名').agg({'単価':'mean','数量':'sum'}) |

‘mean’ や ‘sum’ の代わりに、ラムダ式を使うことも出来ます。
1 |
df.groupby('商品名').agg({'単価': lambda x: x.mean(), '数量': lambda x: x.sum()}) |

また、ユーザー定義関数を作成し、agg から呼び出すことも可能です。
1 2 3 4 5 6 7 8 9 10 |
#列の平均を求めるユーザー定義関数 def my_mean(x): return x.mean() #列の合計を求めるユーザー定義関数 def my_sum(x): return x.sum() #aggにユーザー定義関数を使った例 df.groupby('商品名').agg({'単価': my_mean, '数量': my_sum}) |

集計メソッドに apply を使う
agg はカラム単位に処理を行うメソッドで集計関数しか適用できませんでしたが、apply は行単位に任意の処理を実行できるメソッドです。
df.groupby(グルーピング対象カラム名).apply(ラムダ式又は関数)
ラムダ式又は関数には、グルピングされたデータ(複数レコード)が引数として渡されます。ラムダ式又は関数からは、DataFrame,Series,スカラなどを返すことが可能ですが、それぞれの挙動がややこしいので、ここではDataFrameを返す方法を紹介します。
下記のサンプルでは、全てのカラムに対して、mean()が実行されます。但し sum() の場合は文字列を結合してしまうため、商品名の欄が崩れてしまいます。
1 |
df.groupby('商品名').apply(lambda x:x.mean()) |

ラムダ式の中でカラムを指定できますが、全て同じ計算式を適用しなければなりません。例えば、単価は平均、数量は合計といった事が出来ません。
1 |
df.groupby('商品名').apply(lambda x:x[['単価','数量']].mean()) |

DataFrameに格納して返すことで、任意の集計結果を表形式で得ることが出来ます。今回はカラム名を「単価平均」「数量合計」に変えてみました。
1 |
df.groupby('商品名').apply(lambda x: pd.DataFrame({'単価平均': [x['単価'].mean()], '数量合計': [x['数量'].sum()]})) |

商品目の次に0が並んでいますが、これはインデックスです。グルーピングの単位でラムダ式が呼ばれますが、ラムダ式で1レコードだけを格納したDataFrameを作成しているため、行の先頭にインデックスの0が付加されてしまいました。
以下は関数を使って集計するサンプルです。ラムダ式と違って関数は複雑なことが記述できるので、応用範囲が広がります。
1 2 3 4 5 6 7 8 9 10 |
#関数の定義 def my_func(group): # group を直接書き換えることが出来ないのでコピーしたものを書き換える row = group.iloc[0].copy() row['単価'] = group['単価'].mean() row['単価×数量'] = row['単価'] * row['数量'] return row #applyの実行 df.groupby('商品名').apply(my_func) |

row = group.iloc[0].copy() は グルーピングされた結果(複数行)の先頭レコードを iloc[0] で取得し、それをコピーして row に格納しています。
group を直接編集することが出来ない仕様なので、コピーしたものを使う必要があります。
関数に任意の引数を渡したい場合、呼び出し側は 次の様に記述します。
apply(関数名,引数1,引数2,・・・)
ユーザー関数側は 次の様に記述します。
my_func(groupbyから受け取る引数名,applyから渡される任意の引数1,・・・)
1 2 3 4 5 6 |
#引数が2個の関数定義 def my_func(group,num): return group * num #任意の引数を渡す呼び出し方 df.groupby('商品名').apply(my_func,3) |

resampleを使った集計
resample は時系列に特化したグルーピング集計です。
resample の引数には集計期間を示す文字を指定します。
df.resample(集計期間)

集計期間には次の文字が指定できます。
また、文字の前に数値を書くことで、2日や3ヶ月といった指定も可能です。
集計期間 | 文字 | 使用例 |
---|---|---|
日 | D | resample(‘2D’) ⇒2日ごとの集計 |
週 | W | resample(‘3W’) ⇒3週間ごとの集計 |
月 | M | resample(‘M’) ⇒1か月ごとの集計 |
四半期 | Q | resample(‘Q’) ⇒四半期ごとの集計 |
年 | Y | resample(‘5Y’) ⇒5年ごとの集計 |
先ほどの例では、売上年月日を扱いましたが、あれは文字列として取り扱っていました。
resample で集計を行うには、インデックスが datetime型である必要があります。
read_csv で指定したカラムをdatetime型のインデックスとして取り込むためには、index_col 引数とparse_dates 引数を指定します。
pd.read_dsv(ファイル名,index_col=カラム番号,parse_dates=True)
先ほどのCSVを例に、売上年月日をdatetime型のインデックスとして指定し、月ごとの集計を行うには次の様になります。
1 2 3 4 |
import pandas as pd df = pd.read_csv('p:/group_data.csv',encoding='shift-jis',sep=',',index_col=0,parse_dates=True) df.resample('M').sum() |

今回のサンプルデータは7月のデータしかないので、1か月ごとの集計では1行となります。

まとめ
今回はpandas DataFrame を使った集計について、groupby と resample を紹介いたしました。
単に月ごとや年毎であれば、文字列として扱って groupby すれば事足りますが、2日ごとや1週間ごとなどの集計では resample を使う方が圧倒的に便利です。
resample は datetime 型のインデックスで集計するため、もちろん1時間ごとや5分毎、秒単位での集計も可能です。
CSVデータをサクっと集計するには非常に便利ですが、慣れない間はけっこう悩んでしまうのではないでしょうか。
そんな方のために、今回の記事がお役に立てれば幸いです。