pandasで時系列データの曜日や月、四半期、年ごとの合計や平均を算出

Modified: | Tags: Python, pandas, 時系列データ

pandas.DataFrameのインデックスをDatetimeIndexにすると、日付や時刻など日時の情報を持つ時系列データを処理するのに便利。

DatetimeIndexは属性として曜日や月、四半期、年などの情報を取得できるので、それらを利用して時系列データの期間ごとの合計や平均を算出したりできる。さらに、それらを組み合わせて、例えば四半期毎の曜日別の合計などを算出することも可能。

ここでは、以下の内容について説明する。

  • resample()による任意の期間の集計
  • 曜日: weekday, dayofweek, day_name()
    • 曜日を指定して行を抽出
    • 曜日ごとの合計や平均を算出
  • そのほかの属性
    • 年: year
    • 四半期: quarter
    • 月: month, month_name()
    • 週: week, weekofyear
  • 複数の属性(曜日、月、四半期、年など)を組み合わせて処理

インデックスをdatetime64[ns]型のDatetimeIndexとして設定する方法などについては以下の記事を参照。

例として、以下のcsvデータを使う。

import pandas as pd

df = pd.read_csv('data/src/sample_date.csv', index_col=0, parse_dates=True)
print(df)
#             val_1  val_2
# date                    
# 2017-11-01     65     76
# 2017-11-07     26     66
# 2017-11-18     47     47
# 2017-11-27     20     38
# 2017-12-05     65     85
# 2017-12-12      4     29
# 2017-12-22     31     54
# 2017-12-29     21      8
# 2018-01-03     98     76
# 2018-01-08     48     64
# 2018-01-19     18     48
# 2018-01-23     86     70

print(type(df.index))
# <class 'pandas.core.indexes.datetimes.DatetimeIndex'>

resample()による任意の期間の集計

まず、DatetimeIndexの属性ではなく、resample()を使って任意の期間の集計(合計や平均など)を行う方法を示す。

第一引数にW(週)、M(月)、Q(四半期)、Y(年)などの頻度コードを指定し、さらにsum()などのメソッドを呼び出す。

print(df.resample('W').sum())
#             val_1  val_2
# date                    
# 2017-11-05     65     76
# 2017-11-12     26     66
# 2017-11-19     47     47
# 2017-11-26      0      0
# 2017-12-03     20     38
# 2017-12-10     65     85
# 2017-12-17      4     29
# 2017-12-24     31     54
# 2017-12-31     21      8
# 2018-01-07     98     76
# 2018-01-14     48     64
# 2018-01-21     18     48
# 2018-01-28     86     70

print(df.resample('M').sum())
#             val_1  val_2
# date                    
# 2017-11-30    158    227
# 2017-12-31    121    176
# 2018-01-31    250    258

print(df.resample('Q').sum())
#             val_1  val_2
# date                    
# 2017-12-31    279    403
# 2018-03-31    250    258

print(df.resample('Y').sum())
#             val_1  val_2
# date                    
# 2017-12-31    279    403
# 2018-12-31    250    258

10D(10日間)や3W(3週間)のように数字を組み合わせて期間を指定することもできる。

print(df.resample('10D').sum())
#             val_1  val_2
# date                    
# 2017-11-01     91    142
# 2017-11-11     47     47
# 2017-11-21     20     38
# 2017-12-01     65     85
# 2017-12-11      4     29
# 2017-12-21     52     62
# 2017-12-31    146    140
# 2018-01-10     18     48
# 2018-01-20     86     70

print(df.resample('3W').sum())
#             val_1  val_2
# date                    
# 2017-11-05     65     76
# 2017-11-26     73    113
# 2017-12-17     89    152
# 2018-01-07    150    138
# 2018-01-28    152    182

sum()のほか、mean()などのメソッドが使用可能。agg()で複数のメソッドを同時に適用することもできる。

print(df.resample('Y').mean())
#              val_1   val_2
# date                      
# 2017-12-31  34.875  50.375
# 2018-12-31  62.500  64.500

