技研のまつけんです。

 

Pythonでデータの集計をする際はPandasが便利です。特にgroupby()とcount()やsum()やagg()を組み合わせると、大抵の集計は出来てしまいます。集計する方法などもサーチエンジンなどで検索すると豊富に例が得られます。しかしながら、その結果を取り出す (参照する) 方法については、なかなか情報が見つかりません。結局、index/columns/loc/ilocなどを組み合わせることで取り出すことにしました。今回は、groupby(…).count()の結果をループなどで取り出す方法や、Python標準のdict形式に変換する方法について紹介したいと思います。

テストデータと読み込み

ID、住所(都道府県)、姓、名からなる架空の会員データのようなもの:

id addr surname firstname
1 chiba ito ichiro
2 chiba kato jiro
3 chiba sato ichiro
4 chiba sato jiro
5 saitama kato ichiro
6 saitama kato shiro
7 saitama muto jiro
8 saitama muto shiro
9 saitama sato shiro
10 tokyo ito jiro
11 tokyo kato shiro
12 tokyo muto saburo
13 tokyo sato ichiro
14 tokyo sato shiro

をsample.csvというファイル名で保存し、

import pandas as pd
df = pd.read_csv('sample.csv')

で読み込んで利用します。

集計の基本操作

groupby()やcount()の使い方自体についての情報は豊富にあるので割愛しますが、以下のようなバリエーションが考えられます:

df.groupby('addr').count() # 住所で集計
df.groupby(['addr', 'surname']).count() # 住所と姓で集計
df.groupby(['surname', 'firstname']).count() # 姓と名で集計
df.groupby(['addr', 'surname', 'firstname']).count() # 住所と姓と名で集計

例えば、2番目の「住所と姓で集計」を実行すると、以下のような結果が得られます:

    id firstname
addr surname    
chiba ito 1 1
kato 1 1
sato 2 2
saitama kato 2 2
muto 2 2
sato 1 1
tokyo ito 1 1
kato 1 1
muto 1 1
sato 2 2

この結果から、例えば「chibaのsatoさん」や「saitamaのkatoさん」は2人いることが分かります。

戻り値の参照 (基本)

df.groupby(…).count()の結果は、pandas.DataFrame形式で返されます。「chibaのsatoさんの人数」をn_chiba_satoに代入するには、loc/ilocを使って以下のように記述します:

cnt = df.groupby(['addr', 'surname']).count()
n_chiba_sato = cnt.loc['chiba', 'sato'].iloc[0]

また、「tokyoのkato shiroさんの人数」は、以下のように取り出します:

cnt = df.groupby(['addr', 'surname', 'firstname']).count()
n_tokyo_kato_shiro = cnt.loc['tokyo', 'kato', 'shiro'].iloc[0]

戻り値の参照 (ネストしたdictへの変換)

さて、ここで、cnt.loc['chiba', 'sato'].iloc[0]ではなく、dic['chiba']['sato']のようにネストした (ツリー状の) dict形式で扱いたいという要望が (私個人としては) あります。pandas.DataFrameには、to_dict()というメソッドがありますが、これをそのまま適用しても期待通りになりません。

dic = cnt.to_dict()[cnt.columns[0]]

を実行しても

{('chiba', 'ito', 'ichiro'): 1,
('chiba', 'kato', 'jiro'): 1,
('chiba', 'sato', 'ichiro'): 1,
(中略)
('tokyo', 'sato', 'ichiro'): 1,
('tokyo', 'sato', 'shiro'): 1}

のように、keyがtupleになってしまうからです。なお、dic = cnt.to_dict()[cnt.columns[0]]は、

dic = {}
for index in cnt.index:
dic[index] = cnt.loc[index].iloc[0]

と、同じ結果です。

Keyがtupleなdictは、正規化されていないデータベースのようなものです。ならば、これをデータベースの正規化と同様の処理をしてやれば良いことになります。

def normalise_dict(dic):
dic2 = {}
for key in dic:
recursive = False
if isinstance(key, tuple):
if 2 < len(key):
key2 = key[: -1]
recursive = True
else:
key2 = key[0]
if key2 not in dic2:
dic2[key2] = {}
dic2[key2][key[-1]] = dic[key]
else:
dic2[key] = dic[key]
if recursive:
return normalise_dict(dic2)
else:
return dic2
dic = normalise_dict(dic)

これを実行すると

{'chiba':
{'ito': {'ichiro': 1},
'kato': {'jiro': 1},
'sato': {'ichiro': 1, 'jiro': 1}},
'saitama':
{'kato': {'ichiro': 1, 'shiro': 1},
'muto': {'jiro': 1, 'shiro': 1},
'sato': {'shiro': 1}},
'tokyo':
{'ito': {'jiro': 1},
'kato': {'shiro': 1},
'muto': {'saburo': 1},
'sato': {'ichiro': 1, 'shiro': 1}}}

となり、所望の形に変換できていることが分かります。

このように、ネストしたdictにすることでfor key in dicのようなループでデータ中に存在する都道府県だけ処理するということが簡単に出来ます。

先述の「keyがtupleのdict」にする部分も含めて

def agg2dict(agg, column=None):
if column is None:
column = agg.columns[0]
return agg.to_dict()[column]
def agg2dict_tree(agg, column=None):
return normalise_dict(agg2dict(agg, column=column))

を定義しておけば、dic = agg2dict_tree(cnt)で集計結果をネストしたdictにすることが出来ます。

まとめ

Pandasにおいて、groupby()とcount()を使って得た結果を取り出す (参照する) 方法とネストしたdictに変換する方法について説明しました。

この方法は、count()だけでなく、sum()やmean()に対しても使えます。例えば、今回のテストデータには含まれていませんが、ageとscoreが入っている場合

df['age10'] = (df.age / 10).astype('i') * 10 # 年齢の階級値の列を追加
mn = df.groupby(['addr', 'age10']).mean()
dic = agg2dict_tree(mn, 'score')

を実行することで、dic['tokyo'][10]で「tokyoの10歳代のscoreの平均値」が得られます。

なお、ネストしたdictに正規化されたものを正規化されていない状態に戻すには (3階層ならば)

denorm_dic = {} # denormalised dict
for key1 in dic:
for key2 in dic[key1]:
for key3 in dic[key1][key2]:
denorm_dic[(key1, key2, key3)] = dic[key1][key2][key3]

のようにすれば可能です。こちも再帰呼び出しを使って戻すルーチンを書くことも出来ますが、特に必要性を感じないので作っていません。よろしければ、頭の体操がてら挑戦してみてください。