Edited at

pandasのMultiIndexについて

More than 1 year has passed since last update.

この記事はPython その2 Advent Calendar 2015の19日目の記事です.

Advent Calendar登録してみたものの、最近Pythonそんな書いていない&書いてても業務上特殊なことをやり過ぎてて書けないので、ちょっと今更感はありますがpandasの小ネタです。

データの前処理や集計等でおなじみのpandas、知ってはいるけどちょっと使いどころがわかりにくいMultiindexついて書きます。


執筆時の環境


  • Python 2.7.11 |Anaconda 2.0.1 (x86_64)

  • pandas 0.17.0

  • 実行環境: jupyter notebook

以下出てくるコードは

import pandas as pd

してある前提で書いてあります。また、貼り付けているテーブルはto_htmlしたものをコピペしているので、jupyter notebook上の表示とは若干異なります。


MultiIndexって何

MultiIndex / Advanced Indexing

pandasのDataFrame等で、複数の列をインデックスにする機能。複数の値をキーにしたらデータが一意に特定されるような場合に便利。


データの用意と普通のインデックスのおさらい

普通のインデックスをこの記事ではMultiIndexとの対比で便宜上SingleIndexと呼びます。(そういう型があるわけではない)


データの準備

説明用に次のようなcsvを用意する


sample.csv

name,year,product,expected,actual

Taro,2013,A,116,119
Taro,2013,B,131,149
Taro,2014,A,151,124
Taro,2014,B,125,107
Taro,2015,A,135,109
Taro,2015,B,142,148
Hanako,2013,A,170,130
Hanako,2013,B,113,190
Hanako,2014,A,102,142
Hanako,2014,B,183,125
Hanako,2015,A,169,120
Hanako,2015,B,134,199

これをpandasで読み込む。

df1 = pd.read_csv('sample.csv')




name
year
product
expected
actual




0
Taro
2013
A
116
119


1
Taro
2013
B
131
149


2
Taro
2014
A
151
124


3
Taro
2014
B
125
107


4
Taro
2015
A
135
109


5
Taro
2015
B
142
148


6
Hanako
2013
A
170
130


7
Hanako
2013
B
113
190


8
Hanako
2014
A
102
142


9
Hanako
2014
B
183
125


10
Hanako
2015
A
169
120


11
Hanako
2015
B
134
199

このテーブルの行ラベルになっている0,1,2...data.index、列ラベルの"name", "year",...data.columnsでどちらもpandas.Indexというデータ型になっている。

pandas.Indexnameというメンバを持っているので

df1.index.name = "id"

df1.columns.name = "header"

とすると



header
name
year
product
expected
actual


id









0
Taro
2013
A
116
119


1
Taro
2013
B
131
149


2
Taro
2014
A
151
124


3
Taro
2014
B
125
107


4
Taro
2015
A
135
109


5
Taro
2015
B
142
148


6
Hanako
2013
A
170
130


7
Hanako
2013
B
113
190


8
Hanako
2014
A
102
142


9
Hanako
2014
B
183
125


10
Hanako
2015
A
169
120


11
Hanako
2015
B
134
199

こんなテーブルになる。


データの選択


  • 列で選択

df1["name"]


出力

id

0 Taro
1 Taro
2 Taro
3 Taro
4 Taro
5 Taro
6 Hanako
7 Hanako
8 Hanako
9 Hanako
10 Hanako
11 Hanako
Name: name, dtype: object


  • 行で選択

df1.loc[3]


出力

header

name Taro
year 2014
product B
expected 125
actual 107
Name: 3, dtype: object


  • 行インデックスで選択

df1.iloc[3]

今回はindexが行インデックスなので出力は同じ。


  • 行と列で選択

df1.ix[3:5,"product":"actual"]

出力




product
expected
actual




3
B
125
107


4
A
135
109


5
B
142
148


  • 条件で選択

df1[(df1["name"] == "Hanako")&(df1["product"] == "A")]

出力



header
name
year
product
expected
actual


id









6
Hanako
2013
A
170
130


8
Hanako
2014
A
102
142


10
Hanako
2015
A
169
120

詳しくは他の記事や書籍参照。


MultiIndexの設定

ここからが本題。

先ほど読み込んだデータは、見ればわかるように


  • レコードidのような行でユニークな列がなく、

  • name, year, productの3列合わせるとレコードが1つに決まる。