print(df.resample('Y').agg(['sum', 'mean', 'max', 'min']))
#            val_1                 val_2                
#              sum    mean max min   sum    mean max min
# date                                                  
# 2017-12-31   279  34.875  65   4   403  50.375  85   8
# 2018-12-31   250  62.500  98  18   258  64.500  76  48

indexの値が、Dの場合は期間の開始日時、W, M, Q, Yは終了日時となるという違いがあるので注意。

resample()や頻度コードについての詳細は以下の記事を参照。

曜日: weekday, dayofweek, day_name()

ここからは、DatetimeIndexの属性やマルチインデックスを利用して集計を行う方法について説明する。まずはresample()で扱うのが難しい曜日を例とする。

DatetimeIndexの属性weekdayまたはdayofweekで、月曜日が0、日曜日が6となる整数で曜日の情報を取得できる。どちらでも同じ。

print(df.index.weekday)
# Int64Index([2, 1, 5, 0, 1, 1, 4, 4, 2, 0, 4, 1], dtype='int64', name='date')

print(df.index.dayofweek)
# Int64Index([2, 1, 5, 0, 1, 1, 4, 4, 2, 0, 4, 1], dtype='int64', name='date')

day_name()だと英語表記で取得できる。属性ではなくメソッドなので注意。なお、バージョン0.22まではweekday_name属性で曜日の名前を取得可能だったが、0.23で廃止され、新しくday_name()が追加された。

print(df.index.day_name())
# Index(['Wednesday', 'Tuesday', 'Saturday', 'Monday', 'Tuesday', 'Tuesday',
#        'Friday', 'Friday', 'Wednesday', 'Monday', 'Friday', 'Tuesday'],
#       dtype='object', name='date')

曜日を指定して行を抽出

例えば、月曜日(0)の行を抽出したい場合は、以下のようにする。

print(df[df.index.weekday == 0])
#             val_1  val_2
# date                    
# 2017-11-27     20     38
# 2018-01-08     48     64

曜日ごとの合計や平均を算出

指定した曜日で抽出したデータの合計や平均などの統計量は、sum()mean()メソッドで算出できる。agg()でまとめて適用することも可能。

print(df[df.index.weekday == 0].sum())
# val_1     68
# val_2    102
# dtype: int64

print(df[df.index.weekday == 0].mean())
# val_1    34.0
# val_2    51.0
# dtype: float64

print(df[df.index.weekday == 0].agg(['sum', 'mean']))
#       val_1  val_2
# sum    68.0  102.0
# mean   34.0   51.0

特定の曜日だけでなく、すべての曜日ごとの合計や平均を算出したい場合は、マルチインデックスの仕組みを使うと便利。

set_index()で複数の列をインデックスに指定するとマルチインデックスになる。set_index()については以下の記事を参照。

df_w = df.set_index([df.index.weekday, df.index])
print(df_w)
#                  val_1  val_2
# date date                    
# 2    2017-11-01     65     76
# 1    2017-11-07     26     66
# 5    2017-11-18     47     47
# 0    2017-11-27     20     38
# 1    2017-12-05     65     85
#      2017-12-12      4     29
# 4    2017-12-22     31     54
#      2017-12-29     21      8
# 2    2018-01-03     98     76
# 0    2018-01-08     48     64
# 4    2018-01-19     18     48
# 1    2018-01-23     86     70

このままだとインデックス列の名前が重複しているので、index.names属性を書き換える。

df_w.index.names = ['weekday', 'date']
print(df_w)
#                     val_1  val_2
# weekday date                    
# 2       2017-11-01     65     76
# 1       2017-11-07     26     66
# 5       2017-11-18     47     47
# 0       2017-11-27     20     38
# 1       2017-12-05     65     85
#         2017-12-12      4     29
# 4       2017-12-22     31     54
#         2017-12-29     21      8
# 2       2018-01-03     98     76
# 0       2018-01-08     48     64
# 4       2018-01-19     18     48
# 1       2018-01-23     86     70

