From df535403331568af956c63dfc4f3473c48c79efd Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Thu, 5 Feb 2026 14:52:52 +0000 Subject: [PATCH 1/9] Monitor log hazard ratio instead of hazard ratio --- pySEQTarget/analysis/_hazard.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pySEQTarget/analysis/_hazard.py b/pySEQTarget/analysis/_hazard.py index 06200ba..06e4372 100644 --- a/pySEQTarget/analysis/_hazard.py +++ b/pySEQTarget/analysis/_hazard.py @@ -24,13 +24,13 @@ def _calculate_hazard(self): def _calculate_hazard_single(self, data, idx=None, val=None): - full_hr = _hazard_handler(self, data, idx, 0, self._rng) + full_log_hr = _hazard_handler(self, data, idx, 0, self._rng) - if full_hr is None or np.isnan(full_hr): + if full_log_hr is None or np.isnan(full_log_hr): return _create_hazard_output(None, None, None, val, self) if self.bootstrap_nboot > 0: - boot_hrs = [] + boot_log_hrs = [] for boot_idx in range(len(self._boot_samples)): id_counts = self._boot_samples[boot_idx] @@ -43,27 +43,27 @@ def _calculate_hazard_single(self, data, idx=None, val=None): boot_data = pl.concat(boot_data_list) - boot_hr = _hazard_handler(self, boot_data, idx, boot_idx + 1, self._rng) - if boot_hr is not None and not np.isnan(boot_hr): - boot_hrs.append(boot_hr) + boot_log_hr = _hazard_handler(self, boot_data, idx, boot_idx + 1, self._rng) + if boot_log_hr is not None and not np.isnan(boot_log_hr): + boot_log_hrs.append(boot_log_hr) - if len(boot_hrs) == 0: - return _create_hazard_output(full_hr, None, None, val, self) + if len(boot_log_hrs) == 0: + return _create_hazard_output(np.exp(full_log_hr), None, None, val, self) if self.bootstrap_CI_method == "se": from scipy.stats import norm z = norm.ppf(1 - (1 - self.bootstrap_CI) / 2) - se = np.std(boot_hrs) - lci = full_hr - z * se - uci = full_hr + z * se + se = np.std(boot_log_hrs) + lci = np.exp(full_log_hr - z * se) + uci = np.exp(full_log_hr + z * se) else: - lci = np.quantile(boot_hrs, (1 - self.bootstrap_CI) / 2) - uci = np.quantile(boot_hrs, 1 - (1 - self.bootstrap_CI) / 2) + lci = np.exp(np.quantile(boot_log_hrs, (1 - self.bootstrap_CI) / 2)) + uci = np.exp(np.quantile(boot_log_hrs, 1 - (1 - self.bootstrap_CI) / 2)) else: lci, uci = None, None - return _create_hazard_output(full_hr, lci, uci, val, self) + return _create_hazard_output(np.exp(full_log_hr), lci, uci, val, self) def _hazard_handler(self, data, idx, boot_idx, rng): @@ -191,8 +191,8 @@ def _hazard_handler(self, data, idx, boot_idx, rng): formula=f"`{self.treatment_col}{self.indicator_baseline}`", ) - hr = np.exp(cph.params_.values[0]) - return hr + log_hr = cph.params_.values[0] + return log_hr except Exception as e: print(f"Cox model fitting failed: {e}") return None From a627c954e85aa852f4b8b2c26737f7ec76b5ec9a Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Thu, 5 Feb 2026 14:54:34 +0000 Subject: [PATCH 2/9] Print Hazard ratio instead of Hazard --- pySEQTarget/analysis/_hazard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pySEQTarget/analysis/_hazard.py b/pySEQTarget/analysis/_hazard.py index 06e4372..05809df 100644 --- a/pySEQTarget/analysis/_hazard.py +++ b/pySEQTarget/analysis/_hazard.py @@ -202,13 +202,13 @@ def _create_hazard_output(hr, lci, uci, val, self): if lci is not None and uci is not None: output = pl.DataFrame( { - "Hazard": [hr if hr is not None else float("nan")], + "Hazard ratio": [hr if hr is not None else float("nan")], "LCI": [lci], "UCI": [uci], } ) else: - output = pl.DataFrame({"Hazard": [hr if hr is not None else float("nan")]}) + output = pl.DataFrame({"Hazard ratio": [hr if hr is not None else float("nan")]}) if val is not None: output = output.with_columns(pl.lit(val).alias(self.subgroup_colname)) From b3a6b90b03fea2004ac41147af1586ee60911ca6 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Thu, 5 Feb 2026 14:56:28 +0000 Subject: [PATCH 3/9] Bump version to 0.12.1 --- docs/conf.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ec9b34e..8f384a7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ version = importlib.metadata.version("pySEQTarget") if not version: - version = "0.12.0" + version = "0.12.1" sys.path.insert(0, os.path.abspath("../")) project = "pySEQTarget" diff --git a/pyproject.toml b/pyproject.toml index e81af2a..e25506c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pySEQTarget" -version = "0.12.0" +version = "0.12.1" description = "Sequentially Nested Target Trial Emulation" readme = "README.md" license = {text = "MIT"} From dba3a14c837e2b579ab6a441faef4f4a27bff5a0 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Thu, 5 Feb 2026 15:11:22 +0000 Subject: [PATCH 4/9] Make Python 3.11 the min version Don't test on Python 3.10 --- .github/workflows/python-app.yml | 2 +- pyproject.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index adb9362..e82e99a 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/pyproject.toml b/pyproject.toml index e25506c..2c35aed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,11 @@ description = "Sequentially Nested Target Trial Emulation" readme = "README.md" license = {text = "MIT"} keywords = ["causal inference", "sequential trial emulation", "target trial", "observational studies"] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", From 28c8256ae100cfd6c7add43d0fb1dc3d393c84f2 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Fri, 6 Feb 2026 10:15:59 +0000 Subject: [PATCH 5/9] Bump setup-python to v6 --- .github/workflows/autoformat.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/python-app.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/autoformat.yml b/.github/workflows/autoformat.yml index 2987bea..bf1064d 100644 --- a/.github/workflows/autoformat.yml +++ b/.github/workflows/autoformat.yml @@ -13,7 +13,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb96eef..4c0f474 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index e82e99a..b219d00 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} From 0cd8144906d1d9981cda711dcf9bbd27a02de11e Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Fri, 6 Feb 2026 10:24:13 +0000 Subject: [PATCH 6/9] Fix flake8 warnings in init files --- pySEQTarget/analysis/__init__.py | 28 ++++++++++++++++-------- pySEQTarget/error/__init__.py | 9 ++++++-- pySEQTarget/expansion/__init__.py | 18 +++++++++++----- pySEQTarget/helpers/__init__.py | 30 ++++++++++++++++++-------- pySEQTarget/initialization/__init__.py | 18 +++++++++++----- pySEQTarget/plot/__init__.py | 6 +++++- pySEQTarget/weighting/__init__.py | 30 ++++++++++++++++++-------- 7 files changed, 99 insertions(+), 40 deletions(-) diff --git a/pySEQTarget/analysis/__init__.py b/pySEQTarget/analysis/__init__.py index c5d0202..7b6673d 100644 --- a/pySEQTarget/analysis/__init__.py +++ b/pySEQTarget/analysis/__init__.py @@ -1,9 +1,19 @@ -from ._hazard import _calculate_hazard as _calculate_hazard -from ._outcome_fit import _outcome_fit as _outcome_fit -from ._risk_estimates import _risk_estimates as _risk_estimates -from ._subgroup_fit import _subgroup_fit as _subgroup_fit -from ._survival_pred import _calculate_survival as _calculate_survival -from ._survival_pred import _clamp as _clamp -from ._survival_pred import \ - _get_outcome_predictions as _get_outcome_predictions -from ._survival_pred import _pred_risk as _pred_risk +from ._hazard import _calculate_hazard +from ._outcome_fit import _outcome_fit +from ._risk_estimates import _risk_estimates +from ._subgroup_fit import _subgroup_fit +from ._survival_pred import _calculate_survival +from ._survival_pred import _clamp +from ._survival_pred import _get_outcome_predictions +from ._survival_pred import _pred_risk + +__all__ = [ + "_calculate_hazard", + "_outcome_fit", + "_risk_estimates", + "_subgroup_fit", + "_calculate_survival", + "_clamp", + "_get_outcome_predictions", + "_pred_risk", +] diff --git a/pySEQTarget/error/__init__.py b/pySEQTarget/error/__init__.py index 773be51..fb19cae 100644 --- a/pySEQTarget/error/__init__.py +++ b/pySEQTarget/error/__init__.py @@ -1,2 +1,7 @@ -from ._data_checker import _data_checker as _data_checker -from ._param_checker import _param_checker as _param_checker +from ._data_checker import _data_checker +from ._param_checker import _param_checker + +__all__ = [ + "_data_checker", + "_param_checker", +] diff --git a/pySEQTarget/expansion/__init__.py b/pySEQTarget/expansion/__init__.py index 1262af8..b11771b 100644 --- a/pySEQTarget/expansion/__init__.py +++ b/pySEQTarget/expansion/__init__.py @@ -1,5 +1,13 @@ -from ._binder import _binder as _binder -from ._diagnostics import _diagnostics as _diagnostics -from ._dynamic import _dynamic as _dynamic -from ._mapper import _mapper as _mapper -from ._selection import _random_selection as _random_selection +from ._binder import _binder +from ._diagnostics import _diagnostics +from ._dynamic import _dynamic +from ._mapper import _mapper +from ._selection import _random_selection + +__all__ = [ + "_binder", + "_diagnostics", + "_dynamic", + "_mapper", + "_random_selection", +] diff --git a/pySEQTarget/helpers/__init__.py b/pySEQTarget/helpers/__init__.py index 860e686..093470a 100644 --- a/pySEQTarget/helpers/__init__.py +++ b/pySEQTarget/helpers/__init__.py @@ -1,9 +1,21 @@ -from ._bootstrap import bootstrap_loop as bootstrap_loop -from ._col_string import _col_string as _col_string -from ._format_time import _format_time as _format_time -from ._offloader import Offloader as Offloader -from ._output_files import _build_md as _build_md -from ._output_files import _build_pdf as _build_pdf -from ._pad import _pad as _pad -from ._predict_model import _predict_model as _predict_model -from ._prepare_data import _prepare_data as _prepare_data +from ._bootstrap import bootstrap_loop +from ._col_string import _col_string +from ._format_time import _format_time +from ._offloader import Offloader +from ._output_files import _build_md +from ._output_files import _build_pdf +from ._pad import _pad +from ._predict_model import _predict_model +from ._prepare_data import _prepare_data + +__all__ = [ + "bootstrap_loop", + "_col_string", + "_format_time", + "Offloader", + "_build_md", + "_build_pdf", + "_pad", + "_predict_model", + "_prepare_data", +] diff --git a/pySEQTarget/initialization/__init__.py b/pySEQTarget/initialization/__init__.py index 4f026ca..d021d6c 100644 --- a/pySEQTarget/initialization/__init__.py +++ b/pySEQTarget/initialization/__init__.py @@ -1,5 +1,13 @@ -from ._censoring import _cense_denominator as _cense_denominator -from ._censoring import _cense_numerator as _cense_numerator -from ._denominator import _denominator as _denominator -from ._numerator import _numerator as _numerator -from ._outcome import _outcome as _outcome +from ._censoring import _cense_denominator +from ._censoring import _cense_numerator +from ._denominator import _denominator +from ._numerator import _numerator +from ._outcome import _outcome + +__all__ = [ + "_cense_denominator", + "_cense_numerator", + "_denominator", + "_numerator", + "_outcome", +] diff --git a/pySEQTarget/plot/__init__.py b/pySEQTarget/plot/__init__.py index f417f55..5fd1c53 100644 --- a/pySEQTarget/plot/__init__.py +++ b/pySEQTarget/plot/__init__.py @@ -1 +1,5 @@ -from ._survival_plot import _survival_plot as _survival_plot +from ._survival_plot import _survival_plot + +__all__ = [ + "_survival_plot", +] diff --git a/pySEQTarget/weighting/__init__.py b/pySEQTarget/weighting/__init__.py index 7c6e32d..daed4bc 100644 --- a/pySEQTarget/weighting/__init__.py +++ b/pySEQTarget/weighting/__init__.py @@ -1,9 +1,21 @@ -from ._weight_bind import _weight_bind as _weight_bind -from ._weight_data import _weight_setup as _weight_setup -from ._weight_fit import _fit_denominator as _fit_denominator -from ._weight_fit import _fit_LTFU as _fit_LTFU -from ._weight_fit import _fit_numerator as _fit_numerator -from ._weight_fit import _fit_visit as _fit_visit -from ._weight_offload import _offload_weights as _offload_weights -from ._weight_pred import _weight_predict as _weight_predict -from ._weight_stats import _weight_stats as _weight_stats +from ._weight_bind import _weight_bind +from ._weight_data import _weight_setup +from ._weight_fit import _fit_denominator +from ._weight_fit import _fit_LTFU +from ._weight_fit import _fit_numerator +from ._weight_fit import _fit_visit +from ._weight_offload import _offload_weights +from ._weight_pred import _weight_predict +from ._weight_stats import _weight_stats + +__all__ = [ + "_weight_bind", + "_weight_setup", + "_fit_denominator", + "_fit_LTFU", + "_fit_numerator", + "_fit_visit", + "_offload_weights", + "_weight_predict", + "_weight_stats", +] From 0006e4bd13adf304a7c94dec42aecd8f21d5625c Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Fri, 6 Feb 2026 10:29:20 +0000 Subject: [PATCH 7/9] Fix long line lengths --- pySEQTarget/SEQopts.py | 9 ++++++--- pySEQTarget/SEQoutput.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pySEQTarget/SEQopts.py b/pySEQTarget/SEQopts.py index 4293a99..fb6348e 100644 --- a/pySEQTarget/SEQopts.py +++ b/pySEQTarget/SEQopts.py @@ -19,7 +19,8 @@ class SEQopts: :type bootstrap_CI_method: str :param cense_colname: Column name for censoring effect (LTFU, etc.) :type cense_colname: str - :param cense_denominator: Override to specify denominator patsy formula for censoring models; "1" or "" indicate intercept only model + :param cense_denominator: Override to specify denominator patsy formula for + censoring models; "1" or "" indicate intercept only model :type cense_denominator: Optional[str] or None :param cense_numerator: Override to specify numerator patsy formula for censoring models :type cense_numerator: Optional[str] or None @@ -55,7 +56,8 @@ class SEQopts: :type km_curves: bool :param ncores: Number of cores to use if running in parallel :type ncores: int - :param numerator: Override to specify the outcome patsy formula for numerator models; "1" or "" indicate intercept only model + :param numerator: Override to specify the outcome patsy formula for + numerator models; "1" or "" indicate intercept only model :type numerator: str :param offload: Boolean to offload intermediate model data to disk :type offload: bool @@ -87,7 +89,8 @@ class SEQopts: :type trial_include: bool :param visit_colname: Column name specifying visit number :type visit_colname: str - :param weight_eligible_colnames: List of column names of length treatment_level to identify which rows are eligible for weight fitting + :param weight_eligible_colnames: List of column names of length + treatment_level to identify which rows are eligible for weight fitting :type weight_eligible_colnames: List[str] :param weight_fit_method: The fitting method to be used ["newton", "bfgs", "lbfgs", "nm"], default "newton" :type weight_fit_method: str diff --git a/pySEQTarget/SEQoutput.py b/pySEQTarget/SEQoutput.py index c1c4899..42a848e 100644 --- a/pySEQTarget/SEQoutput.py +++ b/pySEQTarget/SEQoutput.py @@ -102,7 +102,9 @@ def retrieve_data( ) -> pl.DataFrame: """ Getter for data stored within ``SEQoutput`` - :param type: Data which you would like to access, ['km_data', 'hazard', 'risk_ratio', 'risk_difference', 'unique_outcomes', 'nonunique_outcomes', 'unique_switches', 'nonunique_switches'] + :param type: Data which you would like to access, ['km_data', 'hazard', + 'risk_ratio', 'risk_difference', 'unique_outcomes', + 'nonunique_outcomes', 'unique_switches', 'nonunique_switches'] :type type: str """ match type: From b4110c172351ee5923db8c69d4191b9617d289e5 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Fri, 6 Feb 2026 10:43:18 +0000 Subject: [PATCH 8/9] flake8 complexity fix: use helper functions Move spline formula and cast_categories to helper functions --- pySEQTarget/analysis/_outcome_fit.py | 73 +++++++++++++++------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/pySEQTarget/analysis/_outcome_fit.py b/pySEQTarget/analysis/_outcome_fit.py index 7dff090..fc96669 100644 --- a/pySEQTarget/analysis/_outcome_fit.py +++ b/pySEQTarget/analysis/_outcome_fit.py @@ -5,6 +5,44 @@ import statsmodels.formula.api as smf +def _apply_spline_formula(formula, indicator_squared): + spline = "cr(followup, df=3)" + + formula = re.sub(r"(\w+)\s*\*\s*followup\b", rf"\1*{spline}", formula) + formula = re.sub(r"\bfollowup\s*\*\s*(\w+)", rf"{spline}*\1", formula) + formula = re.sub( + rf"\bfollowup{re.escape(indicator_squared)}\b", "", formula + ) + formula = re.sub(r"\bfollowup\b", "", formula) + + formula = re.sub(r"\s+", " ", formula) + formula = re.sub(r"\+\s*\+", "+", formula) + formula = re.sub(r"^\s*\+\s*|\s*\+\s*$", "", formula).strip() + + if formula: + return f"{formula} + I({spline}**2)" + return f"I({spline}**2)" + + +def _cast_categories(self, df_pd): + df_pd[self.treatment_col] = df_pd[self.treatment_col].astype("category") + tx_bas = f"{self.treatment_col}{self.indicator_baseline}" + df_pd[tx_bas] = df_pd[tx_bas].astype("category") + + if self.followup_class and not self.followup_spline: + df_pd["followup"] = df_pd["followup"].astype("category") + squared_col = f"followup{self.indicator_squared}" + if squared_col in df_pd.columns: + df_pd[squared_col] = df_pd[squared_col].astype("category") + + if self.fixed_cols: + for col in self.fixed_cols: + if col in df_pd.columns: + df_pd[col] = df_pd[col].astype("category") + + return df_pd + + def _outcome_fit( self, df: pl.DataFrame, @@ -23,41 +61,10 @@ def _outcome_fit( if self.method == "censoring": df = df.filter(pl.col("switch") != 1) - df_pd = df.to_pandas() - - df_pd[self.treatment_col] = df_pd[self.treatment_col].astype("category") - tx_bas = f"{self.treatment_col}{self.indicator_baseline}" - df_pd[tx_bas] = df_pd[tx_bas].astype("category") - - if self.followup_class and not self.followup_spline: - df_pd["followup"] = df_pd["followup"].astype("category") - squared_col = f"followup{self.indicator_squared}" - if squared_col in df_pd.columns: - df_pd[squared_col] = df_pd[squared_col].astype("category") + df_pd = _cast_categories(self, df.to_pandas()) if self.followup_spline: - spline = "cr(followup, df=3)" - - formula = re.sub(r"(\w+)\s*\*\s*followup\b", rf"\1*{spline}", formula) - formula = re.sub(r"\bfollowup\s*\*\s*(\w+)", rf"{spline}*\1", formula) - formula = re.sub( - rf"\bfollowup{re.escape(self.indicator_squared)}\b", "", formula - ) - formula = re.sub(r"\bfollowup\b", "", formula) - - formula = re.sub(r"\s+", " ", formula) - formula = re.sub(r"\+\s*\+", "+", formula) - formula = re.sub(r"^\s*\+\s*|\s*\+\s*$", "", formula).strip() - - if formula: - formula = f"{formula} + I({spline}**2)" - else: - formula = f"I({spline}**2)" - - if self.fixed_cols: - for col in self.fixed_cols: - if col in df_pd.columns: - df_pd[col] = df_pd[col].astype("category") + formula = _apply_spline_formula(formula, self.indicator_squared) full_formula = f"{outcome} ~ {formula}" From 688c1fc797f20af025b95f55c03571526c9bda0e Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Fri, 6 Feb 2026 10:45:14 +0000 Subject: [PATCH 9/9] flake8 complexity fix - use helper functions --- pySEQTarget/SEQopts.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pySEQTarget/SEQopts.py b/pySEQTarget/SEQopts.py index fb6348e..a6d3982 100644 --- a/pySEQTarget/SEQopts.py +++ b/pySEQTarget/SEQopts.py @@ -158,7 +158,7 @@ class SEQopts: weight_preexpansion: bool = False weighted: bool = False - def __post_init__(self): + def _validate_bools(self): bools = [ "excused", "followup_class", @@ -179,12 +179,11 @@ def __post_init__(self): if not isinstance(getattr(self, i), bool): raise TypeError(f"{i} must be a boolean value.") + def _validate_ranges(self): if not isinstance(self.bootstrap_nboot, int) or self.bootstrap_nboot < 0: raise ValueError("bootstrap_nboot must be a positive integer.") - if self.ncores < 1 or not isinstance(self.ncores, int): raise ValueError("ncores must be a positive integer.") - if not (0.0 <= self.bootstrap_sample <= 1.0): raise ValueError("bootstrap_sample must be between 0 and 1.") if not (0.0 < self.bootstrap_CI < 1.0): @@ -192,14 +191,15 @@ def __post_init__(self): if not (0.0 <= self.selection_sample <= 1.0): raise ValueError("selection_sample must be between 0 and 1.") + def _validate_choices(self): if self.plot_type not in ["risk", "survival", "incidence"]: raise ValueError( "plot_type must be either 'risk', 'survival', or 'incidence'." ) - if self.bootstrap_CI_method not in ["se", "percentile"]: raise ValueError("bootstrap_CI_method must be one of 'se' or 'percentile'") + def _normalize_formulas(self): for i in ( "covariates", "numerator", @@ -211,5 +211,11 @@ def __post_init__(self): if attr is not None and not isinstance(attr, list): setattr(self, i, "".join(attr.split())) + def __post_init__(self): + self._validate_bools() + self._validate_ranges() + self._validate_choices() + self._normalize_formulas() + if self.offload: os.makedirs(self.offload_dir, exist_ok=True)