csvから連続でグラフ作成しJpeg画像で保存するPythonプログラム

に公開

概要

フォルダ内のCSVファイルから簡易的なグラフ画像を連続で自動作成するPythonプログラムを作成した。
例として大学研究室(半導体デバイス系など)の測定機器(電気特性など)において、生データの簡易的なデータ確認の際での利用を想定している。

背景及び使用用途

半導体デバイス系の研究室において電気特性測定後のデータまとめに苦労することがあった。各測定ポイントごとの測定データがCSVファイル形式で出力され、手作業で波形グラフを作成しデータを確認する作業である。要求を満たす最適条件が確認できればそのデータをもとに詳細な分析は手作業で行うが、その最適データであるかの簡易的な判断の為にもファイル数ごとに手作業で確認しなくてはならず、単純作業で無駄な時間がかかる作業であった。
今回この簡易的な判断のためにCSVファイルからグラフ画像を作成するPythonプログラムを作成した。
詳細な分析ではなく簡易的なグラフ形状の確認を目的としている。



As is:測定機で測定後、手作業でCSVからグラフ作成しデータ確認。ファイル数ごとに手作業確認。
To be:プログラム実行で画像ファイルからデータ確認可能。ファイル数に制限なく自動で作成。

ソースコード

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import os
import glob
from pathlib import Path
import atexit

def cleanup(): #プログラム終了時の動作を定義
    print('PLEASE TYPE SOME KEY TO CLOSE')
    input() #exe化した際に一瞬で閉じないように
    
atexit.register(cleanup) #プログラム終了の際に強制的に↑で定義したclaenup関数を呼び出す。これで自動で画面が閉じない

#パスの取得
path = Path.cwd()
print("YOUR PATH IS", path, '\n')

#初期DF作成
df=pd.DataFrame()

#データ処理するファイルパスを表示
print('CSV FILE IN YOUR DIRECTORY IS BELOW')
for i in glob.glob('%s/*.csv'% path):
    filepath = os.path.split(i)[1] #split関数を使用してファイル名のみを表示
    print(filepath)

#ヘッダーを取得する関数
def header_func(DataFrame):
    for i in range(len(DataFrame.columns)): 
        header.append(DataFrame.columns[i])
        print('header%s is:'%[i], header[i])
        
#データクレンジング
def Datacleansing(DataFrame):
     for i in range(len(DataFrame.columns)): 
         DataFrame[header[i]]= pd.to_numeric(DataFrame[header[i]], errors = 'coerce')
    
#グラフ表示する関数
def ScatterPlot_withABS(DataFrame, DataFrame_abs, size = 1):
    fig, ax = plt.subplots(1,2, squeeze = False)
    
    #各列からLinerグラフを作成
    for i in range(1, len(DataFrame.columns)): 
        ax[0,0].scatter(x = DataFrame[header[0]], y = DataFrame[header[i]], label = DataFrame.columns[i], s = size)
    ax[0,0].set_xlabel('x axis')
    ax[0,0].set_ylabel('y axis')
    ax[0,0].legend()
    
    #各列からLog用のグラフを作成
    for i in range(1, len(DataFrame_abs.columns)): 
        ax[0,1].scatter(x = DataFrame[header[0]], y = DataFrame_abs[header[i]], label = DataFrame_abs.columns[i], s = size)#横軸x_axisはABSしないのでDataFrameの方を指定
    ax[0,1].set_xlabel('x axis')
    ax[0,1].set_ylabel('y axis log')
    ax[0,1].legend()
    
    #タイトルや画像サイズの指定
    fig.suptitle(filepath, fontname = 'MS Gothic')
    fig.set_size_inches(12,5)
    plt.yscale('log')
    
    #グラフを画像として保存
    save_file_name = os.path.splitext(filepath)[0] #ファイル保存用に拡張子部分(.csv)をsplit関数で選択しないように
    plt.savefig(str(save_file_name) + '.jpeg') #↑で.csvの部分を削除。そしてファイル名のあとに.jpegを追加。
    
    plt.close() 
    
#メイン処理
for i in glob.glob('%s/*.csv'% path):
    filepath = os.path.split(i)[1]
    df = pd.read_csv(i, header= 0)
    
    print(filepath, '\n')
    print('DF SHOW IN BELOW','\n',  df, '\n')

    #ヘッターを取得
    header = [] #ヘッダーを毎回初期化することでファイルごとにヘッダーがわかっても対応できるように
    print('GETTING HEADER')
    header_func(df)
    
    #データクレンジング実行 数値以外をNANに変更
    print('\n''DATA CLEANSING')
    Datacleansing(df)
    print('DATA CLEANSING OVER')
    
     #絶対値計算してdf_absに格納
    df_abs = df.abs()
    print('\n' 'DF_ABS SHOW IN BELOW ','\n', df_abs, '\n')
    
    #グラフを作成
    print("GENERATING PLOT", '\n')
    ScatterPlot_withABS(df, df_abs, size = 10)
    print('PLOT FINISH''\n''\n')
    
