実験データの解析グラフ化をPythonで全自動化してみた(ORR測定編)

記事
IT・テクノロジー
はじめまして!普段は大学で実験データの山と格闘している理系学生です。今日は、私のポートフォリオ(実績)として、実際に研究室で作成し、「これは便利!」とラボメンバーからも好評だったデータ解析ツールをご紹介したいと思います。「Pythonを使えばこんなことができるんだ」「自分の研究データの整理も頼んでみようかな」そんなふうに思っていただけたら嬉しいです。

開発のきっかけ:単純作業からの解放私の研究分野では、酸素還元反応(ORR)という測定をよく行います。この実験、データ解析が結構大変なんです…。測定ごとのテキストファイルをExcelにまとめる参照電極の電位を補正して、RHE(可逆水素電極)基準に直す電流値を電極面積で割って密度にする回転数ごとにグラフを描くTafelプロットやKoutecky-Levich (K-L) プロットを作成して、電子移動数を計算する…これを毎回手作業でやっていると、日が暮れてしまいます。そこで、「フォルダにデータを入れるだけで、論文に使えるレベルのExcel解析シートが出るプログラム」を自作しました。

作成したプログラムの機能今回公開するのは、以下の2つのツールです。
① ORR_Converter(データ整理係)測定装置から出てくるバラバラのテキストファイル(CV, LSV)を読み込み、自動で以下の処理を行います。ファイル名から回転数(rpm)を自動認識してソート測定条件(pH, 参照極電位, 電極面積)を入力するだけで、全データをRHE基準に自動変換1つのExcelファイルに見やすく集約
② ORR_Analyzer(高度解析係)整理されたデータを読み込み、物理定数に基づいた本格的な解析を行います。
ここがこだわり!:0.1 M KOH溶液の粘度や酸素溶解度など、文献値に基づいた厳密な物理定数を自動計算(温度補正付き)。
直感的な操作:Tafelプロットの直線近似範囲を、グラフを見ながらクリックで選択できます(ここ、一番苦労しました!)。
自動グラフ化:K-Lプロット、自由電子数の算出まで全自動でグラフ化してExcelに出力します。
実際に使用しているAnalyze処理を示します。物理定数の計算など、化学的な正確さには特にこだわっています。

