概要
課題
私はPythonからmatplotlibを使用してグラフを描くことが多いです。
Pythonアプリケーションでデータを生成、整形、matplotlibによるグラフ出力、のような流れで行います。
このとき、以下のような課題を抱えていました。
- グラフのデザインが統一されていない。
- 同じような記述をそれぞれのアプリケーションで行っている。
- デザインとコード(Pythonファイルの記述)が視覚的、言語的に結びついていない。
原因
データを可視化するために記述するコードと、デザインを管理するために記述するコードが
1つのアプリケーション内に書かれているからだと考えました。
対応
デザインを管理するためのコードを設定ファイルとして分離してみました。
詳細
課題として記載した3つのうち、グラフのデザインが統一されていない
というのは、
縦軸や横軸に割り当てたラベルの大きさや凡例、プロットされる点等が統一されていないということです。
これは2番目の課題とも関連するのですが、同じような記述を別のPythonファイルで行っているからでした。
統一されておらず、その場その場で過去のPythonファイルを参照して、コピペをしていたことが問題でした。
どうしてコピペを繰り返してしまうのか、それは私が求めているデザインとコードが
結びついていない、少なくとも直感的ではない、からだと思います。これは3つ目の課題に繋がります。
これら3つの課題の原因は、データとデザインが分離されていないからだと、私は考えました。
そうであるのならば、話は単純でデザインを設定ファイルへ分離すればよいのです。
デザインを設定ファイルに言葉で表現
以下の表のようにデザインと言葉を対応させてみました。
各項目の意味は、
- デザインの分類
- 設定ファイルのパラメータ
- パラメータの意味
- 対応するmatplotlibのコード
- 設定ファイルのパラメータ
です。
-
Size(大きさを設定する)
- figure_x
- 図の横方向の大きさ
- pyplot.figure(figsize=(figure_x, figure_y))
- figure_y
- 図の縦方向の大きさ
- figure_xと同じ
- font_title
- タイトルのフォントの大きさ
- pyplot.title(fontsize=font_title)
- font_x_label
- X軸のフォントの大きさ
- pyplot.figure.add_subplot().ax.set_xlabel(fontsize=font_x_label)
- font_y_label
- Y軸のフォントの大きさ
- pyplot.figure.add_subplot().ax.set_ylabel(fontsize=font_y_label)
- font_tick
- 軸のメモリのフォントの大きさ
- pyplot.tick_params(labelsize=font_tick)
- font_legend
- 凡例のフォントの大きさ
- pyplot.legend(fontsize=font_legend)
- marker
- データをプロットする際のマーカーの大きさ
- pyplot.figure.add_subplot().ax.plot(markersize=marker)
- figure_x
-
Position(位置を設定する)
- subplot
- グラフを配置する場所
- pyplot.figure.add_subplot(subplot)
- legend_location
- 凡例を配置するグラフ内の場所
- pyplot.legend(loc=legend_location)
- subplot
具体的な設定ファイルの表現方法
設定ファイルはいくつかの表現方法、jsonやyaml等を検討しました。
結果、Pythonに標準である**configparser**を使うことにしました。
jsonやyamlでもよいとは思うのですが、階層的に表現できるよりも、直感的に使いやすいほうがいいと考えました。
configparserで設定ファイルを表現すると次のようになります。
[Size]
figure_x=8
figure_y=8
font_title=20
font_x_label=18
font_y_label=18
font_tick=10
font_legend=15
marker=10
[Position]
subplot=111
legend_location=upper right
[Markers]
0=D
1=>
2=.
3=+
4=|
[Color]
0=red
1=blue
2=green
3=black
4=yellow
デザインの分類に当たる項目は**[Size]のように角括弧で囲います。これはセクションと呼ばれます。
その下に設定ファイルのパラメータ=値を書いていきます。これはキー**と呼ばれます。
上記の設定ファイルにはSize、Positionの他に、**Markers(プロットするマーカーの種類)やColor(プロットされるマーカーや線の色)**も表現されています。
設定ファイルに記載されたパラメータをPythonコードから使用
設定ファイルのパラメータへPythonアプリケーションがアクセスするには、次のようにコードを記述します。
import configparser
rule_file = configparser.ConfigParser()
rule_file.read("configファイルのパス", "UTF-8")
hogehoge = rule_file["セクション名"]["キー名"]
注意点として、読み込んだ値は文字列になります。
実際の使用例
以下のコードは、渡されたデータとデザインの設定ファイルをもとに折れ線グラフを作成します。
""" 折れ線グラフ作成関数
渡されたデータを使用して折れ線グラフを描画し、画像データとして保存する。
"""
import configparser
import matplotlib.pyplot as plt
def make_line_graph(data, config="config.ini"):
"""折れ線グラフ描画
渡されたデータを使用して折れ線グラフを作成する。
デザインは別のconfigファイルから読み取る。
Args:
data(dict): プロットするデータが格納されている
config(str): configファイルの名前
Returns:
bool: Trueなら作成完了、Falseなら作成失敗
Note:
引数のdataに含めるべきkeyとvalueについて以下に記載する。
key : value
------------------------
title(str): グラフのタイトル名
label(list): 凡例の説明
x_data(list): x軸のデータ
y_data(list): y軸のデータ
x_ticks(list): x軸のメモリに表示する値
y_ticks(list): y軸のメモリに表示する値
x_label(str): x軸の名前
y_label(str): y軸の名前
save_dir(str): 保存ファイルパス
save_name(str): 保存ファイル名
file_type(str): 保存ファイル形式
"""
rule_file = configparser.ConfigParser()
rule_file.read("./conf/{0}".format(config), "UTF-8")
fig = plt.figure(figsize=(int(rule_file["Size"]["figure_x"]), int(rule_file["Size"]["figure_y"])))
ax = fig.add_subplot(int(rule_file["Position"]["subplot"]))
ax.set_xlabel(data["x_label"], fontsize=int(rule_file["Size"]["font_x_label"]))
ax.set_ylabel(data["y_label"], fontsize=int(rule_file["Size"]["font_y_label"]))
for index in range(len(data["x_data"])):
ax.plot(data["x_data"][index],
data["y_data"][index],
label=data["label"][index],
color=rule_file["Color"][str(index)],
marker=rule_file["Markers"][str(index)],
markersize=int(rule_file["Size"]["marker"]))
plt.title(data["title"], fontsize=int(rule_file["Size"]["font_title"]))
if "x_ticks" in data.keys():
plt.xticks(data["x_ticks"][0], data["x_ticks"][1])
if "y_ticks" in data.keys():
plt.yticks(data["y_ticks"][0], data["y_ticks"][1])
plt.tick_params(labelsize=int(rule_file["Size"]["font_tick"]))
plt.legend(fontsize=rule_file["Size"]["font_legend"], loc=rule_file["Position"]["legend_location"])
plt.savefig("".join([data["save_dir"], "/", data["save_name"], ".", data["file_type"]]))
データを渡すPythonファイルは以下のような感じになります。
from make_line_graph import make_line_graph
data = {
"title": "hogehoge",
"label": ["A", "B"],
"x_data": [x_data1, x_data2],
"y_data": [y_data1, y_data2],
"x_ticks": [x_ticks1, x_ticks2],
"y_ticks": [y_ticks1, y_ticks2],
"x_label": "hogehoge",
"y_label": "hogehoge",
"save_dir": "保存したいフォルダのパス",
"save_name": "保存したいファイル名",
"file_type": "拡張子",
}
make_line_graph(data, config="config.ini")
使ってみた感想
良かった点
デザインが変えやすくなりました。
特に、各種フォントサイズはプロットするデータやラベルに入れる文字数によって変わります。
また設定ファイルを複製してカスタマイズすることで、グラフデザインを変えたくなったときのPythonファイル変更量が少なくなりました。
読み込む設定ファイル名だけ変えればよし。
グラフデザインと設定ファイルが結びついているので、どのデザインがどのコードと対応しているのか忘れても大丈夫です。
悪かった点
どこまで汎用性をもたせるかが難しいです。
作ったmake_line_graph.pyは折れ線グラフ作成関数ですが、似たようなPythonファイルが増えるとよろしくないのでできる限り汎用的にしました。
ただこれではうまくグラフが描画出来ず、それに対応するために別の折れ線グラフ作成関数が乱立すると、振り出しに戻ってしまいそうです・・・。
汎用性を考えればきりがないのかなとも思ったりしてますが・・・。