0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TOF-SIMSのASCファイルをStreamlitで読み込んでPCA・差分解析するWebアプリを作った【PHI形式対応・コード解説】

0
Last updated at Posted at 2026-07-06

はじめに

TOF-SIMS(飛行時間型二次イオン質量分析)のデータ解析では、複数サンプルのスペクトル比較や、主成分分析(PCA)による試料グループの傾向把握が頻繁に発生します。

市販ソフトウェアは高機能ですが、「複数ファイルを一括でPCAしたい」「差分スペクトルをインタラクティブに操作したい」といった用途では、自前でスクリプトを書いたほうが速いことがあります。

この記事では、PHI社製TOF-SIMSが出力するASCファイルを読み込み、ブラウザで解析できるStreamlitアプリの実装を解説します。

  • PHI ASCファイルの構造とパーサー実装
  • m/z軸アライメントのビン化アルゴリズム
  • TOF-SIMSに適した規格化・スケーリング(ポアソンスケーリング)
  • StreamlitアプリへのpytestとGitHub Actions CIの後付け

コードはMITライセンスで公開済みです。サイドバーのチェックボックス1つ(またはURLに ?demo=1)で合成デモデータを読み込めるので、実測データがなくてもすぐ試せます。

GitHub: https://github.com/yharada520/tof-sims-analyzer

スペクトル重ね書き画面

対象読者

  • TOF-SIMSやXPS・AESなどの表面分析装置を使っており、データ処理を自動化したい方
  • Pythonを少し書いたことがある、またはこれから始めたい方
  • Streamlit製の解析アプリの実装パターンに興味がある方

PythonとPandasの基礎説明は省略します。

技術スタック

役割 ライブラリ
Web UI Streamlit 1.35+
データ処理 pandas 2.0+ / numpy 1.24+
統計解析 scikit-learn 1.3+ / scipy 1.11+
可視化 plotly 5.18+ / matplotlib 3.7+

Python 3.10〜3.12でCIテスト済みです。

1. PHI ASCファイルの構造

ULVAC-PHI製TOF-SIMSが出力するASCファイルのデータ本体は、次の3カラム形式です。

    510,     0.7225411,      6
   1022,     0.6806757,      2
   1534,     0.6400596,      4
カラム 内容
1列目 m/z インデックス(整数ビン番号)
2列目 規格化強度(0〜1)
3列目 生イオンカウント数

実装上の落とし穴が2つあります。

  1. カンマが「区切り文字」ではなく各数値の末尾に付いている510,)。split() の結果をそのまま float() に渡すと ValueError になる
  2. ヘッダ行数が装置・測定モードによって異なる。データ開始行を動的に検出する必要がある

実は開発中、1つ目の落とし穴を自分でも踏んでいました。ユニットテストを書いた際に float(parts[0]) が実ファイルで失敗することが発覚し、rstrip(",") を追加して修正しています。パーサーはテストを書いて初めて信用できる、という教訓です。

2. パーサーの実装

def detect_data_start(lines: list[str]) -> int:
    """数値データが始まる行インデックスを動的に検出する"""
    for i, line in enumerate(lines):
        parts = line.strip().split()
        if len(parts) >= 2:
            try:
                float(parts[0].rstrip(","))  # 末尾カンマを除去してから変換
                float(parts[1].rstrip(","))
                return i
            except ValueError:
                continue
    return 0


def parse_phi_asc(file_bytes: bytes, filename: str) -> Optional[pd.Series]:
    text = file_bytes.decode("utf-8", errors="replace")
    lines = text.splitlines()
    start = detect_data_start(lines)

    rows = []
    for line in lines[start:]:
        parts = line.strip().split()
        if len(parts) < 2:
            continue
        try:
            mz = float(parts[0].rstrip(","))
            counts = float(parts[1].rstrip(","))
            rows.append((mz, counts))
        except ValueError:
            continue

    if not rows:
        return None

    df = pd.DataFrame(rows, columns=["mz", "counts"])
    df = df.groupby("mz", as_index=False)["counts"].sum()  # 重複m/z統合
    series = df.set_index("mz")["counts"]
    series.name = filename
    return series

