StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python pandas で日時関連のデータ操作をカンタンに

概要

Python で日時/タイムスタンプ関連の操作をする場合は dateutilarrow を使っている人が多いと思うが、 pandas でもそういった処理がわかりやすく書けるよ、という話。

pandas の本領は多次元データの蓄積/変形/集約処理にあるが、日時操作に関連した強力なメソッド / ユーティリティもいくつか持っている。今回は それらを使って日時操作を簡単に行う方法を書いてく。ということで DataFrameSeries もでてこない pandas 記事のはじまり。

※ ここでいう "日時/タイムスタンプ関連の操作" は文字列パース、日時加算/減算、タイムゾーン設定、条件に合致する日時のリスト生成などを想定。時系列補間/リサンプリングなんかはまた膨大になるので別途。

インストール

以下サンプルには 0.15での追加機能も含まれるため、0.15 以降が必要。

pip install pandas

準備

import pandas as pd

初期化/文字列パース

pd.to_datetime が便利。返り値は Python 標準のdatetime.datetime クラスを継承したpd.Timestamp インスタンスになる。

dt = pd.to_datetime('2014-11-09 10:00')
dt
# Timestamp('2014-11-09 10:00:00')

type(dt)
# pandas.tslib.Timestamp

import datetime
isinstance(dt, datetime.datetime)
# True

pd.to_datetime は、まず pandas 独自の日時パース処理を行い、そこでパースできなければ dateutil.parser.parse を呼び出す。そのため結構無茶なフォーマットもパースできる。

pd.to_datetime('141109 1005')
# Timestamp('2014-11-09 10:05:00')

リスト-likeなものを渡すと DatetimeIndex ( pandas を 普段 使ってない方は 日時のリストみたいなものだと思ってください) が返ってくる。

pd.to_datetime(['2014-11-09 10:00', '2014-11-10 10:00'])
# <class 'pandas.tseries.index.DatetimeIndex'>
# [2014-11-09 10:00:00, 2014-11-10 10:00:00]
# Length: 2, Freq: None, Timezone: None

また、pd.to_datetime は文字列以外の 日付-like なものも処理できる。自分は numpy.datetime64datetime.datetimeに変換するのに使ったりもする。

pd.to_datetime(datetime.datetime(2014, 11, 9))
# Timestamp('2014-11-09 00:00:00')

import numpy as np
pd.to_datetime(np.datetime64('2014-11-09 10:00Z'))
# Timestamp('2014-11-09 10:00:00')

補足 Timestamp.to_datetime()Timestamp -> datetime.datetime へ変換できる

dt.to_datetime()
# datetime.datetime(2014, 11, 9, 10, 0)

日時の加算/減算

pd.Timestamp に対して datetime.timedelta, dateutil.relativedeltaを使って日時の加減算を行うこともできるが、

dt + datetime.timedelta(days=1)
# Timestamp('2014-11-10 10:00:00')

from dateutil.relativedelta import relativedelta
dt + relativedelta(days=1)
# Timestamp('2014-11-10 10:00:00')

一般的な時間単位は pandasoffsets として定義しているため、そちらを使ったほうが直感的だと思う。使えるクラスの一覧は こちら

dt
# Timestamp('2014-11-09 10:00:00')

import pandas.tseries.offsets as offsets
# 翌日
dt + offsets.Day()
# Timestamp('2014-11-10 10:00:00')

# 3日後
dt + offsets.Day(3)
# Timestamp('2014-11-12 10:00:00')

pd.offsets を使うメリットとして、例えば dateutil.relativedelta では daysday を間違えるとまったく違う意味になってしまう。が、pd.offsets ならより明瞭に書ける。

# relativedelta の場合 days なら 3日後
dt + relativedelta(days=3)
# Timestamp('2014-11-12 10:00:00')

# day なら月の 3日目
dt + relativedelta(day=3)
# Timestamp('2014-11-03 10:00:00')

# 月の3日目を取りたいならこっちのが明瞭
dt - offsets.MonthBegin() + offsets.Day(2)
# Timestamp('2014-11-03 10:00:00')

