Sep 22, 2020

[Pandas] sharex=Trueで時系列データの折れ線グラフと棒グラフを共存させる

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

N = 10
np.random.seed(8)
df = pd.DataFrame({
    'A': np. random.randint(10, size=N),
    'B': np. random.randint(10, size=N),
    'C': np. random.randint(10, size=N)
}, index=pd.date_range('2020-09-01', periods=N))
df
A B C
2020-09-01 3 1 5
2020-09-02 4 3 5
2020-09-03 1 9 7
2020-09-04 9 2 9
2020-09-05 5 2 2
2020-09-06 8 6 6
2020-09-07 3 8 9
2020-09-08 8 9 5
2020-09-09 0 3 1
2020-09-10 5 4 6
このような時系列データがあるとし、A列を折れ線グラフ、B,C列を別のグラフに棒グラフで描画したものを、時間軸を合わせて縦に並べたいとする。
そこで、次のようなコードを実行すると、2段のグラフの内、上のグラフが描画されなかった。
fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)
df[['B', 'C']].plot(kind='bar', ax=ax[1], grid=True)
plt.show()

下のグラフを描画しなければ、上のグラフが描画される。

fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)
plt.show()

原因は、次のコードを実行するとわかった。

fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)
print("xlim after 1st plot:", ax[0].get_xlim())
df[['B', 'C']].plot(kind='bar', ax=ax[1], grid=True)
print("xlim after 2nd plot:", ax[0].get_xlim())
実行結果
xlim after 1st plot: (18506.0, 18515.0)
xlim after 2nd plot: (-0.5, 9.5)
つまり、 pandas.DataFrame.plot は kind='line' と kind='bar' とで描画した後のX座標の範囲(xlim)が全く異なり、 matplotlib.pyplot.subplots(sharex=True) した状態でこの2つを描画すると、先に描画した座標系(Axes)のxlimが書き換えられてしまうのが原因である。
色々調べまくったが、棒グラフ表示にこだわると、pandas.DataFrame.plotを使って解決する方法は見つからなかった。(kind='scatter'の点グラフなら同じ問題が起こらないことを確認した。コードは省略)

Seabornを使っても、結果は同じだった。

import seaborn as sns
fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)
print("xlim after 1st plot:", ax[0].get_xlim())
_ = df[['B', 'C']].melt(ignore_index=False).reset_index()
sns.barplot(x='index', y='value', hue='variable', data=_)
print("xlim after 2nd plot:", ax[0].get_xlim())
plt.show()
実行結果
xlim after 1st plot: (18506.0, 18515.0)
xlim after 2nd plot: (-0.5, 9.5)

結局、棒グラフだけ直接matplotlib APIを使って描画すると解決した。

fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)

width = pd.Timedelta('0.4d')
ax[1].bar(df.index - width/2, df['B'], width=width, label='B')
ax[1].bar(df.index + width/2, df['C'], width=width, label='C')
ax[1].set_xlim(df.index[0] - width*2, df.index[-1] + width*2)
ax[1].grid(True)
ax[1].legend()

fig.autofmt_xdate()
plt.show()

See more ...

Posted at 16:41 in PC一般 | WriteBacks (0)
WriteBacks