pandas 日時まわりのリサンプリング/オフセット処理
こちらの続き。
今回のサンプルデータには自分の歩数のデータを使いたい。インスパイヤ元は以下のサイトだ。
データの読み込み
歩数データは iPhone の Health アプリから Export できる。形式は XML なので、そのままでは pandas
で読み込めない。一度 XML から必要な属性を辞書のリストとして取り出した後、pandas
に読み込ませる。
import pandas as pd from xml.etree import ElementTree tree = ElementTree.parse('export.xml') root = tree.getroot() # 属性の辞書のリストを作る data = [e.attrib for e in root.findall('.//Record')] data[0] # {'endDate': '20150511220900+0900', # 'recordCount': '104', # 'source': u'xxx', # 'startDate': '20150511210900+0900', # 'type': 'HKQuantityTypeIdentifierStepCount', # 'unit': 'count', # 'value': '531'} df = pd.DataFrame(data) # 必要な列のみにフィルタ df = df[['startDate', 'type', 'unit', 'value']] df.head() # startDate type unit value # 0 20150511210900+0900 HKQuantityTypeIdentifierStepCount count 531 # 1 20150512000900+0900 HKQuantityTypeIdentifierStepCount count 6 # 2 20150512060900+0900 HKQuantityTypeIdentifierStepCount count 629 # 3 20150512070900+0900 HKQuantityTypeIdentifierStepCount count 312 # 4 20150512080900+0900 HKQuantityTypeIdentifierStepCount count 1483
Series.value_counts()
を使うと、ある列で出現する要素の個数を集計できる。"type" 列を集計すると、Export したデータには歩数以外のデータも含まれていることがわかる。歩数以外は不要なので削除する。
df['type'].value_counts() # HKQuantityTypeIdentifierStepCount 375 # HKQuantityTypeIdentifierDistanceWalkingRunning 374 # HKQuantityTypeIdentifierFlightsClimbed 216 # dtype: int64 # 歩数のみにフィルタ df = df[df['type'] == 'HKQuantityTypeIdentifierStepCount']
また、XML で読み込んだデータはすべて文字列型になっているため、適宜型変換を行う。
# 歩数データは数値に df['value'] = df['value'].astype(float) # 日付は index として設定し、日付型 (DatetimeIndex) に変換 df = df.set_index('startDate') # この時点で index は文字列型になっている df.index # Index([u'20150511210900+0900', u'20150512000900+0900', u'20150512060900+0900', # u'20150512070900+0900', u'20150512080900+0900', u'20150512090900+0900', # ... # u'20150518165000+0900', u'20150518165900+0900', u'20150518171600+0900', # u'20150518181200+0900'], # dtype='object', name=u'startDate', length=965) # to_datetime で日時型に変換 / タイムゾーンを表すオフセットを適宜設定 pd.to_datetime(df.index, utc=True).tz_convert('Asia/Tokyo') # DatetimeIndex(['2015-05-11 21:09:00+09:00', '2015-05-12 00:09:00+09:00', # '2015-05-12 06:09:00+09:00', '2015-05-12 07:09:00+09:00', # ... # '2015-05-18 16:50:00+09:00', '2015-05-18 16:59:00+09:00', # '2015-05-18 17:16:00+09:00', '2015-05-18 18:12:00+09:00'], # dtype='datetime64[ns]', length=965, freq=None, tz='Asia/Tokyo') # index を上書き df.index = pd.to_datetime(df.index, utc=True).tz_convert('Asia/Tokyo') # タイムゾーンは使わないので削除 df.index = df.index.tz_localize(None) # 日付の昇順にソート df = df.sort_index() df.head() # type unit value # 2014-11-25 21:09:00 HKQuantityTypeIdentifierStepCount count 1396.00 # 2014-11-26 21:09:00 HKQuantityTypeIdentifierStepCount count 7020.37 # 2014-11-27 21:09:00 HKQuantityTypeIdentifierStepCount count 6413.63 # 2014-11-28 21:09:00 HKQuantityTypeIdentifierStepCount count 8396.00 # 2014-11-29 21:09:00 HKQuantityTypeIdentifierStepCount count 12411.00
今の機種に買い替えたのが昨年11末なので、買い替え以降すべてのデータが入っているようだ。
部分文字列によるスライシング
pandas
での一般的なデータ選択については以下の記事にまとめた。
加えて、データが 日付型の Index ( DatetimeIndex
) を持つとき、日付-like な文字列で __getitem__
すると、その期間にあてはまるデータを抽出してくれる。例えば 2015-05-17 日分のデータを抽出したければ以下のようにする。この方法を使うと 好きな期間のデータを簡単に確認することができる。詳細は公式ドキュメントを参照。
df['2015-05-17'] # type unit value # 2015-05-17 08:09:00 HKQuantityTypeIdentifierStepCount count 18.0000 # 2015-05-17 11:09:00 HKQuantityTypeIdentifierStepCount count 180.0000 # 2015-05-17 12:09:00 HKQuantityTypeIdentifierStepCount count 934.0000 # 2015-05-17 13:09:00 HKQuantityTypeIdentifierStepCount count 469.0000 # ... ... ... ... # 2015-05-17 21:16:00 HKQuantityTypeIdentifierStepCount count 88.5087 # 2015-05-17 21:17:00 HKQuantityTypeIdentifierStepCount count 23.1214 # 2015-05-17 21:18:00 HKQuantityTypeIdentifierStepCount count 99.3283 # 2015-05-17 21:19:00 HKQuantityTypeIdentifierStepCount count 41.6284 # # [14 rows x 3 columns]
リサンプリング
データは日時で処理したいので、まずは 1日ごとの合計を算出したい。こういうときは DataFrame.resample
。リサンプリングする期間や集約関数は引数として指定できる。
s = df.resample('D', how='sum') # value # 2014-11-25 1396.000000 # 2014-11-26 7020.370000 # 2014-11-27 6413.630000 # 2014-11-28 8396.000000 # ... ... # 2015-05-15 7561.998000 # 2015-05-16 10615.000000 # 2015-05-17 9208.000000 # 2015-05-18 8085.994898 # # [175 rows x 1 columns]
日次に集約した結果をプロットしてみる。
s.plot()
あまり意識はしていないのだが そこそこ歩いているようだ。5月初旬はちょっと出かけていたため 歩数に異常値が出ている。
日付の標準化
日時データの適当な期間での集約は DataFrame.resample
でできた。が、時には日時の補正のみを行い、集約はしたくない場合がある。
DatetimeIndex
を日付ごとにまとめるのに一番簡単なのは .normalize
。
pd.Timestamp('2015-05-01 23:59').normalize() # Timestamp('2015-05-01 00:00:00') pd.Timestamp('2015-05-02 00:00').normalize() # Timestamp('2015-05-02 00:00:00')
他の日時関連のメソッドと同じく、Timestamp
, DatetimeIndex
両方で使える。
df.tail().index # DatetimeIndex(['2015-05-18 18:55:00', '2015-05-18 18:56:00', # '2015-05-18 18:57:00', '2015-05-18 18:58:00', # '2015-05-18 18:59:00'], # dtype='datetime64[ns]', freq=None, tz=None) df.tail().index.normalize() # DatetimeIndex(['2015-05-18', '2015-05-18', # '2015-05-18', '2015-05-18', # '2015-05-18'], # dtype='datetime64[ns]', freq=None, tz=None)
当然、集約すれば 日付での .resample
と同じ結果になる。
s = df.groupby(df.index.normalize())[['value']].sum() s # 略
オフセット
また、より柔軟に日時補正を行うためにオフセットが提供されている ( 前回記事 )。オフセットを利用してデータを補正/集約する例を書きたい。
オフセットを使わずに .resample
で月次平均をとると以下のような結果になる。
s.resample('M', how='mean') # value # 2014-11-30 7012.500000 # 2014-12-31 8435.741935 # 2015-01-31 9134.032258 # 2015-02-28 9323.821429 # 2015-03-31 9326.356129 # 2015-04-30 9938.533333 # 2015-05-31 10973.055439
オフセットを使って同じ処理をするには、日時を月末に補正するオフセット MonthEnd
を使う。オフセットの一覧は 公式ドキュメント を参照。
m = offsets.MonthEnd()
m
# <MonthEnd>
オフセットにはいくつか共通のメソッドがあるため、順に記載する。
まず、ある日付が 当該のオフセット上に存在するか調べるには .onOffset
。MonthEnd.onOffset
の場合は、引数が月末の日付であるとき True
になる。
m.onOffset(pd.Timestamp('2015-04-29')) # False m.onOffset(pd.Timestamp('2015-04-30')) # True m.onOffset(pd.Timestamp('2015-05-01')) # False
オフセットを加減算することにより、日時を次の/前のオフセットへと移動できる。また、オフセットの加算と同一の処理として .apply
がある。
# 日付を常に 次のオフセットへ移動させる。 pd.Timestamp('2015-04-29') + m # Timestamp('2015-04-30 00:00:00') pd.Timestamp('2015-04-30') + m # Timestamp('2015-05-31 00:00:00') pd.Timestamp('2015-05-01') + m # Timestamp('2015-05-31 00:00:00') # 日付を常に 前のオフセットへ移動させる。 pd.Timestamp('2015-04-29') - m # Timestamp('2015-03-31 00:00:00') pd.Timestamp('2015-04-30') - m # Timestamp('2015-03-31 00:00:00') pd.Timestamp('2015-05-01') - m # Timestamp('2015-04-30 00:00:00') # 日付を常に 次のオフセットへ移動させる。 m.apply(pd.Timestamp('2015-04-29')) # Timestamp('2015-04-30 00:00:00') m.apply(pd.Timestamp('2015-04-30')) # Timestamp('2015-05-31 00:00:00') m.apply(pd.Timestamp('2015-05-01')) # Timestamp('2015-05-31 00:00:00')
オフセットに乗っている日時は動かしたくなければ .rollforward
/ .rollback
というメソッドを使う。
# 日付がオフセットに乗っていない場合 次のオフセットへ移動させる。 m.rollforward(pd.Timestamp('2015-04-29')) # Timestamp('2015-04-30 00:00:00') m.rollforward(pd.Timestamp('2015-04-30')) # Timestamp('2015-04-30 00:00:00') m.rollforward(pd.Timestamp('2015-05-01')) # Timestamp('2015-05-31 00:00:00') # 日付がオフセットに乗っていない場合 前のオフセットへ移動させる。 m.rollback(pd.Timestamp('2015-04-29')) # Timestamp('2015-03-31 00:00:00') m.rollback(pd.Timestamp('2015-04-30')) # Timestamp('2015-04-30 00:00:00') m.rollback(pd.Timestamp('2015-05-01')) # Timestamp('2015-04-30 00:00:00')
よって、月次平均の算出をオフセットのメソッドを使って行う場合は以下のようになる。
# 例示のため一部データをスライス s['2014-11'] # value # 2014-11-25 1396.00 # 2014-11-26 7020.37 # 2014-11-27 6413.63 # 2014-11-28 8396.00 # 2014-11-29 12411.00 # 2014-11-30 6438.00 # 日時を月末に補正 s['2014-11'].index.map(m.rollforward) # array([Timestamp('2014-11-30 00:00:00'), Timestamp('2014-11-30 00:00:00'), # Timestamp('2014-11-30 00:00:00'), Timestamp('2014-11-30 00:00:00'), # Timestamp('2014-11-30 00:00:00'), Timestamp('2014-11-30 00:00:00')], dtype=object) s.groupby(s.index.map(m.rollforward)).mean() # value # 2014-11-30 7012.500000 # 2014-12-31 8435.741935 # 2015-01-31 9134.032258 # 2015-02-28 9323.821429 # 2015-03-31 9326.356129 # 2015-04-30 9938.533333 # 2015-05-31 10973.055439
補足 オフセットは .resample
の引数として渡すこともでき、同じ結果になる。
s.resample(m, how='mean') # 略
オフセットを活用した集計
つづけて、カレンダー上の平日 / 休日での差異をみたい。アクセサ ( こちらの記事参照 ) を使って曜日で集計してから処理してもよいが、 BusinessDay
オフセットを使えば簡単。BusinessDay.onOffset
を使えば平日が True
/ 休日を False
として集計できる。以下の結果をみると、休日の方が歩いている傾向があるようだ。
bday = offsets.BusinessDay() s.groupby(bday.onOffset).mean() # value # False 10860.216800 # True 8716.657583
BusinessDay
オフセットは 土日のみを休日として扱うが、任意の祝日も含めて休日として扱いたい場合は CustomBusinessDay
オフセットが使える。拙作のパッケージ で 日本の祝日のカレンダーを定義しているので、それを使って、
import japandas as jpd calendar = jpd.JapaneseHolidayCalendar() cday = pd.offsets.CDay(calendar=calendar) s.groupby(cday.onOffset).mean() # value # False 10799.033448 # True 8600.419640
これまでの内容を使って、月ごとに 平日 / 土日 / 祝日の歩数平均をクロス集計したい。クロス集計を行うには pd.pivot_table
。
avg = pd.pivot_table(s, index=s.index.map(m.rollforward), columns=[s.index.map(bday.onOffset), s.index.map(cday.onOffset)], values='value', aggfunc='mean') avg # False True # False False True # 2014-11-30 9424.500000 NaN 5806.500000 # 2014-12-31 10382.275000 11704.0 7579.354545 # 2015-01-31 9534.193333 10162.5 8851.113000 # 2015-02-28 10943.125000 9446.0 8635.578947 # 2015-03-31 11148.877778 NaN 8580.779091 # 2015-04-30 11349.625000 7110.0 9535.666667 # 2015-05-31 12769.000000 11582.7 9572.544211
左から、
- 土日:
BusinessDay
CustomBusinessDay
ともにFalse
- 祝日:
BusinessDay
はTrue
,CustomBusinessDay
はFalse
- 平日:
BusinessDay
CustomBusinessDay
ともにTrue
となる。ということで上の結果からも 5月の休日は結構歩いたなってことがわかった。
まとめ
日付を index として持つデータに対する リサンプリング/オフセット処理をまとめた。これらは以下のような使い分けをすればよいと思う。
- 比較的単純な集約はリサンプリング
- リサンプリングではできない より細かい補正やクロス集計をしたい場合はオフセット
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る