はじめに
テーブルデータ系のコンペでしょっちゅう書き方を忘れて調べるので備忘録がわりに書いておく。
データを集計していると日付が抜けることがある。
たとえば、販売履歴のデータから日付ごとに販売数を合計して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つある。
これらについては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 withPeriodIndex.asfreq
(so the original index will map one-to-one to the new index).
Otherwise, the new index will be equivalent topd.date_range(start, end, freq=freq)
where start and end are, respectively, the first and last entries in the original index (seepandas.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).
Theresample()
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 |