pandasで時系列データをリサンプリングするresample, asfreq

Posted: | Tags: Python, pandas, 時系列データ

時系列データを元データより高い頻度または低い頻度で再度サンプリングすることをリサンプリングと呼ぶ。以下の二通りがある。

  • アップサンプリング(オーバーサンプリング)
    • より高い頻度(短い周期)でリサンプリング
  • ダウンサンプリング(アンダーサンプリング)
    • より低い頻度(長い周期)でリサンプリング

pandasで時系列データをリサンプリングするにはresample()またはasfreq()を使う。

resample()asfreq()にはそれぞれ以下のような違いがある。

  • resample(): データを集約(合計や平均など)
  • asfreq(): データを選択

ここでは以下の内容について説明する。

  • asfreq()の使い方
    • 基本的な使い方
    • 元データに無い場合に任意の値で穴埋め: 引数fill_value
    • 元データに無い場合に前後の値で穴埋め: 引数method
    • 重複データがある場合
  • resample()の使い方
    • 基本的な使い方
    • ラベルを開始日時にするか終了日時にするかを指定: 引数label
    • 開始日時と終了日時のどちらを期間に含むかを指定: 引数closed
    • インデックスとみなす列名を指定: 引数on
  • アップサンプリングにおける値の補間
    • 特定の値、前後の値でそのまま穴埋め
    • 前後の値を使って補間(線形補間、スプライン補間など)
    • 不等間隔の欠損値の補間

pandas.DataFramepandas.Seriesのインデックスをdatetime64型のDatetimeIndexとして設定し時系列データとして扱う方法などについては以下の記事を参照。

また、集約でも選択でもなく、窓関数を適用する場合はrolling()メソッドを使う。移動平均などを算出するのはrolling()。以下の記事を参照。

データの集約にはマルチインデックスを用いる方法もある。こちらは曜日ごとの集計なども可能。

asfreq()の使い方

以下の2日おきのデータを例とする。

import pandas as pd

df = pd.DataFrame({'value': range(1, 32, 2)},
                  index=pd.date_range('2018-08-01', '2018-08-31', freq='2D'))

print(df)
#             value
# 2018-08-01      1
# 2018-08-03      3
# 2018-08-05      5
# 2018-08-07      7
# 2018-08-09      9
# 2018-08-11     11
# 2018-08-13     13
# 2018-08-15     15
# 2018-08-17     17
# 2018-08-19     19
# 2018-08-21     21
# 2018-08-23     23
# 2018-08-25     25
# 2018-08-27     27
# 2018-08-29     29
# 2018-08-31     31

基本的な使い方

asfreq()の第一引数freqにはD(日次)、W(週次)などの頻度コードを指定する。詳細は以下の記事を参照。

上述のようにasfreq()はデータの選択なので、元のデータに無い日時の値は欠損値NaNとなる。

print(df.asfreq('10D'))
#             value
# 2018-08-01      1
# 2018-08-11     11
# 2018-08-21     21
# 2018-08-31     31

print(df.asfreq('5D'))
#             value
# 2018-08-01    1.0
# 2018-08-06    NaN
# 2018-08-11   11.0
# 2018-08-16    NaN
# 2018-08-21   21.0
# 2018-08-26    NaN
# 2018-08-31   31.0

print(df.asfreq('W'))
#             value
# 2018-08-05    5.0
# 2018-08-12    NaN
# 2018-08-19   19.0
# 2018-08-26    NaN

print(df.asfreq('W-WED'))
#             value
# 2018-08-01    1.0
# 2018-08-08    NaN
# 2018-08-15   15.0
# 2018-08-22    NaN
# 2018-08-29   29.0

以下、元のデータに無い日時の値の穴埋め方法について説明する。asfreq()でできるのは任意の値や前後の値をそのまま使った単純な穴埋め。前後の値を使った線形補間などはresample()を使う。後述。

