LoginSignup
12

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-14

はじめに

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

データを集計していると日付が抜けることがある。

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

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
sales
date
2022-03-04 10
2022-03-05 12
2022-03-06 14
2022-03-07 16
2022-03-08 18
2022-03-11 24
2022-03-12 26
2022-03-13 28
2022-03-16 34
2022-03-17 36
2022-03-18 38
2022-03-19 40

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

  1. pandas.date_range()を使う。
  2. pandas.DataFrame.asfreq()を使う。
  3. pandas.DataFrame.resample()を使う。

これらについてはpandas.DataFrame.asfreq()の公式のドキュメントで以下のように書かれている。

Returns the original data conformed to a new index with the specified frequency.

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).

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).

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(無料版)による訳。

指定された頻度で新しいインデックスに適合する元のデータを返す。

このDataFrameのインデックスがPeriodIndexの場合、新しいインデックスは、元のインデックスをPeriodIndex.asfreqで変換した結果です(したがって、元のインデックスは新しいインデックスに一対一で対応します)。

それ以外の場合、新しいインデックスは pd.date_range(start, end, freq=freq) と等しくなります。ここで start と end はそれぞれ、元のインデックスの最初と最後のエントリです (pandas.date_range() を参照してください)。元のインデックスに存在しない新しいインデックスのタイムステップに対応する値は、そのような未知数を埋めるためのメソッドが提供されていない限り、ヌル(NaN)になります(以下のメソッドパラメータを参照)。

新しい周波数でデータを表現するために、タイムステップの各グループに対する操作(集約など)が必要な場合は、resample()メソッドがより適切である。

pandas.date_range()を使う


[追記] pandas.DataFrame.reindex()を使用した方が良いというコメントをいただきました。

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

pandas.date_range()を使うと、一定期間で連続した時系列のIndexを作成できる。

これを使って日付だけの列を持つDataFrameを作成して、元のDataFrameにマージする。

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")
sales
date
2022-03-04 10.0
2022-03-05 12.0
2022-03-06 14.0
2022-03-07 16.0
2022-03-08 18.0
2022-03-09 NaN
2022-03-10 NaN
2022-03-11 24.0
2022-03-12 26.0
2022-03-13 28.0
2022-03-14 NaN
2022-03-15 NaN
2022-03-16 34.0
2022-03-17 36.0
2022-03-18 38.0
2022-03-19 40.0

欠損値を埋めたい場合、置換pandas.DataFrame.fillna()や補完pandas.DataFrame.interpolate()を使えば良い。

# 線形に補完
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")
sales
date
2022-03-04 10.0
2022-03-05 12.0
2022-03-06 14.0
2022-03-07 16.0
2022-03-08 18.0
2022-03-09 20.0
2022-03-10 22.0
2022-03-11 24.0
2022-03-12 26.0
2022-03-13 28.0
2022-03-14 30.0
2022-03-15 32.0
2022-03-16 34.0
2022-03-17 36.0
2022-03-18 38.0
2022-03-19 40.0

pandas.DataFrame.asfreq()を使う

pandas.DataFrame.asfreq()を利用することで、時系列データをある期間でサンプリングすることができる。

df.asfreq("D")
sales
date
2022-03-04 10.0
2022-03-05 12.0
2022-03-06 14.0
2022-03-07 16.0
2022-03-08 18.0
2022-03-09 NaN
2022-03-10 NaN
2022-03-11 24.0
2022-03-12 26.0
2022-03-13 28.0
2022-03-14 NaN
2022-03-15 NaN
2022-03-16 34.0
2022-03-17 36.0
2022-03-18 38.0
2022-03-19 40.0

欠損値の穴埋めをしたい場合は引数で方法を指定するか、pandas.DataFrame.interpolate()を使うなど。

df.asfreq("D", method="ffill")
sales
date
2022-03-04 10
2022-03-05 12
2022-03-06 14
2022-03-07 16
2022-03-08 18
2022-03-09 18
2022-03-10 18
2022-03-11 24
2022-03-12 26
2022-03-13 28
2022-03-14 28
2022-03-15 28
2022-03-16 34
2022-03-17 36
2022-03-18 38
2022-03-19 40

pandas.DataFrame.resample()を使う

asfreqと近いイメージで利用可能。

pandas.DataFrame.resample()だとpandas.core.resample.DatetimeIndexResamplerクラスオブジェクトが返ってくるので、sum()mean()などで集計し直す必要がある。

ただし、存在しない日の値はsum()の場合は0に、mean()の場合はNaNになることには注意が必要。

df.resample("D")
<pandas.core.resample.DatetimeIndexResampler object at 0xffff67436fd0>
df.resample("D").sum()
sales
date
2022-03-04 10
2022-03-05 12
2022-03-06 14
2022-03-07 16
2022-03-08 18
2022-03-09 0
2022-03-10 0
2022-03-11 24
2022-03-12 26
2022-03-13 28
2022-03-14 0
2022-03-15 0
2022-03-16 34
2022-03-17 36
2022-03-18 38
2022-03-19 40
df.resample("D").mean()
sales
date
2022-03-04 10.0
2022-03-05 12.0
2022-03-06 14.0
2022-03-07 16.0
2022-03-08 18.0
2022-03-09 NaN
2022-03-10 NaN
2022-03-11 24.0
2022-03-12 26.0
2022-03-13 28.0
2022-03-14 NaN
2022-03-15 NaN
2022-03-16 34.0
2022-03-17 36.0
2022-03-18 38.0
2022-03-19 40.0

groupby, applyとの組み合わせ

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

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
sales
item date
apple 2022-03-06 14
2022-03-07 16
2022-03-08 18
2022-03-11 24
2022-03-12 26
2022-03-13 28
grape 2022-03-09 18
2022-03-10 24
2022-03-13 28
2022-03-14 30
2022-03-15 32

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

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)
sales
item date
apple 2022-03-06 14.000000
2022-03-07 16.000000
2022-03-08 18.000000
2022-03-09 20.000000
2022-03-10 22.000000
2022-03-11 24.000000
2022-03-12 26.000000
2022-03-13 28.000000
grape 2022-03-09 18.000000
2022-03-10 24.000000
2022-03-11 25.333333
2022-03-12 26.666667
2022-03-13 28.000000
2022-03-14 30.000000
2022-03-15 32.000000

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12