pandasのMultiIndexについて

この記事は[Python その2 Advent Calendar 2015](http://qiita.com/advent-calendar/2015/yet_another_python)の19日目の記事です.


Advent Calendar登録してみたものの、最近Pythonそんな書いていない&書いてても業務上特殊なことをやり過ぎてて書けないので、ちょっと今更感はありますがpandasの小ネタです。
データの前処理や集計等でおなじみのpandas、知ってはいるけどちょっと使いどころがわかりにくいMultiindexついて書きます。


## 執筆時の環境
* Python 2.7.11 |Anaconda 2.0.1 (x86_64)
* pandas 0.17.0
* 実行環境: jupyter notebook 

以下出てくるコードは

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

## MultiIndexって何
[MultiIndex / Advanced Indexing](http://pandas.pydata.org/pandas-docs/stable/advanced.html)
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で読み込む。

```py
df1 = pd.read_csv('sample.csv')
```
<table border="0" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>name</th>
      <th>year</th>
      <th>product</th>
      <th>expected</th>
      <th>actual</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>Taro</td>
      <td>2013</td>
      <td>A</td>
      <td>116</td>
      <td>119</td>
    </tr>
    <tr>
      <th>1</th>
      <td>Taro</td>
      <td>2013</td>
      <td>B</td>
      <td>131</td>
      <td>149</td>
    </tr>
    <tr>
      <th>2</th>
      <td>Taro</td>
      <td>2014</td>
      <td>A</td>
      <td>151</td>
      <td>124</td>
    </tr>
    <tr>
      <th>3</th>
      <td>Taro</td>
      <td>2014</td>
      <td>B</td>
      <td>125</td>
      <td>107</td>
    </tr>
    <tr>
      <th>4</th>
      <td>Taro</td>
      <td>2015</td>
      <td>A</td>
      <td>135</td>
      <td>109</td>
    </tr>
    <tr>
      <th>5</th>
      <td>Taro</td>
      <td>2015</td>
      <td>B</td>
      <td>142</td>
      <td>148</td>
    </tr>
    <tr>
      <th>6</th>
      <td>Hanako</td>
      <td>2013</td>
      <td>A</td>
      <td>170</td>
      <td>130</td>
    </tr>
    <tr>
      <th>7</th>
      <td>Hanako</td>
      <td>2013</td>
      <td>B</td>
      <td>113</td>
      <td>190</td>
    </tr>
    <tr>
      <th>8</th>
      <td>Hanako</td>
      <td>2014</td>
      <td>A</td>
      <td>102</td>
      <td>142</td>
    </tr>
    <tr>
      <th>9</th>
      <td>Hanako</td>
      <td>2014</td>
      <td>B</td>
      <td>183</td>
      <td>125</td>
    </tr>
    <tr>
      <th>10</th>
      <td>Hanako</td>
      <td>2015</td>
      <td>A</td>
      <td>169</td>
      <td>120</td>
    </tr>
    <tr>
      <th>11</th>
      <td>Hanako</td>
      <td>2015</td>
      <td>B</td>
      <td>134</td>
      <td>199</td>
    </tr>
  </tbody>
</table>

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

`pandas.Index`は`name`というメンバを持っているので

```py
df1.index.name = "id"
df1.columns.name = "header"
```
とすると

<table border="0" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>header</th>
      <th>name</th>
      <th>year</th>
      <th>product</th>
      <th>expected</th>
      <th>actual</th>
    </tr>
    <tr>
      <th>id</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>Taro</td>
      <td>2013</td>
      <td>A</td>
      <td>116</td>
      <td>119</td>
    </tr>
    <tr>
      <th>1</th>
      <td>Taro</td>
      <td>2013</td>
      <td>B</td>
      <td>131</td>
      <td>149</td>
    </tr>
    <tr>
      <th>2</th>
      <td>Taro</td>
      <td>2014</td>
      <td>A</td>
      <td>151</td>
      <td>124</td>
    </tr>
    <tr>
      <th>3</th>
      <td>Taro</td>
      <td>2014</td>
      <td>B</td>
      <td>125</td>
      <td>107</td>
    </tr>
    <tr>
      <th>4</th>
      <td>Taro</td>
      <td>2015</td>
      <td>A</td>
      <td>135</td>
      <td>109</td>
    </tr>
    <tr>
      <th>5</th>
      <td>Taro</td>
      <td>2015</td>
      <td>B</td>
      <td>142</td>
      <td>148</td>
    </tr>
    <tr>
      <th>6</th>
      <td>Hanako</td>
      <td>2013</td>
      <td>A</td>
      <td>170</td>
      <td>130</td>
    </tr>
    <tr>
      <th>7</th>
      <td>Hanako</td>
      <td>2013</td>
      <td>B</td>
      <td>113</td>
      <td>190</td>
    </tr>
    <tr>
      <th>8</th>
      <td>Hanako</td>
      <td>2014</td>
      <td>A</td>
      <td>102</td>
      <td>142</td>
    </tr>
    <tr>
      <th>9</th>
      <td>Hanako</td>
      <td>2014</td>
      <td>B</td>
      <td>183</td>
      <td>125</td>
    </tr>
    <tr>
      <th>10</th>
      <td>Hanako</td>
      <td>2015</td>
      <td>A</td>
      <td>169</td>
      <td>120</td>
    </tr>
    <tr>
      <th>11</th>
      <td>Hanako</td>
      <td>2015</td>
      <td>B</td>
      <td>134</td>
      <td>199</td>
    </tr>
  </tbody>
</table>
こんなテーブルになる。

### データの選択

* 列で選択

```py
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
```

* 行で選択

```py
df1.loc[3]
```

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

* 行インデックスで選択

```py
df1.iloc[3]
```

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

* 行と列で選択

```py
df1.ix[3:5,"product":"actual"]
```
出力

<table border="0" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>product</th>
      <th>expected</th>
      <th>actual</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>3</th>
      <td>B</td>
      <td>125</td>
      <td>107</td>
    </tr>
    <tr>
      <th>4</th>
      <td>A</td>
      <td>135</td>
      <td>109</td>
    </tr>
    <tr>
      <th>5</th>
      <td>B</td>
      <td>142</td>
      <td>148</td>
    </tr>
  </tbody>
</table>

* 条件で選択

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

出力

<table border="0" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>header</th>
      <th>name</th>
      <th>year</th>
      <th>product</th>
      <th>expected</th>
      <th>actual</th>
    </tr>
    <tr>
      <th>id</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>6</th>
      <td>Hanako</td>
      <td>2013</td>
      <td>A</td>
      <td>170</td>
      <td>130</td>
    </tr>
    <tr>
      <th>8</th>
      <td>Hanako</td>
      <td>2014</td>
      <td>A</td>
      <td>102</td>
      <td>142</td>
    </tr>
    <tr>
      <th>10</th>
      <td>Hanako</td>
      <td>2015</td>
      <td>A</td>
      <td>169</td>
      <td>120</td>
    </tr>
  </tbody>
</table>


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


## MultiIndexの設定

ここからが本題。
先ほど読み込んだデータは、見ればわかるように

* レコードidのような行でユニークな列がなく、
* name, year, productの３列合わせるとレコードが1つに決まる。

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

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

<table border="0" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th></th>
      <th></th>
      <th>expected</th>
      <th>actual</th>
    </tr>
    <tr>
      <th>name</th>
      <th>year</th>
      <th>product</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="6" valign="top">Taro</th>
      <th rowspan="2" valign="top">2013</th>
      <th>A</th>
      <td>116</td>
      <td>119</td>
    </tr>
    <tr>
      <th>B</th>
      <td>131</td>
      <td>149</td>
    </tr>
    <tr>
      <th rowspan="2" valign="top">2014</th>
      <th>A</th>
      <td>151</td>
      <td>124</td>
    </tr>
    <tr>
      <th>B</th>
      <td>125</td>
      <td>107</td>
    </tr>
    <tr>
      <th rowspan="2" valign="top">2015</th>
      <th>A</th>
      <td>135</td>
      <td>109</td>
    </tr>
    <tr>
      <th>B</th>
      <td>142</td>
      <td>148</td>
    </tr>
    <tr>
      <th rowspan="6" valign="top">Hanako</th>
      <th rowspan="2" valign="top">2013</th>
      <th>A</th>
      <td>170</td>
      <td>130</td>
    </tr>
    <tr>
      <th>B</th>
      <td>113</td>
      <td>190</td>
    </tr>
    <tr>
      <th rowspan="2" valign="top">2014</th>
      <th>A</th>
      <td>102</td>
      <td>142</td>
    </tr>
    <tr>
      <th>B</th>
      <td>183</td>
      <td>125</td>
    </tr>
    <tr>
      <th rowspan="2" valign="top">2015</th>
      <th>A</th>
      <td>169</td>
      <td>120</td>
    </tr>
    <tr>
      <th>B</th>
      <td>134</td>
      <td>199</td>
    </tr>
  </tbody>
</table>


データを読み込んだ後や、いろいろと処理をして作成したDataFrameにMultiIndexをはる時は
<font color="red">2016/3/18追記</font>
`set_index`関数を使うと、指定した列をインデックスにしてくれる。(これ公式ドキュメントのMultiIndexのページに載っていなかったので知らなかった・・・。)

```py
df2 = df1.set_index(["name","year","product"], drop=True)
```
`drop=True`とすると、インデックスに設定した列は削除される。
戻すときは

```py
df1 = df2.reset_index()
```
とする
<font color="red">追記終わり</font>



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

```py
pd.MultiIndex.from_product((["Taro","Hanako"],[2013,2014,2015],["A","B"]),\
    names=["name","year","product"])
```
とすればいい。

## データの選択

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

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

```:出力
expected    116
actual      119
Name: (Taro, 2013, A), dtype: int64
```

途中までのタプルを渡すと、テーブルが返ってくるが、途中のレベルまでの指定はPerformanceWarningがでるので、一時的に中を見たい時以外は使わないほうがいい。
第一レベル(今回で言うname)もしくは全レベルの指定なら問題ない。
```py
df2.loc[("Taro",2013)]
```

出力 (PerformanceWarning)


<table border="0" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>expected</th>
      <th>actual</th>
    </tr>
    <tr>
      <th>product</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>A</th>
      <td>116</td>
      <td>119</td>
    </tr>
    <tr>
      <th>B</th>
      <td>131</td>
      <td>149</td>
    </tr>
  </tbody>
</table>

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

```py
df2.xs(["Taro","A"],level=[0,2])
```
出力

<table border="0" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>expected</th>
      <th>actual</th>
    </tr>
    <tr>
      <th>year</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2013</th>
      <td>116</td>
      <td>119</td>
    </tr>
    <tr>
      <th>2014</th>
      <td>151</td>
      <td>124</td>
    </tr>
    <tr>
      <th>2015</th>
      <td>135</td>
      <td>109</td>
    </tr>
  </tbody>
</table>

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

## MultiIndexの使い所

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

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

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

```py
df2.loc[("Taro",2013,"A")]
```
同じことをSingleIndexでやろうと思うと、

```py
df1[(df1["name"] == "Taro")&(df1["year"] == 2013)&(df1["product"] == "A")].iloc[0]
```
という感じになって汚い(多分もっといい書き方があるが)。
当然速度にも差がある。と思って５万件くらいのデータ使って試したら１試行では2~3倍くらいしか差がつかなかった。``%%timeit``で調べようとしたが、`df2`の方がキャッシュされてしまうようで比較できなかった。

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

```py
("Taro",2013,"A") in df2.index
```
でいいが、SingleIndexでは

```py
df1[(df1["name"] == "Taro")&(df1["year"] == 2013)&(df1["product"] == "A")].shape[0]>0
```
とかやらないといけない(多分もっといい書き方があるが)

### 表示用

キーとなるインデックスが入れ子になったMultiIndexの出力は人間にとっては視認性が高いので、出力の為だけにMultiIndexを使うというのもあり。
DataFrameはhtml, excel, latex等々多数の形式で出力でき、エクセルであってもちゃんとMultiIndexに合わせてセル結合してくれるので地味に便利。
ちなみに`df.to_csv`では、MultiIndexであってもちゃんと全行フルに出力してくれる。

## その他操作方法など

* １レベル分のインデックスを取り出したい時はdf.index.get_level_valuesを使う
* MultiIndexをSingleIndexに戻すにはdf.reset_index()を使う。各levelの列が追加されて、通し番号がindexになったDataFrameができる。
* その他、スライス等々あるがイマイチ使い方がわかっていないのでまたの機会に