print('\n''COMPLETE!!!')

プログラムの使い方

上記ソースコードをPyinstallerでEXEファイル化し、EXEファイルをグラフ作成したいファイルが有るフォルダに入れてEXEファイル実行するのみ。
画面に「COMPLETE!!」と表示されたら全ての動作が完了。☓ボタンで閉じて構わない。

PyinstallerでのEXEファイル化は下記を参考にした。
https://youtu.be/dt7uuKztjjQ?si=xnGDIh8iyZCtgfVk

グラフ画像について①

左側に縦軸リニアでの表記、右に縦軸Log表記でのグラフを表示。横軸は両方ともリニアである。
Log表記では絶対値を取りマイナス値をプラスに変換した後に対数変換している。
汎用性を持たせるために軸の名前はx axis, y axisという表記にしている。利用者各自適当な名前に変更することを推奨する。

グラフ画像について②

ヘッダーはCSVファイルの一行目から取得。ヘッダー数に制限は無い。
1行目を横軸とし、縦軸にする2行目以降は何列でも読み込んでグラフに表示する仕様。

ソースコード内容

#プログラム終了時の動作を定義
    print('PLEASE TYPE SOME KEY TO CLOSE')
    input() #exe化した際に一瞬で閉じないように
    
atexit.register(cleanup) #プログラム終了の際に強制的に↑で定義したclaenup関数を呼び出す。これで自動で画面が閉じない

cleanup関数ではプログラム終了時の動作を定義している。Pyinstallerで.exeファイル化したPythonプログラムを実行すると画面が一瞬で閉じてしまうのでinput()で入力を受け付けるようにした。
atexit.registerはプログラム終了時に呼び出す関数を指定できる。この関数にcleanup関数を指定して終了時に呼び出すようにしている。

参考文献:
https://www.curict.com/item/fe/feb9886.html

path = Path.cwd()
print("YOUR PATH IS", path, '\n')

#初期DF作成
df=pd.DataFrame()

#データ処理するファイルパスを表示
print('CSV FILE IN YOUR DIRECTORY IS BELOW')
for i in glob.glob('%s/*.csv'% path):
    filepath = os.path.split(i)[1] #split関数を使用してファイル名のみを表示
    print(filepath)

パスの取得では実行している環境のカレントディレクトリ(フォルダ)を表示する。
ファイルパス表示ではこのプログラムが処理するcsvファイルを表示する。
「.csv」が含まれるファイルを検索して表示しており、また仕様上フルパスが表示されてしまうのでsplit関数を使用してフルパスではなくファイル名のみを表示するようにしている。
これらの関数はグラフ作成に必須ではないが、プログラムを使用するユーザーが後で確認できるようログとして残すために表示している。

#ヘッダーを取得する関数
def header_func(DataFrame):
    for i in range(len(DataFrame.columns)): 
        header.append(DataFrame.columns[i])
        print('header%s is:'%[i], header[i])
        
#データクレンジング
def Datacleansing(DataFrame):
     for i in range(len(DataFrame.columns)): 
         DataFrame[header[i]]= pd.to_numeric(DataFrame[header[i]], errors = 'coerce')

ヘッダーを取得する関数ではcsvファイルからグラフ表示に使用する要素を取得している。
一列目(header[0])を横軸にし、二列目(header[1])以降を縦軸にしている。

データクレンジングでは文字列を含んだ行(object)を強制的に数値(float64)に変換している。
本来全ての行が数値であることが望ましいがデータによっては数値しかないのに文字列扱いされている物がある。これを強制的に数値へと変換している。数値へと変換しないと後のグラフ作成の関数が動かないため、強制的に変換している。

参考文献:
https://qiita.com/darkqueenreal/items/70c5250a71830b1fa3b3

#グラフ表示する関数
def ScatterPlot_withABS(DataFrame, DataFrame_abs, size = 1):
    fig, ax = plt.subplots(1,2, squeeze = False)
    
    #各列からLinerグラフを作成
    for i in range(1, len(DataFrame.columns)): 
        ax[0,0].scatter(x = DataFrame[header[0]], y = DataFrame[header[i]], label = DataFrame.columns[i], s = size)
    ax[0,0].set_xlabel('x axis')
    ax[0,0].set_ylabel('y axis')
    ax[0,0].legend()
    
    #各列からLog用のグラフを作成 
    for i in range(1, len(DataFrame_abs.columns)): 
        ax[0,1].scatter(x = DataFrame[header[0]], y = DataFrame_abs[header[i]], label = DataFrame_abs.columns[i], s = size)#横軸x_axisはABSしないのでDataFrameの方を指定
    ax[0,1].set_xlabel('x axis')
    ax[0,1].set_ylabel('y axis log')
    ax[0,1].legend()
    
    #タイトルや画像サイズの指定
    fig.suptitle(filepath, fontname = 'MS Gothic')
    fig.set_size_inches(12,5)
    plt.yscale('log')
    
    #グラフを画像として保存
    save_file_name = os.path.splitext(filepath)[0] #ファイル保存用に拡張子部分(.csv)をsplit関数で選択しないように
    plt.savefig(str(save_file_name) + '.jpeg') #↑で.csvの部分を削除。そしてファイル名のあとに.jpegを追加。
    
    plt.close()

