pandasで欠損行のある時系列データを扱う
等間隔でサンプリングされた時系列データで、ただし欠損値に該当する行が欠落している (NaN などで埋められていない)ようなものを pandas でロードして、 欠損した行を「値が NaN である行」として補完するレシピを書いておく。
pandas で時系列データを扱うなら、まず時間軸をデータフレームのインデックスにしよう。
pandas.DataFrame.resample()
など pandas が用意している様々な時系列用の機能が使えるようになり、便利だ。
たとえば、単純な時系列データであれば今回やりたいコトも
resample(RULE).asfreq()
というメソッドチェーンで一発だ
(RULE
には
offset-alias
などを指定):
>>> import pandas as pd >>> df = pd.DataFrame( ... data={"value": [1, 2, 4]}, ... index=pd.to_datetime(["2000-01-01", "2000-02-01", "2000-04-01"]), ... ) >>> df value 2000-01-01 1 2000-02-01 2 2000-04-01 4 >>> df = df.resample("MS").asfreq() >>> df value 2000-01-01 1.0 2000-02-01 2.0 2000-03-01 NaN 2000-04-01 4.0
ただ、世の中こんなに単純な時系列データばかりではないので、 今回はこのメソッドチェーン一発では処理できない次のようなデータを取り上げたい:
date | source | value |
---|---|---|
2000-01-01 | A | 0.66691 |
2000-01-02 | A | 5.04412 |
2000-01-03 | A | 3.15062 |
2000-01-05 | A | 6.57241 |
2000-01-01 | B | 95.9277 |
2000-01-03 | B | 85.4801 |
2000-01-04 | B | 85.6996 |
2000-01-05 | B | 55.4857 |
date 列を見ると重複があり、source ごとに value の値域が異なっていることから、 おそらく source ごとに異なる時系列データを記録したものを、 一つの表にまとめたものだろうと解釈できる (たとえば「都市別の新型コロナ陽性者数」などは、こんな形かもしれない)。 ということは、表に内包されている時系列データをそれぞれ取り出してやれば、 先に書いたメソッドチェーンで処理できるだろう。
以下、実際に動作する Python スクリプトを書いておく。
import io from textwrap import dedent import pandas as pd csv_data = dedent( """ date,source,value 2000-01-01,A,0.66691 2000-02-01,A,5.04412 2000-03-01,A,3.15062 2000-05-01,A,6.57241 2000-01-01,B,95.9277 2000-03-01,B,85.4801 2000-04-01,B,85.6996 2000-05-01,B,55.4857 """ ) df = pd.read_csv(io.StringIO(csv_data), index_col="date", parse_dates=True) # あとで順番を戻せるよう元々の列名リストを記憶しておく orig_colnames = [str(c) for c in df.columns] # source ごとに欠損日時のデータを NaN で埋める dfs = [] sources = df["source"].unique() for source in sources: # その source の時系列データだけを取り出す tmp = df[df["source"] == source].drop(columns="source") # 欠損行を補完 tmp = tmp.resample("D").asfreq() # あとで結合するために結果を記録 tmp["source"] = source dfs.append(tmp) # source ごとに欠損行を補完したデータフレーム群を結合 df = pd.concat(dfs) # 列の順番を元に戻す df = df.reindex(columns=orig_colnames) print(df)
このスクリプトの実行結果は次のようになる:
source value date 2000-01-01 A 0.66691 2000-02-01 A 5.04412 2000-03-01 A 3.15062 2000-04-01 A NaN 2000-05-01 A 6.57241 2000-01-01 B 95.92770 2000-02-01 B NaN 2000-03-01 B 85.48010 2000-04-01 B 85.69960 2000-05-01 B 55.48570
なお、このような処理を何に使おうとしたのかもメモしておこう。
N 日先あるいは N ヶ月先といった先読みを行う回帰モデルの学習データを用意するとき、
この方法で欠損行の無い等間隔なデータフレームを作成し、
「N 行ずらしてマージ」することで目的変数列を用意した。
先読みの単位が時・分・秒あるいは日であれば、
date 列を秒単位のタイムスタンプに変換すれば「次のレコード」を引き当てるのは簡単だ。
たとえば、ある行から見て 1 時間先の行を引き当てるには
「タイムスタンプが 3600 秒大きい行」を探して merge()
(SQL でいう JOIN) すれば良い。
しかし先読みの単位が月や年になると、1 ヶ月あるいは
1 年間は秒単位で数えると等間隔ではないためアプローチを変える必要がある。
そこで今回のような前処理を行うと、
データフレームの行の間隔が月または年の間隔と一致するため、
行数を基準に merge()
すれば良くなる。
そのような処理例も、書いておこう:
>>> import pandas as pd >>> df = pd.DataFrame( ... data={"value": [1, 2, 4]}, ... index=pd.to_datetime(["2000-01-01", "2000-02-01", "2000-04-01"]), ... ) >>> df = df.resample("MS").asfreq() >>> df value 2000-01-01 1.0 2000-02-01 2.0 2000-03-01 NaN 2000-04-01 4.0 >>> tmp = df.shift(-1).rename(columns={"value": "next_value"}) >>> pd.merge(df, tmp, left_index=True, right_index=True) value next_value 2000-01-01 1.0 2.0 2000-02-01 2.0 NaN 2000-03-01 NaN 4.0 2000-04-01 4.0 NaN
以上。