さらに、sort_index()でソート。

df_w.sort_index(inplace=True)
print(df_w)
#                     val_1  val_2
# weekday date                    
# 0       2017-11-27     20     38
#         2018-01-08     48     64
# 1       2017-11-07     26     66
#         2017-12-05     65     85
#         2017-12-12      4     29
#         2018-01-23     86     70
# 2       2017-11-01     65     76
#         2018-01-03     98     76
# 4       2017-12-22     31     54
#         2017-12-29     21      8
#         2018-01-19     18     48
# 5       2017-11-18     47     47

sum()をそのまま使うと全体の合計が算出されるが、引数levelでインデックス列の名前を指定すると、そのインデックスごとの合計(この場合は各曜日の合計)が算出される。

print(df_w.sum())
# val_1    529
# val_2    661
# dtype: int64

print(df_w.sum(level='weekday'))
#          val_1  val_2
# weekday              
# 0           68    102
# 1          181    250
# 2          163    152
# 4           70    110
# 5           47     47

mean()など、他のメソッドでも同様。

print(df_w.mean(level='weekday'))
#              val_1      val_2
# weekday                      
# 0        34.000000  51.000000
# 1        45.250000  62.500000
# 2        81.500000  76.000000
# 4        23.333333  36.666667
# 5        47.000000  47.000000

agg()の場合はgroupby()と合わせて使う。

print(df_w.groupby(level='weekday').agg(['sum', 'mean']))
#         val_1            val_2           
#           sum       mean   sum       mean
# weekday                                  
# 0          68  34.000000   102  51.000000
# 1         181  45.250000   250  62.500000
# 2         163  81.500000   152  76.000000
# 4          70  23.333333   110  36.666667
# 5          47  47.000000    47  47.000000

そのほかの属性

曜日以外の属性をいくつか示す。一覧は公式ドキュメントを参照。

年: year

DatetimeIndexの属性yearで、西暦の年が取得できる。

print(df.index.year)
# Int64Index([2017, 2017, 2017, 2017, 2017, 2017, 2017, 2017, 2018, 2018, 2018,
#             2018],
#            dtype='int64', name='date')

上で説明した曜日の場合と同じ要領で、年ごとの合計や平均などを算出できる。

df_y = df.set_index([df.index.year, df.index])
df_y.index.names = ['year', 'date']
print(df_y)
#                  val_1  val_2
# year date                    
# 2017 2017-11-01     65     76
#      2017-11-07     26     66
#      2017-11-18     47     47
#      2017-11-27     20     38
#      2017-12-05     65     85
#      2017-12-12      4     29
#      2017-12-22     31     54
#      2017-12-29     21      8
# 2018 2018-01-03     98     76
#      2018-01-08     48     64
#      2018-01-19     18     48
#      2018-01-23     86     70

print(df_y.sum(level='year'))
#       val_1  val_2
# year              
# 2017    279    403
# 2018    250    258

四半期: quarter

DatetimeIndexの属性quarterで、四半期の番号が取得できる。

df_q = df.set_index([df.index.quarter, df.index])
df_q.index.names = ['quarter', 'date']
print(df_q)
#                     val_1  val_2
# quarter date                    
# 4       2017-11-01     65     76
#         2017-11-07     26     66
#         2017-11-18     47     47
#         2017-11-27     20     38
#         2017-12-05     65     85
#         2017-12-12      4     29
#         2017-12-22     31     54
#         2017-12-29     21      8
# 1       2018-01-03     98     76
#         2018-01-08     48     64
#         2018-01-19     18     48
#         2018-01-23     86     70

print(df_q.sum(level='quarter'))
#          val_1  val_2
# quarter              
# 4          279    403
# 1          250    258

月: month

DatetimeIndexの属性monthで、月が取得できる。

