mecobalamin’s diary

人間万事塞翁が馬

Pythonで時系列グラフに水平線と矢印を入れる

以前Pythonで時系列グラフを作成した
mecobalamin.hatenablog.com

このグラフに矢印と水平線を加える
つまりこのグラフの任意の場所に矢印と水平線を加えて
f:id:mecobalamin:20210301160250p:plain
このようにする
f:id:mecobalamin:20210301160303p:plain

元のデータはCSVで500行ぐらいある
その一部を抜粋

Date, a, b, c, d
2020/11/24 16:42:20, 143, 101, 63,
2020/11/24 17:27:19, 137, 101, 65,
2020/11/25 06:31:48, 134, 90, 65,
2020/11/25 11:21:57, 142, 109, 67,
2020/11/25 14:06:42, 131, 94, 74,
2020/11/25 18:54:18, 120, 91, 69,
2020/11/25 20:42:53, 114, 78, 72,
2020/11/26 09:09:53, 113, 84, 76,
2020/11/26 09:17:28, 125, 70, 70, 1021.67
2020/11/26 16:12:06, 126, 89, 65, 1019.64
2020/11/27 06:37:47, 117, 81, 69, 1020.66
2020/11/27 13:31:37, 123, 88, 98, 1019.98
2020/11/27 14:22:03, 126, 86, 89, 1019.64
2020/11/27 17:58:24, 120, 86, 84, 1020.32
2020/11/27 19:35:42, 116, 81, 81, 1021
2020/11/27 21:35:24, 120, 86, 78, 1021.33
2020/11/28 08:30:20, 133, 98, 69, 1022.69
2020/11/28 12:59:11, 127, 82, 72, 1022.01
2020/11/28 20:59:20, 119, 86, 67, 1022.69
2020/11/28 22:06:02, 112, 75, 66, 1023.03

dfという変数に格納されている

今回のポイントは

  1. axesオブジェクトを返す
  2. 矢印をいれる関数を作成する
  3. 水平線はplot関数を使う

1. axesオブジェクトを返す
グラフを作成する関数を書いてある
関数の内部に矢印を入れる命令を書いてもいいのだが
汎用性をもたせたいのでグラフと矢印の描画を分けたい
グラフを以下のようにして描く

fig = plt.figure(figsize = (12.0, 9.0))
ax1 = fig.subplots()

グラフを書いたaxesオブジェクトをretrunで戻す

return ax1

matplotlibについてはこちらを参考にした
matplotlibの描画の基本 - figやらaxesやらがよくわからなくなった人向け - Qiita

2. 矢印をいれる関数を作成する
グラフのデータdfはgraph_dataに入っている
関数の引数として
矢印を書き込むaxesオブジェクト、
矢印の色、矢印の長さ
を渡す
矢印は上下方向にしか引かないので
矢印の長さで向きを示す
正の値が下向き、負の値が上向きになる

日付と矢印を入れるy軸の値で
インスタンスを生成し、
矢印を書き込む関数(メソッド)を呼び出す

CreateGraphs({'2020-12-03 10:00:00':'15', '2020-12-11 10:00:00':'15', '2020-12-18 10:00:00':'15', '2021-01-08 10:00:00':'15', '2021-01-15 10:00:00':'15', '2021-01-22 10:00:00':'15', '2021-02-12 10:00:00':'15', '2021-02-19 10:00:00':'15'}).add_arrows(ax, '#ff4500', -10)

この場合は8個の矢印を描く

矢印を書き込む関数は以下の通り

def add_arrows(self, ax, arrow_color, arrow_direction):
    values_infection = self.graph_data

    date_infection = values_infection.keys()
    for key in date_infection:
        ax.annotate("", xy = (pd.to_datetime(key), int(values_infection[key])),
                xytext = (pd.to_datetime(key), int(values_infection[key]) + arrow_direction),
                arrowprops = dict(shrink = 0, width = 1, headwidth = 8,
                                headlength = 10, connectionstyle = 'arc3',
                                facecolor = arrow_color, edgecolor = arrow_color))

iOSのPythonista3ではheadlengthに関してエラーが出る
環境によってはarrowpropsを以下のように書き換える必要があるかも

arrowprops = dict(arrowstyle = '->, head_width = 0.2, head_length = 0.5', connectionstyle = 'arc3', facecolor = arrow_color, edgecolor = arrow_color)


3. 水平線はplot関数を使う
水平線を入れるメソッドhlinesもあるが
グラフの値を重ねられてしまう

ax.hlines([128], min(df.index), max(df.index), "blue", linestyles = 'dashed')

コードの最後に書いてもグラフに隠れる

そこでplot()を使って水平線を描く

