[pandas/matplotlib]時系列データをプロットするときはデータ型に注意

[pandas/matplotlib] 時系列データをプロットするときはデータ型に注意

pandasで時系列データを作って、matplotlibでプロットするときにエラーが出たけど、調べてみたらデータ型(dtype)を間違えていたせいだった。
時系列データのデータ型には気をつけましょう。
という話のメモ。

準備

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)]

f:id:soratokimitonoaidani:20210718124022p:plain

結果には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_datetimedf_datetime2は同じデータである。そしてdf_datetimedf_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>

f:id:soratokimitonoaidani:20210718124026p:plain

pandasやmatplotlibでなんか変だなと思ったら、データ型(dtype)が期待通りになっているかを再確認したほうが良さそうだ。
dtypeについては、以前公式ドキュメントを翻訳したので、そちらも合わせて参照してください。

linus-mk.hatenablog.com

それでは。