本文を読み飛ばす

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

以上。