元データに無い場合に任意の値で穴埋め: 引数fill_value

元のデータに無い日時の値を任意の値で穴埋めするには引数fill_valueを指定する。

print(df.asfreq('W', fill_value=0))
#             value
# 2018-08-05      5
# 2018-08-12      0
# 2018-08-19     19
# 2018-08-26      0

元データに無い場合に前後の値で穴埋め: 引数method

元のデータに無い日時の値を前後の値で穴埋めするには引数methodを指定する。

method=padまたはmethod=ffillで前の値で穴埋め、method=backfillまたはmethod=bfillで後ろの値で穴埋めとなる。

print(df.asfreq('W', method='pad'))
#             value
# 2018-08-05      5
# 2018-08-12     11
# 2018-08-19     19
# 2018-08-26     25

print(df.asfreq('W', method='ffill'))
#             value
# 2018-08-05      5
# 2018-08-12     11
# 2018-08-19     19
# 2018-08-26     25

print(df.asfreq('W', method='backfill'))
#             value
# 2018-08-05      5
# 2018-08-12     13
# 2018-08-19     19
# 2018-08-26     27

print(df.asfreq('W', method='bfill'))
#             value
# 2018-08-05      5
# 2018-08-12     13
# 2018-08-19     19
# 2018-08-26     27

欠損値NaNが連続している場合も同様。

df_3D = pd.DataFrame({'value': range(1, 32, 3)},
                     index=pd.date_range('2018-08-01', '2018-08-31', freq='3D'))

print(df_3D.asfreq('D', method='bfill'))
#             value
# 2018-08-01      1
# 2018-08-02      4
# 2018-08-03      4
# 2018-08-04      4
# 2018-08-05      7
# 2018-08-06      7
# 2018-08-07      7
# 2018-08-08     10
# 2018-08-09     10
# 2018-08-10     10
# 2018-08-11     13
# 2018-08-12     13
# 2018-08-13     13
# 2018-08-14     16
# 2018-08-15     16
# 2018-08-16     16
# 2018-08-17     19
# 2018-08-18     19
# 2018-08-19     19
# 2018-08-20     22
# 2018-08-21     22
# 2018-08-22     22
# 2018-08-23     25
# 2018-08-24     25
# 2018-08-25     25
# 2018-08-26     28
# 2018-08-27     28
# 2018-08-28     28
# 2018-08-29     31
# 2018-08-30     31
# 2018-08-31     31

前後の値から線形補間などを行いたい場合はresample()を使う。後述。

重複データがある場合

以下のような同じ日付の違う時刻のデータを例とする。

df_h = pd.DataFrame({'value': range(9)},
                    index=pd.date_range('2018-08-01', '2018-08-05', freq='12H'))
print(df_h)
#                      value
# 2018-08-01 00:00:00      0
# 2018-08-01 12:00:00      1
# 2018-08-02 00:00:00      2
# 2018-08-02 12:00:00      3
# 2018-08-03 00:00:00      4
# 2018-08-03 12:00:00      5
# 2018-08-04 00:00:00      6
# 2018-08-04 12:00:00      7
# 2018-08-05 00:00:00      8

これをasfreq()freq='D'(日次)としてダウンサンプリングする場合、先頭の値が選択される。

print(df_h.asfreq('D'))
#             value
# 2018-08-01      0
# 2018-08-02      2
# 2018-08-03      4
# 2018-08-04      6
# 2018-08-05      8

平均値など、重複する複数のデータを集約した値を使いたい場合は次に説明するresample()を使う。

print(df_h.resample('D').mean())
#             value
# 2018-08-01    0.5
# 2018-08-02    2.5
# 2018-08-03    4.5
# 2018-08-04    6.5
# 2018-08-05    8.0

resample()の使い方

ここでも2日おきのデータを例とする。

import pandas as pd

