Python サマータイム (DST) について調べました

前回の記事でヒストリカルデータを使うのにサマータイムを考慮する必要があったので調べました。

環境

  • 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時までの期間の適用に変更された。

東部夏時間 - Wikipedia

2007 年以降とそれ以前で期間の定義が違うみたいです。

この定義はアメリカのもののようです。 現在の日本ではサマータイムってないので、世界のすべての国が同じ期間の定義でサマータイムを適用しているものだと思っていました。

3月最終日曜日の午前3時(夏時間では午前4時)から10月最終日曜日の午前3時(夏時間では午前4時)までは、夏時間の東ヨーロッパ夏時間(Eastern European Summer Time) が使用される。

東ヨーロッパ時間 - Wikipedia

ヨーロッパあたりはまた別の期間の定義で適用しているようでした。

外国為替ではアメリカの定義が適用されているようなので、この定義に従うことにしました。

前回の記事で作った日足のヒストリカルデータでは、 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 にアップしました。

終わり

調べているといろいろ知ることがあります。 前回の記事で日足のヒストリカルデータを作るときに、サマータイムについて調べたことでした。 今回はこれで終わります。