【Python】pandas.Grouper・resample・pandas.date_rangeの処理を比較する

f:id:hesma2:20210121220126p:plain

時系列データを扱う際によく使われる、以下の3つの処理を日次・週次・月次(daily, weekly, monthly)で比較してみます!
どこが同じで、どこが違うのかを確認していきます!

【比較対象】

  • pandas.Grouper
  • resample
  • pandas_date_range

pandas.Grouperについてはこちらの記事で紹介しています。

hesma2.hatenablog.com

動作環境

MacOS Catalina 10.15.7
Python 3.8.6
pandas 1.2.1
VSCode 1.52.1

VSCode拡張機能のJupyterを使用します。 詳細はこちらを参照ください。

dev.classmethod.jp

データ準備

2020年のa店舗、b店舗、c店舗の日次来客数のデータを使用します。

import pandas as pd

store_visits_df = pd.read_csv("./csv/store_visits_2020.csv")

pandas.Grouper、pandas.resample共に、日付データのカラムは datetime型 にしておく必要があります。

store_visits_df.dtypes
date         object
store_id     object
visit_num     int64
dtype: object

dateカラムはobject型になっていると以下のエラーが発生します。

TypeError: Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex, but got an instance of 'Index'

以下のようにしてdatetime型に変換しておきましょう。

store_visits_df["date"] = pd.to_datetime(store_visits_df["date"])

また、resampleでは日付カラムをインデックスに設定する必要があります。

store_visits_df = store_visits_df.set_index("date")

日次の処理(daily)

pandas.Grouperの場合

grouper_daily_df = store_visits_df.groupby(
    pd.Grouper(level="date", freq="D")
).sum()

grouper_daily_df

resampleの場合

resample_daily_df = store_visits_df.resample("D").sum()

resample_daily_df

こちらも同じ結果に!

全く同じDataFrameであることを、 assert_frame_equal を使用して確認します。

from pandas.testing import assert_frame_equal

print(assert_frame_equal(grouper_daily_df, resample_daily_df))

> None

assert_frame_equal についてはこちら ↓

hesma2.hatenablog.com

日次では、pandas.Grouperとresampleが同じ処理をしていることがわかりました。
コード的にはresampleの方がシンプルになりますね!

pandas.date_rangeの場合

ちょっと毛色が違いますが、date_rangeも比較してみます。

from datetime import date

date_range_daily = pd.date_range(
    start=date(2020,1,1),
    end=date(2020,12,31),
    freq="D"
)

date_range_daily

結果はこちら ↓

DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
               '2020-01-05', '2020-01-06', '2020-01-07', '2020-01-08',
               '2020-01-09', '2020-01-10',
               ...
               '2020-12-22', '2020-12-23', '2020-12-24', '2020-12-25',
               '2020-12-26', '2020-12-27', '2020-12-28', '2020-12-29',
               '2020-12-30', '2020-12-31'],
              dtype='datetime64[ns]', length=366, freq='D')

これもpandas.GrouperとresampleのIndexと比較して、同じであることが確認できました。

list(grouper_daily_df.index) == list(date_range_daily)

> True

list(resample_daily_df.index) == list(date_range_daily)

> True

■ 日次処理まとめ

  • freqのオプション指定 D も同じでかつ同じ処理がされる

週次の処理(weekly)

月曜 ~ 日曜の1週間を、月曜表示で扱います。

例
【表示】 2020-01-06
【集計】 2020-01-06 ~ 2020-01-12

f:id:hesma2:20210121234019p:plain

pandas.Grouperの場合

grouper_daily_df = store_visits_df.groupby(
    pd.Grouper(level="date", freq="W-MON", closed="left", label="left")
).sum()

grouper_daily_df

■ オプション解説

freq="W-MON": 月曜基準の週
(デフォルトは "W" で "W-SUN" と同義。日曜基準の週)

closed="left": 基準を左端にするか、右端にするか
(デフォルトは"right" )

label="left": 基準の右側にある基準日を表記するか、左側にある基準日を表記するか
(デフォルトは"right" )

月曜始まりの日曜終わりでちゃんとグルーピングできているかチェックします。

store_visits_df.groupby(
    pd.Grouper(level="date", freq="W-MON", closed="left", label="left")
).get_group("2020-01-06")

2020-01-06(月)から2020-01-12(日)でグルーピングできていました!

resampleの場合

resample_weekly_df = store_visits_df.resample(
    "W-MON", closed="left", label="left"
).sum()

resample_weekly_df

resampleでもpandas.Grouperと同じオプション指定で同じ結果になりました!

念のため、同じDataFrameになったことをチェックします。

print(assert_frame_equal(grouper_weekly_df, resample_weekly_df))

> None

■ closed, labelの詳細はこちら

note.com

f:id:hesma2:20210122001044p:plain
https://note.com/yokkai/n/nda684b0307d5 から引用

詳細なコードや結果は載せませんが、日曜始まり(freq="W"または"W-SUN")の集計においてもpandas.Grouperとresampleの処理に違いはありませんでした。

pandas.date_rangeの場合

date_range_weekly = pd.date_range(
    start=date(2020,1,1),
    end=date(2020,12,31),
    freq="W-MON"
)

