salmopredict 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- salmopredict/__init__.py +30 -0
- salmopredict/cli.py +303 -0
- salmopredict/config.py +103 -0
- salmopredict/core/__init__.py +1 -0
- salmopredict/core/align.py +104 -0
- salmopredict/core/io_tables.py +76 -0
- salmopredict/core/modelinfo.py +71 -0
- salmopredict/core/predict.py +102 -0
- salmopredict/gui/__init__.py +1 -0
- salmopredict/gui/app.py +242 -0
- salmopredict/gui/assets/salmopredict_icon.png +0 -0
- salmopredict/gui/assets/vphs_logo.png +0 -0
- salmopredict/models/.DS_Store +0 -0
- salmopredict/models/model_default/learner.pkl +0 -0
- salmopredict/models/model_default/metadata.json +292 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F1/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F2/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F2/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F3/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F3/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F4/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F4/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F5/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F5/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F1/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F2/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F2/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F3/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F3/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F4/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F4/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F5/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F5/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F1/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F2/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F2/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F3/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F3/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F4/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F4/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F5/model-internals.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F5/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F2/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F3/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F4/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F5/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F2/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F3/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F4/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F5/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F2/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F3/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F4/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F5/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F1/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F2/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F3/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F4/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F5/model.pkl +0 -0
- salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/model.pkl +0 -0
- salmopredict/models/model_default/models/WeightedEnsemble_L2/model.pkl +0 -0
- salmopredict/models/model_default/models/trainer.pkl +0 -0
- salmopredict/models/model_default/predictor.pkl +0 -0
- salmopredict/models/model_default/version.txt +1 -0
- salmopredict/pipeline.py +112 -0
- salmopredict-0.1.0.dist-info/METADATA +126 -0
- salmopredict-0.1.0.dist-info/RECORD +83 -0
- salmopredict-0.1.0.dist-info/WHEEL +5 -0
- salmopredict-0.1.0.dist-info/entry_points.txt +2 -0
- salmopredict-0.1.0.dist-info/licenses/LICENSE +202 -0
- salmopredict-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Read a model directory's recorded versions and compare with the runtime.
|
|
2
|
+
|
|
3
|
+
AutoGluon serialises its version and training Python version into the model
|
|
4
|
+
directory (``version.txt`` and ``metadata.json``). Reading those files is far
|
|
5
|
+
cheaper and more robust than loading the predictor, so ``check`` and
|
|
6
|
+
``build-model`` use this module to warn about version mismatches before an
|
|
7
|
+
unpickle is attempted.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ModelInfo:
|
|
21
|
+
path: Path
|
|
22
|
+
exists: bool
|
|
23
|
+
version_txt: Optional[str] = None # contents of version.txt
|
|
24
|
+
meta_version: Optional[str] = None # metadata.json "version"
|
|
25
|
+
py_version: Optional[str] = None # training-time Python version
|
|
26
|
+
model_best: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def installed_autogluon_version() -> Optional[str]:
|
|
30
|
+
"""Return the installed ``autogluon.tabular`` version, or ``None``.
|
|
31
|
+
|
|
32
|
+
The top-level ``autogluon`` namespace package has no ``__version__``; the
|
|
33
|
+
version lives on the ``autogluon.tabular`` sub-package.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
import autogluon.tabular as agt
|
|
37
|
+
|
|
38
|
+
return getattr(agt, "__version__", None)
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def read_model_info(model_path: Path) -> ModelInfo:
|
|
44
|
+
"""Read version metadata recorded inside a model directory."""
|
|
45
|
+
model_path = Path(model_path)
|
|
46
|
+
if not model_path.is_dir():
|
|
47
|
+
return ModelInfo(path=model_path, exists=False)
|
|
48
|
+
|
|
49
|
+
info = ModelInfo(path=model_path, exists=True)
|
|
50
|
+
|
|
51
|
+
vtxt = model_path / "version.txt"
|
|
52
|
+
if vtxt.is_file():
|
|
53
|
+
info.version_txt = vtxt.read_text(encoding="utf-8").strip()
|
|
54
|
+
|
|
55
|
+
meta = model_path / "metadata.json"
|
|
56
|
+
if meta.is_file():
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(meta.read_text(encoding="utf-8"))
|
|
59
|
+
except (OSError, json.JSONDecodeError):
|
|
60
|
+
data = {}
|
|
61
|
+
info.meta_version = data.get("version")
|
|
62
|
+
info.py_version = data.get("py_version")
|
|
63
|
+
info.model_best = data.get("model_best")
|
|
64
|
+
|
|
65
|
+
return info
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def runtime_py_version() -> str:
|
|
69
|
+
"""Return the running interpreter's ``major.minor.micro`` version."""
|
|
70
|
+
v = sys.version_info
|
|
71
|
+
return f"{v.major}.{v.minor}.{v.micro}"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Model loading and prediction helpers.
|
|
2
|
+
|
|
3
|
+
AutoGluon is imported lazily inside these functions so that ``salmopredict
|
|
4
|
+
check`` / ``gui`` keep working even before the heavy dependency is importable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import warnings
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Union
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
# AutoGluon logs benign "Found N mismatches ..." notices from this logger when a
|
|
18
|
+
# model is loaded on a slightly different Python micro version / OS than it was
|
|
19
|
+
# trained on (e.g. Linux -> macOS).
|
|
20
|
+
_METADATA_LOGGER = "autogluon.common.utils.utils"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@contextmanager
|
|
24
|
+
def _quiet_load_warnings():
|
|
25
|
+
"""Silence benign UserWarnings emitted while loading/predicting.
|
|
26
|
+
|
|
27
|
+
* fastai's ``load_learner`` "insecure pickle" notice -- AutoGluon's neural-net
|
|
28
|
+
sub-model is a fastai learner loaded (at load, or lazily during predict)
|
|
29
|
+
via pickle; the bundled model is trusted.
|
|
30
|
+
* setuptools' ``pkg_resources is deprecated`` notice -- raised while AutoGluon
|
|
31
|
+
collects package versions during load.
|
|
32
|
+
|
|
33
|
+
Only these specific messages are hidden; every other warning is left intact.
|
|
34
|
+
"""
|
|
35
|
+
with warnings.catch_warnings():
|
|
36
|
+
warnings.filterwarnings(
|
|
37
|
+
"ignore", message=r".*load_learner.*", category=UserWarning
|
|
38
|
+
)
|
|
39
|
+
warnings.filterwarnings(
|
|
40
|
+
"ignore", message=r".*insecure pickle.*", category=UserWarning
|
|
41
|
+
)
|
|
42
|
+
warnings.filterwarnings(
|
|
43
|
+
"ignore", message=r".*pkg_resources is deprecated.*", category=UserWarning
|
|
44
|
+
)
|
|
45
|
+
yield
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@contextmanager
|
|
49
|
+
def _quiet_metadata_mismatch():
|
|
50
|
+
"""Hide AutoGluon's load-time Python-micro-version / OS mismatch notices.
|
|
51
|
+
|
|
52
|
+
They are logged at WARNING level by the ``autogluon.common.utils.utils``
|
|
53
|
+
logger and are harmless for inference, so we raise that one logger's
|
|
54
|
+
threshold for the duration of the load and restore it afterwards.
|
|
55
|
+
"""
|
|
56
|
+
lg = logging.getLogger(_METADATA_LOGGER)
|
|
57
|
+
prev = lg.level
|
|
58
|
+
lg.setLevel(logging.ERROR)
|
|
59
|
+
try:
|
|
60
|
+
yield
|
|
61
|
+
finally:
|
|
62
|
+
lg.setLevel(prev)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_predictor(model_path: Union[str, Path]):
|
|
66
|
+
"""Load an AutoGluon ``TabularPredictor`` from ``model_path``.
|
|
67
|
+
|
|
68
|
+
The path is passed as a plain string, never through a shell, so a literal
|
|
69
|
+
``*`` in a directory name is safe.
|
|
70
|
+
"""
|
|
71
|
+
from autogluon.tabular import TabularPredictor
|
|
72
|
+
|
|
73
|
+
model_path = Path(model_path)
|
|
74
|
+
if not model_path.is_dir():
|
|
75
|
+
raise FileNotFoundError(f"Model directory does not exist: {model_path}")
|
|
76
|
+
with _quiet_load_warnings(), _quiet_metadata_mismatch():
|
|
77
|
+
return TabularPredictor.load(str(model_path))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def choose_model(predictor, preferred: str) -> str:
|
|
81
|
+
"""Return ``preferred`` if the predictor has it, else its best model."""
|
|
82
|
+
names = predictor.model_names()
|
|
83
|
+
return preferred if preferred in names else predictor.model_best
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def predict_incidence(predictor, X: pd.DataFrame, model: str) -> pd.Series:
|
|
87
|
+
"""Run prediction with the chosen model, returning a pandas Series."""
|
|
88
|
+
with _quiet_load_warnings():
|
|
89
|
+
return predictor.predict(X, as_pandas=True, model=model)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def clamp_predictions(preds: pd.Series, low: float, high: float):
|
|
93
|
+
"""Clamp ``preds`` into ``[low, high]``.
|
|
94
|
+
|
|
95
|
+
Incidence is a percentage, so predictions must stay within a valid range;
|
|
96
|
+
the regression model can occasionally emit values just outside it. Returns
|
|
97
|
+
``(clamped, n_below, n_above)`` where the counts are how many values were
|
|
98
|
+
raised to ``low`` and lowered to ``high`` respectively (NaNs are ignored).
|
|
99
|
+
"""
|
|
100
|
+
n_below = int((preds < low).sum())
|
|
101
|
+
n_above = int((preds > high).sum())
|
|
102
|
+
return preds.clip(lower=low, upper=high), n_below, n_above
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Streamlit graphical interface for salmopredict."""
|
salmopredict/gui/app.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""salmopredict Streamlit GUI.
|
|
2
|
+
|
|
3
|
+
Launch with ``salmopredict gui`` (or ``streamlit run salmopredict/gui/app.py``).
|
|
4
|
+
|
|
5
|
+
The interface lets the user point at an AutoGluon model directory, upload one
|
|
6
|
+
feature CSV, run prediction with live progress, then view and download the
|
|
7
|
+
prediction table. The predictor is cached so the (slow) model load happens only
|
|
8
|
+
once.
|
|
9
|
+
|
|
10
|
+
Output follows the two-type contract:
|
|
11
|
+
* input without a ``Sample`` column -> a single ``Incidence(%)`` column;
|
|
12
|
+
* input with a ``Sample`` column -> ``Sample`` + ``Incidence(%)``, and,
|
|
13
|
+
if a metadata CSV is attached (joined on ``Sample``), its other columns too.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import io
|
|
19
|
+
import tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import pandas as pd
|
|
23
|
+
import streamlit as st
|
|
24
|
+
|
|
25
|
+
from salmopredict import DESCRIPTION, __version__
|
|
26
|
+
from salmopredict.config import (
|
|
27
|
+
DEFAULT_ENSEMBLE_MODEL,
|
|
28
|
+
PREDICTION_MAX,
|
|
29
|
+
PREDICTION_MIN,
|
|
30
|
+
SAMPLE_COLUMN,
|
|
31
|
+
PredictConfig,
|
|
32
|
+
default_model_path,
|
|
33
|
+
)
|
|
34
|
+
from salmopredict.pipeline import run_pipeline
|
|
35
|
+
|
|
36
|
+
# Branding images, shipped inside the package so they show after a plain install.
|
|
37
|
+
_ASSETS = Path(__file__).resolve().parent / "assets"
|
|
38
|
+
_ICON_PATH = _ASSETS / "salmopredict_icon.png" # salmopredict app icon (square)
|
|
39
|
+
_LOGO_PATH = _ASSETS / "vphs_logo.png" # VPHS lab logo (banner)
|
|
40
|
+
|
|
41
|
+
_STAGE_LABELS = {
|
|
42
|
+
"load": "Loading model",
|
|
43
|
+
"predict": "Predicting",
|
|
44
|
+
"done": "Complete",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@st.cache_resource(show_spinner="Loading model (first time is slow)...")
|
|
49
|
+
def get_predictor(model_path: str):
|
|
50
|
+
"""Load and cache the predictor, keyed by model directory path."""
|
|
51
|
+
from salmopredict.core.predict import load_predictor
|
|
52
|
+
|
|
53
|
+
return load_predictor(model_path)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _input_columns(uploaded):
|
|
57
|
+
"""Peek the header of the uploaded CSV; None if nothing chosen or unreadable."""
|
|
58
|
+
if uploaded is None:
|
|
59
|
+
return None
|
|
60
|
+
try:
|
|
61
|
+
return list(pd.read_csv(io.BytesIO(uploaded.getvalue()), nrows=0).columns)
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _has_sample(columns) -> bool:
|
|
67
|
+
return bool(columns) and any(
|
|
68
|
+
str(c).strip().lower() == SAMPLE_COLUMN.lower() for c in columns
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
st.set_page_config(
|
|
73
|
+
page_title="salmopredict",
|
|
74
|
+
page_icon=str(_ICON_PATH) if _ICON_PATH.exists() else None,
|
|
75
|
+
layout="wide",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Header: salmopredict app icon + title on the left, VPHS lab logo top-right.
|
|
79
|
+
_icon_col, _title_col, _logo_col = st.columns([1, 4, 2])
|
|
80
|
+
with _icon_col:
|
|
81
|
+
if _ICON_PATH.exists():
|
|
82
|
+
st.image(str(_ICON_PATH))
|
|
83
|
+
with _title_col:
|
|
84
|
+
st.title("salmopredict")
|
|
85
|
+
st.caption(
|
|
86
|
+
f"Salmonella incidence prediction from virulence-factor gene features (v{__version__})"
|
|
87
|
+
)
|
|
88
|
+
with _logo_col:
|
|
89
|
+
if _LOGO_PATH.exists():
|
|
90
|
+
st.image(str(_LOGO_PATH))
|
|
91
|
+
|
|
92
|
+
st.markdown(DESCRIPTION)
|
|
93
|
+
|
|
94
|
+
# --------------------------------------------------------------------------- #
|
|
95
|
+
# Parameters
|
|
96
|
+
# --------------------------------------------------------------------------- #
|
|
97
|
+
_default_model = default_model_path()
|
|
98
|
+
with st.sidebar:
|
|
99
|
+
st.header("Model")
|
|
100
|
+
model_path = st.text_input(
|
|
101
|
+
"AutoGluon model directory",
|
|
102
|
+
value=str(_default_model) if _default_model else "",
|
|
103
|
+
key="model_path",
|
|
104
|
+
help="Directory produced by TabularPredictor.save (or the slim deploy clone).",
|
|
105
|
+
)
|
|
106
|
+
model_name = st.text_input(
|
|
107
|
+
"Ensemble model", value=DEFAULT_ENSEMBLE_MODEL, key="model_name",
|
|
108
|
+
help="Falls back to the model's best if this name is absent.",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
st.header("Input")
|
|
112
|
+
uploaded = st.file_uploader(
|
|
113
|
+
"Feature CSV", type=["csv"], accept_multiple_files=False, key="input",
|
|
114
|
+
)
|
|
115
|
+
output_dir = st.text_input("Output folder", value="", key="output_dir")
|
|
116
|
+
|
|
117
|
+
columns = _input_columns(uploaded)
|
|
118
|
+
has_input = uploaded is not None
|
|
119
|
+
input_has_sample = _has_sample(columns)
|
|
120
|
+
if has_input:
|
|
121
|
+
if input_has_sample:
|
|
122
|
+
st.caption(f"Input has a '{SAMPLE_COLUMN}' column, so the output is "
|
|
123
|
+
f"{SAMPLE_COLUMN} plus Incidence(%).")
|
|
124
|
+
elif columns is not None:
|
|
125
|
+
st.caption("No 'Sample' column, so the output is Incidence(%) only "
|
|
126
|
+
"(metadata cannot be attached).")
|
|
127
|
+
|
|
128
|
+
# Metadata attach is only available when the input carries a Sample column.
|
|
129
|
+
attach_file = None
|
|
130
|
+
if input_has_sample:
|
|
131
|
+
st.header("Attach metadata (needs a Sample column)")
|
|
132
|
+
st.caption(
|
|
133
|
+
"Joined on the 'Sample' key. The metadata CSV must also have a "
|
|
134
|
+
"'Sample' column; its other columns are appended to the output. "
|
|
135
|
+
"Example: examples/example_meta.csv."
|
|
136
|
+
)
|
|
137
|
+
attach_file = st.file_uploader(
|
|
138
|
+
"Metadata CSV (joined on Sample)", type=["csv"], key="attach",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
run_clicked = st.button("Run prediction", type="primary", width="stretch", key="run_btn")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# --------------------------------------------------------------------------- #
|
|
145
|
+
# Run
|
|
146
|
+
# --------------------------------------------------------------------------- #
|
|
147
|
+
def _run() -> None:
|
|
148
|
+
if not model_path:
|
|
149
|
+
st.error("Please provide the AutoGluon model directory.")
|
|
150
|
+
return
|
|
151
|
+
if not has_input:
|
|
152
|
+
st.error("Please upload a feature CSV.")
|
|
153
|
+
return
|
|
154
|
+
if not output_dir:
|
|
155
|
+
st.error("Please provide an output folder.")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
out = Path(output_dir)
|
|
159
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
161
|
+
bar = st.progress(0.0, text="Starting...")
|
|
162
|
+
|
|
163
|
+
def cb(stage: str, done: int, total: int, message: str) -> None:
|
|
164
|
+
label = _STAGE_LABELS.get(stage, stage)
|
|
165
|
+
if total and total > 0:
|
|
166
|
+
bar.progress(min(1.0, done / total), text=f"{label}: {message}")
|
|
167
|
+
else:
|
|
168
|
+
bar.progress(0.0, text=f"{label}: {message}")
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# The uploaded file is materialised into a throwaway temp dir (never the
|
|
172
|
+
# output folder), so only pred_<stem>.csv is left behind. The temp dir is
|
|
173
|
+
# removed as soon as the run finishes.
|
|
174
|
+
with tempfile.TemporaryDirectory(prefix="salmopredict_") as tmp:
|
|
175
|
+
tmpd = Path(tmp)
|
|
176
|
+
input_path = tmpd / uploaded.name
|
|
177
|
+
input_path.write_bytes(uploaded.getvalue())
|
|
178
|
+
|
|
179
|
+
attach_path = None
|
|
180
|
+
if input_has_sample and attach_file is not None:
|
|
181
|
+
attach_path = tmpd / attach_file.name
|
|
182
|
+
attach_path.write_bytes(attach_file.getvalue())
|
|
183
|
+
|
|
184
|
+
config = PredictConfig(
|
|
185
|
+
input_path=input_path,
|
|
186
|
+
output_dir=out,
|
|
187
|
+
model_path=Path(model_path),
|
|
188
|
+
ensemble_model=model_name or DEFAULT_ENSEMBLE_MODEL,
|
|
189
|
+
attach_metadata=attach_path,
|
|
190
|
+
force=True,
|
|
191
|
+
)
|
|
192
|
+
predictor = get_predictor(model_path)
|
|
193
|
+
with st.spinner("Running..."):
|
|
194
|
+
result = run_pipeline(config, progress=cb, predictor=predictor)
|
|
195
|
+
except Exception as exc: # surface any pipeline error in the UI
|
|
196
|
+
st.error(f"Run failed: {exc}")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
bar.progress(1.0, text="Complete")
|
|
200
|
+
st.session_state["result"] = result
|
|
201
|
+
st.success(
|
|
202
|
+
f"Done. {result.n_rows} rows, label={result.label}, model={result.model_used}."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if run_clicked:
|
|
207
|
+
_run()
|
|
208
|
+
|
|
209
|
+
# --------------------------------------------------------------------------- #
|
|
210
|
+
# Results
|
|
211
|
+
# --------------------------------------------------------------------------- #
|
|
212
|
+
result = st.session_state.get("result")
|
|
213
|
+
if result is not None:
|
|
214
|
+
st.subheader(result.input_path.name)
|
|
215
|
+
if result.warned:
|
|
216
|
+
st.warning(
|
|
217
|
+
f"{result.missing_fraction:.1%} of model features were missing and "
|
|
218
|
+
f"filled with the fill value; check the input matches this model."
|
|
219
|
+
)
|
|
220
|
+
if result.collisions:
|
|
221
|
+
st.warning(
|
|
222
|
+
f"{len(result.collisions)} normalised column-name collision(s): "
|
|
223
|
+
f"{', '.join(result.collisions)}"
|
|
224
|
+
)
|
|
225
|
+
st.dataframe(result.predictions, width="stretch", hide_index=True)
|
|
226
|
+
st.download_button(
|
|
227
|
+
f"Download pred_{result.input_path.stem}.csv",
|
|
228
|
+
result.predictions.to_csv(index=False).encode("utf-8"),
|
|
229
|
+
file_name=f"pred_{result.input_path.stem}.csv",
|
|
230
|
+
mime="text/csv",
|
|
231
|
+
key=f"dl_{result.input_path.stem}",
|
|
232
|
+
)
|
|
233
|
+
with st.expander(f"Imputed features ({len(result.imputed)}), filled with fill value"):
|
|
234
|
+
st.write(", ".join(result.imputed) if result.imputed else "None")
|
|
235
|
+
if result.capped or result.floored:
|
|
236
|
+
st.caption(
|
|
237
|
+
f"Clamped to [{PREDICTION_MIN:g}, {PREDICTION_MAX:g}]: "
|
|
238
|
+
f"{result.capped} capped at {PREDICTION_MAX:g}, "
|
|
239
|
+
f"{result.floored} floored at {PREDICTION_MIN:g}."
|
|
240
|
+
)
|
|
241
|
+
st.caption(f"Output columns: {', '.join(result.predictions.columns)}")
|
|
242
|
+
st.caption(f"Written to: {result.output_path}")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|