import pandas as pd
import os
import tkinter as tk
from tkinter import filedialog
import numpy as np
from datetime import datetime
from xlsxwriter.utility import xl_rowcol_to_cell, xl_col_to_name
import sys
import math
import matplotlib.pyplot as plt
# --- 基準定数 ---
CONST_F = 96485 # C/mol
# 解析電圧デフォルト
DEFAULT_TARGET_POTENTIALS = [0.3, 0.4, 0.5, 0.6, 0.7]
# グラフ配色
CHART_COLORS = ['#4472C4', '#ED7D31', '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47', '#264478', '#9E480E']
# フォント設定
plt.rcParams['font.family'] = 'sans-serif'
import matplotlib.font_manager as fm
font_names = [f.name for f in fm.fontManager.ttflist]
if 'Meiryo' in font_names: plt.rcParams['font.family'] = 'Meiryo'
elif 'Yu Gothic' in font_names: plt.rcParams['font.family'] = 'Yu Gothic'
elif 'MS Gothic' in font_names: plt.rcParams['font.family'] = 'MS Gothic'
def calculate_phys_params(temp_c):
    """
    0.1 M KOH 用の物理定数計算 
    方針:
      C0: 純水の値 (Battino et al.) - 塩析効果無視
      D : 純水の値 (Han & Bartels) × 粘度補正 (Stokes-Einstein)
      v : 純水の値 (CRC Handbook) - 0.1 M KOHの粘度上昇は無視 (純水近似が最も安全)
    """
    # 1. 酸素溶解度 C0 (mol/cm3) - 純水
    temps_ref = [0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60]
    c0_ref = [
        2.18e-6, 1.91e-6, 1.70e-6, 1.52e-6, 1.38e-6,
        1.26e-6, 1.16e-6, 1.09e-6, 1.03e-6, 0.92e-6, 0.83e-6
    ]
    C0 = np.interp(temp_c, temps_ref, c0_ref)
    # 2. 水の粘度 (Vogel Eq) [mPa s]
    mu_water = np.exp(-3.7188 + 578.919 / (temp_c + 137.546))
    # 3. 動粘度 v [cm2/s] - 純水近似を採用
    # 密度 rho_water approx 1.0 g/cm3
    # v = mu / rho. ここでは純水の値をそのまま使う
    # mu (mPa s) = 0.01 * mu (Poise)
    # v (cm2/s) = (mu_water * 0.01) / 1.0
    v = mu_water * 0.01
    # 4. 拡散係数 D [cm2/s] - 0.1 M KOH補正あり
    # D_pure_water (Han & Bartels)
    Tk = temp_c + 273.15
    # D_water(T) = A * (T/mu_water) ... ではなく文献値ベースで計算
    # Han & Bartels近似: D = 0.069 * exp(-19600/RT) ... 簡易的にStokes-Einstein scalingを使う
    # D_water(25C) = 2.1e-5 (Han data is slightly higher than 1.9e-5 commonly used in electrochem)
    # ここでは電気化学で慣習的な 1.90e-5 @ 25C を基準に温度スケーリング
    mu_25 = np.exp(-3.7188 + 578.919 / (25 + 137.546))
    D_water = 1.90e-5 * (Tk / 298.15) * (mu_25 / mu_water)
    # 0.1 M KOH 補正: 粘度が純水よりわずかに高いため、Dは少し下がる (約95%程度)
    # ここでは安全側に倒して「純水の値 × 0.95」を採用するか、厳密に粘度比で割るか。
    # 指示通り「今のExcelに入っている補正(係数)」を採用 = 純水の粘度比でスケーリングした上で、
    # 0.1 M KOHの影響係数 (約0.95) を掛けるのがベスト。
    D = D_water * 0.95
    return C0, D, v
def get_range_interactive(x_data, y_data, title, xlabel, ylabel):
    if len(x_data) < 2: return None, None
    fig, ax = plt.subplots(figsize=(9, 6))
    ax.scatter(x_data, y_data, s=15, c='blue', alpha=0.6, label='Data')
    ax.set_xlabel(xlabel, fontsize=12)
    ax.set_ylabel(ylabel, fontsize=12)
    ax.set_title(f"【{title}】\n近似直線の範囲(始点・終点)をクリック\n(ウィンドウを閉じるとスキップ)", fontsize=12)
    ax.grid(True, linestyle='--', alpha=0.6)
    clicked = []
    def onclick(event):
        if event.xdata and event.ydata:
            clicked.append(event.xdata)
            ax.axvline(x=event.xdata, color='red', ls='--', alpha=0.8)
            fig.canvas.draw()
            if len(clicked) >= 2:
                plt.pause(0.3); plt.close(fig)
    fig.canvas.mpl_connect('button_press_event', onclick)
    try:
        fig.canvas.manager.window.attributes('-topmost', 1)
        fig.canvas.manager.window.attributes('-topmost', 0)
    except: pass
    plt.show()
    return (min(clicked), max(clicked)) if len(clicked) >= 2 else (None, None)
def select_excel_file():
    root = tk.Tk(); root.withdraw()
    return filedialog.askopenfilename(title="Excelファイルを選択", filetypes=[("Excel Files", "*.xlsx")])
