From 72ee866258e6374ba56f7d29a185a08813d23a95 Mon Sep 17 00:00:00 2001 From: DylanAdlard Date: Fri, 19 Dec 2025 12:39:21 +0200 Subject: [PATCH] verbose --- README.md | 16 +++------ demo_files/output.pdf | Bin 44306 -> 44088 bytes demo_files/output.txt | 15 ++++---- gui.py | 18 +++++++--- src/ecoff_fitter/cli.py | 4 +-- src/ecoff_fitter/core.py | 26 ++++++++++++++ src/ecoff_fitter/report.py | 30 ++++++++++++++-- tests/test_ecoff_fitter.py | 36 ++++++++++++++++++++ tests/test_report.py | 68 +++++++++++++++++++++++++++++++++---- 9 files changed, 180 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 548bf7a..02fcc36 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,9 @@ Demo input files are provided in `demo_files/` to illustrate basic use. ## 🛠 Installation -### Install from PyPI - -```bash -pip install ecoffitter -``` - ### Install from source -Assuming in project directory: +Assuming in project directory (after git cloning): ```bash pip install -e . @@ -127,8 +121,6 @@ If `--outfile` ends in `.txt`, the tool writes: - ECOFF estimate - fitted mean & variance (per component) - mixture weights (if multiple distributions) -- percentiles -- likelihood values ### 2. PDF Report @@ -137,8 +129,8 @@ If `--outfile` ends in `.pdf`, the tool writes: - histogram of observed MICs - fitted distribution curve(s) - ECOFF location marker -- table of model parameters -- censoring diagnostics +- fitted mean & variance (per component) +- mixture weights (if multiple distributions) ## 🚀 Command-Line Usage @@ -147,7 +139,7 @@ Once installed, you can call the CLI. Example using demo files: ```bash -ecoff-fitter --input demo_files/input.txt --params demo_files/params.txt --outfile demo_files/output.txt +ecoff-fitter --input demo_files/censored.txt --params demo_files/params.txt --outfile demo_files/output.txt ``` Instead of using a parameter file, you can also specify parameters directly. diff --git a/demo_files/output.pdf b/demo_files/output.pdf index 4ba591a18c56d01c8ff9a5a3e73fb630a54e2778..acee096c9370f4a78921eefc352842e18b0adb54 100644 GIT binary patch delta 14307 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 delta 14483 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

