はじめまして!普段は大学で実験データの山と格闘している理系学生です。今日は、私のポートフォリオ(実績)として、実際に研究室で作成し、「これは便利!」とラボメンバーからも好評だったデータ解析ツールをご紹介したいと思います。「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 を用いたシミュレーション・モデリング
研究室での「面倒な作業」の効率化ツール作成
「プロのエンジニアに頼むほどではないけれど、手作業でやるのは辛い…」 「理系の用語やデータの意味がわかる人に、解析を任せたい」
そんな時は、ぜひ現役大学院生の私にお任せください! 学生ならではのフットワークの軽さと、研究で培った粘り強さで、丁寧に対応させていただきます。
まずはお気軽にメッセージでご相談ください!