MultiindexにしたままDatetimeIndexで期間指定するときはslice()を使う

pandasを触っていてちょっとハマったのでメモ。
**MultiindexにしたままDatetimeIndexで期間指定するときはslice()を使う。**

# DataFrameの準備

```python
>>> import pandas as pd
```

```python
>>> df=pd.DataFrame([
... ['A','2018-12-8',90],
... ['A','2018-12-9',100],
... ['B','2018-12-8',80],
... ['B','2018-12-9',70],
... columns=['name','time','score'])
```

```python
>>> df
  name       time  score
0    A  2018-12-8     90
1    A  2018-12-9    100
2    B  2018-12-8     80
3    B  2018-12-9     70
>>> df['time']
0    2018-12-8
1    2018-12-9
2    2018-12-8
3    2018-12-9
Name: time, dtype: object
```
こんな感じのDataFrameを解析したいというシチュエーションを想定する。
　　
このとき、'time'列はまだ文字列型なので、datetime64型に変換する。

```python

>>> df['time']=pd.to_datetime(df['time'])
>>> df['time']
0   2018-12-08
1   2018-12-09
2   2018-12-08
3   2018-12-09
Name: time, dtype: datetime64[ns]
```
無事にdatetime64型に変換できた。
　　
# 単一インデックスでのDatetimeIndex
ここで、この'time'のみをindexに指定すると、

```python

>>> df_time=df.set_index('time')
           name  score
time
2018-12-08    A     90
2018-12-09    A    100
2018-12-08    B     80
2018-12-09    B     70

>>> df_time.index
DatetimeIndex(['2018-12-08', '2018-12-09', '2018-12-08', '2018-12-09'], dtype='datetime64[ns]', name='time', freq=None)
```
ちゃんとDatetimeIndexが貼られている。

```py

>>> df_time['2018']
           name  score
time
2018-12-08    A     90
2018-12-09    A    100
2018-12-08    B     80
2018-12-09    B     70

>>> df_time['2018':'2019']
           name  score
time
2018-12-08    A     90
2018-12-09    A    100
2018-12-08    B     80
2018-12-09    B     70


>>> df_time.loc['2018':'2019']
           name  score
time
2018-12-08    A     90
2018-12-09    A    100
2018-12-08    B     80
2018-12-09    B     70
```
DatetimeIndexであれば、上記のように期間で指定した日付を取り出せるので便利。
.locでも期間指定が可能。

# MultiindexでのDatetimeIndex

pandasにはmultiindexという便利な機能もあるのだが、いくつか落とし穴がある。

```py

>>> df.set_index(['name','time'])
                score
name time
A    2018-12-08     90
     2018-12-09    100
B    2018-12-08     80
     2018-12-09     70

>>> df_m_time.loc[('A','2018')]
Traceback (most recent call last):
KeyError: 'the label [2018] is not in the [columns]'

>>> df_m_time.loc[('A','2018'),:]
                 score
name time
A    2018-12-08     90
     2018-12-09    100
>>>
```
'2018'という指定で、2018年のデータすべてを抽出できる。
単一インデックスのときはcolumnの指定が省略可能だが、マルチインデックスのときは、1項目のタプルがリストに変換されてしまうためか明示的に指定する必要がある。
　　



```python

>>> df_m_time.loc[('A',['2018':'2018']),:]
  File "<stdin>", line 1
    df_m_time.loc[('A',['2018':'2018']),:]
                              ^
SyntaxError: invalid syntax

>>> df_m_time.loc[('A',slice('2018','2019')),:]
                 score
name time
A    2018-12-08     90
     2018-12-09    100
>>>

```
.locで期間指定するときは、特殊なスライス指定が必要。なお、全指定のときは':'の代わりにslice(None)を使う。
https://note.nkmk.me/python-pandas-multiindex-indexing/
　　


```python

>>> df_m_time.loc[('A',['2018-11','2018-12']),:]
                 score
name time
A    2018-12-08     90
     2018-12-09    100
```
カンマ区切りのリストだと行けたりするので、Datetime型に特異な問題かと思ってしまい上記の特殊なスライスにたどり着くのに時間がかかった。蓋を開けてみれば、multiindexそのものの話だった。

# 余談
.locの挙動はけっこう厄介で、

```py

>>> df
  name       time  score
0    A  2018-12-8     90
1    A  2018-12-9    100
2    B  2018-12-8     80
3    B  2018-12-9     70

>>> df.loc[(slice(-4,3)),:]
  name       time  score
0    A  2018-12-8     90
1    A  2018-12-9    100
2    B  2018-12-8     80
3    B  2018-12-9     70

>>> df.sort_values('score', inplace=True)
>>> df
  name       time  score
3    B  2018-12-9     70
2    B  2018-12-8     80
0    A  2018-12-8     90
1    A  2018-12-9    100

>>> df.loc[(slice(-4,3)),:]
KeyError: -4
```

sortすると結果が変わる。内部的には整列しているときは数値として処理されて存在しないkeyでも範囲として有効だが、並び替えられると文字列のように処理されて存在しないkeyを指定するとKeyErrorになってしまうのだろうか。このあたりはあまりdocumentにも書いてなかったりするので、ハマるときはハマりそう。ちなみに両端が含まれるのはそういう仕様らしい。


# 補足
なお、インデックスを貼らないときのDatetime型に対する期間指定は以下のようにする。他の列の条件とも組み合わせることができたり自由度は高いが、ただの期間指定ということがやや分かりにくい。

```python

>>> df[('2018'<=df['time'])&(df['time']<='2019')]
  name       time  score
0    A  2018-12-8     90
1    A  2018-12-9    100
2    B  2018-12-8     80
3    B  2018-12-9     70
```
　　

df.queryでは（少なくともdefaultのままでは）うまく指定できない。parserやengineなどを指定してうまいこと書くとできるのかもしれないが…。

```py

>>> df.query('time < 2018-12-9')
TypeError: '>' not supported between instances of 'numpy.ndarray' and 'str'
```



# 結論
datetime型で期間指定したレコードを抽出したいときは、

- multiindexを使いつつ、.loc + slice()で指定する
- 単一indexで期間指定してからindexを貼り直す

というあたりが良さそうです。