グラフ作成ではリニア軸とLog軸の2種作成するので引数にDataFrame, DataFrame_absの二種類用意した。ABSは絶対値のことで、マイナス値を絶対値変換した後にLog変換したものを扱う。なお絶対値変換はこの関数内での処理ではなく後の関数で行っている。
x軸はcsvファイルの一列目のheader[0]を指定し、y軸は二列目以降を指定している。何列でも読み込む仕様にするためイテレーターiを用いてheader[i]としている。
Log用のグラフ作成部分では読み込むy軸部分をDataFrame_absにしている。plt.yscale('log')でy軸をLogに変換してプロットする。
グラフ画像の保存ではファイル名から「.csv」をsplit関数を用いて削除し、「.jpeg」を追加して保存している。

参考文献:
https://qiita.com/takuma-1234/items/30b16386f89b3f15e392
https://qiita.com/skotaro/items/08dc0b8c5704c94eafb9
https://qiita.com/Masahiro_T/items/bdd0482a8efd84cdd270
https://qiita.com/KntKnk0328/items/5ef40d9e77308dd0d0a4
https://qiita.com/supersaiakujin/items/543053ca4610437112df
https://qiita.com/irs/items/cd1556c568887ff2bdd7

#メイン処理
for i in glob.glob('%s/*.csv'% path):
    filepath = os.path.split(i)[1]
    df = pd.read_csv(i, header= 0)
    
    print(filepath, '\n')
    print('DF SHOW IN BELOW','\n',  df, '\n')

    #ヘッターを取得
    header = [] #ヘッダーを毎回初期化することでファイルごとにヘッダーがわかっても対応できるように
    print('GETTING HEADER')
    header_func(df)
    
    #データクレンジング実行 数値以外をNANに変更
    print('\n''DATA CLEANSING')
    Datacleansing(df)
    print('DATA CLEANSING OVER')
    
     #絶対値計算してdf_absに格納
    df_abs = df.abs()
    print('\n' 'DF_ABS SHOW IN BELOW ','\n', df_abs, '\n')
    
    #グラフを作成
    print("GENERATING PLOT", '\n')
    ScatterPlot_withABS(df, df_abs, size = 10)
    print('PLOT FINISH''\n''\n')
    
print('\n''COMPLETE!!!')

glob.globでcsvファイルを読み出し、一つ一つのファイルの内容を表示している。
またログのためにグラフ作成に用いるヘッダーを表示。
データクレンジングを実行し、その際に実行されたかどうか分かるようPrint分で実行前・実行後に都度表記するようにした。
絶対値計算ではLog表記ようにabs計算したものをdf_absにかくのぷしている。また正しく処理されたかが分かるように絶対値処理したデータフレームを表示するようにしている。
グラフ作成では上記で定義したScatterPlot_withABSを呼び出してグラフ作成をしている。
全ての処理が終わればCOMPLETE!!!と表示するようにしている。
ここまで来た際に最初に定義したatexit.register(cleanup)が呼び出される。

Python Version

本プログラムは下記の環境で作成。EXEファイル化の際は「pyinstaller --onefile」で変換し動作確認した。

Python: 3.12.5
pandas version: 2.2.2
matplotlib version: 3.10.0
numpy version: 2.0.1
Pyinstaller: 6.12.0

所感

本プログラムを作成するにあたり最近では使用するのが当たり前となったChatGPTやGeminiといったAIツールは使用していない。自力で全てコーディングしている。
(グラフ画像は参考のために適当にGeminiに出力させたものです。)
また作者はプログラミングを行う研究・業務ではないため、普通では無い書き方をしている部分もあるかもしれない。
今振り返ればメイン処理はPythonでは一般的な書き方とされている__init__.pyを用いるべきであると考えている。
AIを用いていないためかなりの時間と労力を消費したが、行き詰まった際の自力での解決・ソースコード全ての理解といった点では良い経験を積むことができた。
課題として上記に上げた__init__.pyの仕様やClass定義の使用などが出来ていない部分があるが、今後理解を深めていきたい。

使用にあたって

同じような問題を抱えている研究生がCSVデータのグラフ波形の簡易確認の為に本コードが参考になれば幸いです。
本プログラムを利用した際に発生した全トラブルにおいて、作者は一切の責任を負いかねます。

Discussion