df = pd.DataFrame({'value': range(1, 32, 2)},
                  index=pd.date_range('2018-08-01', '2018-08-31', freq='2D'))

print(df)
#             value
# 2018-08-01      1
# 2018-08-03      3
# 2018-08-05      5
# 2018-08-07      7
# 2018-08-09      9
# 2018-08-11     11
# 2018-08-13     13
# 2018-08-15     15
# 2018-08-17     17
# 2018-08-19     19
# 2018-08-21     21
# 2018-08-23     23
# 2018-08-25     25
# 2018-08-27     27
# 2018-08-29     29
# 2018-08-31     31

補間してアップサンプリングする例については後述。

基本的な使い方

resample()の第一引数ruleにも、asfreq()と同様にD(日次)、W(週次)などの頻度コードを指定する。詳細は以下の記事を参照。

resample()が返すのはDatetimeIndexResampler型のオブジェクトで、それ自体をprint()で出力しても値は表示されない。

print(df.resample('W'))
# DatetimeIndexResampler [freq=<Week: weekday=6>, axis=0, closed=right, label=right, convention=start, base=0]

print(type(df.resample('W')))
# <class 'pandas.core.resample.DatetimeIndexResampler'>

mean()(平均値)やmedian()(中央値)、sum()(合計)などのメソッドを呼ぶことで集約された値が算出される。以下は週ごとの平均などの例。

print(df.resample('W').mean())
#             value
# 2018-08-05      3
# 2018-08-12      9
# 2018-08-19     16
# 2018-08-26     23
# 2018-09-02     29

print(df.resample('W').median())
#             value
# 2018-08-05      3
# 2018-08-12      9
# 2018-08-19     16
# 2018-08-26     23
# 2018-09-02     29

print(df.resample('W').sum())
#             value
# 2018-08-05      9
# 2018-08-12     27
# 2018-08-19     64
# 2018-08-26     69
# 2018-09-02     87

グループ化したオブジェクトから計算用のメソッドを呼ぶということで、使い方のイメージとしてはgroupby()に近い。

ほかにも、先頭の値、末尾の値を出力するfirst(), last()、個数を出力するcount()、OHLC(Open: 始値、High: 高値、Low: 安値、Close: 終値)を出力するohlc()もある。

print(df.resample('W').first())
#             value
# 2018-08-05      1
# 2018-08-12      7
# 2018-08-19     13
# 2018-08-26     21
# 2018-09-02     27

print(df.resample('W').last())
#             value
# 2018-08-05      5
# 2018-08-12     11
# 2018-08-19     19
# 2018-08-26     25
# 2018-09-02     31

print(df.resample('W').count())
#             value
# 2018-08-05      3
# 2018-08-12      3
# 2018-08-19      4
# 2018-08-26      3
# 2018-09-02      3

print(df.resample('W').ohlc())
#            value               
#             open high low close
# 2018-08-05     1    5   1     5
# 2018-08-12     7   11   7    11
# 2018-08-19    13   19  13    19
# 2018-08-26    21   25  21    25
# 2018-09-02    27   31  27    31

ohlc()についての詳細は以下の記事を参照。

そのほか、任意の関数を指定できるapply()や、複数の関数をまとめて指定できるagg()などもある。

print(df.resample('W').apply(list))
#                        value
# 2018-08-05         [1, 3, 5]
# 2018-08-12        [7, 9, 11]
# 2018-08-19  [13, 15, 17, 19]
# 2018-08-26      [21, 23, 25]
# 2018-09-02      [27, 29, 31]

print(df.resample('W').agg(['min', 'max', 'sum']))
#            value        
#              min max sum
# 2018-08-05     1   5   9
# 2018-08-12     7  11  27
# 2018-08-19    13  19  64
# 2018-08-26    21  25  69
# 2018-09-02    27  31  87

DatetimeIndexResampler型のオブジェクトで使えるメソッドの一覧は公式ドキュメントを参照。