こんな時はMultiIndexを使うとデータがすっきりとまとまる。

df2 = pd.read_csv("sample.csv",index_col=["name","year","product"])






expected
actual


name
year
product






Taro
2013
A
116
119


B
131
149


2014
A
151
124


B
125
107


2015
A
135
109


B
142
148


Hanako
2013
A
170
130


B
113
190


2014
A
102
142


B
183
125


2015
A
169
120


B
134
199

データを読み込んだ後や、いろいろと処理をして作成したDataFrameにMultiIndexをはる時は

2016/3/18追記

set_index関数を使うと、指定した列をインデックスにしてくれる。(これ公式ドキュメントのMultiIndexのページに載っていなかったので知らなかった・・・。)

df2 = df1.set_index(["name","year","product"], drop=True)

drop=Trueとすると、インデックスに設定した列は削除される。

戻すときは

df1 = df2.reset_index()

とする

追記終わり

pd.MultiIndex.from_arrays()

pd.MultiIndex.from_product()
pd.MultiIndex.from_tuples()

あたりを使えばなんとかなる。ちなみにpd.MultiIndex.from_productは複数のコレクションを渡してやるとネストしたループのインデックスを作る(itertools.productと同じ)。例えばdf2と同じインデックスを作るには

pd.MultiIndex.from_product((["Taro","Hanako"],[2013,2014,2015],["A","B"]),\

names=["name","year","product"])

とすればいい。


データの選択

行を選択する時はインデックスのタプルを渡せば良い。

df2.loc[("Taro",2013,"A")]


出力

expected    116

actual 119
Name: (Taro, 2013, A), dtype: int64

途中までのタプルを渡すと、テーブルが返ってくるが、途中のレベルまでの指定はPerformanceWarningがでるので、一時的に中を見たい時以外は使わないほうがいい。

第一レベル(今回で言うname)もしくは全レベルの指定なら問題ない。

py

df2.loc[("Taro",2013)]

出力 (PerformanceWarning)




expected
actual


product






A
116
119


B
131
149

途中のレベルまでの指定とか、途中だけの指定とかはxsを使う

df2.xs(["Taro","A"],level=[0,2])

出力




expected
actual


year






2013
116
119


2014
151
124


2015
135
109

これでWarnningは出なくなったが、上と同じ処理を書いてもこっちの方がちょっと遅い・・・


MultiIndexの使い所

pandas.Indexはpandas.Seriesではないので、普通の列のような感覚で使えないことが結構あり、地味にストレスがたまる。じゃあSingleIndexとSeriesのままでいいじゃん と言いたいところだが、せっかくあるんだから使いどころを考える


複数列をキーにして一意に決まるデータを探してくる

先ほども書きましたがこれが主な使い所でしょう。

df2.loc[("Taro",2013,"A")]

同じことをSingleIndexでやろうと思うと、

df1[(df1["name"] == "Taro")&(df1["year"] == 2013)&(df1["product"] == "A")].iloc[0]

という感じになって汚い(多分もっといい書き方があるが)。

当然速度にも差がある。と思って5万件くらいのデータ使って試したら1試行では2~3倍くらいしか差がつかなかった。%%timeitで調べようとしたが、df2の方がキャッシュされてしまうようで比較できなかった。

また、指定したキーに該当するデータがあるかどうかわからない時、MultiIndexなら

("Taro",2013,"A") in df2.index

でいいが、SingleIndexでは

df1[(df1["name"] == "Taro")&(df1["year"] == 2013)&(df1["product"] == "A")].shape[0]>0

とかやらないといけない(多分もっといい書き方があるが)


表示用

キーとなるインデックスが入れ子になったMultiIndexの出力は人間にとっては視認性が高いので、出力の為だけにMultiIndexを使うというのもあり。

DataFrameはhtml, excel, latex等々多数の形式で出力でき、エクセルであってもちゃんとMultiIndexに合わせてセル結合してくれるので地味に便利。

ちなみにdf.to_csvでは、MultiIndexであってもちゃんと全行フルに出力してくれる。


その他操作方法など


  • 1レベル分のインデックスを取り出したい時はdf.index.get_level_valuesを使う

  • MultiIndexをSingleIndexに戻すにはdf.reset_index()を使う。各levelの列が追加されて、通し番号がindexになったDataFrameができる。

  • その他、スライス等々あるがイマイチ使い方がわかっていないのでまたの機会に