372w*os~1aCn}~5 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 diff --git a/demo_files/output.txt b/demo_files/output.txt index 837b7ad..d0b1ca3 100644 --- a/demo_files/output.txt +++ b/demo_files/output.txt @@ -1,6 +1,9 @@ -ECOFF: 0.29 -99th percentile: 0.29 -97.5th percentile: 0.22 -95th percentile: 0.17 -μ₁: 0.049303392476164935, σ₁: 2.1439340245815863 -μ₂: 14.283983662174911, σ₂: 1.354070842559145 + ECOFF: 0.3963 + log scale: -1.3354 + + Component 1: + μ = 0.0835 + σ (folds) = 1.9534 + Component 2: + μ = 3.8329 + σ (folds) = 1.4984 diff --git a/gui.py b/gui.py index 07b2547..92791a5 100644 --- a/gui.py +++ b/gui.py @@ -55,6 +55,16 @@ def __init__(self, root): self.percentile_entry.insert(0, "99") self.percentile_entry.pack(side="left", padx=10) + self.verbose_var = tk.BooleanVar(value=False) + verbose_frame = tk.Frame(root) + verbose_frame.pack(pady=5, anchor="w") + tk.Checkbutton( + verbose_frame, + text="model diagnostics", + variable=self.verbose_var, + ).pack(side="left") + + # OUTPUT FILE output_frame = tk.Frame(root) output_frame.pack(pady=5, anchor="w") @@ -125,6 +135,7 @@ def run_ecoff(self): tails = int(self.tails_entry.get()) if self.tails_entry.get().strip() else None percentile = float(self.percentile_entry.get()) outfile = self.output_entry.get() + verbose = self.verbose_var.get() if not input_file: messagebox.showerror("Error", "You must select an input MIC data file.") @@ -163,16 +174,15 @@ def run_ecoff(self): text = "ECOFF RESULTS\n=====================================\n\n" global_report = GenerateReport.from_fitter(global_fitter, global_result) + if len(individual_results) > 1: - - text += global_report.to_text("GLOBAL FIT") + text += global_report.to_text("GLOBAL FIT", verbose=verbose) text += "\nINDIVIDUAL FITS:\n-------------------------------------\n" - # Individual fits for name, (fitter, result) in individual_results.items(): rep = GenerateReport.from_fitter(fitter, result) - text += rep.to_text(label=name) + text += rep.to_text(label=name, verbose=verbose) if outfile: diff --git a/src/ecoff_fitter/cli.py b/src/ecoff_fitter/cli.py index 11dd165..ef7cd7a 100644 --- a/src/ecoff_fitter/cli.py +++ b/src/ecoff_fitter/cli.py @@ -117,13 +117,13 @@ def main(argv: Optional[List[str]] = None) -> None: global_report = GenerateReport.from_fitter(global_fitter, global_result) if len(individual_results) > 1: - text += global_report.to_text("GLOBAL FIT") + text += global_report.to_text("GLOBAL FIT", verbose=args.verbose) text += "\nINDIVIDUAL FITS:\n-------------------------------------\n" # Individual fits for name, (fitter, result) in individual_results.items(): rep = GenerateReport.from_fitter(fitter, result) - text += rep.to_text(label=name) + text += rep.to_text(label=name, verbose=args.verbose) if args.outfile: validate_output_path(args.outfile) diff --git a/src/ecoff_fitter/core.py b/src/ecoff_fitter/core.py index 0b30c3f..33406db 100644 --- a/src/ecoff_fitter/core.py +++ b/src/ecoff_fitter/core.py @@ -326,3 +326,29 @@ def compute_ecoff(self, percentile: float) -> Tuple[Any, ...]: mus_sigmas.extend([mus[k], sigmas[k]]) return (ecoff, z_percentile, *mus_sigmas) + + def model_summary(self) -> dict[str, Any]: + K = self.distributions + n = self.obj_df.observations.sum() + wt_idx = int(np.argmin(self.mus_)) + + summary = { + "model_family": "interval-censored log-normal", + "model_type": "mixture" if K > 1 else "single", + "components": K, + "wild_type_component": wt_idx + 1, + "dilution_factor": self.dilution_factor, + "boundary_support": self.boundary_support, + "n_observations": n, + "log_likelihood": self.loglike_, + "converged": self.converged_, + "n_iter": self.n_iter_, + "pis": self.pis_, + } + + # Information criteria (if identifiable) + k_params = 2 * K + (K - 1) + summary["aic"] = 2 * k_params - 2 * self.loglike_ + summary["bic"] = np.log(n) * k_params - 2 * self.loglike_ + + return summary diff --git a/src/ecoff_fitter/report.py b/src/ecoff_fitter/report.py index 08515b4..e24bc75 100644 --- a/src/ecoff_fitter/report.py +++ b/src/ecoff_fitter/report.py @@ -108,10 +108,10 @@ def to_text(self, label: str | None = None, verbose: bool = False) -> str: lines.append(sigma_line) # Verbose model details - if verbose and self.model is not None: + if verbose: lines.append("") lines.append("--- Model details ---") - lines.append(str(self.model)) + lines.extend(self._format_model_summary()) return "\n".join(lines) + "\n" @@ -175,6 +175,28 @@ def _make_pdf(self, title: Optional[str] = None) -> Figure: fig.tight_layout(rect=(0, 0, 1, 0.95)) return fig + + def _format_model_summary(self) -> list[str]: + """ + Format a structured model summary from the fitter, if available. + """ + if not hasattr(self.fitter, "model_summary"): + return [" (model summary unavailable)"] + + summary = self.fitter.model_summary() + lines: list[str] = [] + + for key, value in summary.items(): + if value is None: + continue + + # Format arrays nicely + if isinstance(value, np.ndarray): + value = ", ".join(f"{v:.4f}" for v in value) + + lines.append(f" {key.replace('_', ' ')}: {value}") + + return lines class CombinedReport: @@ -212,13 +234,13 @@ def write_out(self) -> None: lines.append(f"\n===== INDIVIDUAL FIT: {name} =====") lines.append(report.to_text(label=name)) - # Join and write file text = "\n".join(lines) with open(self.outfile, "w") as f: f.write(text) print(f"\nCombined text report saved to: {self.outfile}") + def save_pdf(self) -> None: from matplotlib.backends.backend_pdf import PdfPages @@ -239,3 +261,5 @@ def save_pdf(self) -> None: print(f"Combined PDF saved to {self.outfile}") print(f"Combined PDF saved to {self.outfile}") + + diff --git a/tests/test_ecoff_fitter.py b/tests/test_ecoff_fitter.py index 192dc44..dee18e7 100644 --- a/tests/test_ecoff_fitter.py +++ b/tests/test_ecoff_fitter.py @@ -236,3 +236,39 @@ def fit(self, **kwargs): assert fitter.converged_ assert len(fitter.mus_) == 2 assert np.isfinite(fitter.mus_[0]) + +def test_model_summary_basic(simple_data): + """ + model_summary() should return a structured, self-consistent summary + after fitting. + """ + fitter = ECOFFitter(simple_data, distributions=2) + fitter.fit() + + summary = fitter.model_summary() + + # basic structure + assert isinstance(summary, dict) + + # required keys + required_keys = { + "model_family", + "model_type", + "components", + "wild_type_component", + "n_observations", + "log_likelihood", + "converged", + "pis", + "aic", + "bic", + } + assert required_keys.issubset(summary.keys()) + + # internal consistency + assert summary["components"] == fitter.distributions + assert summary["n_observations"] == fitter.obj_df.observations.sum() + assert np.isclose(np.sum(summary["pis"]), 1.0) + assert summary["converged"] is True + assert summary["bic"] >= summary["aic"] + diff --git a/tests/test_report.py b/tests/test_report.py index 0d8714c..007037f 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -2,7 +2,7 @@ import sys import pytest from unittest.mock import MagicMock, patch, call, ANY - +import numpy as np import ecoff_fitter.cli as cli import ecoff_fitter.report as report @@ -205,15 +205,29 @@ def test_generate_report_to_text_two_dist_verbose(): fitter.dilution_factor = 2 fitter.mus_ = [1, 2] fitter.sigmas_ = [0.2, 0.5] - fitter.model_ = "FAKE_MODEL_DETAILS" - r = cli.GenerateReport(fitter=fitter, ecoff=4.0, z=(10, 8, 6)) + fitter.model_summary.return_value = { + "model_family": "interval-censored log-normal", + "model_type": "mixture", + "components": 2, + "wild_type_component": 1, + "n_observations": 100, + "log_likelihood": -123.45, + "converged": True, + "pis": [0.7, 0.3], + "aic": 260.9, + "bic": 270.1, + } + + r = report.GenerateReport(fitter=fitter, ecoff=4.0, z=(10, 8, 6)) text = r.to_text(verbose=True) assert "Component 1" in text assert "Component 2" in text assert "--- Model details ---" in text - assert "FAKE_MODEL_DETAILS" in text + assert "model family: interval-censored log-normal" in text + assert "components: 2" in text + def test_combined_report_write_out(tmp_path): # CombinedReport now writes to its outfile @@ -235,8 +249,7 @@ def test_combined_report_write_out(tmp_path): individual_reports={"A": report_A, "B": report_B}, ) - # Act - combined.write_out() # NO ARGUMENT NOW + combined.write_out() text = outfile.read_text() @@ -253,3 +266,46 @@ def test_combined_report_write_out(tmp_path): report_A.to_text.assert_called_with(label="A") report_B.to_text.assert_called_with(label="B") + +def test_generate_report_format_model_summary(): + """ + _format_model_summary() should format key/value pairs from + fitter.model_summary() into readable text lines. + """ + fitter = MagicMock() + fitter.model_summary.return_value = { + "model_family": "interval-censored log-normal", + "model_type": "mixture", + "components": 2, + "wild_type_component": 1, + "n_observations": 100, + "log_likelihood": -123.45, + "converged": True, + "pis": np.array([0.7, 0.3]), + "aic": 260.9, + "bic": 270.1, + } + + r = report.GenerateReport( + fitter=fitter, + ecoff=4.0, + z=(10, 8, 6), + ) + + lines = r._format_model_summary() + + # basic structure + assert isinstance(lines, list) + assert all(isinstance(line, str) for line in lines) + + # content checks (formatted, not raw) + assert any("model family: interval-censored log-normal" in line for line in lines) + assert any("model type: mixture" in line for line in lines) + assert any("components: 2" in line for line in lines) + assert any("wild type component: 1" in line for line in lines) + assert any("log likelihood: -123.45" in line for line in lines) + assert any("converged: True" in line for line in lines) + assert any("pis: 0.7000, 0.3000" in line for line in lines) + assert any("aic: 260.9" in line for line in lines) + assert any("bic: 270.1" in line for line in lines) +