[pandas/matplotlib] 時系列データをプロットするときはデータ型に注意
pandasで時系列データを作って、matplotlibでプロットするときにエラーが出たけど、調べてみたらデータ型(dtype)を間違えていたせいだった。
時系列データのデータ型には気をつけましょう。
という話のメモ。
- 準備
- 失敗例 axvspanを実行するとエラーになった
- 結果には2つの問題点がある。原因は共通で、データ型が不適切だった。
- 対処法:日付を扱うためのデータ型に変換する
- 時系列データをのグラフで、axvspan、axvlineを使う
準備
import datetime import pandas as pd import matplotlib.pyplot as plt pd.options.display.notebook_repr_html = False # jupyter notebook上での出力形式を制御するために書いています。無くても動きます。
# 動作環境の確認 print(pd.__version__) import matplotlib print(matplotlib.__version__) # -------------------- 1.1.2 3.3.1
失敗例 axvspanを実行するとエラーになった
まず適当な時系列データを作ります。
# 2021年の祝日を適当に抜き出して並べただけで、データに意味はありません date_str_list = ['2021-01-11', '2021-02-11', '2021-03-20', '2021-04-29', '2021-05-05']
val_list = [10, 30, 20, 50, 40]
df_date_str = pd.DataFrame({ 'date' : date_str_list, 'val' : val_list })
df_date_str # -------------------- date val 0 2021-01-11 10 1 2021-02-11 30 2 2021-03-20 20 3 2021-04-29 50 4 2021-05-05 40
df_date_str.dtypes # -------------------- date object val int64 dtype: object
さて、matplotlibを使ってこのデータをグラフにする。
そして、axvspan関数を使って、一部の背景に色を付ける……と、何やらエラーが出てきた。
axvspan関数は横軸の範囲を指定して(今回の例では、3月1日〜4月1日)、その範囲に色を付けるmatplotlibの関数である。
下記のページを参考にした。
matplotlibで一定区間に背景色をつける方法 – 分析小箱
fig, ax = plt.subplots(figsize=(12,4)) ax.plot(df_date_str['date'], df_date_str['val']) # 参考:https://bunsekikobako.com/axvspan-and-axhspan/ start_datetime = datetime.datetime(2021, 3,1) end_datetime = datetime.datetime(2021, 4,1) ax.axvspan(start_datetime, end_datetime, color="gray", alpha=0.3) # -------------------- --------------------------------------------------------------------------- TypeError Traceback (most recent call last) /usr/local/lib/python3.8/site-packages/matplotlib/axis.py in convert_units(self, x) 1519 try: -> 1520 ret = self.converter.convert(x, self.units, self) 1521 except Exception as e: /usr/local/lib/python3.8/site-packages/matplotlib/category.py in convert(value, unit, axis) 60 # force an update so it also does type checking ---> 61 unit.update(values) 62 return np.vectorize(unit._mapping.__getitem__, otypes=[float])(values) /usr/local/lib/python3.8/site-packages/matplotlib/category.py in update(self, data) 210 # OrderedDict just iterates over unique values in data. --> 211 cbook._check_isinstance((str, bytes), value=val) 212 if convertible: /usr/local/lib/python3.8/site-packages/matplotlib/cbook/__init__.py in _check_isinstance(_types, **kwargs) 2234 if not isinstance(v, types): -> 2235 raise TypeError( 2236 "{!r} must be an instance of {}, not a {}".format( TypeError: 'value' must be an instance of str or bytes, not a datetime.datetime The above exception was the direct cause of the following exception: ConversionError Traceback (most recent call last) <ipython-input-8-40d5c36b235b> in <module> 7 end_datetime = datetime.datetime(2021, 4,1) 8 ----> 9 ax.axvspan(start_datetime, end_datetime, color="gray", alpha=0.3) /usr/local/lib/python3.8/site-packages/matplotlib/axes/_axes.py in axvspan(self, xmin, xmax, ymin, ymax, **kwargs) 1105 1106 # first we need to strip away the units -> 1107 xmin, xmax = self.convert_xunits([xmin, xmax]) 1108 ymin, ymax = self.convert_yunits([ymin, ymax]) 1109 /usr/local/lib/python3.8/site-packages/matplotlib/artist.py in convert_xunits(self, x) 173 if ax is None or ax.xaxis is None: 174 return x --> 175 return ax.xaxis.convert_units(x) 176 177 def convert_yunits(self, y): /usr/local/lib/python3.8/site-packages/matplotlib/axis.py in convert_units(self, x) 1520 ret = self.converter.convert(x, self.units, self) 1521 except Exception as e: -> 1522 raise munits.ConversionError('Failed to convert value(s) to axis ' 1523 f'units: {x!r}') from e 1524 return ret ConversionError: Failed to convert value(s) to axis units: [datetime.datetime(2021, 3, 1, 0, 0), datetime.datetime(2021, 4, 1, 0, 0)]
結果には2つの問題点がある。原因は共通で、データ型が不適切だった。
結果の問題点は2つある。
- グラフの横軸が等間隔になっている(日付の間隔が違うことが考慮されていない)
- axvspanのところでエラーが出た
この2つの原因は共通である。データを作るときのデータ型(dtype)がおかしかったのだ。
上でdf_date_str.dtypes
を実行すると、date型はobjectと書いてある。
これは(やや乱暴にいえば)文字列を入れるための型である。したがって、pandasやmatplotlibはdate列を日付(時刻)とは解釈せず、文字列として扱っている。
'2021-01-11'というただの文字で、'AAA', 'BBB' みたいな文字列と全く同じと考えれば良い。
type(df_date_str.loc[0, 'date']) # -------------------- str
これによって、2つの問題点はいずれも説明がつく。
グラフの横軸が等間隔になっている(日付の間隔が違うことが考慮されていない)
→ただの文字列として扱っているので、matplotlibは「それぞれの日付の間隔が違う」ことを理解できない。したがって等間隔でグラフを書く。
→今回は元のデータが等間隔でないから気づいたが、元のデータが等間隔(毎日、毎週、毎月……)だと一見して気づかない。axvspanのところでエラーが出た
→ただの文字列として扱っているので、matplotlibは「2021年3月1日がグラフ中のどこか?」を理解できない。したがってエラーを出す。
対処法:日付を扱うためのデータ型に変換する
原因は分かったので、対処法について述べる。
日付を文字列ではなく日付として扱うようにデータを作る必要がある。そのために、datetimeモジュールを使う。
datetime_list = [ datetime.datetime(2021, 1, 11), datetime.datetime(2021, 2, 11), datetime.datetime(2021, 3, 20), datetime.datetime(2021, 4, 29), datetime.datetime(2021, 5, 5), ]
df_datetime = pd.DataFrame({ 'date' : datetime_list, 'val' : val_list })
df_datetime # -------------------- date val 0 2021-01-11 10 1 2021-02-11 30 2 2021-03-20 20 3 2021-04-29 50 4 2021-05-05 40
普通にdataframeを表示しただけでは、「文字列の2021-01-11」と「日付の2021-01-11」は見分けがつかない。
データ型dtypeを確認するのが重要である。
df_datetime.dtypes # -------------------- date datetime64[ns] val int64 dtype: object
type(df_datetime.loc[0, 'date']) # -------------------- pandas._libs.tslibs.timestamps.Timestamp
date列がdatetime64[ns]
となっている。
これは日付や時刻を扱うためのデータ型(dtype)である。
また、最初のDataFrame(df_date_str
)から正しいデータを作り直す場合には、文字列のカラムをto_datetimeで日付型に変換する。
df_datetime2 = df_date_str.copy() df_datetime2['date'] = pd.to_datetime(df_datetime2['date'])
df_datetime2.dtypes # -------------------- date datetime64[ns] val int64 dtype: object
df_datetime
とdf_datetime2
は同じデータである。そしてdf_datetime
とdf_date_str
はデータ型が違うので、見た目は一緒でも違うデータである。
df.equals を使ってDataFrameが同一のものか確認しよう。
df_datetime.equals(df_datetime2) # -------------------- True
df_datetime.equals(df_date_str) # -------------------- False
時系列データをのグラフで、axvspan、axvlineを使う
これで正しいグラフを描ける。
- グラフの横軸が、日付の間隔を考慮したものになる
- axvspanが正しく実行できる(ついでにaxvline関数も入れておいた。こちらは縦線を描く関数。)
下記のページを参考にした(再掲)。
matplotlibで一定区間に背景色をつける方法 – 分析小箱
fig, ax = plt.subplots(figsize=(12,4)) ax.plot(df_datetime['date'], df_datetime['val']) #★ # 横軸の範囲を指定して、一定区間に背景色をつける start_datetime = datetime.datetime(2021, 3,1) end_datetime = datetime.datetime(2021, 4,1) ax.axvspan(start_datetime, end_datetime, color="gray", alpha=0.3) # 横軸の位置を指定して、縦線を描く ax.axvline(datetime.datetime(2021,2,1), color="red") # -------------------- <matplotlib.lines.Line2D at 0x121f075e0>
pandasやmatplotlibでなんか変だなと思ったら、データ型(dtype)が期待通りになっているかを再確認したほうが良さそうだ。
dtypeについては、以前公式ドキュメントを翻訳したので、そちらも合わせて参照してください。
それでは。