Pandasの日付が縦に並んでいるデータフレームで、欠けている日付の行を補う方法と気をつけること

解決するのに少し手間取ったのでメモ

# 環境設定
```{python}
import pandas as pd
from datetime import date, timedelta
```
PCはmacです。

# 何をやりたいのか

日付が並んでいるデータフレームに対し、欠けている日付を補います。
例えば、2/14から2/18の売上データがあるとします。

```{python}
tmp = pd.DataFrame(
    {"売上個数": [2, 3, 1]},
    index=[pd.to_datetime("2018-2-14"), pd.to_datetime("2018-2-16"), pd.to_datetime("2018-2-17")])
tmp
```
<img width="147" alt="スクリーンショット 2018-02-15 0.05.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/175024/f6b4c01a-a905-6a6d-73b2-d06e2d881eae.png">

このデータフレームでは2/15と2/18のデータが欠けています(2/14-2/18の売上データなので)。
売上個数が欠測した理由はさておき、2/15と2/18に売上個数NAの行を追加したい場合、どのようにすれば良いでしょうか。

# どうすればできるのか

補完したい期間の日付を入れたデータフレームを作ってマージしましょう。
日付を入れる場所はindexでも列でも大丈夫です。

実際にやってみます。

```{python}
'''
補完したい日付のデータフレームを作る
'''
# indexに日付を入れる場合
dates_DF = pd.DataFrame(index=[pd.to_datetime("2018-2-14") + timedelta(days=i) for i in range(5)])
# okadateさんのコメントを反映、こちらの方が綺麗
dates_DF = pd.DataFrame(index=pd.date_range('2018-2-14', periods=5, freq='D'))

# 列に日付を入れる場合
dates_DF2 = pd.DataFrame({"日付": [pd.to_datetime("2018-2-14") + timedelta(days=i) for i in range(5)]})

'''
マージ
'''
# index同士を結合させる場合
tmp.merge(dates_DF, how="outer", left_index=True, right_index=True)

# indexと列を結合させる場合
tmp.merge(dates_DF2, how="outer", left_index=True, right_on="日付")
```

<img width="140" alt="スクリーンショット 2018-02-15 0.25.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/175024/14129268-299c-a157-a638-b2f9e4789ee2.png">

2/15と2/18の行を補うことが出来ました。  
indexしかない空の日付データフレームを作るのがポイントです。
(列に日付を入れたデータフレームを作っても結果は変わりません)


## 追記

欠測している日付がデータの間である場合は`pd.resample`で補完が出来るそうです。

```{python}
# resampleは時系列データの解像度(頻度)を変更する関数
# データの圧縮などに使えるが、この場合は欠けている日付を補う事も出来る
tmp.resample('D').mean()
```

![スクリーンショット 2018-02-15 12.25.48.png](https://qiita-image-store.s3.amazonaws.com/0/175024/b812ec04-4150-4160-6974-320efaefcbc5.png)


# 気をつけること

`pandas.to_datetime()`はdatetimeではなくTimestampを返すことに注意しましょう。
indexでマージする場合は問題ありませんが、列でマージする場合、Timestampとdatetime.dateをマージするとTimestampが日付からUNIX時間(1970年1月1日 午前0時0分0秒からの経過秒数)になったりエラーを吐いたりします。
特に理由がなければマージはindexで行うか、列で行う場合は元の日付の型がTimestampなのかdatetime.dateなのか確認して、それに合わせた型を準備しましょう。

まず、型を確認しましょう。

```{python}
type(pd.to_datetime("2018-2-14")) # => pandas._libs.tslib.Timestamp
type(date(2018,2,14)) # => datetime.date
```

`pd.to_datetime()`はTimestamp、`date()`はdatetime.dateであることがわかります。  

次に、datetime.dateとTimestampでマージをしてみましょう。

```{python}
'''
前処理
'''
# datetime.dateの売上データを作る
tmp_dt = pd.DataFrame(
    {"売上個数": [2, 3, 1]},
    index=[date(2018, 2, 14), date(2018, 2, 16), date(2018, 2, 17)])

# datetime.dateの日付データを作る
# timedeltaはTimestampに対してもdatetime.dateに対しても計算出来る
dates_DF_dt = pd.DataFrame(index=[date(2018, 2, 14) + timedelta(days=i) for i in range(5)])

# Timestamp型であることを明示する
tmp_ts = tmp
dates_DF_ts = dates_DF_ts
```
## 売上:Timestamp, 日付: datetime.dateの場合

```{python}
# indexでマージ
tmp_ts.merge(dates_DF_dt, how="outer", left_index=True, right_index=True) # 問題ない

# 列でマージ
# reset_index()でindexにある日付を列に持ってこられる
# その場合列名は"index"となる
tmp_ts.reset_index().merge(dates_DF_dt.reset_index(), how="outer", on="index") # エラー
```

indexでマージをする場合は問題ありませんが、列でマージをするとエラーを吐きます。

## 売上: datetime.date, 日付: Timestampの場合

```{python}
# indexでマージ
tmp_dt.merge(dates_DF_ts, how="outer", left_index=True, right_index=True) # 問題ない

# 列でマージ
tmp_dt.reset_index().merge(dates_DF_ts.reset_index(), how="outer", on="index") # エラーは吐かないが上手くマージ出来ない
```

indexでマージする場合は問題ありませんが、列でマージした場合出力は以下のようになります。

<img width="222" alt="スクリーンショット 2018-02-15 1.03.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/175024/9e20e648-a0a7-88cb-6db0-3e9e94bd67ab.png">

日付データフレームのTimestampがUNIX時刻(int)になっていることがわかります。

# まとめ
* 日付が縦に並んでいるデータフレームに対し欠けている日付の行を補いたいときは、日付のデータフレームを作ってマージする
* `resample`や`date_range`を使った方が綺麗
* `pandas.to_datetime()`はTimestampを返すので注意
* マージはindexで行った方が安全