def run_excel_maker_v22():
    print("=== ORR Excel解析作成ツール  (0.1 M KOH 最適化版) ===")
    print(" -> 定数計算ロジックを更新:")
    print(" C0: 純水の値 (Battino)")
    print(" D : 純水×0.95 (Han & Bartels + 粘度補正)")
    print(" v : 純水の値 (CRC Handbook) - 過剰補正を排除")
    print(" -> これにより、信頼性の高いn数が算出されます")
    input_file = select_excel_file()
    if not input_file: return
    # --- ユーザー入力 ---
    print("\n【設定 1/2】 解析電圧(V vs RHE)")
    pot_in = input(f" > 入力 [Enterでデフォルト {DEFAULT_TARGET_POTENTIALS}]: ")
    try: target_potentials = [float(x) for x in pot_in.replace('、', ',').split(',')] if pot_in.strip() else DEFAULT_TARGET_POTENTIALS
    except: target_potentials = DEFAULT_TARGET_POTENTIALS
    print("\n【設定 2/2】 温度(℃)の初期値")
    temp_in = input(" > 入力 [Enterでデフォルト 25]: ")
    try: temp_c = float(temp_in)
    except: temp_c = 25.0
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_file = f"ORR_Analysis_v22_{timestamp}.xlsx"
    try:
        with open(output_file, 'a'): pass
    except PermissionError:
        print(f"エラー: {output_file} を閉じてください。"); return
    except: pass
    try: xls = pd.ExcelFile(input_file)
    except: print("ファイル読み込みエラー"); return
    data_map = {}
    global_area, global_ph = None, None
    for sheet in xls.sheet_names:
        df_meta = pd.read_excel(input_file, sheet_name=sheet, header=None, nrows=10)
        p_dict = {}
        try:
            for i, r in df_meta.iterrows(): p_dict[str(r[0])] = r[1]
        except: pass
        gas = str(p_dict.get('Gas', '')).upper()
        try: rpm = int(p_dict.get('RPM', 0))
        except: rpm = 0
        if p_dict.get('Area (cm2)'): global_area = float(p_dict['Area (cm2)'])
        if p_dict.get('Input pH'): global_ph = float(p_dict['Input pH'])
        if rpm > 0:
            df = pd.read_excel(input_file, sheet_name=sheet)
            cols = df.columns
            rhe = [c for c in cols if "RHE" in c]
            cur = [c for c in cols if "Current(A)" in c]
            if rhe and cur:
                cdf = df[[rhe[0], cur[0]]].dropna().astype(float)
                cdf.columns = ['V', 'I_raw']
                if rpm not in data_map: data_map[rpm] = {}
                if "O2" in gas: data_map[rpm]['O2'] = cdf
                elif "N2" in gas: data_map[rpm]['N2'] = cdf
    if not data_map: print("データなし"); return
    print(f"\n【重要確認】電極面積: {global_area} cm2 で合っていますか?")
    area_in = input(" > 変更する場合は数値を入力 (Enterでそのまま): ")
    if area_in.strip(): global_area = float(area_in)
    rpm_list = sorted([k for k in data_map.keys() if 'O2' in data_map[k]], reverse=True)
    # --- Tafel範囲選択 ---
    print("\n=== Tafelプロット範囲選択 ===")
    tafel_ranges = {}
    tafel_results_calc = {}
    for rpm in rpm_list:
        d_o2 = data_map[rpm]['O2']
        if 'N2' in data_map[rpm]:
            n2_i = np.interp(d_o2['V'], data_map[rpm]['N2']['V'][::-1], data_map[rpm]['N2']['I_raw'][::-1])
            net = d_o2['I_raw'] - n2_i
        else: net = d_o2['I_raw']
        j_mA = (net / global_area) * 1000
        mask = np.abs(j_mA) > 0.0001
        if sum(mask) > 5:
            l_j = np.log10(np.abs(j_mA[mask])).values
            eta = (1.23 - d_o2['V'][mask]).values
            x_min, x_max = get_range_interactive(l_j, eta, f"{rpm} rpm", "Log |j|", "Overpotential")
            tafel_ranges[rpm] = (x_min, x_max)
            if x_min is not None:
                calc_mask = (l_j >= x_min) & (l_j <= x_max)
                if calc_mask.sum() > 1:
                    x_fit = l_j[calc_mask]
                    y_fit = eta[calc_mask]
                    coeffs = np.polyfit(x_fit, y_fit, 1)
                    slope = coeffs[0]
                    intercept = coeffs[1]
                    p = np.poly1d(coeffs)
                    yhat = p(x_fit)
                    ybar = np.mean(y_fit)
                    ssreg = np.sum((yhat - ybar)**2)
                    sstot = np.sum((y_fit - ybar)**2)
                    r2 = ssreg / sstot if sstot != 0 else 0
                    tafel_results_calc[rpm] = {
                        'slope_mv': slope * 1000,
                        'intercept': intercept,
                        'r2': r2
                    }
                else:
                    tafel_results_calc[rpm] = None
            else:
                tafel_results_calc[rpm] = None
        else:
            tafel_ranges[rpm] = (None, None)
            tafel_results_calc[rpm] = None
    # ================= Excel Writing =================
    with pd.ExcelWriter(output_file, engine='xlsxwriter') as writer:
        wb = writer.book
        fmt_head = wb.add_format({'bold': True, 'bg_color': '#DDEBF7', 'border': 1, 'align': 'center'})
        fmt_num = wb.add_format({'num_format': '0.000'})
        fmt_sci = wb.add_format({'num_format': '0.00E+00'})
        fmt_calc = wb.add_format({'bg_color': '#FFF2CC', 'border': 1})
        fmt_lock = wb.add_format({'bg_color': '#F2F2F2', 'border': 1, 'font_color': 'gray'})
        fmt_source = wb.add_format({'italic': True, 'font_color': 'blue', 'text_wrap': True, 'valign': 'top'})
        # --- Sheet 0: Reference Data ---
        ws_ref = wb.add_worksheet("Reference_Data")
        ws_ref.write_row(0, 0, ["Temp (C)", "C0 (mol/cm3)", "D (cm2/s)", "v (cm2/s)"], fmt_head)
        ws_ref.set_column(0, 3, 15)
        for t in range(1, 61):
            c0_val, d_val, v_val = calculate_phys_params(t)
            ws_ref.write(t, 0, t)
            ws_ref.write(t, 1, c0_val, fmt_sci)
            ws_ref.write(t, 2, d_val, fmt_sci)
            ws_ref.write(t, 3, v_val, fmt_num)
        ws_ref.set_column(5, 5, 85)
        source_text = (
            "【Data Sources & Calculation Basis (for 0.1 M KOH)】\n"
            "本プログラムは 0.1 M KOH (または希薄水溶液) を想定しています。\n"
            "以下の推奨値設定に基づき計算されています。\n\n"
            "1. Oxygen Solubility (C0):\n"
            " Value used: Values for pure water (Battino et al., 1983).\n"
            " Reason: Salting-out effect is negligible (>99% of pure water).\n\n"
            "2. Diffusion Coefficient (D):\n"
            " Value used: Pure water value (Han & Bartels, 1996) * 0.95.\n"
            " Reason: Correction for viscosity increase in 0.1 M KOH (approx. 95% of pure water).\n\n"
            "3. Kinematic Viscosity (ν):\n"
            " Value used: Pure water value (CRC Handbook).\n"
            " Reason: Viscosity increase is minimal (1.01-1.02x). Using pure water value is the safest approximation."
        )
        ws_ref.merge_range('F2:F20', source_text, fmt_source)
        # --- Sheet 1: Parameters ---
        ws_p = wb.add_worksheet("Parameters")
        ws_p.set_column(0, 0, 30); ws_p.set_column(1, 1, 15)
        ws_p.write_row(0,0,["Parameter","Value","Unit"], fmt_head)
        vals = [
            ("Temperature (T)", temp_c, "°C", "B2"),
            ("Electrode Area", global_area, "cm2", "B3"),
            ("pH", global_ph, "-", "B4"),
            ("Faraday Const (F)", CONST_F, "C/mol", "B5")
        ]
        for i, (name, val, unit, cell) in enumerate(vals, 1):
            ws_p.write(i, 0, name)
            ws_p.write(i, 1, val, fmt_calc if i<3 else None)
            ws_p.write(i, 2, unit)
        ws_p.write(6, 0, "O2 Solubility (C0)")
        ws_p.write_formula(6, 1, f"=VLOOKUP(B2, Reference_Data!$A$2:$D$61, 2, FALSE)", fmt_sci)
        ws_p.write(6, 2, "mol/cm3 [Pure Water]", fmt_lock)
        ws_p.write(7, 0, "Diffusion Coeff (D)")
        ws_p.write_formula(7, 1, f"=VLOOKUP(B2, Reference_Data!$A$2:$D$61, 3, FALSE)", fmt_sci)
        ws_p.write(7, 2, "cm2/s [Pure Water * 0.95]", fmt_lock)
        ws_p.write(8, 0, "Kinematic Viscosity (v)")
        ws_p.write_formula(8, 1, f"=VLOOKUP(B2, Reference_Data!$A$2:$D$61, 4, FALSE)", fmt_num)
        ws_p.write(8, 2, "cm2/s [Pure Water]", fmt_lock)
        ws_p.write(10, 0, "Theoretical B (n=4)")
        ws_p.write_formula(10, 1, f"=0.62*4*B5*B7*(B8^(2/3))*(B9^(-1/6))", fmt_sci)
        ws_p.write(10, 2, "A/(cm2 rad^0.5)", fmt_lock)
        ws_p.write(12, 0, "NOTE:", fmt_head)
        ws_p.write(12, 1, "Values are optimized for 0.1 M KOH. See Reference_Data sheet for details.", fmt_source)
        ref_B = "Parameters!$B$11"
        ref_A = "Parameters!$B$3"
        # --- Sheet 2: Data Processing ---
        ws_d = wb.add_worksheet("Data_Processing")
        ws_d.write(0,0, "★計算シート", wb.add_format({'bold':True, 'font_color':'red'}))
        curr_c = 0; kl_ref = {}; tafel_info = {}
        for rpm in rpm_list:
            df = data_map[rpm]['O2']
            n2 = data_map[rpm].get('N2')
            if n2 is not None:
                n2_val = np.interp(df['V'], n2['V'][::-1], n2['I_raw'][::-1])
            else: n2_val = np.zeros(len(df))
            headers = [f"V (RHE)", "O2_Raw", "N2_Raw", "Net_A", "mA/cm2", "Log|J|", "η(V)", "Fit_X", "Fit_Y"]
            ws_d.merge_range(1, curr_c, 1, curr_c+8, f"{rpm} rpm", fmt_head)
            ws_d.write_row(2, curr_c, headers, fmt_head)
            ws_d.set_column(curr_c, curr_c+8, 15)
            x_min, x_max = tafel_ranges[rpm]
            r_len = len(df)
            for i in range(r_len):
                r = 3 + i
                ws_d.write(r, curr_c, df['V'].iloc[i], fmt_num)
                ws_d.write(r, curr_c+1, df['I_raw'].iloc[i], fmt_sci)
                ws_d.write(r, curr_c+2, n2_val[i], fmt_sci)
                cV, cO, cN = [xl_rowcol_to_cell(r, curr_c+k) for k in range(3)]
                cNet, cDen = [xl_rowcol_to_cell(r, curr_c+k) for k in [3, 4]]
                cLog = xl_rowcol_to_cell(r, curr_c+5)
                ws_d.write_formula(r, curr_c+3, f"={cO}-{cN}", fmt_sci)
                ws_d.write_formula(r, curr_c+4, f"=({cNet}/{ref_A})*1000", fmt_num)
                ws_d.write_formula(r, curr_c+5, f"=IF(ABS({cDen})>1E-4, LOG10(ABS({cDen})), NA())", fmt_num)
                ws_d.write_formula(r, curr_c+6, f"=1.23-{cV}", fmt_num)
                if x_min is not None:
                    cond = f"AND({cLog}>={x_min:.4f}, {cLog}<={x_max:.4f})"
                    ws_d.write_formula(r, curr_c+7, f"=IF({cond}, {cLog}, NA())", fmt_num)
                    ws_d.write_formula(r, curr_c+8, f"=IF({cond}, 1.23-{cV}, NA())", fmt_num)
                else:
                    ws_d.write(r, curr_c+7, "#N/A"); ws_d.write(r, curr_c+8, "#N/A")
            cols = [xl_col_to_name(curr_c+k) for k in range(9)]
            kl_ref[rpm] = {'v': cols[0], 'd': cols[4], 'len': r_len}
            tafel_info[rpm] = {'log': cols[5], 'eta': cols[6], 'fx': cols[7], 'fy': cols[8], 'len': r_len}
            curr_c += 10
        # --- Sheet 3: Summary (LSV) ---
        ws_sum = wb.add_worksheet("Summary_Chart")
        ch_lsv = wb.add_chart({'type': 'scatter', 'subtype': 'smooth'})
        for i, rpm in enumerate(rpm_list):
            ref = kl_ref[rpm]
            ch_lsv.add_series({
                'name': f'{rpm} rpm',
                'categories': f"=Data_Processing!${ref['v']}$4:${ref['v']}${3+ref['len']}",
                'values': f"=Data_Processing!${ref['d']}$4:${ref['d']}${3+ref['len']}",
                'line': {'width': 1.5, 'color': CHART_COLORS[i%8]},
                'marker': {'type': 'none'}
            })
        ch_lsv.set_plotarea({'border': {'color': 'black', 'width': 1}})
        ch_lsv.set_x_axis({'name': 'Potential (V vs RHE)', 'min': 0, 'max': 1, 'line': {'color': 'black'}, 'label_position': 'low', 'crossing': 'min', 'major_gridlines': {'visible': False}})
        ch_lsv.set_y_axis({'name': 'Current Density (mA cm⁻²)', 'line': {'color': 'black'}, 'label_position': 'low', 'crossing': 'min', 'major_gridlines': {'visible': False}})
        ch_lsv.set_legend({'position': 'right'})
        ws_sum.insert_chart('B2', ch_lsv)
        # --- Sheet 4: Tafel Plots ---
        ws_tf = wb.add_worksheet("Tafel_Plots")
        ws_tf.set_column(16, 18, 15)
        r_pos = 1
        for rpm in rpm_list:
            inf = tafel_info[rpm]
            rlen = inf['len']
            cat_all = f"=Data_Processing!${inf['log']}$4:${inf['log']}${3+rlen}"
            val_all = f"=Data_Processing!${inf['eta']}$4:${inf['eta']}${3+rlen}"
            cat_fit = f"=Data_Processing!${inf['fx']}$4:${inf['fx']}${3+rlen}"
            val_fit = f"=Data_Processing!${inf['fy']}$4:${inf['fy']}${3+rlen}"
            # Chart 1: Fitあり
            ch1 = wb.add_chart({'type': 'scatter'})
            ch1.add_series({'name': f'{rpm} rpm', 'categories': cat_all, 'values': val_all,
                            'marker': {'type': 'circle', 'size': 3, 'fill': {'color':'black'}, 'border':{'color':'black'}},
                            'line': {'none': True}})
            if tafel_ranges[rpm][0] is not None:
                ch1.add_series({'name': 'Fit', 'categories': cat_fit, 'values': val_fit,
                                'marker': {'type': 'none'}, 'line': {'none': True},
                                'trendline': {'type': 'linear', 'line': {'color': 'red', 'dash_type': 'long_dash'},
                                              'display_equation': True, 'display_r_squared': True}}) # 式表示ON
            ch1.set_title({'name': f'{rpm} rpm (With Fit)'})
            ch1.set_plotarea({'border': {'color': 'black', 'width': 1}})
            ch1.set_x_axis({'name': 'Log |j|', 'line': {'color': 'black'}, 'label_position': 'low', 'crossing': 'min', 'major_gridlines': {'visible': False}})
            ch1.set_y_axis({'name': 'Overpotential (V)', 'line': {'color': 'black'}, 'label_position': 'low', 'crossing': 'min', 'major_gridlines': {'visible': False}})
            ws_tf.insert_chart(f'A{r_pos}', ch1)
            # Chart 2: Rawのみ
            ch2 = wb.add_chart({'type': 'scatter'})
            ch2.add_series({'name': f'{rpm} rpm', 'categories': cat_all, 'values': val_all,
                            'marker': {'type': 'circle', 'size': 3, 'fill': {'color':'black'}, 'border':{'color':'black'}},
                            'line': {'none': True}})
            ch2.set_title({'name': f'{rpm} rpm (Raw)'})
            ch2.set_plotarea({'border': {'color': 'black', 'width': 1}})
            ch2.set_x_axis({'name': 'Log |j|', 'line': {'color': 'black'}, 'label_position': 'low', 'crossing': 'min', 'major_gridlines': {'visible': False}})
            ch2.set_y_axis({'name': 'Overpotential (V)', 'line': {'color': 'black'}, 'label_position': 'low', 'crossing': 'min', 'major_gridlines': {'visible': False}})
            ws_tf.insert_chart(f'I{r_pos}', ch2)
            if tafel_results_calc[rpm] is not None:
                res = tafel_results_calc[rpm]
                ws_tf.write(r_pos-1, 16, f"Results ({rpm}rpm)", fmt_head)
                ws_tf.write(r_pos, 16, "Tafel Slope")
                ws_tf.write(r_pos, 17, res['slope_mv'], fmt_num)
                ws_tf.write(r_pos, 18, "mV/dec")
                ws_tf.write(r_pos+1, 16, "Intercept")
                ws_tf.write(r_pos+1, 17, res['intercept'], fmt_num)
                ws_tf.write(r_pos+1, 18, "V")
                ws_tf.write(r_pos+2, 16, "R²")
                ws_tf.write(r_pos+2, 17, res['r2'], fmt_num)
            r_pos += 16
        # --- Sheet 5: KL Analysis ---
        ws_kl = wb.add_worksheet("KL_Analysis")
        ws_kl.set_column(0, 20, 15)
        ws_kl.write_row(2,0, ["RPM", "ω", "ω^-0.5"] + [x for p in target_potentials for x in ["J", "1/J"]], fmt_head)
        r_idx = 3; n_rpm = len(rpm_list)
        ws_kl.write(0,0,"Voltage:", fmt_head)
        for i,p in enumerate(target_potentials): ws_kl.write(0, 4+i*2, p, fmt_calc)
        for rpm in rpm_list:
            ws_kl.write(r_idx, 0, rpm)
            ws_kl.write_formula(r_idx, 1, f"=2*PI()*A{r_idx+1}/60", fmt_num)
            ws_kl.write_formula(r_idx, 2, f"=B{r_idx+1}^(-0.5)", fmt_num)
            ref = kl_ref[rpm]
            rngV = f"Data_Processing!${ref['v']}$4:${ref['v']}${3+ref['len']}"
            rngD = f"Data_Processing!${ref['d']}$4:${ref['d']}${3+ref['len']}"
            for i in range(len(target_potentials)):
                c = 3+i*2
                vT = f"{xl_col_to_name(4+i*2)}$1"
                ws_kl.write_formula(r_idx, c, f"=INDEX({rngD}, MATCH({vT}, {rngV}, -1))", fmt_num)
                cJ = xl_rowcol_to_cell(r_idx, c)
                ws_kl.write_formula(r_idx, c+1, f"=IF(ABS({cJ})<1E-3, NA(), 1/ABS({cJ}))", fmt_num)
            r_idx += 1
        res_r = r_idx + 2
        ws_kl.write(res_r, 0, "Results", fmt_head)
        ws_kl.write_row(res_r+1, 0, ["Voltage", "Slope", "Intercept", "R²", "n"], fmt_head)
        ch_kd = wb.add_chart({'type': 'scatter'})
        ch_kc = wb.add_chart({'type': 'scatter'})
        ch_n = wb.add_chart({'type': 'scatter', 'subtype': 'straight_with_markers'})
        for i, pot in enumerate(target_potentials):
            col = xl_col_to_name(4+i*2)
            c_rng = f"KL_Analysis!$C$4:$C${3+n_rpm}"
            v_rng = f"KL_Analysis!${col}$4:${col}${3+n_rpm}"
            nm = f"=KL_Analysis!${xl_col_to_name(4+i*2)}$1"
            colr = CHART_COLORS[i%8]
            for ch, show in [(ch_kd, True), (ch_kc, False)]:
                ch.add_series({'name': nm, 'categories': c_rng, 'values': v_rng,
                               'marker': {'type': 'circle', 'size': 5, 'fill': {'color': colr}, 'border': {'color': colr}},
                               'trendline': {'type': 'linear', 'line': {'color': colr, 'dash_type': 'long_dash'},
                                             'display_equation': show, 'display_r_squared': show, 'name': f"Fit {pot}V"}})
            cr = res_r + 2 + i
            ws_kl.write_formula(cr, 0, nm)
            ws_kl.write_formula(cr, 1, f"=IFERROR(SLOPE({v_rng}, {c_rng}), \"\")", fmt_num)
            ws_kl.write_formula(cr, 2, f"=IFERROR(INTERCEPT({v_rng}, {c_rng}), \"\")", fmt_num)
            ws_kl.write_formula(cr, 3, f"=IFERROR(RSQ({v_rng}, {c_rng}), \"\")", fmt_num)
            slp = xl_rowcol_to_cell(cr, 1)
            ws_kl.write_formula(cr, 4, f"=IFERROR(((1/{slp})/1000)/({ref_B}/4), \"\")", fmt_calc)
        for ch in [ch_kd, ch_kc]:
            ch.set_plotarea({'border': {'color': 'black', 'width': 1}})
            ch.set_x_axis({'name': 'ω⁻¹ᐟ²', 'line': {'color': 'black'}, 'label_position': 'low', 'crossing': 'min', 'major_gridlines': {'visible': False}})
            ch.set_y_axis({'name': 'j⁻¹', 'line': {'color': 'black'}, 'label_position': 'low', 'crossing': 'min', 'major_gridlines': {'visible': False}})
            ch.set_legend({'position': 'right'})
        ws_kl.insert_chart(res_r+10, 1, ch_kd)
        ws_kl.insert_chart(res_r+35, 1, ch_kc)
        n_cat = f"=(KL_Analysis!$A${res_r+3}:KL_Analysis!$A${res_r+2+len(target_potentials)})"
        n_val = f"=(KL_Analysis!$E${res_r+3}:KL_Analysis!$E${res_r+2+len(target_potentials)})"
        ch_n.add_series({'categories': n_cat, 'values': n_val, 'marker': {'type': 'circle', 'size': 6}, 'line': {'color': 'blue'}})
        ch_n.set_plotarea({'border': {'color': 'black', 'width': 1}})
        ch_n.set_x_axis({'name': 'V vs RHE', 'line': {'color': 'black'}, 'major_gridlines': {'visible': False}})
        ch_n.set_y_axis({'name': 'n', 'min': 0, 'max': 5, 'line': {'color': 'black'}, 'major_gridlines': {'visible': False}})
        ws_kl.insert_chart(res_r+10, 9, ch_n)
    print(f"\n完了!保存先: {output_file}")
    input("Enterを押して終了...")
if __name__ == "__main__":
    run_excel_maker

このツールのように、「理系の専門知識」と「プログラミング(自動化)」を組み合わせた課題解決が得意です。

Pythonによるデータ分析・グラフ作成(Matplotlib, Pandas)

大量の実験データの自動整理

Simulink / MATLAB を用いたシミュレーション・モデリング

研究室での「面倒な作業」の効率化ツール作成

「プロのエンジニアに頼むほどではないけれど、手作業でやるのは辛い…」 「理系の用語やデータの意味がわかる人に、解析を任せたい」

そんな時は、ぜひ現役大学院生の私にお任せください! 学生ならではのフットワークの軽さと、研究で培った粘り強さで、丁寧に対応させていただきます。

まずはお気軽にメッセージでご相談ください!
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す ココナラコンテンツマーケット ノウハウ記事・テンプレート・デザイン素材はこちら