df_m = df.set_index([df.index.month, df.index])
df_m.index.names = ['month', 'date']
print(df_m)
#                   val_1  val_2
# month date                    
# 11    2017-11-01     65     76
#       2017-11-07     26     66
#       2017-11-18     47     47
#       2017-11-27     20     38
# 12    2017-12-05     65     85
#       2017-12-12      4     29
#       2017-12-22     31     54
#       2017-12-29     21      8
# 1     2018-01-03     98     76
#       2018-01-08     48     64
#       2018-01-19     18     48
#       2018-01-23     86     70

print(df_m.sum(level='month'))
#        val_1  val_2
# month              
# 11       158    227
# 12       121    176
# 1        250    258

月の名前を返すmonth_name()もある

print(df.index.month_name())
# Index(['November', 'November', 'November', 'November', 'December', 'December',
#        'December', 'December', 'January', 'January', 'January', 'January'],
#       dtype='object', name='date')

週: week, weekofyear

DatetimeIndexの属性weekまたはweekofyearで、週番号を取得できる。

df_w2 = df.set_index([df.index.week, df.index])
df_w2.index.names = ['week', 'date']
print(df_w2)
#                  val_1  val_2
# week date                    
# 44   2017-11-01     65     76
# 45   2017-11-07     26     66
# 46   2017-11-18     47     47
# 48   2017-11-27     20     38
# 49   2017-12-05     65     85
# 50   2017-12-12      4     29
# 51   2017-12-22     31     54
# 52   2017-12-29     21      8
# 1    2018-01-03     98     76
# 2    2018-01-08     48     64
# 3    2018-01-19     18     48
# 4    2018-01-23     86     70

print(df_w2.sum(level='week'))
#       val_1  val_2
# week              
# 44       65     76
# 45       26     66
# 46       47     47
# 48       20     38
# 49       65     85
# 50        4     29
# 51       31     54
# 52       21      8
# 1        98     76
# 2        48     64
# 3        18     48
# 4        86     70

週番号はISO 8601の定義に従っている。1月1日が第1週とは限らないので注意。月曜始まり。

この記法では、ある年における「最初の木曜日を含む週が、その年の第1週である。」と規定されている。「その年の第1週は、1月4日を含む週である。」としても、基準としては等しい。 ISO 8601 - Wikipedia

print(pd.date_range('2017-01-01', '2017-01-07').week)
# Int64Index([52, 1, 1, 1, 1, 1, 1], dtype='int64')

print(pd.date_range('2017-01-01', '2017-01-07').day_name())
# Index(['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
#        'Saturday'],
#       dtype='object')

複数の属性(曜日、月、四半期、年など)を組み合わせて処理

マルチインデックスの列を増やすことで、複数の属性(曜日、月、四半期、年など)を組み合わせて処理することもできる。

単純な年ごとや月ごとの処理はresample()のほうが簡単だが、複数の属性を組み合わせた処理はマルチインデックスを利用すると便利。

例として、年と曜日で分類してみる。

df_yw = df.set_index([df.index.year, df.index.weekday, df.index])
df_yw.index.names = ['year', 'weekday', 'date']
df_yw.sort_index(inplace=True)
print(df_yw)
#                          val_1  val_2
# year weekday date                    
# 2017 0       2017-11-27     20     38
#      1       2017-11-07     26     66
#              2017-12-05     65     85
#              2017-12-12      4     29
#      2       2017-11-01     65     76
#      4       2017-12-22     31     54
#              2017-12-29     21      8
#      5       2017-11-18     47     47
# 2018 0       2018-01-08     48     64
#      1       2018-01-23     86     70
#      2       2018-01-03     98     76
#      4       2018-01-19     18     48

sum()mean()の引数levelによって、全データの曜日別データを集計したり、年ごとの曜日別データを集計したりできる。

print(df_yw.sum(level='weekday'))
#          val_1  val_2
# weekday              
# 0           68    102
# 1          181    250
# 2          163    152
# 4           70    110
# 5           47     47

print(df_yw.sum(level=['year', 'weekday']))
#               val_1  val_2
# year weekday              
# 2017 0           20     38
#      1           95    180
#      2           65     76
#      4           52     62
#      5           47     47
# 2018 0           48     64
#      1           86     70
#      2           98     76
#      4           18     48

