この記事は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を用意する
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.Index
はname
というメンバを持っているので
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)もしくは全レベルの指定なら問題ない。
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ができる。
- その他、スライス等々あるがイマイチ使い方がわかっていないのでまたの機会に