Skip to content

Commit 5e0d3cc

Browse files
authored
Merge pull request #402 from CITCOM-project/jmafoster1/marie-kondo
Jmafoster1/marie kondo
2 parents 355f04d + e753b1e commit 5e0d3cc

60 files changed

Lines changed: 2240 additions & 3400 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,31 @@ repos:
33
rev: v6.0.0
44
hooks:
55
- id: trailing-whitespace
6-
files: ^causal_testing/
6+
files: ^(causal_testing|tests)/
77
- id: end-of-file-fixer
8-
files: ^causal_testing/
8+
files: ^(causal_testing|tests)/
99
- id: check-added-large-files
10-
files: ^causal_testing/
10+
files: ^(causal_testing|tests)/
1111
- id: check-merge-conflict
12-
files: ^causal_testing/
12+
files: ^(causal_testing|tests)/
1313
- id: debug-statements
14-
files: ^causal_testing/
14+
files: ^(causal_testing|tests)/
1515
- id: mixed-line-ending
16-
files: ^causal_testing/
16+
files: ^(causal_testing|tests)/
1717

1818
- repo: https://github.com/psf/black
1919
rev: 25.9.0
2020
hooks:
2121
- id: black
2222
args: ['--line-length=120', '--target-version=py311']
23-
files: ^causal_testing/
23+
files: ^(causal_testing|tests)/
2424

2525
- repo: https://github.com/pycqa/isort
2626
rev: 7.0.0
2727
hooks:
2828
- id: isort
2929
args: ['--profile', 'black', '--line-length', '120']
30-
files: ^causal_testing/
30+
files: ^(causal_testing|tests)/
3131

3232
- repo: local
3333
hooks:

causal_testing/__main__.py

Lines changed: 77 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
"""This module contains the main entrypoint functionality to the Causal Testing Framework."""
22

33
import argparse
4+
import json
45
import logging
56
from enum import Enum
67
from importlib.metadata import entry_points
78
from typing import Optional, Sequence
9+
from warnings import warn
810

911
import networkx as nx
1012
import pandas as pd
1113

1214
from causal_testing.causal_testing_framework import CausalTestingFramework, read_dataframe
13-
from causal_testing.testing.metamorphic_relation import generate_causal_tests
15+
from causal_testing.specification.causal_dag import CausalDAG
1416

1517
logger = logging.getLogger(__name__)
1618

@@ -23,6 +25,7 @@ class Command(Enum):
2325
TEST = "test"
2426
GENERATE = "generate"
2527
DISCOVER = "discover"
28+
EVALUATE = "evaluate"
2629

2730

2831
def setup_logging(level: str) -> None:
@@ -40,15 +43,6 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
4043
"A causal inference-driven framework for functional black-box testing of complex software.",
4144
)
4245

