Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 14 additions & 1 deletion .github/workflows/draft-pdf.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
name: Draft PDF
on: [push]

on:
push:
paths:
- paper.md
- paper.bib
- figures/**
- .github/workflows/draft-pdf.yml
pull_request:
paths:
- paper.md
- paper.bib
- figures/**
- .github/workflows/draft-pdf.yml

permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ htmlcov/
/metbit/__pycache__
*.pyc
*.pyc
.claude
Binary file modified figures/graphical_abstract.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified figures/workflow_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions manuscript/figures/generate_graphical_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,7 @@ def mini_heatmap(ax, cx, cy, w=1.0, h=0.6):
color=WHITE, fontweight="bold", zorder=8)

# ── footer ────────────────────────────────────────────────────────────────────
ax.text(8, 0.62,
"pip install metbit · github.com/aeiwz/metbit · MIT License",
ha="center", va="center", fontsize=9, color=DARK, alpha=0.7, zorder=5)

ax.text(0.35, 0.62,
"Bubpamala T. · kawa-technology · 2024 (AI-assisted writing: Claude Sonnet 4.6, GPT-5.2)",
ha="left", va="center", fontsize=8, color=DARK, alpha=0.55, zorder=5)

# ── save ──────────────────────────────────────────────────────────────────────
out = "graphical_abstract.png"
Expand Down
5 changes: 0 additions & 5 deletions manuscript/figures/generate_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,6 @@ def node(x, y, w, color, title, items,
# ─────────────────────────────────────────────────────────────────────────────
# FOOTER
# ─────────────────────────────────────────────────────────────────────────────
ax.text(CX, LEG_Y - 0.32,
"Bubpamala T. · kawa-technology · github.com/aeiwz/metbit "
"(AI writing assistance: Claude Sonnet 4.6, GPT-5.2)",
ha="center", va="center", fontsize=7.5,
color=DARK, alpha=0.55, zorder=5)

# ─────────────────────────────────────────────────────────────────────────────
# SAVE – clip canvas to content
Expand Down
Binary file modified manuscript/figures/graphical_abstract.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified manuscript/figures/workflow_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified manuscript/metbit_manuscript.docx
Binary file not shown.
12 changes: 6 additions & 6 deletions manuscript/metbit_manuscript.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

**Authors**

Theerayut Bubpamala¹\*
Theerayut Bubpamala<sup>1,2,*</sup>\*
<sup>1</sup> Medical Biochemistry and Molecular Biology Graduate Study Program, Faculty of Medicine, Khon Kaen University, Khon Kaen, Thailand
<sup>2</sup> kawa-technology, Independent Research & Development

¹ kawa-technology, Independent Research & Development

\* Corresponding author: theerayut_aeiw_123@hotmail.com
GitHub: https://github.com/aeiwz/metbit
\* <sup>*</sup> Corresponding author: theerayut_aeiw_123@hotmail.com<br>
GitHub: https://github.com/aeiwz/metbit<br>
PyPI: https://pypi.org/project/metbit/

---
Expand All @@ -34,7 +34,7 @@ Theerayut Bubpamala¹\*

## 1. Introduction

Proton nuclear magnetic resonance (¹H NMR) spectroscopy is an essential platform for untargeted metabolomics due to its quantitative accuracy and non-destructive sample handling (Emwas et al., 2019). However, the journey from raw free-induction decay (FID) files to interpretable multivariate models involves complex sequential steps, including preprocessing, normalization, and alignment. While open-source tools such as NMRglue (Helmus and Jaroniec, 2013) and MetaboAnalyst (Pang et al., 2022) address parts of this pipeline, the Python ecosystem lacks a single, scriptable library that integrates the entire NMR metabolomics workflow natively.
Proton nuclear magnetic resonance (<sup>1</sup>H NMR) spectroscopy is an essential platform for untargeted metabolomics due to its quantitative accuracy and non-destructive sample handling (Emwas et al., 2019). However, the journey from raw free-induction decay (FID) files to interpretable multivariate models involves complex sequential steps, including preprocessing, normalization, and alignment. While open-source tools such as NMRglue (Helmus and Jaroniec, 2013) and MetaboAnalyst (Pang et al., 2022) address parts of this pipeline, the Python ecosystem lacks a single, scriptable library that integrates the entire NMR metabolomics workflow natively.

We present **metbit**, an end-to-end Python package that consolidates NMR data processing and multivariate modeling into a coherent API. metbit bridges the gap between raw spectral acquisitions and biological conclusions, delivering publication-quality interactive visualizations and reproducible workflows.

Expand Down
Binary file added manuscript/~$tbit_manuscript.docx
Binary file not shown.
81 changes: 51 additions & 30 deletions metbit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,71 @@
# -*- coding: utf-8 -*-

# Public classes/functions re-exported for convenience
from .metbit import opls_da, pca
from .utility import UnivarStats, Normalise
from .lazy_opls_da import lazy_opls_da

# Backwards-compat star imports (intended public surface)
from .spec_norm import * # noqa: F401,F403
from .peak_processe import peak_chops
from .STOCSY import STOCSY
from .ui_stocsy import STOCSY_app
from .ui_picky_peak import pickie_peak
from .take_intensity import * # noqa: F401,F403
from .nmr_preprocess import nmr_preprocessing
from .calibrate import calibrate
from .annotate_peak import annotate_peak
from .baseline import baseline_correct, bline
from .alignment import detect_multiplets, icoshift_align, PeakAligner

# Package metadata
__author__ = "aeiwz"
__email__ = "theerayut_aeiw_123@hotmail.com"
__maintainer__ = "aeiwz"
__status__ = "Development"
__copyright__ = "Copyright 2024, Theerayut"
__version__ = "9.0.0-dev"

# Core analysis
from metbit.analysis.opls_da import opls_da
from metbit.analysis.pca import pca
from metbit.analysis.stocsy import STOCSY

# NMR processing
from metbit.nmr.preprocess import nmr_preprocessing
from metbit.nmr.calibrate import calibrate
from metbit.nmr.alignment import detect_multiplets, icoshift_align, PeakAligner
from metbit.nmr.peaks import peak_chops

# Statistics
from metbit.stats.normalise import Normalise
from metbit.stats.univariate import UnivarStats

# Preprocessing
from metbit.preprocessing.baseline import baseline_correct, bline
from metbit.preprocessing.normalize import Normalization

# Dash apps (optional heavy deps)
try:
from metbit.apps.annotate import annotate_peak
except Exception:
pass

try:
from metbit.apps.stocsy_app import STOCSY_app
except Exception:
pass

# Optional version string to keep in sync with setup.py
__version__ = "8.7.7"
try:
from metbit.apps.peak_picker import pickie_peak
except Exception:
pass

# Legacy flat-layout class not yet moved to a sub-package
try:
from metbit.lazy_opls_da import lazy_opls_da
except Exception:
pass

# Explicit export list for primary API (star imports remain for compatibility)
__all__ = [
"opls_da",
"pca",
"UnivarStats",
"Normalise",
"lazy_opls_da",
"peak_chops",
"STOCSY",
"STOCSY_app",
"pickie_peak",
"nmr_preprocessing",
"calibrate",
"annotate_peak",
"baseline_correct",
"bline",
"detect_multiplets",
"icoshift_align",
"PeakAligner",
"peak_chops",
"Normalise",
"UnivarStats",
"baseline_correct",
"bline",
"Normalization",
"annotate_peak",
"STOCSY_app",
"pickie_peak",
"lazy_opls_da",
]
Binary file modified metbit/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file modified metbit/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file modified metbit/__pycache__/lazy_opls_da.cpython-314.pyc
Binary file not shown.
70 changes: 70 additions & 0 deletions metbit/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Backwards-compatibility re-exports from sub-package paths.
# Importing from old flat paths (e.g. `from metbit.nmr_preprocess import ...`)
# still works via the originals; this module provides the NEW canonical paths
# so external consumers can start migrating to the sub-package imports.

# --- nmr/ ---
from .nmr.preprocess import ( # noqa: F401
nmr_preprocessing,
read_fid,
remove_digital_filter,
generate_ppm_scale,
phasing,
)
from .nmr.calibrate import calibrate # noqa: F401
from .nmr.alignment import ( # noqa: F401
detect_multiplets,
icoshift_align,
PeakAligner,
Multiplet,
)
from .nmr.denoise import Denoise # noqa: F401
from .nmr.peaks import peak_chops # noqa: F401

# --- models/ ---
from .models.base import nipals # noqa: F401
from .models.pls import PLS # noqa: F401
from .models.opls import OPLS # noqa: F401
from .models.vip import vip_scores # noqa: F401
from .models.cross_validation import CrossValidation # noqa: F401

# --- preprocessing/ ---
from .preprocessing.scaler import Scaler as SimpleScaler # noqa: F401
from .preprocessing.scaler_ext import Scaler # noqa: F401
from .preprocessing.normalize import Normalization # noqa: F401
from .preprocessing.baseline import baseline_correct, bline # noqa: F401

# --- analysis/ ---
from .analysis.stocsy import STOCSY # noqa: F401

# --- viz/ ---
from .viz.ellipse import confidence_ellipse # noqa: F401

# --- higher-level classes (canonical sub-package paths) ---
from .analysis.opls_da import opls_da # noqa: F401
from .analysis.pca import pca # noqa: F401

try:
from .lazy_opls_da import lazy_opls_da # noqa: F401
except Exception:
pass

# --- utility classes (canonical sub-package paths) ---
from .stats.univariate import UnivarStats # noqa: F401
from .stats.normalise import Normalise # noqa: F401

# --- Dash apps (optional heavy deps) ---
try:
from .apps.annotate import annotate_peak # noqa: F401
except Exception:
pass

try:
from .apps.stocsy_app import STOCSY_app # noqa: F401
except Exception:
pass

try:
from .apps.peak_picker import pickie_peak # noqa: F401
except Exception:
pass
File renamed without changes.
57 changes: 57 additions & 0 deletions metbit/_internal/pairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-

import pandas as pd


class lazypair:
"""Utility for generating all pairwise groupings and related dataset splits."""

def __init__(self, dataset: pd.DataFrame, column_name: str):
if not isinstance(dataset, pd.DataFrame):
raise ValueError("meta should be a pandas dataframe")
if not isinstance(column_name, str):
raise ValueError("column_name should be a string")
if column_name not in dataset.columns:
raise KeyError(f"{column_name} not found in dataset")
if dataset[column_name].nunique() < 2:
raise ValueError("Group should contain at least 2 groups")

self.meta = dataset
self.column_name = column_name

groups = dataset[column_name].unique()
self.pairs = [(g1, g2) for i, g1 in enumerate(groups) for g2 in groups[i + 1:]]
self.index_ = [
dataset.index[dataset[column_name] == g1].tolist()
+ dataset.index[dataset[column_name] == g2].tolist()
for g1, g2 in self.pairs
]
self.names = [f"{g1}_vs_{g2}".replace("/", "_") for g1, g2 in self.pairs]

def get_index(self):
index_ = self.index_
return index_

def get_name(self):
names = self.names
return names

def get_meta(self):
meta = self.meta
column_name = self.column_name
return meta[column_name]

def get_column_name(self):
column_name = self.column_name
return column_name

def get_dataset(self):
df = self.meta
index_ = self.index_
list_of_df = []
for i in range(len(index_)):
list_of_df.append(df.loc[index_[i]])

#Create object attribute
self.list_of_df = list_of_df
return list_of_df
Loading
Loading