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


