Pandas の Rolling の使い方

株価や新型コロナウイルス陽性患者数など、細かく数値が上下するデータの大まかな傾向を掴むには、ある時点の前後の数値を含めた平均を取ります。このような操作を行う Pandas の関数である rolling についてメモします。

## 普通の使い方


```python
import pandas as pd
```

まず rolling の基本動作を確認するために、10 個の 1 が並んだ Series を作り ones と名付けます。


```python
ones = pd.Series([1] * 10)
ones
```




    0    1
    1    1
    2    1
    3    1
    4    1
    5    1
    6    1
    7    1
    8    1
    9    1
    dtype: int64



rolling を使って ones を４つずつ足してゆきます。1 の数値を 4 つずつ足してゆくだけなので当然結果は 4 の列になります。ただ、最初にデータが 4 つ揃わない部分の結果は NaN になります。元の列と比較するために、ones と足した結果の sum-4 を並べてみました。

このように window で集計するデータの個数 (window の幅) を指定します。4 つの窓が最初から最後まで移動しながら集計してゆくイメージです。集計結果は window の最後に記録されるので、sum-4 の最初の 3 つの値は NaN になっています。


```python
pd.DataFrame({
    'ones': ones,
    'sum-4': ones.rolling(window=4).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>ones</th>
      <th>sum-4</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2</th>
      <td>1</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>4</th>
      <td>1</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>5</th>
      <td>1</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>6</th>
      <td>1</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>7</th>
      <td>1</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>8</th>
      <td>1</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1</td>
      <td>4.0</td>
    </tr>
  </tbody>
</table>
</div>



center=True を指定すると、集計結果を window の最後に記録する代わりに真ん中に記録する事も出来ます。これを sum-center として並べてみます。window が偶数の時は中心の後ろに記録します。今度は最後の要素が NaN になりました。


```python
pd.DataFrame({
    'ones': ones,
    'sum-4': ones.rolling(window=4).sum(),
    'sum-center': ones.rolling(window=4, center=True).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>ones</th>
      <th>sum-4</th>
      <th>sum-center</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2</th>
      <td>1</td>
      <td>NaN</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>4</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>5</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>6</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>7</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>8</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1</td>
      <td>4.0</td>
      <td>NaN</td>
    </tr>
  </tbody>
</table>
</div>



デフォルトではデータの端っこの window の幅が足りない部分の集計は行いません。しかし min_periods を指定すると、最低限 min_periods の分があれば集計します。この例では、min_period=2 を指定していますので2つ目の行から結果が現れます。


```python
pd.DataFrame({
    'ones': ones,
    'sum-4': ones.rolling(4).sum(),
    'min-periods': ones.rolling(window=4, min_periods=2).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>ones</th>
      <th>sum-4</th>
      <th>min-periods</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>NaN</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>2</th>
      <td>1</td>
      <td>NaN</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>4</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>5</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>6</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>7</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>8</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1</td>
      <td>4.0</td>
      <td>4.0</td>
    </tr>
  </tbody>
</table>
</div>



ここまでは window にかかった部分の要素を全て使って集計していましたが、`win_type` を使うと集計する値の重みを変えられるそうです。scipy のインストールが必要。


```python
pd.DataFrame({
    'ones': ones,
    'sum-4': ones.rolling(4).sum(),
    'win_type': ones.rolling(window=4, win_type='nuttall').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>ones</th>
      <th>sum-4</th>
      <th>win_type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2</th>
      <td>1</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1</td>
      <td>4.0</td>
      <td>1.059185</td>
    </tr>
    <tr>
      <th>4</th>
      <td>1</td>
      <td>4.0</td>
      <td>1.059185</td>
    </tr>
    <tr>
      <th>5</th>
      <td>1</td>
      <td>4.0</td>
      <td>1.059185</td>
    </tr>
    <tr>
      <th>6</th>
      <td>1</td>
      <td>4.0</td>
      <td>1.059185</td>
    </tr>
    <tr>
      <th>7</th>
      <td>1</td>
      <td>4.0</td>
      <td>1.059185</td>
    </tr>
    <tr>
      <th>8</th>
      <td>1</td>
      <td>4.0</td>
      <td>1.059185</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1</td>
      <td>4.0</td>
      <td>1.059185</td>
    </tr>
  </tbody>
</table>
</div>



## インデックスに日付を使う

ここまでは window に要素の個数を指定しました。ここで Series の index が日付や時刻の時だけ時間や日数などの期間を window に指定する事が出来ます。早速サンプルのデータを作ってみます。


```python
date_ones = pd.Series(
    index=[
        pd.Timestamp('2020-01-01'),
        pd.Timestamp('2020-01-02'),
        pd.Timestamp('2020-01-07'),
        pd.Timestamp('2020-01-08'),
        pd.Timestamp('2020-01-09'),
        pd.Timestamp('2020-01-16'),
    ],
    data=[1,1,1,1,1,1]
)
date_ones
```




    2020-01-01    1
    2020-01-02    1
    2020-01-07    1
    2020-01-08    1
    2020-01-09    1
    2020-01-16    1
    dtype: int64



ここで window='7d' のように指定すると、7日前までの要素を使って集計します。


```python
pd.DataFrame({
    'ones': date_ones,
    'rolling-7d': date_ones.rolling('7D').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>ones</th>
      <th>rolling-7d</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2020-01-01</th>
      <td>1</td>
      <td>1.0</td>
    </tr>
    <tr>
      <th>2020-01-02</th>
      <td>1</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>2020-01-07</th>
      <td>1</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2020-01-08</th>
      <td>1</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2020-01-09</th>
      <td>1</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2020-01-16</th>
      <td>1</td>
      <td>1.0</td>
    </tr>
  </tbody>
</table>
</div>

この例では

* 2020-01-01: 前に何もないので 1
* 2020-01-02: 7日前以内に一つ有るので 2
* 2020-01-07: 7日前以内には 2020-01-01, 2020-01-02, 2020-01-07 があるので 3
* 2020-01-08: 7日前以内には 2020-01-02, 2020-01-07, 2020-01-08 があるので 3
* 2020-01-16: 7日前以内には何もないので 1

となります。

この '7D' のような指定方法を offset と呼びます。[Offset aliases](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries-offset-aliases) に説明があります。

window に offset を使うとデータが少なくとも集計が行われるので勝手に NaN が記録される事はありません。逆に min_periods を指定すると、指定した offset 期間に指定した以上のデータが無いと NaN になります。


```python
pd.DataFrame({
    'ones': date_ones,
    'rolling-7d': date_ones.rolling('7D').sum(),
    'rolling-min-periods': date_ones.rolling('7D', min_periods=3).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>ones</th>
      <th>rolling-7d</th>
      <th>rolling-min-periods</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2020-01-01</th>
      <td>1</td>
      <td>1.0</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2020-01-02</th>
      <td>1</td>
      <td>2.0</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>2020-01-07</th>
      <td>1</td>
      <td>3.0</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2020-01-08</th>
      <td>1</td>
      <td>3.0</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2020-01-09</th>
      <td>1</td>
      <td>3.0</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2020-01-16</th>
      <td>1</td>
      <td>1.0</td>
      <td>NaN</td>
    </tr>
  </tbody>
</table>
</div>



## 移動平均の例

さて、ここまでの知識を使って東京都の新型コロナウイルス感染者数の移動平均グラフを描いてみます。

データは [東京都 新型コロナウイルス陽性患者発表詳細](https://catalog.data.metro.tokyo.lg.jp/dataset/t000010d0000000068) から拝借します。


```python
covid_src = pd.read_csv('https://stopcovid19.metro.tokyo.lg.jp/data/130001_tokyo_covid19_patients.csv', parse_dates=['公表_年月日'])
covid_src
```




<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>No</th>
      <th>全国地方公共団体コード</th>
      <th>都道府県名</th>
      <th>市区町村名</th>
      <th>公表_年月日</th>
      <th>曜日</th>
      <th>発症_年月日</th>
      <th>患者_居住地</th>
      <th>患者_年代</th>
      <th>患者_性別</th>
      <th>患者_属性</th>
      <th>患者_状態</th>
      <th>患者_症状</th>
      <th>患者_渡航歴の有無フラグ</th>
      <th>備考</th>
      <th>退院済フラグ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-01-24</td>
      <td>金</td>
      <td>NaN</td>
      <td>湖北省武漢市</td>
      <td>40代</td>
      <td>男性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>1.0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>2</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-01-25</td>
      <td>土</td>
      <td>NaN</td>
      <td>湖北省武漢市</td>
      <td>30代</td>
      <td>女性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>1.0</td>
    </tr>
    <tr>
      <th>2</th>
      <td>3</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-01-30</td>
      <td>木</td>
      <td>NaN</td>
      <td>湖南省長沙市</td>
      <td>30代</td>
      <td>女性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>1.0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>4</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-02-13</td>
      <td>木</td>
      <td>NaN</td>
      <td>都内</td>
      <td>70代</td>
      <td>男性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>1.0</td>
    </tr>
    <tr>
      <th>4</th>
      <td>5</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-02-14</td>
      <td>金</td>
      <td>NaN</td>
      <td>都内</td>
      <td>50代</td>
      <td>女性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>1.0</td>
    </tr>
    <tr>
      <th>...</th>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
    </tr>
    <tr>
      <th>28131</th>
      <td>28031</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-10-14</td>
      <td>水</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>10歳未満</td>
      <td>男性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>28132</th>
      <td>28032</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-10-14</td>
      <td>水</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>20代</td>
      <td>男性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>28133</th>
      <td>28033</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-10-14</td>
      <td>水</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>70代</td>
      <td>男性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>28134</th>
      <td>28034</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-10-14</td>
      <td>水</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>20代</td>
      <td>女性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
    <tr>
      <th>28135</th>
      <td>28035</td>
      <td>130001</td>
      <td>東京都</td>
      <td>NaN</td>
      <td>2020-10-14</td>
      <td>水</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>40代</td>
      <td>女性</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
      <td>NaN</td>
    </tr>
  </tbody>
</table>
<p>28136 rows × 16 columns</p>
</div>



必要なのは人数だけなので、groupby と size で日付ごとの件数を出します。


```python
covid_daily = covid_src.groupby("公表_年月日").size()
covid_daily.index.name = 'date'
covid_daily
```




    date
    2020-01-24      1
    2020-01-25      1
    2020-01-30      1
    2020-02-13      1
    2020-02-14      2
                 ... 
    2020-10-10    249
    2020-10-11    146
    2020-10-12     78
    2020-10-13    166
    2020-10-14    177
    Length: 239, dtype: int64



単に日付ごとの件数をグラフに描くとこのようにジグザグになります。


```python
covid_daily.plot()
```



![output_23_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35350/eb99a197-7fbf-2a0f-2912-9560ddd91440.png)
    


一週間の window を設定して陽性患者の傾向を掴みます。ここで、今までの sum() の代わりに mean() を使うと移動平均になります。

```python
covid_daily.rolling(window='7D').mean().plot()
```




![output_25_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35350/277feb87-275c-c231-e792-4f6f222efc0f.png)

    


## 参考

* [Pandasで隣り合う複数の要素をまとめるrolling関数の使い方](https://deepage.net/features/pandas-rolling.html)
* [pandas.Series.rolling](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.rolling.html)