Pandas.DataFrameでとびとびの時系列データを補完する

## はじめに

テーブルデータ系のコンペでしょっちゅう書き方を忘れて調べるので備忘録がわりに書いておく。

データを集計していると日付が抜けることがある。<br>
たとえば、販売履歴のデータから日付ごとに販売数を合計してDataFrameを作成したとき、販売履歴がない(1つも売れなかった)日があると、下のように日付が抜けたりする。


```python
import pandas as pd

df = pd.DataFrame(
    [
        ["2022-03-04", 10],
        ["2022-03-05", 12],
        ["2022-03-06", 14],
        ["2022-03-07", 16],
        ["2022-03-08", 18],
        # 2022/3/9~2022/3/10が抜けている
        ["2022-03-11", 24],
        ["2022-03-12", 26],
        ["2022-03-13", 28],
        # 2022/3/14~2022/3/15が抜けている
        ["2022-03-16", 34],
        ["2022-03-17", 36],
        ["2022-03-18", 38],
        ["2022-03-19", 40],
    ],
    columns=["date", "sales"],
)

df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date")
df
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2022-03-04</th>
      <td>10</td>
    </tr>
    <tr>
      <th>2022-03-05</th>
      <td>12</td>
    </tr>
    <tr>
      <th>2022-03-06</th>
      <td>14</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28</td>
    </tr>
    <tr>
      <th>2022-03-16</th>
      <td>34</td>
    </tr>
    <tr>
      <th>2022-03-17</th>
      <td>36</td>
    </tr>
    <tr>
      <th>2022-03-18</th>
      <td>38</td>
    </tr>
    <tr>
      <th>2022-03-19</th>
      <td>40</td>
    </tr>
  </tbody>
</table>
</div>



こういったケースで抜けてしまっている日付のデータを追加する方法は大きく3つある。

1. [`pandas.date_range()`](https://pandas.pydata.org/docs/reference/api/pandas.date_range.html)を使う。
2. [`pandas.DataFrame.asfreq()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.asfreq.html)を使う。
3. [`pandas.DataFrame.resample()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.resample.html)を使う。

これらについては[`pandas.DataFrame.asfreq()`の公式のドキュメント](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.asfreq.html)で以下のように書かれている。

>Returns the original data conformed to a new index with the specified frequency.<br>
>If the index of this DataFrame is a PeriodIndex, the new index is the result of transforming the original index with `PeriodIndex.asfreq` (so the original index will map one-to-one to the new index).<br>
>Otherwise, the new index will be equivalent to `pd.date_range(start, end, freq=freq)` where start and end are, respectively, the first and last entries in the original index (see `pandas.date_range()`). The values corresponding to any timesteps in the new index which were not present in the original index will be null (NaN), unless a method for filling such unknowns is provided (see the method parameter below).<br>
>The `resample()` method is more appropriate if an operation on each group of timesteps (such as an aggregate) is necessary to represent the data at the new frequency.


以下、[DeepL（無料版）](https://www.deepl.com/translator)による訳。

>指定された頻度で新しいインデックスに適合する元のデータを返す。<br>
>このDataFrameのインデックスがPeriodIndexの場合、新しいインデックスは、元のインデックスをPeriodIndex.asfreqで変換した結果です（したがって、元のインデックスは新しいインデックスに一対一で対応します）。<br>
>それ以外の場合、新しいインデックスは pd.date_range(start, end, freq=freq) と等しくなります。ここで start と end はそれぞれ、元のインデックスの最初と最後のエントリです (pandas.date_range() を参照してください)。元のインデックスに存在しない新しいインデックスのタイムステップに対応する値は、そのような未知数を埋めるためのメソッドが提供されていない限り、ヌル（NaN）になります（以下のメソッドパラメータを参照）。<br>
>新しい周波数でデータを表現するために、タイムステップの各グループに対する操作（集約など）が必要な場合は、resample()メソッドがより適切である。<br>

## [`pandas.date_range()`](https://pandas.pydata.org/docs/reference/api/pandas.date_range.html)を使う

---
[追記] [`pandas.DataFrame.reindex()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.reindex.html)を使用した方が良いという[コメント](https://qiita.com/aras/items/17551669c3b0ba8f60bd#comment-af10b40f83a4fbe7fc51)をいただきました。

```python
df.reindex(pd.date_range(start=df.index.min(), end=df.index.max(), name=df.index.name))
```
---


[`pandas.date_range()`](https://pandas.pydata.org/docs/reference/api/pandas.date_range.html)を使うと、一定期間で連続した時系列のIndexを作成できる。<br>
これを使って日付だけの列を持つDataFrameを作成して、元のDataFrameにマージする。


```python
df.merge(
    pd.DataFrame(
        pd.date_range(start=df.index.min(), end=df.index.max()), columns=["date"]
    ),
    how="right",
    left_index=True,
    right_on="date",
).set_index("date")
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2022-03-04</th>
      <td>10.0</td>
    </tr>
    <tr>
      <th>2022-03-05</th>
      <td>12.0</td>
    </tr>
    <tr>
      <th>2022-03-06</th>
      <td>14.0</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16.0</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18.0</td>
    </tr>
    <tr>
      <th>2022-03-09</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24.0</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26.0</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28.0</td>
    </tr>
    <tr>
      <th>2022-03-14</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-15</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-16</th>
      <td>34.0</td>
    </tr>
    <tr>
      <th>2022-03-17</th>
      <td>36.0</td>
    </tr>
    <tr>
      <th>2022-03-18</th>
      <td>38.0</td>
    </tr>
    <tr>
      <th>2022-03-19</th>
      <td>40.0</td>
    </tr>
  </tbody>
</table>
</div>



欠損値を埋めたい場合、置換[`pandas.DataFrame.fillna()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.fillna.html)や補完[`pandas.DataFrame.interpolate()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html)を使えば良い。


```python
# 線形に補完
df.merge(
    pd.DataFrame(
        pd.date_range(start=df.index.min(), end=df.index.max()), columns=["date"]
    ),
    how="right",
    left_index=True,
    right_on="date",
).set_index("date").interpolate(method="linear", limit_direction="forward")
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2022-03-04</th>
      <td>10.0</td>
    </tr>
    <tr>
      <th>2022-03-05</th>
      <td>12.0</td>
    </tr>
    <tr>
      <th>2022-03-06</th>
      <td>14.0</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16.0</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18.0</td>
    </tr>
    <tr>
      <th>2022-03-09</th>
      <td>20.0</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>22.0</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24.0</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26.0</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28.0</td>
    </tr>
    <tr>
      <th>2022-03-14</th>
      <td>30.0</td>
    </tr>
    <tr>
      <th>2022-03-15</th>
      <td>32.0</td>
    </tr>
    <tr>
      <th>2022-03-16</th>
      <td>34.0</td>
    </tr>
    <tr>
      <th>2022-03-17</th>
      <td>36.0</td>
    </tr>
    <tr>
      <th>2022-03-18</th>
      <td>38.0</td>
    </tr>
    <tr>
      <th>2022-03-19</th>
      <td>40.0</td>
    </tr>
  </tbody>
</table>
</div>



## [`pandas.DataFrame.asfreq()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.asfreq.html)を使う

[`pandas.DataFrame.asfreq()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.asfreq.html)を利用することで、時系列データをある期間でサンプリングすることができる。


```python
df.asfreq("D")
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2022-03-04</th>
      <td>10.0</td>
    </tr>
    <tr>
      <th>2022-03-05</th>
      <td>12.0</td>
    </tr>
    <tr>
      <th>2022-03-06</th>
      <td>14.0</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16.0</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18.0</td>
    </tr>
    <tr>
      <th>2022-03-09</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24.0</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26.0</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28.0</td>
    </tr>
    <tr>
      <th>2022-03-14</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-15</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-16</th>
      <td>34.0</td>
    </tr>
    <tr>
      <th>2022-03-17</th>
      <td>36.0</td>
    </tr>
    <tr>
      <th>2022-03-18</th>
      <td>38.0</td>
    </tr>
    <tr>
      <th>2022-03-19</th>
      <td>40.0</td>
    </tr>
  </tbody>
</table>
</div>



欠損値の穴埋めをしたい場合は引数で方法を指定するか、[`pandas.DataFrame.interpolate()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html)を使うなど。


```python
df.asfreq("D", method="ffill")
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2022-03-04</th>
      <td>10</td>
    </tr>
    <tr>
      <th>2022-03-05</th>
      <td>12</td>
    </tr>
    <tr>
      <th>2022-03-06</th>
      <td>14</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18</td>
    </tr>
    <tr>
      <th>2022-03-09</th>
      <td>18</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>18</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28</td>
    </tr>
    <tr>
      <th>2022-03-14</th>
      <td>28</td>
    </tr>
    <tr>
      <th>2022-03-15</th>
      <td>28</td>
    </tr>
    <tr>
      <th>2022-03-16</th>
      <td>34</td>
    </tr>
    <tr>
      <th>2022-03-17</th>
      <td>36</td>
    </tr>
    <tr>
      <th>2022-03-18</th>
      <td>38</td>
    </tr>
    <tr>
      <th>2022-03-19</th>
      <td>40</td>
    </tr>
  </tbody>
</table>
</div>



##  [`pandas.DataFrame.resample()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.resample.html)を使う

asfreqと近いイメージで利用可能。<br>
`pandas.DataFrame.resample()`だと`pandas.core.resample.DatetimeIndexResampler`クラスオブジェクトが返ってくるので、`sum()`や`mean()`などで集計し直す必要がある。<br>
ただし、存在しない日の値は`sum()`の場合は0に、`mean()`の場合はNaNになることには注意が必要。


```python
df.resample("D")
```




    <pandas.core.resample.DatetimeIndexResampler object at 0xffff67436fd0>




```python
df.resample("D").sum()
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2022-03-04</th>
      <td>10</td>
    </tr>
    <tr>
      <th>2022-03-05</th>
      <td>12</td>
    </tr>
    <tr>
      <th>2022-03-06</th>
      <td>14</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18</td>
    </tr>
    <tr>
      <th>2022-03-09</th>
      <td>0</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>0</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28</td>
    </tr>
    <tr>
      <th>2022-03-14</th>
      <td>0</td>
    </tr>
    <tr>
      <th>2022-03-15</th>
      <td>0</td>
    </tr>
    <tr>
      <th>2022-03-16</th>
      <td>34</td>
    </tr>
    <tr>
      <th>2022-03-17</th>
      <td>36</td>
    </tr>
    <tr>
      <th>2022-03-18</th>
      <td>38</td>
    </tr>
    <tr>
      <th>2022-03-19</th>
      <td>40</td>
    </tr>
  </tbody>
</table>
</div>




```python
df.resample("D").mean()
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2022-03-04</th>
      <td>10.0</td>
    </tr>
    <tr>
      <th>2022-03-05</th>
      <td>12.0</td>
    </tr>
    <tr>
      <th>2022-03-06</th>
      <td>14.0</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16.0</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18.0</td>
    </tr>
    <tr>
      <th>2022-03-09</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24.0</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26.0</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28.0</td>
    </tr>
    <tr>
      <th>2022-03-14</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-15</th>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2022-03-16</th>
      <td>34.0</td>
    </tr>
    <tr>
      <th>2022-03-17</th>
      <td>36.0</td>
    </tr>
    <tr>
      <th>2022-03-18</th>
      <td>38.0</td>
    </tr>
    <tr>
      <th>2022-03-19</th>
      <td>40.0</td>
    </tr>
  </tbody>
</table>
</div>



## groupby, applyとの組み合わせ

ここまでは単一商品のケースだったが、複数商品の販売履歴などが1つのテーブルに含まれており、上のようにそのままは使えないケースもある。


```python
df = pd.DataFrame(
    [
        # りんご
        ["2022-03-06", "apple", 14],
        ["2022-03-07", "apple", 16],
        ["2022-03-08", "apple", 18],
        # appleは3/9~3/10が抜けている
        ["2022-03-11", "apple", 24],
        ["2022-03-12", "apple", 26],
        ["2022-03-13", "apple", 28],
        # ぶどう
        ["2022-03-09", "grape", 18],
        ["2022-03-10", "grape", 24],
        # grapeは3/11~3/12が抜けている
        ["2022-03-13", "grape", 28],
        ["2022-03-14", "grape", 30],
        ["2022-03-15", "grape", 32],
    ],
    columns=["date", "item", "sales"],
)

df["date"] = pd.to_datetime(df["date"])
df = df.set_index(["item", "date"])
df
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>item</th>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="6" valign="top">apple</th>
      <th>2022-03-06</th>
      <td>14</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28</td>
    </tr>
    <tr>
      <th rowspan="5" valign="top">grape</th>
      <th>2022-03-09</th>
      <td>18</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>24</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28</td>
    </tr>
    <tr>
      <th>2022-03-14</th>
      <td>30</td>
    </tr>
    <tr>
      <th>2022-03-15</th>
      <td>32</td>
    </tr>
  </tbody>
</table>
</div>



こういった場合は`groupby()`と`apply()`の組み合わせなどで商品ごとに補完できる。


```python
def fill_missing_date(df):
    # 抜けている日付を埋めつつ線形に補完する
    return (
        df.merge(
            pd.DataFrame(
                pd.date_range(
                    start=df.index.get_level_values("date").min(),
                    end=df.index.get_level_values("date").max(),
                ),
                columns=["date"],
            ),
            how="right",
            left_on="date",
            right_on="date",
        )
        .set_index("date")
        .interpolate(method="linear", limit_direction="forward")
    )


df.groupby(level=0).apply(fill_missing_date)
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th></th>
      <th>sales</th>
    </tr>
    <tr>
      <th>item</th>
      <th>date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="8" valign="top">apple</th>
      <th>2022-03-06</th>
      <td>14.000000</td>
    </tr>
    <tr>
      <th>2022-03-07</th>
      <td>16.000000</td>
    </tr>
    <tr>
      <th>2022-03-08</th>
      <td>18.000000</td>
    </tr>
    <tr>
      <th>2022-03-09</th>
      <td>20.000000</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>22.000000</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>24.000000</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26.000000</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28.000000</td>
    </tr>
    <tr>
      <th rowspan="7" valign="top">grape</th>
      <th>2022-03-09</th>
      <td>18.000000</td>
    </tr>
    <tr>
      <th>2022-03-10</th>
      <td>24.000000</td>
    </tr>
    <tr>
      <th>2022-03-11</th>
      <td>25.333333</td>
    </tr>
    <tr>
      <th>2022-03-12</th>
      <td>26.666667</td>
    </tr>
    <tr>
      <th>2022-03-13</th>
      <td>28.000000</td>
    </tr>
    <tr>
      <th>2022-03-14</th>
      <td>30.000000</td>
    </tr>
    <tr>
      <th>2022-03-15</th>
      <td>32.000000</td>
    </tr>
  </tbody>
</table>
</div>