43-
main_parser.add_argument(
44-
"-l",
45-
"--log_level",
46-
default="WARNING",
47-
type=str.upper,
48-
choices=["NONE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
49-
help="Set the logging level (default: WARNING).",
50-
)
51-
5246
subparsers = main_parser.add_subparsers(
5347
help="The action you want to run - call `causal_testing {action} -h` for further details", dest="command"
5448
)
@@ -57,24 +51,6 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
5751
parser_generate = subparsers.add_parser(Command.GENERATE.value, help="Generate causal tests from a DAG")
5852
parser_generate.add_argument("-D", "--dag-path", help="Path to the DAG file (.dot)", required=True)
5953
parser_generate.add_argument("-o", "--output", help="Path for output file (.json)", required=True)
60-
parser_generate.add_argument(
61-
"-e",
62-
"--estimator",
63-
help="The name of the estimator class to use when evaluating tests (defaults to LinearRegressionEstimator)",
64-
default="LinearRegressionEstimator",
65-
)
66-
parser_generate.add_argument(
67-
"-T",
68-
"--effect-type",
69-
help="The effect type to estimate {direct, total}",
70-
default="direct",
71-
)
72-
parser_generate.add_argument(
73-
"-E",
74-
"--estimate-type",
75-
help="The estimate type to use when evaluating tests (defaults to coefficient)",
76-
default="coefficient",
77-
)
7854
parser_generate.add_argument(
7955
"-i", "--ignore-cycles", help="Ignore cycles in DAG", action="store_true", default=False
8056
)
@@ -87,11 +63,10 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
8763
parser_test.add_argument("-D", "--dag-path", help="Path to the DAG file (.dot)", required=True)
8864
parser_test.add_argument("-o", "--output", help="Path for output file (.json)", required=True)
8965
parser_test.add_argument("-i", "--ignore-cycles", help="Ignore cycles in DAG", action="store_true", default=False)
90-
parser_test.add_argument("-d", "--data-paths", help="Paths to data files (.csv)", nargs="+", required=True)
9166
parser_test.add_argument("-t", "--test-config", help="Path to test configuration file (.json)", required=True)
9267
parser_test.add_argument("-q", "--query", help="Query string to filter data (e.g. 'age > 18')", type=str)
9368
parser_test.add_argument(
94-
"-a", "--adequacy", help="Calculate causal test adequacy for each test case", action="store_true", default=False
69+
"-A", "--adequacy", help="Calculate causal test adequacy for each test case", action="store_true", default=False
9570
)
9671
parser_test.add_argument(
9772
"-b",
@@ -108,18 +83,35 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
10883
default=False,
10984
)
11085

86+
# DAG evaluation
87+
parser_evaluate = subparsers.add_parser(
88+
Command.EVALUATE.value, help="Evaluate how well a causal DAG fits a dataset"
89+
)
90+
parser_evaluate.add_argument("-D", "--dag-path", help="Path to the DAG file (.dot)", required=True)
91+
parser_evaluate.add_argument("-o", "--output", help="Path for output file (.csv)", required=True)
92+
parser_evaluate.add_argument(
93+
"-i", "--ignore-cycles", help="Ignore cycles in DAG", action="store_true", default=False
94+
)
95+
parser_evaluate.add_argument("-q", "--query", help="Query string to filter data (e.g. 'age > 18')", type=str)
96+
parser_evaluate.add_argument(
97+
"-b",
98+
"--adequacy-bootstrap-size",
99+
dest="bootstrap_size",
100+
help="Number of bootstrap samples for causal test adequacy. Defaults to 100",
101+
type=int,
102+
default=100,
103+
)
104+
parser_evaluate.add_argument(
105+
"-s",
106+
"--silent",
107+
action="store_true",
108+
help="Do not crash on error. If set to true, errors are recorded as test results.",
109+
default=False,
110+
)
111+
parser_evaluate.add_argument("-t", "--test-config", help="Path to test configuration file (.json)")
112+
111113
# Discovery
112114
parser_discover = subparsers.add_parser(Command.DISCOVER.value, help="Discover causal structures from data")
113-
parser_discover.add_argument("-d", "--data-paths", help="Paths to data files (.csv)", nargs="+", required=True)
114-
parser_discover.add_argument(
115-
"-a",
116-
"--alpha",
117-
help=(
118-
"The significance level of the confidence intervals used to determine causality. "
119-
"This should be a value between 0 and 1. Defaults to 0.05 for 95%% confidence intervals."
120-
),
121-
default=0.05,
122-
)
123115
parser_discover.add_argument(
124116
"-t",
125117
"--technique",
@@ -147,6 +139,26 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
147139
default=[],
148140
)
149141

142+
for parser in [parser_generate, parser_discover, parser_test, parser_evaluate]:
143+
parser.add_argument(
144+
"-l",
145+
"--log_level",
146+
default="WARNING",
147+
type=str.upper,
148+
choices=["NONE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
149+
help="Set the logging level (default: WARNING).",
150+
)
151+
parser.add_argument(
152+
"-a",
153+
"--alpha",
154+
help=(
155+
"The significance level of the confidence intervals used to determine causality. "
156+
"This should be a value between 0 and 1. Defaults to 0.05 for 95%% confidence intervals."
157+
),
158+
default=0.05,
159+
)
160+
parser.add_argument("-d", "--data-paths", help="Paths to data files (.csv)", nargs="+", required=True)
161+
150162
args = main_parser.parse_args(args)
151163

152164
# Assume the user wants test adequacy if they're setting bootstrap_size
@@ -175,16 +187,14 @@ def main() -> None:
175187
match args.command:
176188
case Command.GENERATE:
177189
logging.info("Generating causal tests")
178-
generate_causal_tests(
179-
args.dag_path,
180-
args.output,
181-
args.ignore_cycles,
182-
args.threads,
183-
effect_type=args.effect_type,
184-
estimate_type=args.estimate_type,
185-
estimator=args.estimator,
190+
df = pd.concat(read_dataframe(path) for path in args.data_paths)
191+
causal_dag = CausalDAG(args.dag_path, ignore_cycles=args.ignore_cycles, datatypes=df.dtypes)
192+
causal_tests = causal_dag.generate_causal_tests(
193+
threads=args.threads,
186194
skip=False,
187195
)
196+
with open(args.output, "w", encoding="utf-8") as f:
197+
json.dump({"tests": [test.to_dict() for test in causal_tests]}, f)
188198
logging.info("Causal test generation completed successfully.")
189199

190200
case Command.DISCOVER:
@@ -211,8 +221,8 @@ def main() -> None:
211221
# Drop unnamed columns
212222
unnamed_columns = [c for c in df.columns if c.startswith("Unnamed: ")]
213223
if unnamed_columns:
214-
logger.warning(f"Dropping unnamed columns: {unnamed_columns}")
215-
df = df.drop(unnamed_columns)
224+
warn(f"Dropping unnamed columns: {unnamed_columns}")
225+
df = df.drop(unnamed_columns, axis=1)
216226

217227
discover_class = discover_map[args.technique].load()
218228
discover = discover_class(
@@ -246,6 +256,23 @@ def main() -> None:
246256
framework.save_results(args.output)
247257

248258
logging.info("Causal testing completed successfully.")
259+
case Command.EVALUATE:
260+
# Create and setup framework
261+
framework = CausalTestingFramework()
262+
framework.load_data(args.data_paths, query=args.query)
263+
framework.load_dag(args.dag_path, args.ignore_cycles)
264+
framework.dag.datatypes = framework.df.dtypes
265+
266+
if args.test_config:
267+
framework.load_test_cases_from_json(args.test_config)
268+
else:
269+
framework.test_cases = framework.dag.generate_causal_tests()
270+
271+
logging.info("Running tests on entire dataset")
272+
results = framework.evaluate_dag(alpha=args.alpha, bootstrap_size=args.bootstrap_size)
273+
logging.info("Causal testing completed successfully.")
274+
logging.info("Running tests on bootstrap samples")
275+
results.to_csv(args.output)
249276

250277

251278
if __name__ == "__main__":

0 commit comments

Comments
 (0)