時刻部分を丸めたければ normalize=True

dt + offsets.MonthEnd(normalize=True)
# Timestamp('2014-11-30 00:00:00')

また、 pd.offsetsdatetime.datetime, np.datetime64 にも適用できる。

datetime.datetime(2014, 11, 9, 10, 00) + offsets.Week(weekday=2)
# Timestamp('2014-11-12 10:00:00')

np.datetime64 に対して適用する場合は 加算/減算オペレータではなく、pd.offsets インスタンス.apply メソッドを使う。.applyTimestamp / datetime にも使える。(というか加減算オペレータも裏側では .apply を使っている)。

offsets.Week(weekday=2).apply(np.datetime64('2014-11-09 10:00:00Z'))
# Timestamp('2014-11-12 10:00:00')

offsets.Week(weekday=2).apply(dt)
# Timestamp('2014-11-12 10:00:00')

ある日付が offsets に乗っているかどうか調べる場合は .onOffset。例えば ある日付が水曜日 ( weekday=2 ) かどうか調べたければ、

# 11/09は日曜日 ( weekday=6 )
dt
# Timestamp('2014-11-09 10:00:00')

pd.offsets.Week(weekday=2).onOffset(dt)
# False

pd.offsets.Week(weekday=2).onOffset(dt + offsets.Day(3))
# True

タイムゾーン

タイムゾーン関連の処理は tz_localizetz_convert 二つのメソッドで行う。引数は 文字列、もしくは pytz, dateutil.tz インスタンス (後述)。

上でつくった Timestamp について、現在の表示時刻 (10:00) をそのままにして タイムゾーンを付与する場合は tz_localize

dt
# Timestamp('2014-11-09 10:00:00')

dt.tz_localize('Asia/Tokyo')
# Timestamp('2014-11-09 10:00:00+0900', tz='Asia/Tokyo')

現在の表示時刻を世界標準時 (GMTで10:00) とみて タイムゾーン付与する場合は まず GMTtz_localize してから tz_convert

dt.tz_localize('GMT').tz_convert('Asia/Tokyo')
# Timestamp('2014-11-09 19:00:00+0900', tz='Asia/Tokyo')

一度 タイムゾーンを付与したあとは、tz_convertタイムゾーンを変更したり、

dttz = dt.tz_localize('Asia/Tokyo')
dttz
# Timestamp('2014-11-09 10:00:00+0900', tz='Asia/Tokyo')

dttz.tz_convert('US/Eastern')
# Timestamp('2014-11-08 20:00:00-0500', tz='US/Eastern')

タイムゾーンを削除したりできる。

dttz
# Timestamp('2014-11-09 10:00:00+0900', tz='Asia/Tokyo')

dttz.tz_localize(None)
# Timestamp('2014-11-09 10:00:00')

dttz.tz_convert(None)
# Timestamp('2014-11-09 01:00:00')

また、tz_localize, tz_convertpytz, dateutil.tz を区別なく扱える。標準の datetime.datetime では pytzdateutil.tzタイムゾーンの初期化方法が違ったりするのでこれはうれしい。

import pytz
ptz = pytz.timezone('Asia/Tokyo')

dt.tz_localize(ptz)
# Timestamp('2014-11-09 10:00:00+0900', tz='Asia/Tokyo')

from dateutil.tz import gettz
dtz = gettz('Asia/Tokyo')
dt.tz_localize(dtz)
# Timestamp('2014-11-09 10:00:00+0900', tz='dateutil//usr/share/zoneinfo/Asia/Tokyo')

条件に合致する日時のリスト生成

単純な条件での生成

pd.date_range で以下で指定する引数の条件に合致した日時リストを一括生成できる。渡せるのは 下の4つのうち 3つ。例えば start, periods, freq を渡せば end は自動的に計算される。

  • start : 開始時刻
  • end : 終端時刻
  • periods : 内部に含まれる要素の数
  • freq : 生成する要素ごとに変化させる時間単位/周期 ( 1時間ごと、1日ごとなど)。freq として指定できる文字列は こちら。また pd.offsets も使える (後述)。