ラベルを開始日時にするか終了日時にするかを指定: 引数label

インデックス列のラベルを開始日時にするか終了日時にするかは引数labelで指定する。

label='left'とすると開始日時、label='right'とすると終了日時がラベルとなる。

デフォルトでは開始日時がラベルとなる(label='left')が、M, Q, A, Y, BM, BQ, BA, BY(それぞれ月、四半期、年の末日・最終営業日)及びWはデフォルトがlabel='right'となる。

print(df.resample('W').apply(list))
#                        value
# 2018-08-05         [1, 3, 5]
# 2018-08-12        [7, 9, 11]
# 2018-08-19  [13, 15, 17, 19]
# 2018-08-26      [21, 23, 25]
# 2018-09-02      [27, 29, 31]

print(df.resample('W', label='left').apply(list))
#                        value
# 2018-07-29         [1, 3, 5]
# 2018-08-05        [7, 9, 11]
# 2018-08-12  [13, 15, 17, 19]
# 2018-08-19      [21, 23, 25]
# 2018-08-26      [27, 29, 31]

開始日時と終了日時のどちらを期間に含むかを指定: 引数closed

引数closedで開始日時と終了日時のどちらを期間に含むかを指定する。

closed='left'とすると開始日時 <= 期間 < 終了日時となり、closed='right'とすると開始日時 < 期間 <= 終了日時となる。

labelと同じく、デフォルトではclosed='left'M, Q, A, Y, BM, BQ, BA, BY(それぞれ月、四半期、年の末日・最終営業日)及びWはデフォルトがclosed='right'となる。

基本的にはlabelclosedは同じ値にしておかないと直感に反する。

print(df.resample('W', label='left').apply(list))
#                        value
# 2018-07-29         [1, 3, 5]
# 2018-08-05        [7, 9, 11]
# 2018-08-12  [13, 15, 17, 19]
# 2018-08-19      [21, 23, 25]
# 2018-08-26      [27, 29, 31]

print(df.resample('W', label='left', closed='left').apply(list))
#                        value
# 2018-07-29            [1, 3]
# 2018-08-05     [5, 7, 9, 11]
# 2018-08-12      [13, 15, 17]
# 2018-08-19  [19, 21, 23, 25]
# 2018-08-26      [27, 29, 31]

インデックスとみなす列名を指定: 引数on

これまでの例のようにインデックス列が日時データであればそのままで問題ないが、インデックスではない列に日時データが格納されている場合、引数onに日時データが格納された列名を指定するとリサンプリングが可能。

df_reset = df.reset_index()
print(df_reset)
#         index  value
# 0  2018-08-01      1
# 1  2018-08-03      3
# 2  2018-08-05      5
# 3  2018-08-07      7
# 4  2018-08-09      9
# 5  2018-08-11     11
# 6  2018-08-13     13
# 7  2018-08-15     15
# 8  2018-08-17     17
# 9  2018-08-19     19
# 10 2018-08-21     21
# 11 2018-08-23     23
# 12 2018-08-25     25
# 13 2018-08-27     27
# 14 2018-08-29     29
# 15 2018-08-31     31

print(df_reset.resample('W', on='index').sum())
#             value
# index            
# 2018-08-05      9
# 2018-08-12     27
# 2018-08-19     64
# 2018-08-26     69
# 2018-09-02     87

アップサンプリングにおける値の補間

アップサンプリングする場合、元のデータに含まれない日時のデータを補間する必要がある。

以下の時系列データを例とする。

import pandas as pd

df = pd.DataFrame({'value': [1, 15, 31]},
                  index=pd.to_datetime(['2018-01-01', '2018-01-15', '2018-01-31']))

print(df)
#             value
# 2018-01-01      1
# 2018-01-15     15
# 2018-01-31     31

特定の値、前後の値でそのまま穴埋め

