Skip to content

Commit e6a19c7

Browse files
authored
Merge pull request #385 from Jake248Newman/main
SURE - Discovering causal structure
2 parents 9f06df2 + 67ca9ac commit e6a19c7

28 files changed

Lines changed: 1511 additions & 182 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,5 @@ temp/
6767

6868
# log files
6969
*.log
70+
71+
causal_test_results.json

causal_testing/__main__.py

Lines changed: 109 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
import logging
55
import os
66
import tempfile
7+
from importlib.metadata import entry_points
78
from pathlib import Path
89

10+
import networkx as nx
11+
import pandas as pd
12+
913
from causal_testing.testing.metamorphic_relation import generate_causal_tests
1014

1115
from .main import CausalTestingFramework, CausalTestingPaths, Command, parse_args, setup_logging
@@ -20,74 +24,113 @@ def main() -> None:
2024

2125
# Parse arguments
2226
args = parse_args()
23-
24-
if args.command == Command.GENERATE:
25-
logging.info("Generating causal tests")
26-
generate_causal_tests(
27-
args.dag_path,
28-
args.output,
29-
args.ignore_cycles,
30-
args.threads,
31-
effect_type=args.effect_type,
32-
estimate_type=args.estimate_type,
33-
estimator=args.estimator,
34-
skip=False,
35-
)
36-
logging.info("Causal test generation completed successfully")
37-
return
38-
3927
# Setup logging
40-
setup_logging(args.verbose)
41-
42-
# Create paths object
43-
paths = CausalTestingPaths(
44-
dag_path=args.dag_path,
45-
data_paths=args.data_paths,
46-
test_config_path=args.test_config,
47-
output_path=args.output,
48-
)
49-
50-
# Create and setup framework
51-
framework = CausalTestingFramework(paths, ignore_cycles=args.ignore_cycles, query=args.query)
52-
framework.setup()
53-
54-
# Load and run tests
55-
framework.load_tests()
56-
57-
if args.batch_size > 0:
58-
logging.info(f"Running tests in batches of size {args.batch_size}")
59-
with tempfile.TemporaryDirectory() as tmpdir:
60-
output_files = []
61-
for i, results in enumerate(
62-
framework.run_tests_in_batches(
63-
batch_size=args.batch_size,
64-
silent=args.silent,
65-
adequacy=args.adequacy,
66-
bootstrap_size=args.bootstrap_size,
28+
setup_logging(args.log_level)
29+
30+
match args.command:
31+
case Command.GENERATE:
32+
logging.info("Generating causal tests")
33+
generate_causal_tests(
34+
args.dag_path,
35+
args.output,
36+
args.ignore_cycles,
37+
args.threads,
38+
effect_type=args.effect_type,
39+
estimate_type=args.estimate_type,
40+
estimator=args.estimator,
41+
skip=False,
42+
)
43+
logging.info("Causal test generation completed successfully.")
44+
45+
case Command.DISCOVER:
46+
discover_map = {ff.name: ff for ff in entry_points(group="discovery")}
47+
if args.technique not in discover_map:
48+
raise ValueError(
49+
f"Unsupported technique {args.technique}. Supported: {sorted(discover_map)}. "
50+
"If you have implemented a custom technique, you will need to add this to your entrypoints via "
51+
"your pyproject.toml file."
6752
)
68-
):
69-
temp_file_path = os.path.join(tmpdir, f"output_{i}.json")
70-
framework.save_results(results, temp_file_path)
71-
output_files.append(temp_file_path)
72-
del results
73-
74-
# Now stitch the results together from the temporary files
75-
all_results = []
76-
for file_path in output_files:
77-
with open(file_path, "r", encoding="utf-8") as f:
78-
all_results.extend(json.load(f))
79-
80-
output_path = Path(args.output)
81-
output_path.parent.mkdir(parents=True, exist_ok=True)
82-
83-
with open(args.output, "w", encoding="utf-8") as f:
84-
json.dump(all_results, f, indent=4)
85-
else:
86-
logging.info("Running tests in regular mode")
87-
results = framework.run_tests(silent=args.silent, adequacy=args.adequacy, bootstrap_size=args.bootstrap_size)
88-
framework.save_results(results)
89-
90-
logging.info("Causal testing completed successfully.")
53+
kwargs = {}
54+
for argument in args.technique_kwargs:
55+
split = argument.split("=")
56+
if len(split) != 2:
57+
raise ValueError(f"Malformed argument {argument}. Should be specified as `arg_name=arg_value`")
58+
kwargs[split[0]] = split[1]
59+
60+
logging.info("Discovering causal structure")
61+
# Need to reset index to allow for multiple files having the same index (i.e. starting at zero).
62+
# Otherwise you end up with duplicate indices, which causes problems further down the line
63+
df = pd.concat([pd.read_csv(path) for path in args.data_paths]).reset_index()
64+
if args.variables:
65+
df = df[args.variables]
66+
67+
discover_class = discover_map[args.technique].load()
68+
discover = discover_class(
69+
df=df,
70+
exclude_edges=(
71+
list(nx.nx_pydot.read_dot(args.exclude_edges).edges()) if args.exclude_edges is not None else []
72+
),
73+
include_edges=(
74+
list(nx.nx_pydot.read_dot(args.include_edges).edges()) if args.include_edges is not None else []
75+
),
76+
alpha=args.alpha,
77+
**kwargs,
78+
)
79+
evolved_dag = discover.discover()
80+
discover.write_dot(evolved_dag, args.output)
81+
logging.info("Causal structure discovery completed successfully.")
82+
case Command.TEST:
83+
# Create paths object
84+
paths = CausalTestingPaths(
85+
dag_path=args.dag_path,
86+
data_paths=args.data_paths,
87+
test_config_path=args.test_config,
88+
output_path=args.output,
89+
)
90+
91+
# Create and setup framework
92+
framework = CausalTestingFramework(paths, ignore_cycles=args.ignore_cycles, query=args.query)
93+
framework.setup()
94+
95+
# Load and run tests
96+
framework.load_tests()
97+
98+
if args.batch_size > 0:
99+
logging.info(f"Running tests in batches of size {args.batch_size}")
100+
with tempfile.TemporaryDirectory() as tmpdir:
101+
output_files = []
102+
for i, results in enumerate(
103+
framework.run_tests_in_batches(
104+
batch_size=args.batch_size,
105+
silent=args.silent,
106+
adequacy=args.adequacy,
107+
bootstrap_size=args.bootstrap_size,
108+
)
109+
):
110+
temp_file_path = os.path.join(tmpdir, f"output_{i}.json")
111+
framework.save_results(results, temp_file_path)
112+
output_files.append(temp_file_path)
113+
del results
114+
115+
# Now stitch the results together from the temporary files
116+
all_results = []
117+
for file_path in output_files:
118+
with open(file_path, "r", encoding="utf-8") as f:
119+
all_results.extend(json.load(f))
120+
121+
output_path = Path(args.output)
122+
output_path.parent.mkdir(parents=True, exist_ok=True)
123+
124+
with open(args.output, "w", encoding="utf-8") as f:
125+
json.dump(all_results, f, indent=4)
126+
else:
127+
logging.info("Running tests in regular mode")
128+
results = framework.run_tests(
129+
silent=args.silent, adequacy=args.adequacy, bootstrap_size=args.bootstrap_size
130+
)
131+
framework.save_results(results)
132+
133+
logging.info("Causal testing completed successfully.")
91134

92135

93136
if __name__ == "__main__":

causal_testing/discovery/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)