返り値は上でも少しふれた DatetimeIndex になる。print された DatetimeIndex の読み方として、表示結果の 2行目が 生成された日時の始点 (11/01 10:00) と終端 (11/04 09:00)、3行目が生成されたリストの長さ (72コ)と周期 ( H = 1時間ごと) になる。

dtidx = pd.date_range('2014-11-01 10:00', periods=72, freq='H')
dtidx
# <class 'pandas.tseries.index.DatetimeIndex'>
# [2014-11-01 10:00:00, ..., 2014-11-04 09:00:00]
# Length: 72, Freq: H, Timezone: None

生成された DatetimeIndex から Timestamp を取得する場合、ふつうに要素選択するか、 .asobject.tolist() を使う。.asobject.tolist() の意味は、

  • asobject : DatetimeIndex 内の要素を Timestamp オブジェクトに明示的に変換 (メソッドでなくプロパティなので注意)
  • tolist() : DatetimeIndex 自身と同じ中身のリストを生成
dtidx[1]
# Timestamp('2014-11-01 11:00:00', offset='H')

dtidx.asobject.tolist()
# [Timestamp('2014-11-01 10:00:00', offset='H'),
#  Timestamp('2014-11-01 11:00:00', offset='H'),
#  ...
#  Timestamp('2014-11-04 08:00:00', offset='H'),
#  Timestamp('2014-11-04 09:00:00', offset='H')]

少し複雑 (n個おき) な条件での生成

また、freqoffsets を与えればもう少し複雑な条件でもリスト生成できる。ある日時から3時間ごとの Timestamp を生成したい場合は以下のように書ける。

pd.date_range('2014-11-01 10:00', periods=72, freq=offsets.Hour(3))
# <class 'pandas.tseries.index.DatetimeIndex'>
# [2014-11-01 10:00:00, ..., 2014-11-10 07:00:00]
# Length: 72, Freq: 3H, Timezone: None

さらに複雑な条件 (任意の条件) での生成

さらに複雑な条件を使いたい場合は、一度単純な条件でリスト生成し、必要な部分をスライシングして取得するのがよい。例えば 深夜 0時から5時間ごとに1時間目/4時間目の時刻をリストとして取得したい場合は以下のようにする。

これで dateutil.rrule でやるような複雑な処理も (大部分? すべて?) カバーできるはず。

dtidx = pd.date_range('2014-11-01 00:00', periods=20, freq='H')
selector = [False, True, False, False, True] * 4
dtidx[selector].asobject.tolist()
# [Timestamp('2014-11-01 01:00:00'),
#  Timestamp('2014-11-01 04:00:00'),
#  Timestamp('2014-11-01 06:00:00'),
#  Timestamp('2014-11-01 09:00:00'),
#  Timestamp('2014-11-01 11:00:00'),
#  Timestamp('2014-11-01 14:00:00'),
#  Timestamp('2014-11-01 16:00:00'),
#  Timestamp('2014-11-01 19:00:00')]

DatetimeIndex に対する一括処理

最後に、先のセクションで記載した 時刻 加減算, タイムゾーン処理の方法/メソッドDatetimeIndex に対しても利用できる。全体について 何かまとめて処理したい場合は DatetimeIndex の時点で処理しておくと楽。

dtidx
# <class 'pandas.tseries.index.DatetimeIndex'>
# [2014-11-01 00:00:00, ..., 2014-11-01 19:00:00]
# Length: 20, Freq: H, Timezone: None

# 1日後にずらす
dtidx + offsets.Day()
# <class 'pandas.tseries.index.DatetimeIndex'>
# [2014-11-02 00:00:00, ..., 2014-11-02 19:00:00]
# Length: 20, Freq: H, Timezone: None

# タイムゾーンを設定
dtidx.tz_localize('Asia/Tokyo')
# <class 'pandas.tseries.index.DatetimeIndex'>
# [2014-11-01 00:00:00+09:00, ..., 2014-11-01 19:00:00+09:00]
# Length: 20, Freq: H, Timezone: Asia/Tokyo

まとめ

pandasなら日時/タイムスタンプ関連の操作もカンタン。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理