From db4fd0bb6e984e6317fe9202327ecce4d8c4ba8b Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Sun, 19 Apr 2026 06:24:49 +0100 Subject: [PATCH 1/8] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c97a2c..af8c688 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pySEQTarget" -version = "0.12.9" +version = "0.13.0" description = "Sequentially Nested Target Trial Emulation" readme = "README.md" license = {text = "MIT"} From 9e89e8509cb79857bbd9cce77d6e9f30e7cf84f1 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Sun, 19 Apr 2026 06:25:08 +0100 Subject: [PATCH 2/8] Preserve eligible_col during expansion when referenced by weight_eligible_colnames --- pySEQTarget/expansion/_binder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pySEQTarget/expansion/_binder.py b/pySEQTarget/expansion/_binder.py index bba0e45..3b5a723 100644 --- a/pySEQTarget/expansion/_binder.py +++ b/pySEQTarget/expansion/_binder.py @@ -89,10 +89,14 @@ def _binder(self, kept_cols): for c in baseline_cols ] + to_drop = [f"{self.eligible_col}{self.indicator_baseline}"] + if self.eligible_col not in kept_cols: + to_drop.append(self.eligible_col) + DT = ( DT.with_columns(bas) .filter(pl.col(f"{self.eligible_col}{self.indicator_baseline}") == 1) - .drop([f"{self.eligible_col}{self.indicator_baseline}", self.eligible_col]) + .drop(to_drop) ) # Truncate each (id, trial) at the first outcome event so that subjects who From 204285f0854ab5f904958a8368238c8cf4e2697d Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Sun, 19 Apr 2026 06:25:36 +0100 Subject: [PATCH 3/8] Validate weight_eligible_colnames entries exist in data upfront --- pySEQTarget/error/_data_checker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pySEQTarget/error/_data_checker.py b/pySEQTarget/error/_data_checker.py index e044233..15c0817 100644 --- a/pySEQTarget/error/_data_checker.py +++ b/pySEQTarget/error/_data_checker.py @@ -18,6 +18,10 @@ def _data_checker(self): for col in self.weight_eligible_colnames: if col is not None: + if col not in self.data.columns: + raise ValueError( + f"weight_eligible_colnames entry '{col}' not found in data columns." + ) _check_binary(self.data, col) check = self.data.group_by(self.id_col).agg( From 33fc7b7cd6ca5ae8f0a3f15763a8c921a43dd22f Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Sun, 19 Apr 2026 06:38:27 +0100 Subject: [PATCH 4/8] Skip bootstrap replicates that fail with singular Hessian --- pySEQTarget/helpers/_bootstrap.py | 45 ++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/pySEQTarget/helpers/_bootstrap.py b/pySEQTarget/helpers/_bootstrap.py index 2a1b319..b59fb61 100644 --- a/pySEQTarget/helpers/_bootstrap.py +++ b/pySEQTarget/helpers/_bootstrap.py @@ -1,5 +1,6 @@ import copy import time +import warnings from concurrent.futures import ProcessPoolExecutor, as_completed from functools import wraps @@ -103,7 +104,7 @@ def wrapper(self, *args, **kwargs): self.DT = None with ProcessPoolExecutor(max_workers=ncores) as executor: - futures = [ + futures = { executor.submit( _bootstrap_worker, self, @@ -113,13 +114,24 @@ def wrapper(self, *args, **kwargs): seed, args, kwargs, - ) + ): i for i in range(nboot) - ] + } + skipped = 0 for j in tqdm( as_completed(futures), total=nboot, desc="Bootstrapping..." ): - results.append(j.result()) + boot_idx = futures[j] + try: + results.append(j.result()) + except np.linalg.LinAlgError as e: + skipped += 1 + warnings.warn( + f"Bootstrap iteration {boot_idx + 1} failed " + f"({e}); skipping replicate.", + UserWarning, + stacklevel=2, + ) self._rng = original_rng self.DT = self._offloader.load_dataframe(original_DT_ref) @@ -131,6 +143,7 @@ def wrapper(self, *args, **kwargs): else: original_DT_ref = original_DT + skipped = 0 for i in tqdm(range(nboot), desc="Bootstrapping..."): self._current_boot_idx = i + 1 if seed is not None: @@ -140,12 +153,30 @@ def wrapper(self, *args, **kwargs): if self._offloader.enabled: del tmp self.bootstrap_nboot = 0 - boot_fit = method(self, *args, **kwargs) - results.append(boot_fit) + try: + boot_fit = method(self, *args, **kwargs) + results.append(boot_fit) + except np.linalg.LinAlgError as e: + skipped += 1 + warnings.warn( + f"Bootstrap iteration {i + 1} failed " + f"({e}); skipping replicate.", + UserWarning, + stacklevel=2, + ) - self.bootstrap_nboot = nboot self.DT = self._offloader.load_dataframe(original_DT_ref) + self.bootstrap_nboot = len(results) - 1 + if skipped > 0: + warnings.warn( + f"{skipped} of {nboot} bootstrap replicate(s) skipped due to " + "singular Hessian; effective bootstrap_nboot is " + f"{self.bootstrap_nboot}.", + UserWarning, + stacklevel=2, + ) + end = time.perf_counter() self._model_time = _format_time(start, end) From dc1d395f0a1dd9bdf03d9f61e926d867028963d0 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Sun, 19 Apr 2026 06:42:01 +0100 Subject: [PATCH 5/8] Silence non-interactive backend plt.show warnings in test suite --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index af8c688..88c7df9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,3 +83,6 @@ SEQdata = ["data/*.csv"] [tool.pytest.ini_options] pythonpath = ["."] testpaths = ["tests"] +filterwarnings = [ + "ignore:FigureCanvasAgg is non-interactive:UserWarning", +] From a97ef07dad32033d4611a09ae5ae50f8f2a9df87 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Sun, 19 Apr 2026 14:38:27 +0100 Subject: [PATCH 6/8] Bump astral-sh/setup-uv to v8.1.0 --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index b5429b8..9f8c443 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v6 - name: Install uv - uses: astral-sh/setup-uv@v8.0.0 + uses: astral-sh/setup-uv@v8.1.0 - name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }} From 06fc974b4b3bf57904f9e04a6a3c927bf5a135bd Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Sun, 19 Apr 2026 14:46:46 +0100 Subject: [PATCH 7/8] Suppress flake8 E402 warnings for imports that must follow matplotlib.use("Agg") --- tests/test_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_plot.py b/tests/test_plot.py index ce94eef..3c9a72d 100644 --- a/tests/test_plot.py +++ b/tests/test_plot.py @@ -7,8 +7,8 @@ matplotlib.use("Agg") # non-interactive backend — no windows opened -from pySEQTarget import SEQopts, SEQuential -from pySEQTarget.data import load_data +from pySEQTarget import SEQopts, SEQuential # noqa: E402 +from pySEQTarget.data import load_data # noqa: E402 @pytest.fixture(autouse=True) From b049a6ed2b2e6bd5e5f41e87778d6155f6935306 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Sun, 19 Apr 2026 14:48:40 +0100 Subject: [PATCH 8/8] Fix flake8 warning: remove unused warnings import --- pySEQTarget/helpers/_predict_model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pySEQTarget/helpers/_predict_model.py b/pySEQTarget/helpers/_predict_model.py index 41a35ba..7ccb3b1 100644 --- a/pySEQTarget/helpers/_predict_model.py +++ b/pySEQTarget/helpers/_predict_model.py @@ -1,5 +1,3 @@ -import warnings - import numpy as np from ._fix_categories import _fix_categories_for_predict