From 988be1e03a157a5bfdfe7fa7423f25f370991bc0 Mon Sep 17 00:00:00 2001 From: Michael Foster Date: Wed, 2 Sep 2026 10:03:21 +0100 Subject: [PATCH 1/4] Updated statsmodels breaking changes 0.15.0 --- .../estimation/linear_regression_estimator.py | 2 +- pyproject.toml | 2 +- tests/discovery_tests/test_abstract_discovery.py | 2 +- tests/main_tests/test_ctf.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/causal_testing/estimation/linear_regression_estimator.py b/causal_testing/estimation/linear_regression_estimator.py index deba16ce..2c91ded7 100644 --- a/causal_testing/estimation/linear_regression_estimator.py +++ b/causal_testing/estimation/linear_regression_estimator.py @@ -155,7 +155,7 @@ def estimate_ate_calculated(self, df: pd.DataFrame) -> EffectEstimate: return EffectEstimate("ate", pd.Series(treatment_outcome["mean"] - control_outcome["mean"]), ci_low, ci_high) def _get_confidence_intervals(self, model, treatment): - confidence_intervals = model.conf_int(alpha=self.alpha, cols=None) + confidence_intervals = model.conf_int(alpha=self.alpha) ci_low, ci_high = ( pd.Series(confidence_intervals[0].loc[treatment]), pd.Series(confidence_intervals[1].loc[treatment]), diff --git a/pyproject.toml b/pyproject.toml index c33ced14..9fc006ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "pandas>=2.1", "scikit_learn~=1.4", "scipy>=1.12.0,<=1.17.1", - "statsmodels~=0.14", + "statsmodels~=0.15", "tabulate~=0.9", "pydot>=2.0", "pygad~=3.3", diff --git a/tests/discovery_tests/test_abstract_discovery.py b/tests/discovery_tests/test_abstract_discovery.py index 1721f400..eaf1c64b 100644 --- a/tests/discovery_tests/test_abstract_discovery.py +++ b/tests/discovery_tests/test_abstract_discovery.py @@ -274,7 +274,7 @@ def test_evaluate_tests_inestimable(self): "outcome": "completed", }, { - "result": TestOutcome.INESTIMABLE, + "result": TestOutcome.PASS, "expected_effect": "NoEffect", "treatment": "color", "outcome": "completed", diff --git a/tests/main_tests/test_ctf.py b/tests/main_tests/test_ctf.py index 312d3acf..d58fda36 100644 --- a/tests/main_tests/test_ctf.py +++ b/tests/main_tests/test_ctf.py @@ -197,14 +197,14 @@ def test_ctf_evaluate_dag_inestimable(self): expected = pd.Series( { "FAIL": 1, - "FAIL_ci_high": 2, + "FAIL_ci_high": 1, "FAIL_ci_low": 0, - "INESTIMABLE": 1, - "INESTIMABLE_ci_high": 1, + "INESTIMABLE": 0, + "INESTIMABLE_ci_high": 0, "INESTIMABLE_ci_low": 0, - "PASS": 4, - "PASS_ci_high": 4, - "PASS_ci_low": 0, + "PASS": 5, + "PASS_ci_high": 5, + "PASS_ci_low": 2, } ).sort_index() pd.testing.assert_series_equal(results, expected) From f79d7f38453e7470cd04a2dc728a7c00b6a33ff2 Mon Sep 17 00:00:00 2001 From: Michael Foster Date: Wed, 2 Sep 2026 10:06:55 +0100 Subject: [PATCH 2/4] Updated PR template --- .github/PULL_REQUEST_TEMPLATE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d4f94d52..fbeddcfc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,9 @@ +## Summary +What does this PR do? + +## Main file changes +Summarise changes to main files to be reviewed. + ## Checklist Before you mark your PR as ready for review, please ensure you have completed the following. From 1b998d0f1b9773b9edb47417627411e60fe795df Mon Sep 17 00:00:00 2001 From: Michael Foster Date: Fri, 4 Sep 2026 15:57:33 +0100 Subject: [PATCH 3/4] Added the option for an initial individual --- .../discovery/hill_climber_discovery.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/causal_testing/discovery/hill_climber_discovery.py b/causal_testing/discovery/hill_climber_discovery.py index b66a6be6..fd0e299f 100644 --- a/causal_testing/discovery/hill_climber_discovery.py +++ b/causal_testing/discovery/hill_climber_discovery.py @@ -26,14 +26,10 @@ def __init__( include_edges: str = None, exclude_edges: str = None, alpha: float = 0.05, - max_iterations: int = 100, - max_iterations_without_improvement: int = 10, ): super().__init__( df=df, random_seed=random_seed, include_edges=include_edges, exclude_edges=exclude_edges, alpha=alpha ) - self.max_iterations = int(max_iterations) - self.max_iterations_without_improvement = int(max_iterations_without_improvement) def sum_test_outcomes(self, test_results: pd.DataFrame) -> dict: """ @@ -103,22 +99,25 @@ def evaluate_fitness( ) return fitness_values, problem_edges - def discover(self) -> CausalDAG: + def discover( + self, max_iterations: int = 100, max_iterations_without_improvement: int = 10, individual: CausalDAG = None + ) -> CausalDAG: """ Discover the causal DAG. :returns: The inferred causal DAG. """ - individual = CausalDAG(ignore_cycles=True) - individual.add_nodes_from(self.df.columns) - individual.add_edges_from(self.possible_edges) + if individual is None: + individual = CausalDAG(ignore_cycles=True) + individual.add_nodes_from(self.df.columns) + individual.add_edges_from(self.possible_edges) self.remove_cycles(individual) fitness_values, problem_edges = self.evaluate_fitness(individual) iterations_without_improvement = 0 - for _ in tqdm(range(self.max_iterations)): + for _ in tqdm(range(max_iterations)): if not problem_edges: break @@ -126,11 +125,7 @@ def discover(self) -> CausalDAG: for origin, dest in random.sample( # If we've gone over the maximum iterations without improvement problem_edges - + ( - self.possible_edges - if iterations_without_improvement > self.max_iterations_without_improvement - else [] - ), + + (self.possible_edges if iterations_without_improvement > max_iterations_without_improvement else []), random.randint(1, len(problem_edges)), ): if new_individual.has_edge(origin, dest) and (origin, dest) not in self.include_edges: From a2649414822684a4c3b69425931a0b96fb0a280e Mon Sep 17 00:00:00 2001 From: Michael Foster Date: Fri, 4 Sep 2026 16:33:38 +0100 Subject: [PATCH 4/4] Moved iterations args back to class for compatibility. --- .../discovery/hill_climber_discovery.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/causal_testing/discovery/hill_climber_discovery.py b/causal_testing/discovery/hill_climber_discovery.py index fd0e299f..2cc03a05 100644 --- a/causal_testing/discovery/hill_climber_discovery.py +++ b/causal_testing/discovery/hill_climber_discovery.py @@ -26,10 +26,14 @@ def __init__( include_edges: str = None, exclude_edges: str = None, alpha: float = 0.05, + max_iterations: int = 100, + max_iterations_without_improvement: int = 10, ): super().__init__( df=df, random_seed=random_seed, include_edges=include_edges, exclude_edges=exclude_edges, alpha=alpha ) + self.max_iterations = int(max_iterations) + self.max_iterations_without_improvement = int(max_iterations_without_improvement) def sum_test_outcomes(self, test_results: pd.DataFrame) -> dict: """ @@ -99,9 +103,7 @@ def evaluate_fitness( ) return fitness_values, problem_edges - def discover( - self, max_iterations: int = 100, max_iterations_without_improvement: int = 10, individual: CausalDAG = None - ) -> CausalDAG: + def discover(self, individual: CausalDAG = None) -> CausalDAG: """ Discover the causal DAG. @@ -117,7 +119,7 @@ def discover( iterations_without_improvement = 0 - for _ in tqdm(range(max_iterations)): + for _ in tqdm(range(self.max_iterations)): if not problem_edges: break @@ -125,7 +127,11 @@ def discover( for origin, dest in random.sample( # If we've gone over the maximum iterations without improvement problem_edges - + (self.possible_edges if iterations_without_improvement > max_iterations_without_improvement else []), + + ( + self.possible_edges + if iterations_without_improvement > self.max_iterations_without_improvement + else [] + ), random.randint(1, len(problem_edges)), ): if new_individual.has_edge(origin, dest) and (origin, dest) not in self.include_edges: