前回の記事でヒストリカルデータを使うのにサマータイムを考慮する必要があったので調べました。
環境
- Ubuntu 18.04 LTS
- Python 3.6
- pandas 0.23.0
サマータイム (DST)
2006年までは、4月の第1日曜日午前2時から10月の最終日曜日午前2時までの期間の適用であったが、2007年以降は、3月の第2日曜日午前2時から11月の第1日曜日午前2時までの期間の適用に変更された。
2007 年以降とそれ以前で期間の定義が違うみたいです。
この定義はアメリカのもののようです。 現在の日本ではサマータイムってないので、世界のすべての国が同じ期間の定義でサマータイムを適用しているものだと思っていました。
3月最終日曜日の午前3時(夏時間では午前4時)から10月最終日曜日の午前3時(夏時間では午前4時)までは、夏時間の東ヨーロッパ夏時間(Eastern European Summer Time) が使用される。
ヨーロッパあたりはまた別の期間の定義で適用しているようでした。
外国為替ではアメリカの定義が適用されているようなので、この定義に従うことにしました。
前回の記事で作った日足のヒストリカルデータでは、 UTC+3 や UTC+2 を設定することで月曜日の 0 時から始まるようにしましたけれども、サマータイムの期間の定義だけアメリカに従うって、少し混乱しました。 UTC+3 や UTC+2 は東ヨーロッパ時間なのに、アメリカ(東部標準時(UTC-5)や東部夏時間(UTC-4))のサマータイムの定義なの?って。 まあ、 UTC+3 や UTC+2 を設定したのは便宜的に日足を 5 本にしたかったからですし、外国為替のサマータイムがアメリカの期間の定義を適用しているのはルールですし、そういうものだと思って進めます。
やりたいことは前回の記事に書きましたが、日足のヒストリカルデータを作るにあたって、サマータイムのときは UTC+3 に、それ以外のときは UTC+2 に設定したいということでした。
モジュール
ここから Jupyter Notebook を使ってセルに入力しながら進めていきます。
最初にモジュールを読み込みました。
import calendar
import datetime
import functools
import pandas as pd
import pytz
pandas
pandas.Timestamp の dst 関数でサマータイムを判断することができるようでした。
# pandasのTimestampのdst()でDSTを判断できる
pd.Timestamp('2018-03-11 06:59:59', tz='UTC').tz_convert('America/New_York').dst() # datetime.timedelta(0)
pd.Timestamp('2018-03-11 07:00:00', tz='UTC').tz_convert('America/New_York').dst() # datetime.timedelta(0, 3600)
pd.Timestamp('2018-11-04 05:59:59', tz='UTC').tz_convert('America/New_York').dst() # datetime.timedelta(0, 3600)
pd.Timestamp('2018-11-04 06:00:00', tz='UTC').tz_convert('America/New_York').dst() # datetime.timedelta(0)
datetime.timedelta の 2 つ目は seconds のようで、サマータイムは 3600 秒、 1 時間早くなっていることがわかります。
2018 年の東部標準時から東部夏時間に切り替わる時間、東部夏時間から東部標準時に切り替わる時間を確認しました。
# サマータイムは次のようになっている
# サマータイムが始まる日のESTの2時になると1時間進んでDSTの3時になる
# サマータイムが終わる日のDSTの2時になると1時間戻ってESTの1時になる
pd.Timestamp('2018-03-11 06:59:59', tz='UTC').tz_convert('America/New_York') # Timestamp('2018-03-11 01:59:59-0500', tz='America/New_York')
pd.Timestamp('2018-03-11 07:00:00', tz='UTC').tz_convert('America/New_York') # Timestamp('2018-03-11 03:00:00-0400', tz='America/New_York')
pd.Timestamp('2018-11-04 05:59:59', tz='UTC').tz_convert('America/New_York') # Timestamp('2018-11-04 01:59:59-0400', tz='America/New_York')
pd.Timestamp('2018-11-04 06:00:00', tz='UTC').tz_convert('America/New_York') # Timestamp('2018-11-04 01:00:00-0500', tz='America/New_York')
ニューヨークのタイムゾーンでは、サマータイムが始まる日の 2 時からの 1 時間がないみたいです。 逆にサマータイムが終わる日の 1 時からの 1 時間は東部夏時間と東部標準時で重複するみたいです。
サマータイムの扱いを理解したところで、 pandas.Timestamp のパフォーマンスを計測してみました。
# pandasのTimestampは遅い
%time [pd.Timestamp('2018-03-11 06:59:59', tz='UTC').tz_convert('America/New_York').dst() for i in range(1000000)]
ヒストリカルデータの 1 分足は 500 万行程度あったので、とりあえず 100 万行くらいで試してみました。
結果です。
CPU times: user 44.3 s, sys: 2.48 s, total: 46.8 s
Wall time: 45.4 s
遅いです。 ただ、サマータイムかそうでないか判断するだけで 45 秒もかかることになります。 500 万行だとこの 5 倍はかかる見込みです。
pytz
pytz でもサマータイムを判断できるようでした。
# pytzでもDSTを考慮してくれる
# pandasのdst()はpytzのdst()なのかな?
pytz.timezone('UTC').localize(datetime.datetime(2018, 3, 11, 6, 59, 59)).astimezone(pytz.timezone('America/New_York')).dst() # datetime.timedelta(0)
pytz.timezone('UTC').localize(datetime.datetime(2018, 3, 11, 7, 0, 0)).astimezone(pytz.timezone('America/New_York')).dst() # datetime.timedelta(0, 3600)
pytz.timezone('UTC').localize(datetime.datetime(2018, 11, 4, 5, 59, 59)).astimezone(pytz.timezone('America/New_York')).dst() # datetime.timedelta(0, 3600)
pytz.timezone('UTC').localize(datetime.datetime(2018, 11, 4, 6, 0, 0)).astimezone(pytz.timezone('America/New_York')).dst() # datetime.timedelta(0)
pytz でもパフォーマンスを計測してみました。
# pytzは遅い
tzny = pytz.timezone('America/New_York')
%time [pytz.utc.localize(datetime.datetime(2018, 3, 11, 6, 59, 59)).astimezone(tzny).dst() for i in range(1000000)]
結果です。
CPU times: user 14.7 s, sys: 31.2 ms, total: 14.7 s
Wall time: 14.7 s
pandas.Timestamp より速いみたいでした。 けれども、まだ遅いです。
datetime
Python の標準の datetime でもパフォーマンスを計測してみました。
# datetimeは比較的速い
%time [datetime.datetime(2018, 3, 11, 6, 59, 59) for i in range(1000000)]
結果です。
CPU times: user 781 ms, sys: 156 ms, total: 938 ms
Wall time: 996 ms
1 秒以内で 100 万行を処理することができました。
でも datetime だけではサマータイムを判断することができないみたいでした。
サマータイムの実装
ここまでの考え方として、 1 分足のヒストリカルデータの 500 万行に対して 1 行ごとにサマータイムであるか判断しようとしていました。 するとどうしても 500 万行に対しての処理が必要になります。
そうじゃなくて、サマータイムの期間を求めて、その期間にあるものをまとめて UTC+3 に設定する。 それ以外の期間にあるものはまとめて UTC+2 に設定する。 のようにすれば 500 万行の繰り返しは必要なさそうだと考えました。
pytz の dst のような関数が「サマータイムの期間はいつか?」という結果を返してくれればいいのですけれども、そういう関数はなさそうでした。 「この日時はサマータイムか?」という結果を返す関数しかなさそうでした。
だから datetime を使ってサマータイムの期間を自分で求めることにしました。
Calendar クラスの itermonthdays2 を使うと、その年月の日と曜日のイテレーターを返してくれるみたいです。 これを使って 3 月の第 2 日曜日と 11 月の第 1 日曜日を判断しました。
def dstdt(year, month, week, hours):
day = [i for i in calendar.Calendar().itermonthdays2(year, month) if i[0] != 0 and i[1] == calendar.SUNDAY][week][0]
return datetime.datetime(year, month, day, 2, 0) + datetime.timedelta(hours=-hours)
def dst(year):
dic = {
1918: [(4, 0, -5), (10, -1, -4)],
2007: [(3, 1, -5), (11, 0, -4)]
}
key = functools.reduce(lambda x, y: x if year < y else y, dic.keys(), 0)
if key == 0:
return []
return [dstdt(year, i[0], i[1], i[2]) for i in dic[key]]
year = 2018
%time dst(year)
結果です。
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 102 µs
[datetime.datetime(2018, 3, 11, 7, 0), datetime.datetime(2018, 11, 4, 6, 0)]
パフォーマンスを計測してみました。
# 1900年から2100年までの200年間でも19.7ms程度
%time [dst(i) for i in range(1900, 2100 + 1)]
ちょっとフェアなパフォーマンスの比較じゃない(100 万行のパフォーマンスじゃない)ですけれども、求めたい結果に対しては 1 秒もかからないパフォーマンスが得られました。
結果です。
CPU times: user 15.6 ms, sys: 0 ns, total: 15.6 ms
Wall time: 9.43 ms
サマータイムの適用の期間として、最初 1900 年からとしていたのですが、 pytz の結果を見ると 1918 からサマータイムの運用が開始されたように見えました。
# pytzの定義によると1918年からDSTが始まったように見える
[(pd.Timestamp(f'{i}-08-01', tz='America/New_York').dst(), i) for i in range(1900, 2020 + 1)]
結果です。
[(datetime.timedelta(0), 1900),
(datetime.timedelta(0), 1901),
(datetime.timedelta(0), 1902),
(datetime.timedelta(0), 1903),
(datetime.timedelta(0), 1904),
(datetime.timedelta(0), 1905),
(datetime.timedelta(0), 1906),
(datetime.timedelta(0), 1907),
(datetime.timedelta(0), 1908),
(datetime.timedelta(0), 1909),
(datetime.timedelta(0), 1910),
(datetime.timedelta(0), 1911),
(datetime.timedelta(0), 1912),
(datetime.timedelta(0), 1913),
(datetime.timedelta(0), 1914),
(datetime.timedelta(0), 1915),
(datetime.timedelta(0), 1916),
(datetime.timedelta(0), 1917),
(datetime.timedelta(0, 3600), 1918),
(datetime.timedelta(0, 3600), 1919),
(datetime.timedelta(0, 3600), 1920),
(datetime.timedelta(0, 3600), 1921),
...略...
(datetime.timedelta(0, 3600), 2018),
(datetime.timedelta(0, 3600), 2019),
(datetime.timedelta(0, 3600), 2020)]
Gist
Jupyter Notebook のファイルを Gist にアップしました。
終わり
調べているといろいろ知ることがあります。 前回の記事で日足のヒストリカルデータを作るときに、サマータイムについて調べたことでした。 今回はこれで終わります。