ax.plot([min(df.index), max(df.index)],[128, 128], "blue", linestyle='dashed')

コードをまとめるとこの様になる

# -*- coding: utf-8 -*-

import os, sys, datetime
from pandas import Series, DataFrame
import pandas as pd

import numpy as np

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns

class ReadFiles:
    def __init__(self):
        pass

    def get_current_path(self):
        current_path = os.getcwd()
        current_path = os.chdir(os.path.dirname(os.path.abspath(__file__)))
        current_path = os.getcwd()

        return(current_path)

    def create_directories(self, path_to_dir):
        try:
            os.makedirs(path_to_dir)
        except FileExistsError:
            pass

    def read_files(self, path_to_file):
        print('open a csv file.')
        try:
            df = pd.read_csv(path_to_file)
        except Exception as e:
            print('cannot open file: ', path_to_file)
            print(str(e))
            sys.exit(1)
        else:
            print('Success!')

        return(df)

class CreateGraphs:
    def __init__(self, graph_data):
        self.graph_data = graph_data

    def create_line_graph(self):
        print('Create line plot')

        df = self.graph_data

        fig = plt.figure(figsize = (12.0, 9.0))
        ax1 = fig.subplots()
        plt.subplots_adjust(left=0.125, right=0.9, bottom=0.3, top=0.9)

        plt.ylim(0, 200)
        plt.ylabel('a/b')
        ax1.plot(df.index, df.iloc[:, 0], linestyle = '-', marker = 'o', color = '#ff7f50', label = df.columns[0])
        ax1.plot(df.index, df.iloc[:, 1], linestyle = ':', marker = '+', color = '#00ff7f', label = df.columns[1])

        ax2 = ax1.twinx()
        plt.ylim(40, 300)
        plt.ylabel('c')
        ax2.plot(df.index, df.iloc[:, 2], linestyle = '--', marker = '.', color = '#00bfff', label = df.columns[2])

        locator = mdates.AutoDateLocator()
        formatter = mdates.ConciseDateFormatter(locator)
        ax1.xaxis.set_major_locator(locator)
        ax1.xaxis.set_major_formatter(formatter)

        handles1, labels1 = ax1.get_legend_handles_labels()
        handles2, labels2 = ax2.get_legend_handles_labels()
        ax1.legend(handles1 + handles2, labels1 + labels2, loc = 'upper right')

        return ax1

    def add_arrows(self, ax, arrow_color, arrow_direction):
        values_infection = self.graph_data

        date_infection = values_infection.keys()
        for key in date_infection:
            ax.annotate("", xy = (pd.to_datetime(key), int(values_infection[key])),
                xytext = (pd.to_datetime(key), int(values_infection[key]) + arrow_direction),
                arrowprops = dict(shrink = 0, width = 1, headwidth = 8,
                                headlength = 10, connectionstyle = 'arc3',
                                facecolor = arrow_color, edgecolor = arrow_color))

if __name__ == '__main__':

    date_today = datetime.date.today()
    day_save_file = str(date_today.year) + str(date_today.month).zfill(2) + str(date_today.day).zfill(2)

    input_file_name = 'Python_Sample.csv'
    output_dir_name = 'graph'
    output_graph_name = day_save_file + '_graph_sample.png'

    path_input_file = ReadFiles().get_current_path() + '\\' + input_file_name
    path_output_dir = ReadFiles().get_current_path() + '\\' + output_dir_name + '\\'
    print(path_input_file)

    ReadFiles().create_directories(path_output_dir)
    df = ReadFiles().read_files(path_input_file)
    print(df.iloc[-1])
    print(df.mean())

    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index('Date')
    ax = CreateGraphs(df).create_line_graph()

    CreateGraphs({'2020-12-03 10:00:00':'15', '2020-12-11 10:00:00':'15', '2020-12-18 10:00:00':'15', '2021-01-08 10:00:00':'15', '2021-01-15 10:00:00':'15', '2021-01-22 10:00:00':'15', '2021-02-12 10:00:00':'15', '2021-02-19 10:00:00':'15'}).add_arrows(ax, '#ff4500', -10)
    CreateGraphs({'2020-12-25 10:00:00':'15', '2021-01-01 10:00:00':'15', '2021-01-29 10:00:00':'15', '2021-02-26 10:00:00':'15'}).add_arrows(ax, '#a9a9a9', -10)
    CreateGraphs({'2021-02-03 10:00:00':'15'}).add_arrows(ax, '#00008b', -10)

    ax.plot([min(df.index), max(df.index)],[128, 128], "blue", linestyle='dashed')

    plt.savefig(path_output_dir + '\\' + output_graph_name)
    plt.close()

    print('Operation completed')