StreamlitのUploadedFileは .read() でバイト列を返すので、そのまま渡せます。

for f in uploaded_files:
    series = parse_phi_asc(f.read(), f.name)

3. m/z軸のアライメント(ビン化)

複数ファイル比較の最大の障壁は、サンプル間でm/z軸が一致しないことです。測定条件や測定日が違うと、m/zの刻みがわずかにずれます。numpy.digitize によるビン化で共通軸に揃えます。

def bin_spectra(series_list: list[pd.Series], bin_width: float) -> pd.DataFrame:
    # 全サンプルのm/z範囲を統合して共通グリッドを作成
    all_mz = np.concatenate([s.index.values for s in series_list])
    mz_min = np.floor(all_mz.min() / bin_width) * bin_width
    mz_max = np.ceil(all_mz.max() / bin_width) * bin_width
    common_mz = np.round(np.arange(mz_min, mz_max + bin_width, bin_width), 6)

    binned = {}
    for s in series_list:
        bins = np.arange(mz_min - bin_width / 2, mz_max + bin_width, bin_width)
        bin_idx = np.clip(np.digitize(s.index.values, bins) - 1,
                          0, len(common_mz) - 1)
        agg = pd.Series(0.0, index=range(len(common_mz)))
        for bi, cnt in zip(bin_idx, s.values):
            agg[bi] += cnt
        agg.index = common_mz
        binned[s.name] = agg

    return pd.DataFrame(binned).T.fillna(0)  # 行=サンプル, 列=m/z

bin_width はUIから 0.1 / 0.5 / 1.0 Da を選択できます。高分解能データには 0.1、粗い比較には 1.0 という使い分けです。

これで行=サンプル、列=m/zの数値行列が得られ、以降のPCAとクラスタリングの入力になります。

4. 規格化とスケーリング

TOF-SIMSのスペクトル比較では、測定条件差による絶対強度の違いを除去する前処理が重要です。規格化とスケーリングを分離して実装しています。

TIC規格化(サンプル間の絶対値差を補正)

def apply_normalization(df: pd.DataFrame, method: str) -> pd.DataFrame:
    if method == "TIC規格化":
        row_sum = df.sum(axis=1).replace(0, np.nan)
        return df.div(row_sum, axis=0).fillna(0)
    return df.copy()

replace(0, np.nan)fillna(0) はゼロ除算対策の定石です。全ゼロ行があってもNaNが伝播しません。

スケーリング(PCA前処理)

def apply_scaling(df: pd.DataFrame, method: str) -> pd.DataFrame:
    if method == "平均中心化":
        return df - df.mean(axis=0)
    elif method == "オートスケーリング":
        centered = df - df.mean(axis=0)
        std = df.std(axis=0).replace(0, np.nan)
        return centered.div(std, axis=1).fillna(0)
    elif method == "ポアソンスケーリング":
        # TOF-SIMSのショットノイズ(カウント統計)に対応
        sqrt_mean = df.mean(axis=0).apply(np.sqrt).replace(0, np.nan)
        centered = df - df.mean(axis=0)
        return centered.div(sqrt_mean, axis=1).fillna(0)
    return df.copy()

ポアソンスケーリングはTOF-SIMS特有の選択肢です。イオンカウントデータはポアソン分布に従うため、標準偏差の代わりに平均の平方根 sqrt(μ) で割ることで、低カウントm/zのノイズ過大評価を防ぎます。XPSやRamanのスペクトル前処理とは異なる点です。

目的 推奨の組み合わせ
サンプル間の組成比較 TIC規格化 + 平均中心化
全m/zを等重みでPCA TIC規格化 + オートスケーリング
カウント統計を考慮したPCA TIC規格化 + ポアソンスケーリング

5. PCAと差分スペクトル

Streamlitはウィジェット操作のたびにスクリプト全体を再実行するため、コールバック登録なしでインタラクティブなPCAが書けます。

X = df_processed.values
pca = PCA(n_components=min(n_comp, X.shape[0], X.shape[1]))
scores = pca.fit_transform(X)
loadings = pca.components_
explained = pca.explained_variance_ratio_ * 100

