はじめに
時系列グラフを作ったのですが、その中の指定位置にテキストを表示する方法でつまずき、調べたのでアップします。当方の環境は以下の通り。作業は Juputer notebook 上で行っています。
- iMac (Retina 4K, 21.5-inch, 2017)
- macOS Catalina
- Python 3.8,1
- matplotlib 3.2.1
作例
工夫したところ
作例に示すグラフのなかで、流量データの欠測期間は灰色で塗りつぶし、その期間をテキストで表示しています。この欠測期間のテキスト表示が今回の投稿の目玉です。
時系列グラフの軸設定については、私が以前投稿した以下の記事に従ってやっています。
https://qiita.com/damyarou/items/19f19658b618fd05b3b6
時系列グラフへのテキスト表示方法
欠測期間の開始・終点を示す文字列リストを作成し、datetime型に変換
# start of no discharge data(欠測期間始点)
_sss=['2014-10-29',
'2014-12-01',
'2017-08-01',
'2018-01-01',
'2018-06-01',
'2019-06-01']
# end of no discharge data(欠測期間終点)
_sse=['2014-10-31',
'2014-12-31',
'2017-12-31',
'2018-04-30',
'2018-08-31',
'2019-12-31']
sss=[]
sse=[]
for ss,se in zip(_sss,_sse):
ss = datetime.datetime.strptime(ss, '%Y-%m-%d')
se = datetime.datetime.strptime(se, '%Y-%m-%d')
sss=sss+[ss]
sse=sse+[se]
欠測期間別にループを回しながら必要箇所を灰色で塗りつぶしテキストを表示
for ss,se in zip(sss,sse):
xx=dates.date2num(ss)+(dates.date2num(se)-dates.date2num(ss))/2 # テキスト表示位置の中心x座標を指定
x1=dates.date2num(xmin) # グラフのx座標始点
x2=dates.date2num(xmax) # グラフのx座標終点
if x1<xx<x2: # テキスト表示位置がグラフの始点と終点の間にあればテキスト描画
plt.axvspan(ss,se,color='#cccccc')
xs=dates.num2date(xx)
ys=ymin+0.3*(ymax-ymin)
tstr1 = ss.strftime('%Y/%m/%d')
tstr2 = se.strftime('%Y/%m/%d')
sstr=tstr1+'~'+tstr2+'\nno discharge data'
plt.text(xs,ys,sstr,ha='center',va='bottom',fontsize=fsz,rotation=90,linespacing=1.5)
灰色での塗つぶしは、axvspan
でやっています。
テキスト表示位置の指定には、import matplotlib.dates as dates
しておき、dates.date2num()
(西暦1年1月1日午前0時+1日からの日数を浮動小数点に変換)および dates.num2date()
(浮動小数点をdatetime
に変換) という関数を使っています。
プログラムでは、複数年で1年毎に同じような図を作っているので、「グラフ毎にx軸の始点と終点を確認し、テキスト表示位置がその間に入っているときのみ描画する」という処理をしないと、とんでもないところにテキストを表示してしまうため、このようなやり方をしています。
テキストは2行にわたり1つのplt.text()
で出力しています。この行間はlinespacing=1.5
により調整しています。
関数については以下を参照。
気がついているがやっていないこと
グラフをよく見ると、10月末の欠測期間の前後および12月の欠測期間の前に1日分の空白が空いているのがわかると思います。
これは、データの示す日付の時刻がその日の午前0時となっているため、その日の0時から24時までの間が塗りつぶされずに残っているためです。
プロットデータは、日単位データなので、bar
で塗りつぶすのが本来なのですが、描画にとても時間がかかるため、fill_between
で塗りつぶしを行い、よしとしています。
プログラム全文
# Time series drawing
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import datetime
import matplotlib.dates as dates
import matplotlib.dates as mdates
def inp_rf():
fnameR='xls_rdata_20190630.xlsx'
df = pd.read_excel(fnameR,index_col=0)
df.index = pd.to_datetime(df.index, format='%Y/%m/%d')
df_rf=df['2013/01/01':'2019/12/31']
return df_rf
def inp_qq():
fnameR='df_qq1.csv'
df = pd.read_csv(fnameR,index_col=0)
df.index = pd.to_datetime(df.index, format='%Y/%m/%d')
df_qq=df['2013/01/01':'2019/12/31']
return df_qq
def drawfig(df_rf,df_qq):
# start of no discharge data
_sss=['2014-10-29',
'2014-12-01',
'2017-08-01',
'2018-01-01',
'2018-06-01',
'2019-06-01']
# end of no discharge data
_sse=['2014-10-31',
'2014-12-31',
'2017-12-31',
'2018-04-30',
'2018-08-31',
'2019-12-31']
sss=[]
sse=[]
for ss,se in zip(_sss,_sse):
ss = datetime.datetime.strptime(ss, '%Y-%m-%d')
se = datetime.datetime.strptime(se, '%Y-%m-%d')
sss=sss+[ss]
sse=sse+[se]
yyyy=np.array([2013,2014,2015,2016,2017,2018,2019])
fsz=12
st='01-01'
ed='12-31'
for year in yyyy:
sxmin=str(year)+'-'+st
sxmax=str(year)+'-'+ed
plt.figure(figsize=(16,6),facecolor='w')
plt.rcParams['font.size']=fsz
xmin = datetime.datetime.strptime(sxmin, '%Y-%m-%d')
xmax = datetime.datetime.strptime(sxmax, '%Y-%m-%d')
ymin=0
ymax=400
plt.xlim([xmin,xmax])
plt.ylim([ymin,ymax])
sxlabel='Date ({0})'.format(year)
plt.xlabel(sxlabel)
plt.ylabel('Daily discharge Q (m$^3$/s)')
plt.grid(which='major',axis='both',color='#999999',linestyle='--')
for ss,se in zip(sss,sse):
xx=dates.date2num(ss)+(dates.date2num(se)-dates.date2num(ss))/2
x1=dates.date2num(xmin)
x2=dates.date2num(xmax)
if x1<xx<x2:
plt.axvspan(ss,se,color='#cccccc')
xs=dates.num2date(xx)
ys=ymin+0.3*(ymax-ymin)
tstr1 = ss.strftime('%Y/%m/%d')
tstr2 = se.strftime('%Y/%m/%d')
sstr=tstr1+'~'+tstr2+'\nno discharge data'
plt.text(xs,ys,sstr,ha='center',va='bottom',fontsize=fsz,rotation=90,linespacing=1.5)
plt.fill_between(df_qq.index,df_qq['q_tot'],0,color='#ff00ff',label='Q (total)')
plt.fill_between(df_qq.index,df_qq['q_lll']+df_qq['q_rrr'],0,color='#00ff00',label='Q (Right)')
plt.fill_between(df_qq.index,df_qq['q_lll'],0,color='#ff0000',label='Q (Left)')
plt.twinx()
plt.ylim([ymax,ymin])
plt.ylabel('Daily rainfall RF (mm/day)')
plt.fill_between(df_rf.index,df_rf['RF'],0,color='#0000ff',label='RF in basin by JWA')
plt.fill_between([0],[0],0,color='#ff00ff',label='Q (total)')
plt.fill_between([0],[0],0,color='#00ff00',label='Q (Right)')
plt.fill_between([0],[0],0,color='#ff0000',label='Q (Left)')
plt.legend(bbox_to_anchor=(1, 1.01), loc='lower right', borderaxespad=0.1, ncol=4, shadow=True, fontsize=fsz-2)
#plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%d-%b-%Y'))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%d-%b'))
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=1))
plt.gca().xaxis.set_minor_locator(mdates.MonthLocator(interval=1))
plt.gcf().autofmt_xdate()
fnameF='fig_'+str(year)+'.png'
plt.savefig(fnameF, dpi=100, bbox_inches="tight", pad_inches=0.1)
plt.show()
def main():
df_rf=inp_rf()
df_qq=inp_qq()
drawfig(df_rf,df_qq)
#==============
# Execution
#==============
if __name__ == '__main__': main()
以 上