LoginSignup
3
2

More than 3 years have passed since last update.

matplotlib 時系列グラフにテキストを書き込む

Last updated at Posted at 2020-04-01

はじめに

時系列グラフを作ったのですが、その中の指定位置にテキストを表示する方法でつまずき、調べたのでアップします。当方の環境は以下の通り。作業は Juputer notebook 上で行っています。

  • iMac (Retina 4K, 21.5-inch, 2017)
  • macOS Catalina
  • Python 3.8,1
  • matplotlib 3.2.1

作例

fig_test.png

工夫したところ

作例に示すグラフのなかで、流量データの欠測期間は灰色で塗りつぶし、その期間をテキストで表示しています。この欠測期間のテキスト表示が今回の投稿の目玉です。

時系列グラフの軸設定については、私が以前投稿した以下の記事に従ってやっています。

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

以 上

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2