マルチインデックスの場合の列の選択、抽出は若干クセがある。詳細は以下の記事を参照。

例をいくつか示す。

print(df_yw.loc[(2017, 1), :])
#             val_1  val_2
# date                    
# 2017-11-07     26     66
# 2017-12-05     65     85
# 2017-12-12      4     29

print(df_yw.xs(1, level='weekday'))
#                  val_1  val_2
# year date                    
# 2017 2017-11-07     26     66
#      2017-12-05     65     85
#      2017-12-12      4     29
# 2018 2018-01-23     86     70

print(df_yw.loc[(2017, [0, 4]), :])
#                          val_1  val_2
# year weekday date                    
# 2017 0       2017-11-27     20     38
#      4       2017-12-22     31     54
#              2017-12-29     21      8

print(df_yw.loc[pd.IndexSlice[:, [0, 4]], :])
#                          val_1  val_2
# year weekday date                    
# 2017 0       2017-11-27     20     38
#      4       2017-12-22     31     54
#              2017-12-29     21      8
# 2018 0       2018-01-08     48     64
#      4       2018-01-19     18     48

元の時系列データの範囲が広い場合は、最初にマルチインデックスの列をたくさん設定しておくと様々な切り口からデータを集計できるので便利。

df_yqmw = df.set_index([df.index.year, df.index.quarter, df.index.month, df.index.weekday, df.index])
df_yqmw.index.names = ['year', 'quarter', 'month', 'weekday', 'date']
df_yqmw.sort_index(inplace=True)
print(df_yqmw)
#                                        val_1  val_2
# year quarter month weekday date                    
# 2017 4       11    0       2017-11-27     20     38
#                    1       2017-11-07     26     66
#                    2       2017-11-01     65     76
#                    5       2017-11-18     47     47
#              12    1       2017-12-05     65     85
#                            2017-12-12      4     29
#                    4       2017-12-22     31     54
#                            2017-12-29     21      8
# 2018 1       1     0       2018-01-08     48     64
#                    1       2018-01-23     86     70
#                    2       2018-01-03     98     76
#                    4       2018-01-19     18     48

print(df_yqmw.sum(level='month'))
#        val_1  val_2
# month              
# 11       158    227
# 12       121    176
# 1        250    258

通年の曜日別の傾向と四半期毎の曜日別の傾向などを簡単に分析したりできる。

print(df_yqmw.sum(level='weekday'))
#          val_1  val_2
# weekday              
# 0           68    102
# 1          181    250
# 2          163    152
# 5           47     47
# 4           70    110

print(df_yqmw.sum(level=['quarter', 'weekday']))
#                  val_1  val_2
# quarter weekday              
# 4       0           20     38
#         1           95    180
#         2           65     76
#         5           47     47
#         4           52     62
# 1       0           48     64
#         1           86     70
#         2           98     76
#         4           18     48

繰り返しになるが、マルチインデックスの場合の列の選択、抽出は若干クセがあるので、詳細は以下の記事を参照。

いくつか例を示しておく。

print(df_yqmw.xs(1, level='weekday'))
#                                val_1  val_2
# year quarter month date                    
# 2017 4       11    2017-11-07     26     66
#              12    2017-12-05     65     85
#                    2017-12-12      4     29
# 2018 1       1     2018-01-23     86     70

print(df_yqmw.xs((1, 2017), level=('weekday', 'year')))
#                           val_1  val_2
# quarter month date                    
# 4       11    2017-11-07     26     66
#         12    2017-12-05     65     85
#               2017-12-12      4     29

print(df_yqmw.loc[pd.IndexSlice[2017, :, :, [0, 4]], :])
#                                        val_1  val_2
# year quarter month weekday date                    
# 2017 4       11    0       2017-11-27     20     38
#              12    4       2017-12-22     31     54
#                            2017-12-29     21      8

関連カテゴリー

関連記事