デモデータ(Si表面を模擬したグループAと有機汚染を模擬したグループB、各3サンプル)では、PC1(寄与率97.7%)で2グループが明確に分離します。

PCAスコアプロット

差分スペクトルは規格化後の行列の引き算だけです。

diff = df_norm.loc[sample_b].values - df_norm.loc[sample_a].values

Plotlyの棒グラフで正の差分(B > A)と負の差分(A > B)を色分けし、pd.Series(diff).abs().nlargest(20) で注目すべきm/zを自動抽出しています。良品・不良品の2群比較がそのまま実務で使えます。

下図はデモデータのグループA(Si表面)とグループB(有機汚染あり)の差分です。m/z 73(PDMS汚染マーカー)とm/z 15(CH3+)がB側で増加し、m/z 28(Si+)がA側で増加している——という表面汚染解析の典型的なパターンが差分と上位ピーク表から読み取れます。

差分スペクトル

6. StreamlitアプリにpytestとCIを後付けする

StreamlitアプリはモジュールレベルでUIコードが実行されるため、素直に import app するとテストが書けません。2段構えで対処しました。

conftest.pyでStreamlitをスタブに差し替え

# tests/conftest.py
import sys
from unittest.mock import MagicMock

def _ctx():
    cm = MagicMock()
    cm.__enter__ = MagicMock(return_value=cm)
    cm.__exit__ = MagicMock(return_value=False)
    return cm

st = MagicMock()
st.sidebar = _ctx()
st.file_uploader.return_value = []                      # 空 → st.stop()が呼ばれる
st.tabs.return_value = tuple(_ctx() for _ in range(4))  # withブロック対応
sys.modules["streamlit"] = st

st.sidebarst.tabs()with 文で使われるため、__enter__/__exit__ を持つモックにする必要があります。

execで関数定義だけを抽出

# tests/test_basic.py
import sys, types
from pathlib import Path

_app_module = types.ModuleType("app")
sys.modules["app"] = _app_module  # 先に登録しておくのがポイント

try:
    exec(compile(Path("app.py").read_text(encoding="utf-8"), "app.py", "exec"),
         _app_module.__dict__)
except Exception:
    pass  # UI部分の実行エラーは無視(関数定義は取得済み)

app = _app_module

app.py を1行も書き換えずにテストを追加できるので、既存Streamlitアプリへの後付けに向いています。この方法でパーサー・ビン化・規格化・スケーリングを対象に22テストを実装し、GitHub Actions(Python 3.10/3.11/3.12マトリクス)で全件パスしています。

7. 公開リポジトリの構成

tof-sims-analyzer/
├── app.py                        # メインStreamlitアプリ
├── requirements.txt / pyproject.toml
├── .gitignore                    # 実測データ・生バイナリを完全除外
├── examples/
│   ├── generate_sample_data.py   # 合成ダミーデータ生成
│   └── sample_*.csv              # 正/負イオン・深さプロファイル等
├── legacy/                       # Streamlit以前のデスクトップGUI版
├── tests/                        # 22ユニットテスト
└── .github/workflows/ci.yml      # CI (3.10/3.11/3.12)

実測データは .gitignore のパターン(日付フォルダ・.raw.tdc など装置固有拡張子)で除外し、リポジトリには numpy で生成した合成データのみを含めています。分析装置系のコードを公開する際は、測定データの混入チェックを.gitignoreとファイル一覧の目視の両方で行うことをおすすめします。

まとめ

実装済み 今後の予定
ASCパーサー(ヘッダ自動検出) 深さ方向プロファイル可視化
m/zビン化アライメント 2Dイオンイメージ表示
TIC規格化・ポアソンスケーリング ピーク自動同定
PCA・HCA・差分スペクトル Docker対応
22テスト + GitHub Actions CI 英語UI

TOF-SIMSのデータ処理をPythonで自動化したい方の参考になれば幸いです。MITライセンスなので自由に改変してください。フィードバック・IssueはGitHubからどうぞ。

GitHub: https://github.com/yharada520/tof-sims-analyzer

インストール手順や「Pythonなしでどう使うか」については、一般向けのNote記事も書いています。

Note: https://note.com/tukkidney/n/n6eeb4efc58b9?app_launch=false

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?