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.
Files changed (83) hide show
  1. salmopredict/__init__.py +30 -0
  2. salmopredict/cli.py +303 -0
  3. salmopredict/config.py +103 -0
  4. salmopredict/core/__init__.py +1 -0
  5. salmopredict/core/align.py +104 -0
  6. salmopredict/core/io_tables.py +76 -0
  7. salmopredict/core/modelinfo.py +71 -0
  8. salmopredict/core/predict.py +102 -0
  9. salmopredict/gui/__init__.py +1 -0
  10. salmopredict/gui/app.py +242 -0
  11. salmopredict/gui/assets/salmopredict_icon.png +0 -0
  12. salmopredict/gui/assets/vphs_logo.png +0 -0
  13. salmopredict/models/.DS_Store +0 -0
  14. salmopredict/models/model_default/learner.pkl +0 -0
  15. salmopredict/models/model_default/metadata.json +292 -0
  16. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F1/model-internals.pkl +0 -0
  17. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F1/model.pkl +0 -0
  18. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F2/model-internals.pkl +0 -0
  19. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F2/model.pkl +0 -0
  20. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F3/model-internals.pkl +0 -0
  21. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F3/model.pkl +0 -0
  22. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F4/model-internals.pkl +0 -0
  23. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F4/model.pkl +0 -0
  24. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F5/model-internals.pkl +0 -0
  25. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/S1F5/model.pkl +0 -0
  26. salmopredict/models/model_default/models/NeuralNetFastAI_r100_BAG_L1/model.pkl +0 -0
  27. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F1/model-internals.pkl +0 -0
  28. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F1/model.pkl +0 -0
  29. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F2/model-internals.pkl +0 -0
  30. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F2/model.pkl +0 -0
  31. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F3/model-internals.pkl +0 -0
  32. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F3/model.pkl +0 -0
  33. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F4/model-internals.pkl +0 -0
  34. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F4/model.pkl +0 -0
  35. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F5/model-internals.pkl +0 -0
  36. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/S1F5/model.pkl +0 -0
  37. salmopredict/models/model_default/models/NeuralNetFastAI_r134_BAG_L1/model.pkl +0 -0
  38. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F1/model-internals.pkl +0 -0
  39. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F1/model.pkl +0 -0
  40. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F2/model-internals.pkl +0 -0
  41. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F2/model.pkl +0 -0
  42. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F3/model-internals.pkl +0 -0
  43. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F3/model.pkl +0 -0
  44. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F4/model-internals.pkl +0 -0
  45. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F4/model.pkl +0 -0
  46. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F5/model-internals.pkl +0 -0
  47. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/S1F5/model.pkl +0 -0
  48. salmopredict/models/model_default/models/NeuralNetFastAI_r156_BAG_L1/model.pkl +0 -0
  49. salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F1/model.pkl +0 -0
  50. salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F2/model.pkl +0 -0
  51. salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F3/model.pkl +0 -0
  52. salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F4/model.pkl +0 -0
  53. salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/S1F5/model.pkl +0 -0
  54. salmopredict/models/model_default/models/NeuralNetTorch_r121_BAG_L1/model.pkl +0 -0
  55. salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F1/model.pkl +0 -0
  56. salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F2/model.pkl +0 -0
  57. salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F3/model.pkl +0 -0
  58. salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F4/model.pkl +0 -0
  59. salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/S1F5/model.pkl +0 -0
  60. salmopredict/models/model_default/models/NeuralNetTorch_r1_BAG_L1/model.pkl +0 -0
  61. salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F1/model.pkl +0 -0
  62. salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F2/model.pkl +0 -0
  63. salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F3/model.pkl +0 -0
  64. salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F4/model.pkl +0 -0
  65. salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/S1F5/model.pkl +0 -0
  66. salmopredict/models/model_default/models/NeuralNetTorch_r30_BAG_L1/model.pkl +0 -0
  67. salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F1/model.pkl +0 -0
  68. salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F2/model.pkl +0 -0
  69. salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F3/model.pkl +0 -0
  70. salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F4/model.pkl +0 -0
  71. salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/S1F5/model.pkl +0 -0
  72. salmopredict/models/model_default/models/NeuralNetTorch_r79_BAG_L1/model.pkl +0 -0
  73. salmopredict/models/model_default/models/WeightedEnsemble_L2/model.pkl +0 -0
  74. salmopredict/models/model_default/models/trainer.pkl +0 -0
  75. salmopredict/models/model_default/predictor.pkl +0 -0
  76. salmopredict/models/model_default/version.txt +1 -0
  77. salmopredict/pipeline.py +112 -0
  78. salmopredict-0.1.0.dist-info/METADATA +126 -0
  79. salmopredict-0.1.0.dist-info/RECORD +83 -0
  80. salmopredict-0.1.0.dist-info/WHEEL +5 -0
  81. salmopredict-0.1.0.dist-info/entry_points.txt +2 -0
  82. salmopredict-0.1.0.dist-info/licenses/LICENSE +202 -0
  83. 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."""
@@ -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