上述のように、asfreq()の引数fill_value, methodを指定すると、特定の値で穴埋めしたり前後の値いずれかで穴埋めしたりできる。

print(df.asfreq('5D'))
#             value
# 2018-01-01    1.0
# 2018-01-06    NaN
# 2018-01-11    NaN
# 2018-01-16    NaN
# 2018-01-21    NaN
# 2018-01-26    NaN
# 2018-01-31   31.0

print(df.asfreq('5D', fill_value=15))
#             value
# 2018-01-01      1
# 2018-01-06     15
# 2018-01-11     15
# 2018-01-16     15
# 2018-01-21     15
# 2018-01-26     15
# 2018-01-31     31

print(df.asfreq('5D', method='pad'))
#             value
# 2018-01-01      1
# 2018-01-06      1
# 2018-01-11      1
# 2018-01-16     15
# 2018-01-21     15
# 2018-01-26     15
# 2018-01-31     31

print(df.asfreq('5D', method='bfill'))
#             value
# 2018-01-01      1
# 2018-01-06     15
# 2018-01-11     15
# 2018-01-16     31
# 2018-01-21     31
# 2018-01-26     31
# 2018-01-31     31

resample()でもffill(), bfill()メソッドが用意されている。

print(df.resample('5D').ffill())
#             value
# 2018-01-01      1
# 2018-01-06      1
# 2018-01-11      1
# 2018-01-16     15
# 2018-01-21     15
# 2018-01-26     15
# 2018-01-31     31

print(df.resample('5D').bfill())
#             value
# 2018-01-01      1
# 2018-01-06     15
# 2018-01-11     15
# 2018-01-16     31
# 2018-01-21     31
# 2018-01-26     31
# 2018-01-31     31

resample()ではnearest()メソッドがあり、前後どちらか近い方の値で穴埋めできる。

print(df.resample('5D').nearest())
#             value
# 2018-01-01      1
# 2018-01-06      1
# 2018-01-11     15
# 2018-01-16     15
# 2018-01-21     15
# 2018-01-26     31
# 2018-01-31     31

前後の値を使って補間(線形補間、スプライン補間など)

resample()にはinterpolate()メソッドが用意されている。デフォルトでは前後の値から線形補間される。

print(df.resample('5D').interpolate())
#             value
# 2018-01-01    1.0
# 2018-01-06    6.0
# 2018-01-11   11.0
# 2018-01-16   16.0
# 2018-01-21   21.0
# 2018-01-26   26.0
# 2018-01-31   31.0

interpolate()ではアップサンプリング後のデータを使って補間が行われるので注意。

例えば中央の値を変更しても、中央の値が選択されない間隔でアップサンプリングすると、中央の値が考慮されずに補間されてしまう。

df.loc['2018-01-15', 'value'] = 100
print(df)
#             value
# 2018-01-01      1
# 2018-01-15    100
# 2018-01-31     31

print(df.resample('5D').interpolate())
#             value
# 2018-01-01    1.0
# 2018-01-06    6.0
# 2018-01-11   11.0
# 2018-01-16   16.0
# 2018-01-21   21.0
# 2018-01-26   26.0
# 2018-01-31   31.0

中央の値が選択される間隔でアップサンプリングしてから任意の間隔にダウンサンプリングすればOK。

print(df.resample('D').interpolate())
#                  value
# 2018-01-01    1.000000
# 2018-01-02    8.071429
# 2018-01-03   15.142857
# 2018-01-04   22.214286
# 2018-01-05   29.285714
# 2018-01-06   36.357143
# 2018-01-07   43.428571
# 2018-01-08   50.500000
# 2018-01-09   57.571429
# 2018-01-10   64.642857
# 2018-01-11   71.714286
# 2018-01-12   78.785714
# 2018-01-13   85.857143
# 2018-01-14   92.928571
# 2018-01-15  100.000000
# 2018-01-16   95.687500
# 2018-01-17   91.375000
# 2018-01-18   87.062500
# 2018-01-19   82.750000
# 2018-01-20   78.437500
# 2018-01-21   74.125000
# 2018-01-22   69.812500
# 2018-01-23   65.500000
# 2018-01-24   61.187500
# 2018-01-25   56.875000
# 2018-01-26   52.562500
# 2018-01-27   48.250000
# 2018-01-28   43.937500
# 2018-01-29   39.625000
# 2018-01-30   35.312500
# 2018-01-31   31.000000