date_range_weekly

結果 ↓

DatetimeIndex(['2020-01-06', '2020-01-13', '2020-01-20', '2020-01-27',
               '2020-02-03', '2020-02-10', '2020-02-17', '2020-02-24',
               '2020-03-02', '2020-03-09', '2020-03-16', '2020-03-23',
               '2020-03-30', '2020-04-06', '2020-04-13', '2020-04-20',
               '2020-04-27', '2020-05-04', '2020-05-11', '2020-05-18',
               '2020-05-25', '2020-06-01', '2020-06-08', '2020-06-15',
               '2020-06-22', '2020-06-29', '2020-07-06', '2020-07-13',
               '2020-07-20', '2020-07-27', '2020-08-03', '2020-08-10',
               '2020-08-17', '2020-08-24', '2020-08-31', '2020-09-07',
               '2020-09-14', '2020-09-21', '2020-09-28', '2020-10-05',
               '2020-10-12', '2020-10-19', '2020-10-26', '2020-11-02',
               '2020-11-09', '2020-11-16', '2020-11-23', '2020-11-30',
               '2020-12-07', '2020-12-14', '2020-12-21', '2020-12-28'],
              dtype='datetime64[ns]', freq='W-MON')

pandas.date_rangeでもfreqの指定("W"や"W-MON")は共通でした。

しかし日付リストには違いが出ました。

list(grouper_weekly_df.index) == list(date_range_weekly)

> False

差分も出してみます。

set(grouper_weekly_df.index) - set(date_range_weekly)

> {Timestamp('2019-12-30 00:00:00', freq='W-MON')}

このことから、pandas.date_rangeは、
start_date から end_date が所属する週のリストを出すのではなく、
start_date から end_date の中にある週の基準日のリストを出す ことがわかります。

■ 週次処理まとめ

  • freqのオプション指定("W"や"W-MON")は3つとも同じ
  • pandas.Grouperとresampleでは、closedやlabelのオプションが同じように扱えて処理結果も同じ
  • pandas.date_rangeではエッジケースの処理が他2つとは異なる

月次の処理(monthly)

月初日を基準に集計します。

例
【表示】 2020-01-01
【集計】 2020-01-01 ~ 2020-01-31

pandas.Grouperの場合

月初日を基準とする場合、freq="MS"とします。
(freq="M" では月末日が基準となります)

grouper_monthly_df = store_visits_df.groupby(
    pd.Grouper(level="date", freq="MS")
).sum()

grouper_monthly_df

グルーピングも確認します。

store_visits_df.groupby(
    pd.Grouper(level="date", freq="MS")
).get_group("2020-01-01")

resampleの場合

resample_monthly_df = store_visits_df.resample("MS").sum()

resample_monthly_df

月次においても、pandas.GrouperとDataFrameが一致しました!

print(assert_frame_equal(grouper_monthly_df, resample_monthly_df))

> None

pandas.date_rangeの場合

date_range_monthly = pd.date_range(
    start=date(2020,1,1),
    end=date(2020,12,31),
    freq="MS"
)

date_range_monthly

結果 ↓

DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01', '2020-04-01',
               '2020-05-01', '2020-06-01', '2020-07-01', '2020-08-01',
               '2020-09-01', '2020-10-01', '2020-11-01', '2020-12-01'],
              dtype='datetime64[ns]', freq='MS')

pandas.Grouperのインデックスと比較します。

list(grouper_monthly_df.index) == list(date_range_monthly)

> True

2020-01-01 ~ 2020-12-31 を指定して実行したので同じ結果になりましたが、週次でエッジケースの処理が異なることを考慮すると月次でも同じことが起きるはずです。

試しに、2020-01-02 ~ 2020-12-31 で処理を行ってみます。

date_range_monthly = pd.date_range(
    start=date(2020,1,2),
    end=date(2020,12,31),
    freq="MS"
)

date_range_monthly

結果 ↓

DatetimeIndex(['2020-02-01', '2020-03-01', '2020-04-01', '2020-05-01',
               '2020-06-01', '2020-07-01', '2020-08-01', '2020-09-01',
               '2020-10-01', '2020-11-01', '2020-12-01'],
              dtype='datetime64[ns]', freq='MS')

やはり 2020-01-01 が抜けましたね。この処理の違いには注意が必要そうです。

■ 月次処理まとめ

  • freqのオプション指定("M"や"MS")は3つとも同じ
  • pandas.Grouperとresampleでは処理結果は同じ
  • pandas.date_rangeではエッジケースの処理が他2つとは異なる

まとめ

■ 日次処理まとめ

  • freqのオプション指定 D も同じでかつ同じ処理がされる

■ 週次処理まとめ

  • freqのオプション指定("W"や"W-MON")は3つとも同じ
  • pandas.Grouperとresampleでは、closedやlabelのオプションが同じように扱えて処理結果も同じ
  • pandas.date_rangeではエッジケースの処理が他2つとは異なる

■ 月次処理まとめ

  • freqのオプション指定("M"や"MS")は3つとも同じ
  • pandas.Grouperとresampleでは処理結果は同じ
  • pandas.date_rangeではエッジケースの処理が他2つとは異なる