8
4

More than 1 year has passed since last update.

俺は長文matplotlibをやめるぞ!ジョジョーーーッ!!

Last updated at Posted at 2022-04-02

タイトルオチです。

記事の背景

あきとしのスクラップノートさんの以下の記事を読み、感銘を受けました。

[python] context manger を使ってmatplotlibの図を大量生産する

そう、matplotlibってやたら行数多くなるんですよね…!
まだ読まれてない方はこの記事より先にぜひ読んでください。

記事の内容をgithubでも公開してくださっています。

contextplt -github-

本記事では上記の記事に全面的に依拠しつつ、
データの渡し方をdictからdataclassにアレンジしてみました。
ささやかな差分ですが、自分の備忘録代わりにメモします。

※ 2022-04-03 一部内容変更

キーワード引数の渡し方をより簡単な方法に変更しました(set_xticksのところ)。

dataclassにしてみたコード

from typing import NamedTuple, Optional
import dataclasses
from dataclasses import dataclass

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

@dataclass
class BasicPlotArgs:
    xlim: Optional[tuple[float]] = None
    ylim: Optional[tuple[float]] = None
    xlabel: str = ""
    ylabel: str = ""
    title: str = ""
    save_path: Optional[str] = None
    figsize: tuple[int] = (6,4)
    dpi: int = 150
    tight: bool = False
    show: bool = True


class BasicPlot():
    def __init__(self, pa: Optional[BasicPlotArgs]=None):
        if not pa:
            pa = BasicPlotArgs()
        self.fig = plt.figure(figsize=pa.figsize, dpi=pa.dpi)
        self.ax = self.fig.add_subplot(111)
        self.ax.set_xlabel(pa.xlabel)
        self.ax.set_ylabel(pa.ylabel)
        self.ax.set_xlim(pa.xlim) if pa.xlim else None
        self.ax.set_ylim(pa.ylim) if pa.ylim else None
        self.save_path = pa.save_path
        self.title = pa.title
        self.tight = pa.tight
        self.show = pa.show

    def __enter__(self):
        return(self)

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.option()
        plt.title(self.title)
        plt.tight_layout() if self.tight else None 
        plt.savefig(self.save_path) if self.save_path else None
        plt.show() if self.show else None

    def option(self):
        '''This method is for additional graphic setting. 
        See DatePlot for example.'''
        pass

xx = np.linspace(-5,5,20)
yy = xx*xx

with BasicPlot() as p:
    p.ax.plot(xx,yy)

output.png

dataclassを定義した以外は元の記事とほぼそのままです。

元の記事の中ほどに出てきますが、複数の引数を辞書で渡すやり方も紹介されています。
辞書の代わりにdataclassを使うことで、図の設定を少し流用しやすくするという意図です。

BasicPlotArgsを
@dataclass
class NewPlotArgs(BasicPlotArgs):
    xlabel:str = 'NEW!'

newplotargs = NewPlotArgs(xlim=(0,10), ylim=(0,10))

with BasicPlot(newplotargs) as p:
    p.ax.plot(xx,yy)

output2.png

いくつかの図の設定をテンプレート的に使い分けたいときや、
BasicPlotを外部モジュールとして、複数のファイルから利用するときに便利かもしれません。

ただし、dataclassを使うことによる不便もあります。
新たな引数を設定するときに、Plot用のクラスとdataclassの2箇所変更する必要が出てきます。

アドホックな分析であれば、with文の中に書く方が手っ取り早いでしょう。
たとえば新たにx軸の目盛りをいじりたいときは、以下のようにします。

xx = np.linspace(-5,5,20)
yy = xx*xx

with BasicPlot() as p:
    p.ax.set_xticks(range(-5,6))
    p.ax.plot(xx,yy)

output3.png

簡単ですね!

もし頻繁にその設定を変更する必要があれば、クラスのオーバーライドをしてもいいかもしれません。

@dataclass
class AnotherPlotArgs(BasicPlotArgs):
    xticks: Any = None


class AnotherPlot(BasicPlot):
    def __init__(self, pa: Optional[AnotherPlotArgs] = None):
        super().__init__(pa)
        self.ax.set_xticks(pa.xticks)

先ほどのset_xticksはシンプルに1つの位置引数を取りました。

もしset_xtickslabelsのようなキーワード引数も渡したいときは、
やや強引ですが、位置引数をリスト、キーワード引数を辞書にして、
Plot用のクラスの中でアンパックするやり方が使えるかもしれません。

@dataclass
class AnotherPlotArgs(BasicPlotArgs):
    xticks: Optional[Args] = None


class AnotherPlot(BasicPlot):
    def __init__(self, pa: Optional[AnotherPlotArgs] = None):
        super().__init__(pa)
        self.ax.set_xticks(*pa.xticks.ag, **pa.xticks.kw) if pa.xticks else None


class Args:
    def __init__(self, *args, **kwargs):
        self.ag = args
        self.kw = kwargs


xticks = Args(np.arange(5), labels=['G1', 'G2', 'G3', 'G4', 'G5'])
# 変更前: xticks = Args([np.arange(5)], {'labels':['G1', 'G2', 'G3', 'G4', 'G5']}) 

apa = AnotherPlotArgs(xticks=xticks)
with AnotherPlot(apa) as p:
    p.ax.plot(xx,yy)

output5.png

が、この煩雑さに見合うほど楽になるかというと正直微妙です。
特にキーワード引数をいちいち辞書にするのがめんどうです。1

やろうと思えば可能ではある(が、めんどくさい)くらいの認識がちょうどよいかもしれません。

修正前(クリックで表示)
class Args(NamedTuple):
    ag: list = []
    kw: dict = {}

@dataclass
class AnotherPlotArgs(BasicPlotArgs):
    xticks: Optional[Args] = None

class AnotherPlot(BasicPlot):
    def __init__(self, pa: Optional[AnotherPlotArgs] = None):
        super().__init__(pa)
        self.ax.set_xticks(*pa.xticks.ag,**pa.xticks.kw) if pa.xticks else None
        self.ax.bar(*pa.bar.ag, **pa.bar.kw) if pa.bar else None

xticks = Args([np.arange(5)], {'labels':['G1', 'G2', 'G3', 'G4', 'G5']})

apa = AnotherPlotArgs(xticks=xticks))

with AnotherPlot(apa) as p:
    p.ax.plot(xx,yy)

まとめ

基本的にはwith文に都度書いていくのが1番手っ取り早そうです。
繰り返しが多くなってテンプレート化したくなったら、この記事の内容が役に立つかもしれません。

  1. 辞書にせずに済むようになりました(2022-04-03変更)。

8
4
4

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
8
4