Python 入門講座 第8回: 未来のために、過去を振り返る。¶

今日の目標

Archive Appliance データのweb Viewerからデータを入手してグラフを作成します。

前回に続き、pandas, matplotlib モジュールなどを使います。

logging, os, subprocess などのモジュールも使います。

In [1]:
import os, logging, os.path, subprocess
import pandas, numpy, matplotlib
import matplotlib.pyplot as pyplot
import matplotlib.dates as mdates

「Archiver Appliance WEB閲覧」からデータを入手する。¶

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コマンドを引数にわたすことができます。

ここでは、

  1. loadコマンドを使って「Archiver Appliance WEB閲覧」からダウンロードした20211123T110243.036.pltを読み込んだ後、
  2. 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ファイルを生成する。¶

ダウンロードしたgnuplot スクリプトファイルからcsvファイルを生成する手続きを、 pythonから実行してみます。

subprocessモジュールのrun関数に実行するプログラム名とその引数をリストとしてあたえます。

In [2]:
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")
Out[2]:
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プログラム中からシェルプログラムなどを実行する。¶

python中から、シェルコマンドなど他のプログラムを実行することができます。

  1. subprocessモジュールの runコマンド
  2. osモジュールのsystemコマンド(subprocess.run( <cmd string>, shell=True)で置き換え可能)
In [3]:
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)
Out[3]:
CompletedProcess(args='gnuplot -e "load \\"20211123T110243.036.plt\\";set table \\"data.csv\\";replot ;unset table"', returncode=0)
In [4]:
import shlex
shlex.split(gpl_cmd)
Out[4]:
['gnuplot',
 '-e',
 'load "20211123T110243.036.plt";set table "data.csv";replot ;unset table']

例外(Exception)に備える。¶

このプログラムでは、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 用語集より¶

Python: EAFP「認可をとるより許しを請う方が容易 (easier to ask for forgiveness than permission、マーフィーの法則)」

C/C++: LBYL「ころばぬ先の杖 (look before you leap)」

例外を使わない方法(LBYL)も。¶

1) ディレクトリが存在するかどうか確認して(os.path.exists), 2) 存在すればメッセージを出力, 3) さもなければ、ディレクトリを作成する(os.mkdir)

というアプローチも, もちろん可能です。

In [5]:
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モジュールの設定を変えることで、メッセージを端末に出力するだけでなく、ファイル、データベース、ログサーバーなどに記録するようにもできます。

In [6]:
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変数を使って、 読み書きすることができます。

In [7]:
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
In [8]:
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"に設定されています。

In [9]:
%%sh
PYTHON_LOG_LEVEL=INFO python3 -c \
 "import os;print(os.environ[\"PYTHON_LOG_LEVEL\"])"
INFO
In [10]:
%%sh
PYTHON_LOG_LEVEL=DEBUG python3 -c \
 "import os;print(os.environ[\"PYTHON_LOG_LEVEL\"])"
DEBUG

CSVファイルの中身をpythonプログラムで読み込む。¶

出来上がった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

という様に、チャンネル毎のデータが空白行とコメント行(#)で区切られて記録されていることがわかります。

pandas.dataframeに読み込む¶

また各チャンネルのデータは一行毎にx, y, yhigh, ylow, typeの値が空白(\s)で区切られて記録されています。 データ数が981であることもわかります。このファイルをpandas.read_csv()関数で読み込んでみます。

各行の最初の項はデータの時刻ですから、日付/時刻として読み取ってやることが必要です。 最初のチャンネルはコメントを四行読み飛ばした後から、981行続いています。 次のチャンネルのデータは、その先さらに空行とコメントをあわせて五行読みとばした後から、981行続いています。

各列のラベルはx, y, ylow, yhigh, typeと設定します。(pandas.csv_readは各列のラベルをファイルから読み込むことも できますが、この例の場合行頭の'#'がじゃまになるので、手動で設定しています。)

この例の場合行頭の'#'がじゃまになる

#が最初のラベルとして認識されてしまうため、ラベルがひとつずれてしまいます。

In [11]:
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']
                  )

読み込んだDataframeの中身を確認してみます。¶

In [12]:
DF0.loc[:5]
Out[12]:
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
In [13]:
DF1.loc[:5]
Out[13]:
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

Dataframeからグラフをプロットして見よう¶

得られた二つのデータフレームを一つのグラフに表示してみます。

最初のplotの戻り値(axes)を,次のplotのaxにわたすことで、一つ以上データを同一のグラフに表示します。

In [14]:
ax=DF0.plot("x","y",figsize=(12,6.75),style="r.")
DF1.plot("x","y", xlabel="t", ylabel="PWR", ax=ax ,style="g-")
Out[14]:
<AxesSubplot:xlabel='t', ylabel='PWR'>

こんどはmatplotlibを使ってグラフを作成¶

matplotlib.pyplot を使って、データをグラフ化してみます。

In [15]:
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の調整を行います。

In [16]:
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();

軸ラベルの調整(その2)¶

In [17]:
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();

エラーバーの表示¶

In [18]:
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ファイル名が与えられた時に,

  • pltファイルからcsvファイルを作成する関数
  • csvファイルからグラフとpngファイルを作成する関数
  • これら二つの関数を組み合わせて、pltファイルからグラフとpngファイルを作成する関数

を作ってみます。

pltファイルからcsvファイルを作成する関数¶

In [20]:
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")

csvファイルからグラフとpngファイルを作成する関数¶

In [21]:
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")

二つの関数を組み合わせて、グラフと.pngファイルを作成する関数¶

In [22]:
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()関数の働き¶

本日作成した`pl2png()`関数の働き

今回紹介したPythonの文法要素¶

dict.getメソッド¶

辞書型データのdのキーkの値は、

v=d[k]

で取り出すことができます。しかし、辞書型データdがこのキーkを含まない場合には、エラー(KeyError)となってしまいます。 このような時に、.getメソッドを使い、

v=d.get(k, "undefined")

とすることで、dがキーkをもっている時にはd[k]そうでないときには"undefined"が変数vに割り当てられます。

In [23]:
d=dict(a=1,b=2)
print(d["a"])
print(d.get("c","undefined"))
1
undefined

logging モジュール¶

loggingモジュールではいくつかのクラスが定義され、それらが組み合わされてうごいています。

  • ロガーは、アプリケーションコードが直接使うインターフェースを公開します。
  • ハンドラは、(ロガーによって生成された) ログ記録を適切な送信先に送ります。
  • フィルタは、どのログ記録を出力するかを決定する、きめ細かい機能を提供します。
  • フォーマッタは、ログ記録が最終的に出力されるレイアウトを指定します。

ロガー(Logger)¶

loggingモジュールを使ったメッセージの出力先は、 端末だけではなく、ファイル、syslogシステムなどにも出力可能です。 また、これらの出力先を同時に組み合わせることも可能です。 この機能は、loggingモジュールのロガー(Logger)、ハンドラー(handler), フォーマッタ(Formatter)クラスを使って実現されています。

デフォルトのロガーで記録するログのレベルを変更するには、次の慣用句を使います。

logging.get_Logger().setLevel(logging.INFO)

loglevelとlogging 関数¶

ロガーのログレベルは、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文)