pandasの時系列カラムの時刻を特定書式の文字列に変換する方法
最近、このような状況が発生した。
- データ分析用にダミーの簡単なデータを作る必要がある
- そのデータは時刻カラムを含む
- 時刻カラムは、タイムゾーンが設定されていて、UTCである
- 実際のデータの表示書式はYYYY-MM-DDThh:mm:ssZ の形式 (例 2020-07-27T02:12:40Z)であるため、ダミーデータについても同じ書式で作成したい
- どうすれば実現できるか?
注意:以下の説明で、「time zone naive = タイムゾーンが設定されていない」「time zone aware = タイムゾーンが設定されている」という意味である。
時刻表現 TやZの意味 | No pain,No gain.
で書いてあるように、2020-07-27T02:12:40Z という時刻の形式がある。
時刻を表現するときの国際的な規格として定められている、ISOもしくはRFCに従った形式である。
ISO規格だと ISO 8601
RFC規格だとRFC 3339
らしい。(ほぼ同じと見ていいらしい。Wikipediaの情報だけど)
pandasの時刻データをこの形で作る方法を調べた。
準備
import pandas as pd import datetime pd.options.display.notebook_repr_html = False # jupyter notebook上での出力形式を制御するために書いています。無くても動きます。
# 動作環境の確認 print(pd.__version__) # -------------------- 1.1.2
時刻の列として、適当な時刻を3つほど作成する。datetime オブエジェクトを作り、日付と時までを適当に埋めよう。
datetime_list = [ datetime.datetime(2022, 1, 2, 3, 0), datetime.datetime(2022, 2, 3, 4, 0), datetime.datetime(2022, 3, 4, 5, 0), ]
val_list = [10, 30, 20]
df_datetime = pd.DataFrame({ 'datetime' : datetime_list, 'val' : val_list })
df_datetime # -------------------- datetime val 0 2022-01-02 03:00:00 10 1 2022-02-03 04:00:00 30 2 2022-03-04 05:00:00 20
pandasの時刻カラムのタイムゾーン有無を調べる
自分の理解を整理するために、Q&Aの形式で書いていく。
Q1. このDataFrameのdatetimeカラムは、タイムゾーンがある時刻か、ない時刻か?
A1. タイムゾーンがない時刻である。
Q2. タイムゾーンがないということはどうして分かるのか?
A2. 以下2つの方法がある。
1つ目の方法は、カラムを調べることである。Series(カラム)にタイムゾーンがないことは、Seriesのdtypeを見れば分かる。
https://pandas.pydata.org/docs/user_guide/timeseries.html#time-zone-series-operations
A Series with time zone naive values is represented with a dtype of datetime64[ns].
A Series with a time zone aware values is represented with a dtype of datetime64[ns, tz] where tz is the time zone.
拙訳:タイムゾーンが設定されていない値を持つSeriesは、datetime64[ns]
というdtypeで表される。
タイムゾーンが設定されている値を持つSeriesは、datetime64[ns, tz]
というdtypeで表される。ここで、tzはタイムゾーンである。
df_datetime.dtypes # -------------------- datetime datetime64[ns] val int64 dtype: object
2つ目の方法は、入っている時刻データを調べることである。
https://pandas.pydata.org/docs/user_guide/timeseries.html#time-zone-handling
By default, pandas objects are time zone unaware:
拙訳:デフォルトでは、pandasのオブジェクトにはタイムゾーンが設定されていない。
この公式ドキュメントによれば、tzという属性がNoneならタイムゾーンが設定されてないようだ。見てみよう。
datetime1 = df_datetime.loc[0, 'datetime'] datetime1 # -------------------- Timestamp('2022-01-02 03:00:00')
# 注:datetime1.tzと単に書くと、jupyter notebook上で結果のNoneが表示されないので、明示的にprintをつけてNoneを表示させている。 print(datetime1.tz) # -------------------- None
どちらの方法にせよ、datetimeカラムにはタイムゾーンが設定されていないことが分かった。 さて、欲しいデータはUTCなので、タイムゾーンを設定しよう。
pandasの時刻カラムにタイムゾーンを設定する
Q3. 下記のコードでは、タイムゾーンを設定しようとしてSeries.tz_localize()
を使っている。なんでエラーになるの?
A3. Series.tz_localize()
はindexの時刻をローカライズ処理するため。Seriesの値をローカライズするには、Series.dt.tz_localize()を使う。
https://pandas.pydata.org/docs/reference/api/pandas.Series.tz_localize.html Localize tz-naive index of a Series or DataFrame to target time zone.
This operation localizes the Index. To localize the values in a timezone-naive Series, use Series.dt.tz_localize().
拙訳:SeriesまたはDataFrameの、タイムゾーンの設定されていないindexを指定されたタイムゾーンにローカライズする。
この操作はインデックスをローカライズする。タイムゾーンの設定されていないSeriesの値をローカライズするには、Series.dt.tz_localize()
を使うこと。
……と公式ドキュメントに書いてあるとおりで、Seriesに対して直接tz_localize
を実行しようとindexの時刻を変更しようとする。
今回はindexが時刻ではなくて数値なので「(indexの時刻を変更しようとしたら)indexが時刻じゃないんだけど」とエラーが出ている。
df_datetime['datetime_utc'] = df_datetime['datetime'].tz_localize(tz='UTC') # -------------------- --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-10-d343d953d675> in <module> ----> 1 df_datetime['datetime_utc'] = df_datetime['datetime'].tz_localize(tz='UTC') /usr/local/lib/python3.8/site-packages/pandas/core/generic.py in tz_localize(self, tz, axis, level, copy, ambiguous, nonexistent) 9643 if level not in (None, 0, ax.name): 9644 raise ValueError(f"The level {level} is not valid") -> 9645 ax = _tz_localize(ax, tz, ambiguous, nonexistent) 9646 9647 result = self.copy(deep=copy) /usr/local/lib/python3.8/site-packages/pandas/core/generic.py in _tz_localize(ax, tz, ambiguous, nonexistent) 9625 if len(ax) > 0: 9626 ax_name = self._get_axis_name(axis) -> 9627 raise TypeError( 9628 f"{ax_name} is not a valid DatetimeIndex or PeriodIndex" 9629 ) TypeError: index is not a valid DatetimeIndex or PeriodIndex
# https://pandas.pydata.org/docs/reference/api/pandas.Series.dt.tz_localize.html df_datetime['datetime_utc'] = df_datetime['datetime'].dt.tz_localize(tz='UTC')
df_datetime # -------------------- datetime val datetime_utc 0 2022-01-02 03:00:00 10 2022-01-02 03:00:00+00:00 1 2022-02-03 04:00:00 30 2022-02-03 04:00:00+00:00 2 2022-03-04 05:00:00 20 2022-03-04 05:00:00+00:00
Q4. Series.dt.tz_localize()
を使って時刻変換したけど、このdtって何?
A4. dtは時刻形式のSeriesに対するアクセサ(accessor)である。
Series.dt.xxx という形で、時刻情報の一部を抽出したり、今回のtz_localizeのように時刻関係のメソッドを使ったりできる。
Series.dt.xxx の一覧は https://pandas.pydata.org/docs/reference/series.html#datetimelike-properties
dtについては https://pandas.pydata.org/docs/user_guide/basics.html#dt-accessor を参照。
さて、UTCに設定したら希望の書式になってくれるかと思ったが、そうではなかった。
2022-01-02 03:00:00+00:00 という形式になってしまった。
2022-01-02T03:00:00Z という形式が欲しいんだけど。
Q5. 日付と時刻の間にあるTはどういう意味? 半角空白の場合と何が違うの?
A5. ISO 8601では日付と時刻の間にTという文字を書く必要がある。(半角空白にすることは認められていない)
Q6. 時刻の末尾にあるZはどういう意味? +00:00と何が違うの?
A6. Zと+00:00 はどちらもISO 8601で認められた表記法で、タイムゾーンがUTCであることを示す。
df_datetime.dtypes # -------------------- datetime datetime64[ns] val int64 datetime_utc datetime64[ns, UTC] dtype: object
# で、このままcsvに出力しても希望通りの形式にはならない。 df_datetime.to_csv("temp1.csv") # csvの中身を表示する ! cat temp1.csv # -------------------- ,datetime,val,datetime_utc 0,2022-01-02 03:00:00,10,2022-01-02 03:00:00+00:00 1,2022-02-03 04:00:00,30,2022-02-03 04:00:00+00:00 2,2022-03-04 05:00:00,20,2022-03-04 05:00:00+00:00
時刻が特定の書式になっているCSVを作る2つの方法
3つの方法が考えられる。
- 時刻のデータ(dtype
datetime64[ns, UTC]
)のまま、表示方法を変更する。 - 時刻のデータをcsvに保存する際に、時刻形式を変更する。
- 時刻のデータから、希望する形式の文字列に変換する。
このうち2番目と3番目の方法は実現可能である。
Q7. 時刻のデータ(dtype datetime64[ns, UTC]
)のまま、表示方法を変更する方法はあるのか?
A7. 多分ないと思う。あったら教えて下さい。
時刻のデータをcsvに保存する際に、時刻形式を変更する方法
Q8. 時刻のデータをcsvに保存する際に、時刻形式を変更する方法はあるのか?
A8. ある。to_csvの引数にdate_formatを指定する。
# to_csvの引数にdate_formatを指定すると、csvに書き出すときに時刻を希望の書式にすることができる df_datetime.to_csv("temp2.csv", date_format="%Y/%m/%dT%H:%M:%SZ") # csvの中身を表示する # date_format引数はdatetime,datetime_utc の両方の列に適用されている ! cat temp2.csv # -------------------- ,datetime,val,datetime_utc 0,2022/01/02T03:00:00Z,10,2022/01/02T03:00:00Z 1,2022/02/03T04:00:00Z,30,2022/02/03T04:00:00Z 2,2022/03/04T05:00:00Z,20,2022/03/04T05:00:00Z
時刻のデータから、希望する形式の文字列に変換する方法
Q9. 時刻のデータをcsvに保存する際に、時刻形式を変更する方法はあるのか?
A9. ある。Series.dt.strftime()
を使ってフォーマットを指定する。
Q10. 出来上がったカラムのdtypeはどうなってるのか?
A10. 文字列、objectである。
df_datetime['datetime_utc'].dt.strftime("%Y/%m/%dT%H:%M:%SZ") # -------------------- 0 2022/01/02T03:00:00Z 1 2022/02/03T04:00:00Z 2 2022/03/04T05:00:00Z Name: datetime_utc, dtype: object
最初は訳わからなくてこれで作ってた。
df_datetime['datetime_utc'].apply(lambda t: t.to_pydatetime().strftime("%Y/%m/%dT%H:%M:%SZ")) # -------------------- 0 2022/01/02T03:00:00Z 1 2022/02/03T04:00:00Z 2 2022/03/04T05:00:00Z Name: datetime_utc, dtype: object
一応、上のやり方を解説しておこう。
Seriesに対してapplyを使うので、tに該当するのは、 datetime64[ns, UTC]
型の1つの時刻である。
to_pydatatime()
はPandasのTimestampオブジェクトをPythonのdatetimeオブジェクトに変換するもの。
https://pandas.pydata.org/docs/reference/api/pandas.Timestamp.to_pydatetime.html
で、datetimeオブジェクトに対してstrftime()
関数で文字列に変換している。
以上、2つの方法で、YYYY-MM-DDThh:mm:ssZ という形式で時刻を出力することができた。
……これ実は、手動で「Z」という文字を付け加えているから、「タイムゾーン情報」としてZという文字を付加しているわけではない。タイムゾーンを設定しなくても指定書式でcsvが作れるな。
まぁ、試行錯誤の結果ということで、このままにしておきます……。
しかし今回調べてみると、 Series.dt.xxx でできることが意外と多かった。strftimeを使うと文字列に変換もできるのか。
python標準のdatetimeと対応関係を見てみよう。 例えば、df_datetime['datetime_utc'].dt.hourの場合。
python_dt = datetime.datetime(2022, 1, 2, 3, 0)
python_dt.hour # -------------------- 3
df_datetime['datetime_utc'].dt.hour # -------------------- 0 3 1 4 2 5 Name: datetime_utc, dtype: int64
こう見ると、「python_dt」と「df_datetime['datetime_utc'].dt」が対応している。
次に、df_datetime['datetime_utc'].dt.strftime()の場合。
python_dt.strftime("%Y/%m/%dT%H:%M:%SZ") # -------------------- '2022/01/02T03:00:00Z'
df_datetime['datetime_utc'].dt.strftime("%Y/%m/%dT%H:%M:%SZ") # -------------------- 0 2022/01/02T03:00:00Z 1 2022/02/03T04:00:00Z 2 2022/03/04T05:00:00Z Name: datetime_utc, dtype: object
これも、「python_dt」と「df_datetime['datetime_utc'].dt」が対応している。
Series.dtは単なるアクセサであるが、
「Series.dt は、PythonのdatetimeオブジェクトからなるSeriesのようなもの」と考えておくと、対応が分かりやすいのかもしれない……?
それでは。