pandasの時系列カラムの時刻を特定書式の文字列に変換する方法

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のようなもの」と考えておくと、対応が分かりやすいのかもしれない……?
それでは。