print(df.resample('D').interpolate().asfreq('5D'))
#                 value
# 2018-01-01   1.000000
# 2018-01-06  36.357143
# 2018-01-11  71.714286
# 2018-01-16  95.687500
# 2018-01-21  74.125000
# 2018-01-26  52.562500
# 2018-01-31  31.000000

スプライン補間の例。interpolate()の第一引数method'spline'を指定した上で引数orderに次数を指定する。

print(df.resample('D').interpolate('spline', order=2))
#                  value
# 2018-01-01    1.000000
# 2018-01-02   13.004464
# 2018-01-03   24.250000
# 2018-01-04   34.736607
# 2018-01-05   44.464286
# 2018-01-06   53.433036
# 2018-01-07   61.642857
# 2018-01-08   69.093750
# 2018-01-09   75.785714
# 2018-01-10   81.718750
# 2018-01-11   86.892857
# 2018-01-12   91.308036
# 2018-01-13   94.964286
# 2018-01-14   97.861607
# 2018-01-15  100.000000
# 2018-01-16  101.379464
# 2018-01-17  102.000000
# 2018-01-18  101.861607
# 2018-01-19  100.964286
# 2018-01-20   99.308036
# 2018-01-21   96.892857
# 2018-01-22   93.718750
# 2018-01-23   89.785714
# 2018-01-24   85.093750
# 2018-01-25   79.642857
# 2018-01-26   73.433036
# 2018-01-27   66.464286
# 2018-01-28   58.736607
# 2018-01-29   50.250000
# 2018-01-30   41.004464
# 2018-01-31   31.000000

print(df.resample('D').interpolate('spline', order=2).asfreq('5D'))
#                  value
# 2018-01-01    1.000000
# 2018-01-06   53.433036
# 2018-01-11   86.892857
# 2018-01-16  101.379464
# 2018-01-21   96.892857
# 2018-01-26   73.433036
# 2018-01-31   31.000000

不等間隔の欠損値の補間

等間隔にアップサンプリングする場合ではなく、例えば以下のような不等間隔の日時データに欠損値NaNがある場合についても触れておく。

import numpy as np

df_nan = pd.DataFrame({'value': [1, np.nan, np.nan, np.nan, 31]},
                      index=pd.to_datetime(['2018-01-01', '2018-01-02', '2018-01-15', '2018-01-20', '2018-01-31']))

print(df_nan)
#             value
# 2018-01-01    1.0
# 2018-01-02    NaN
# 2018-01-15    NaN
# 2018-01-20    NaN
# 2018-01-31   31.0

pandas.DataFrameinterpolate()メソッドで補間できるが、デフォルトの線形補間(第一引数method='linear')の場合、インデックスの日時は考慮されない。

print(df_nan.interpolate())
#             value
# 2018-01-01    1.0
# 2018-01-02    8.5
# 2018-01-15   16.0
# 2018-01-20   23.5
# 2018-01-31   31.0

第一引数method='time'とすると日時が考慮された線形補間となる。

print(df_nan.interpolate('time'))
#             value
# 2018-01-01    1.0
# 2018-01-02    2.0
# 2018-01-15   15.0
# 2018-01-20   20.0
# 2018-01-31   31.0

pandas.DataFrameのメソッドinterpolate()についての詳細は以下の記事を参照。

indexを変更したい場合はreindex()を使う。

関連カテゴリー

関連記事