今日の目標
Archive Appliance データのweb Viewerからデータを入手してグラフを作成します。
前回に続き、pandas, matplotlib モジュールなどを使います。
logging, os, subprocess などのモジュールも使います。
import os, logging, os.path, subprocess
import pandas, numpy, matplotlib
import matplotlib.pyplot as pyplot
import matplotlib.dates as mdates
J-PARC制御システムWebの「Archiver Appliance WEB閲覧」ページでは、 J-PARC加速器制御システムでアーカイブされたデータをグラフとして表示できます。表示されたグラフを再現するgnuplot用スクリプトは、簡単な操作でローカルファイルとしてダウンロード可能です。 今日の講座では、このグラフをPythonを使って、書き直してみようというのが目的です。
gnuplot スクリプト ファイルには、グラフ作成に使われたデータが、グラフ作成のgnyplotスクリプトと共に含まれています。これらのデータを、このスクリプトに次の一行のgnuplotスクリプト を追加することで、csvファイルとして取り出します。
set table 'data.csv';replot ;unset table
gnuplot
コマンド¶gnuplot
コマンドは-e
オプションを使って、実行するgnuplotコマンドを引数にわたすことができます。
ここでは、
load
コマンドを使って「Archiver Appliance WEB閲覧」からダウンロードした20211123T110243.036.plt
を読み込んだ後、set table \"data.csv\";replot ;unset table
コマンドを実行する。によって、csv形式のデータファイル"data.csv"を作成しています。
gnuplot -e "load \"20211123T110243.036.plt\";set table \"data.csv\";replot ;unset table"
ダウンロードしたgnuplot スクリプトファイルからcsvファイルを生成する手続きを、 pythonから実行してみます。
subprocess
モジュールのrun
関数に実行するプログラム名とその引数をリストとしてあたえます。
import os,logging, os.path
import subprocess
# logging.getLogger().setLevel(logging.INFO)
try:
os.mkdir("./tmp")
except FileExistsError as errmsg:
logging.info(f"{errmsg} -> ignored")
pass
if os.path.exists("data.csv"):
os.remove("data.csv") #念の為に出力ファイルを取り除いておく。
subprocess.run(["gnuplot",
"-e",
"load \"20211123T110243.036.plt\";set table \"data.csv\";replot ;unset table;"
])
os.stat("data.csv")
os.stat_result(st_mode=33188, st_ino=53268723, st_dev=16777230, st_nlink=1, st_uid=501, st_gid=20, st_size=103494, st_atime=1638414816, st_mtime=1638414816, st_ctime=1638414816)
python中から、シェルコマンドなど他のプログラムを実行することができます。
subprocess
モジュールの run
コマンドos
モジュールのsystem
コマンド(subprocess.run( <cmd string>, shell=True)
で置き換え可能)import os
gpl_cmd="gnuplot -e \"load \\\"20211123T110243.036.plt\\\";set table \\\"data.csv\\\";replot ;unset table\""
os.system(gpl_cmd)
subprocess.run(['gnuplot',
"-e",
"load \"20211123T110243.036.plt\";set table \"data.csv\";replot ;unset table;"
],
)
subprocess.run(gpl_cmd, shell=True)
CompletedProcess(args='gnuplot -e "load \\"20211123T110243.036.plt\\";set table \\"data.csv\\";replot ;unset table"', returncode=0)
import shlex
shlex.split(gpl_cmd)
['gnuplot', '-e', 'load "20211123T110243.036.plt";set table "data.csv";replot ;unset table']
このプログラムでは、gnuplotスクリプトが画像データを保存するための./tmp
ディレクトリをos
モジュールの.mkdir()
関数を使って作成しています。
./tmp
ディレクトリが既に存在する場合には、os.mkdir("./tmp")
の呼び出しは失敗して、FileExistsError
例外が発生します。 しかし、このFileExistsError
は./tmp
が既に存在していることを示しているだけですから、プログラムの実行を続けても問題ありません。
これをpythonのtry
文を使って記述したのが以下のプログラムです。
try:
os.mkdir("./tmp") # これを試して、... 成功なら"./tmp"が作成される。
except FileExistsError as errmsg:
logging.ifo(errmsg)
pass #FileExistsErrorの時はなにもしないで、プログラムの実行を続ける。
Python: EAFP「認可をとるより許しを請う方が容易 (easier to ask for forgiveness than permission、マーフィーの法則)」
C/C++: LBYL「ころばぬ先の杖 (look before you leap)」
1) ディレクトリが存在するかどうか確認して(os.path.exists
),
2) 存在すればメッセージを出力,
3) さもなければ、ディレクトリを作成する(os.mkdir
)
というアプローチも, もちろん可能です。
import os.path
if os.path.exists("./tmp"):
logging.info("./tmp directory already exists.")
else:
os.mkdir("./tmp")
try:
os.remove("xxxx.csv") #念の為に出力ファイルを取り除いておく。
except FileNotFoundError as msg:
logging.info(msg)
logging
モジュール を使って、必要な時に実行ログを残す。¶プログラム開発中には、プログラムの実行状態を示すメッセージを残したいことがあります。
この際にprint()
関数を使ってしまうと、「必要がなくなった場合には、このprint()
関数を取り除き、必要となったら再度かきたす。」といった手間が発生してしまいます。
logging
モジュールのdebug()
,info()
,warning()
, fatal()
(あるいはcritical()
)などの
関数を使って、実行時メッセージを出力するようにしておくと、logging levelの設定に応じてメッセージの出力を制御できます。
上記の例では、loglevelをINFOに設定しておくと, FileExistsError
例外が発生した時、端末にエラーメッセージが表示されます。
loggingモジュールの設定を変えることで、メッセージを端末に出力するだけでなく、ファイル、データベース、ログサーバーなどに記録するようにもできます。
import os,logging
logging.getLogger().setLevel(logging.INFO)
try:
os.mkdir("./tmp")
except FileExistsError as errmsg:
logging.info(f"{errmsg} -> ignored")
pass
logging.getLogger().setLevel(logging.WARN)
INFO:root:[Errno 17] File exists: './tmp' -> ignored
さらに、プログラムを書き換えることなくロギングレベルを変更することができれば、 必要に応じてプログラムからのメッセージ出力の有無を切り替えることができて便利です。
プログラム実行時にロギングレベルを指定する方法にはいろいろな可能性がありますが、 ここでは環境変数を使う方法を紹介します。
pythonプログラムでは実行中のプロセスがもつ環境変数は、os
モジュールのos.environ
変数を使って、
読み書きすることができます。
import os
PY_LOG_LEVEL=os.environ.get("PYTHON_LOG_LEVEL","") #環境変数"PYTHON_LOG_LEVEL"が定義されていればその値、さもなければ""
# モジュール`logging`の辞書からPY_LOG_LEVEL(文字列)をloggingのログレベル(整数値)に変換する。
LOG_LEVEL=logging.__dict__.get(PY_LOG_LEVEL,logging.WARN)
logging.getLogger().setLevel(LOG_LEVEL)
print(f"{PY_LOG_LEVEL = }, {LOG_LEVEL =}")
try:
os.mkdir("./tmp")
except FileExistsError as errmsg:
logging.info(f"{errmsg} -> ignored.")
pass
os.system("gnuplot 20211123T110243.036.plt");
PY_LOG_LEVEL = '', LOG_LEVEL =30
import os
try:
del os.environ["PYTHON_LOG_LEVEL"]
except KeyError:
pass
pythonを実行する際に、sh/bashなどでは、
PYTHON_LOG_LEVEL=INFO python3
あるいは cshなどでは、
env PYTHON_LOG_LEVEL=INFO python3
としてpythonプロセスを起動すると、実行中のpythonプログラムでは、環境変数PYTHON_LOG_LEVEL
が"INFO"に設定されています。
%%sh
PYTHON_LOG_LEVEL=INFO python3 -c \
"import os;print(os.environ[\"PYTHON_LOG_LEVEL\"])"
INFO
%%sh
PYTHON_LOG_LEVEL=DEBUG python3 -c \
"import os;print(os.environ[\"PYTHON_LOG_LEVEL\"])"
DEBUG
出来上がったdata.csv
ファイルの中身をエディタで覗いてみます。
# Curve 0 of 2, 981 points
# Curve title: "MRMON:DCCT_073_1:VAL:MRPWR"
# x y ylow yhigh type
"2020 Jan.22 00:00:00" 504.467 504.126 504.673 i
"2020 Jan.22 00:00:32" 504.287 503.774 504.579 i
"2020 Jan.22 00:01:04" 504.212 503.507 504.593 i
...
"2020 Jan.22 08:59:44" 504.45 504.004 505.006 i
# Curve 1 of 2, 981 points
# Curve title: "MRMON:DCCT_073_2:VAL:MRPWR"
# x y ylow yhigh type
"2020 Jan.22 00:00:00" 511.526 511.123 511.714 i
"2020 Jan.22 00:00:32" 511.312 510.748 511.641 i
"2020 Jan.22 00:01:04" 511.245 510.463 511.704 i
....
"2020 Jan.22 08:59:44" 511.646 511.173 512.269 i
という様に、チャンネル毎のデータが空白行とコメント行(#
)で区切られて記録されていることがわかります。
また各チャンネルのデータは一行毎にx, y, yhigh, ylow, type
の値が空白(\s
)で区切られて記録されています。
データ数が981
であることもわかります。このファイルをpandas.read_csv()
関数で読み込んでみます。
各行の最初の項はデータの時刻ですから、日付/時刻として読み取ってやることが必要です。 最初のチャンネルはコメントを四行読み飛ばした後から、981行続いています。 次のチャンネルのデータは、その先さらに空行とコメントをあわせて五行読みとばした後から、981行続いています。
各列のラベルはx, y, ylow, yhigh, type
と設定します。(pandas.csv_read
は各列のラベルをファイルから読み込むことも
できますが、この例の場合行頭の'#'がじゃまになるので、手動で設定しています。)
この例の場合行頭の'#'がじゃまになる
#
が最初のラベルとして認識されてしまうため、ラベルがひとつずれてしまいます。
import pandas
DF0=pandas.read_csv("data.csv",sep="\s+", parse_dates=[0],infer_datetime_format=True,
skiprows=4,nrows=981,
names=['x', 'y', 'ylow', 'yhigh', 'type']
)
DF1=pandas.read_csv("data.csv",sep="\s+", parse_dates=[0],infer_datetime_format=True,
skiprows=4+5+981,
names=['x', 'y', 'ylow', 'yhigh', 'type']
)
DF0.loc[:5]
x | y | ylow | yhigh | type | |
---|---|---|---|---|---|
0 | 2020-01-22 00:00:00 | 504.467 | 504.126 | 504.673 | i |
1 | 2020-01-22 00:00:32 | 504.287 | 503.774 | 504.579 | i |
2 | 2020-01-22 00:01:04 | 504.212 | 503.507 | 504.593 | i |
3 | 2020-01-22 00:01:36 | 504.361 | 504.078 | 504.730 | i |
4 | 2020-01-22 00:02:08 | 504.394 | 504.039 | 504.736 | i |
5 | 2020-01-22 00:02:40 | 504.383 | 503.852 | 504.779 | i |
DF1.loc[:5]
x | y | ylow | yhigh | type | |
---|---|---|---|---|---|
0 | 2020-01-22 00:00:00 | 511.526 | 511.123 | 511.714 | i |
1 | 2020-01-22 00:00:32 | 511.312 | 510.748 | 511.641 | i |
2 | 2020-01-22 00:01:04 | 511.245 | 510.463 | 511.704 | i |
3 | 2020-01-22 00:01:36 | 511.403 | 511.035 | 511.833 | i |
4 | 2020-01-22 00:02:08 | 511.428 | 511.090 | 511.684 | i |
5 | 2020-01-22 00:02:40 | 511.427 | 510.874 | 511.835 | i |
得られた二つのデータフレームを一つのグラフに表示してみます。
最初のplot
の戻り値(axes
)を,次のplot
のax
にわたすことで、一つ以上データを同一のグラフに表示します。
ax=DF0.plot("x","y",figsize=(12,6.75),style="r.")
DF1.plot("x","y", xlabel="t", ylabel="PWR", ax=ax ,style="g-")
<AxesSubplot:xlabel='t', ylabel='PWR'>
matplotlib.pyplot を使って、データをグラフ化してみます。
import matplotlib.dates as mdates, matplotlib.pyplot as pyplot
fig=pyplot.figure(figsize=(12,6.75))
ax=fig.add_subplot(1,1,1)
ax.plot(DF0.x, DF0.y,"r-",label="DCCT_073_1")
ax.plot(DF1.x, DF1.y,"g.",label="DCCT_073_2")
ax.legend()
pyplot.show()
軸のラベルとTicksの調整を行います。
import matplotlib.dates as mdates
fig=pyplot.figure(figsize=(12*0.8,6.75*0.8))
ax=fig.add_subplot(1,1,1)
ax.xaxis.set_major_locator(mdates.MinuteLocator(interval=60))
ax.xaxis.set_minor_locator(mdates.MinuteLocator(interval=30))
ax.xaxis.set_major_formatter(
mdates.ConciseDateFormatter(ax.xaxis.get_major_locator()))
ax.plot(DF0.x, DF0.y,"r.",label="DCCT_073_1")
ax.plot(DF1.x, DF1.y,"g-",label="DCCT_073_2")
ax.legend();
fig=pyplot.figure(figsize=(12*0.8,6.75*0.8))
ax=fig.add_subplot(1,1,1)
ax.xaxis.set_major_locator(mdates.MinuteLocator(byminute=(0,)))
ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=(30,)))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H-%M'))
# Rotates and right-aligns the x labels so they don't crowd each other.
for label in ax.get_xticklabels(which='major'):
label.set(rotation=60, horizontalalignment='right')
ax.plot("x", "y", "r-",data=DF0, label="DCCT_073_1")
ax.plot("x", "y", "g.",data=DF1, label="DCCT_073_2")
ax.legend();
import matplotlib.dates as mdates
fig=pyplot.figure(figsize=(12*0.8,6.75*0.8))
fig.tight_layout()
ax=fig.add_subplot(1,1,1)
ax.xaxis.set_major_locator(mdates.HourLocator(interval=1))
ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=(30,15,45)))
ax.xaxis.set_major_formatter(
mdates.ConciseDateFormatter(ax.xaxis.get_major_locator()))
for label in ax.get_xticklabels(which='major'):
label.set(rotation=60, horizontalalignment='right')
ax.errorbar(DF0.x, DF0.y, yerr=(DF1.yhigh-DF0.ylow)/2, errorevery=5 ,label="DCCT_073_1",linestyle=":",color="r")
ax.errorbar(DF1.x, DF1.y, yerr=(DF0.yhigh-DF1.ylow)/2, errorevery=5, label="DCCT_073_2",linestyle="-.",color="g")
ax.legend();
これまで述べたことで、一つの.pltファイルに対して何をなすべきかはわかりました。 しかし、異なる日付のデータを取得する毎にプログラムを書き換えるのは面倒です。 ということで、.pltファイル名が与えられた時に,
を作ってみます。
import os,logging
import subprocess
# logging.getLogger().setLevel(logging.INFO)
try:
os.mkdir("./tmp")
except FileExistsError as errmsg:
logging.info(f"{errmsg} -> ignored")
pass
if os.path.exists("data.csv"):
os.remove("data.csv") #念の為に出力ファイルを取り除いておく。
subprocess.run(['gnuplot',
"-e",
"load \"20211123T110243.036.plt\";set table \"data.csv\";replot ;unset table;"
])
os.stat("data.csv")
os.stat_result(st_mode=33188, st_ino=53268753, st_dev=16777230, st_nlink=1, st_uid=501, st_gid=20, st_size=103494, st_atime=1638414818, st_mtime=1638414818, st_ctime=1638414818)
import os,logging
import subprocess
def plt2csv(plt_file:str, csv_file:str):
try:
os.mkdir("./tmp")
except FileExistsError as errmsg:
logging.info(f"{errmsg} -> ignored")
pass
if os.path.exists(csv_file):
os.remove(csv_file) #念の為に出力ファイルを取り除いておく。
subprocess.run(['gnuplot',
"-e",
f"load \"{plt_file}\";set table \"{csv_file}\";replot ;unset table;"
])
plt2csv("./20211123T110243.036.plt", "data2.csv")
def csv2png(csv_file:str, png_file:str):
DF0=pandas.read_csv(csv_file,sep="\s+", parse_dates=[0],infer_datetime_format=True,
skiprows=4,nrows=981,
names=['x', 'y', 'ylow', 'yhigh', 'type']
)
DF1=pandas.read_csv(csv_file,sep="\s+", parse_dates=[0],infer_datetime_format=True,
skiprows=4+5+981,
names=['x', 'y', 'ylow', 'yhigh', 'type']
)
fig=pyplot.figure(figsize=(12,6.75))
ax=fig.add_subplot(1,1,1)
ax.plot(DF0.x, DF0.y,"r-",label="DCCT_073_1")
ax.plot(DF1.x, DF1.y,"g.",label="DCCT_073_2")
ax.legend()
ax.figure.savefig(png_file)
csv2png("data2.csv","plot.png")
import os.path
def plt2png(plt_file):
fn,ext=os.path.splitext(plt_file)
csv_file=os.path.extsep.join((fn,"csv"))
plt2csv(plt_file, csv_file)
csv2png(csv_file, os.path.extsep.join((fn,"png")))
plt2png("./20211123T110243.036.plt")
このplt2png()
関数に与えるgnuplotスクリプトファイルの名前を変えるだけで、
グラフが.png
ファイルに作成されます。
pl2png()
関数の働き¶d=dict(a=1,b=2)
print(d["a"])
print(d.get("c","undefined"))
1 undefined
logging
モジュールではいくつかのクラスが定義され、それらが組み合わされてうごいています。
loggingモジュールを使ったメッセージの出力先は、 端末だけではなく、ファイル、syslogシステムなどにも出力可能です。 また、これらの出力先を同時に組み合わせることも可能です。 この機能は、loggingモジュールのロガー(Logger)、ハンドラー(handler), フォーマッタ(Formatter)クラスを使って実現されています。
デフォルトのロガーで記録するログのレベルを変更するには、次の慣用句を使います。
logging.get_Logger().setLevel(logging.INFO)
ロガーのログレベルは、loggingで定義されている定数を使って設定します。
loggingモジュールのメッセージ関数は、呼び出し時のログレベルが自分自身のレベル と同じかそれ以下である時に、実際にメッセージを出力します。
レベル 定数 | メッセージ関数 | 用途 |
---|---|---|
NOTSET | - | 全てのメッセージ |
DEBUG | debug() | デバッグのためにメッセージを出力 |
INFO | info() | プログラム実行に影響しない |
WARNING | warning() | プログラム実行は継続するが、異常につながる可能性がある |
ERROR | error() | プログラム実行中にエラーが発生。 |
CRITICAL | critical() / fatal() | 重大なエラー。プログラム実行を継続できない. |
try
文¶プログラム実行中に発生した例外(実行時エラーなど)が発生した場合、 適切な処理をおこなうことで、プログラムの実行を継続が可能な場合があります。 また、プログラムの実行継続が不能な場合には、エラーの原因などの情報と、それにより 「実行が不能になった」というメッセージをユーザに通知することは有用です。
このようなエラー(例外)発生時の対処法をプログラム中に書いておく時、try
文が使われます。
try
文の 概観は次のようになっています。
try:
...
except <Exception> [as <id>]:
...
except:
...
else: # 例外が発生しなかった場合の処理
...
finally: #例外発生 or not に関わらない、後始末処理
...
try
文の正確な定義は:
try_stmt ::= try1_stmt | try2_stmt
try1_stmt ::= "try" ":" suite
("except" [expression ["as" identifier]] ":" suite)+
["else" ":" suite]
["finally" ":" suite]
try2_stmt ::= "try" ":" suite
"finally" ":" suite
try-except-...
構文とtry-finally
構文。try:
の後にはexcept:
節かfinally:
節のどちらかが続く必要があるということ。
Pythonが発生する標準的な例外(組み込み例外)には次のようなものがあります
(一部のみを示します、全ての組み込み例外は help(__builtins__)
を実行して表示させます。)
一つの例外
はBaseException
を祖先にもつ、クラスです。
BaseException
Exception
ArithmeticError
FloatingPointError
OverflowError
ZeroDivisionError
AttributeError
EOFError
MemoryError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError
Warning
KeyboardInterrupt
ユーザー独自の例外
を定義することも可能です( 例外
を継承したクラスを定義します。)
Archive Appliance からデータを入手して、Pythonで処理する方法
- os, logging, os.path
- pandas, numpy, matplotlib
- matplotlib.pyplot
- matplotlib.dates
dict
型データの.get()
メソッドの使い方例外処理の基本(
try
文)
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(x=DF0.x,
y=DF0.y,
),
)
fig.add_trace(go.Scatter(x=DF1.x,
y=DF1.y)
)
#fig.add_trace(go.Scatter(y=[2, 1, 4, 3,])
# )
#fig.add_trace(go.Bar(y=[1, 4, 3, 2]))
fig.update_layout(title = 'Hello Figure')
fig.show()