24
24
from causal_testing .testing .causal_test_result import CausalTestResult
25
25
from causal_testing .testing .estimators import Estimator
26
26
from causal_testing .testing .base_test_case import BaseTestCase
27
+ from causal_testing .testing .causal_test_adequacy import DataAdequacy
27
28
28
29
logger = logging .getLogger (__name__ )
29
30
@@ -66,9 +67,8 @@ def set_paths(self, json_path: str, dag_path: str, data_paths: list[str] = None)
66
67
data_paths = []
67
68
self .input_paths = JsonClassPaths (json_path = json_path , dag_path = dag_path , data_paths = data_paths )
68
69
69
- def setup (self , scenario : Scenario ):
70
+ def setup (self , scenario : Scenario , data = None ):
70
71
"""Function to populate all the necessary parts of the json_class needed to execute tests"""
71
- data = []
72
72
self .scenario = scenario
73
73
self ._get_scenario_variables ()
74
74
self .scenario .setup_treatment_variables ()
@@ -81,9 +81,9 @@ def setup(self, scenario: Scenario):
81
81
# Populate the data
82
82
if self .input_paths .data_paths :
83
83
data = pd .concat ([pd .read_csv (data_file , header = 0 ) for data_file in self .input_paths .data_paths ])
84
- if len (data ) == 0 :
84
+ if data is None or len (data ) == 0 :
85
85
raise ValueError (
86
- "No data found, either provide a path to a file containing data or manually populate the .data "
86
+ "No data found. Please either provide a path to a file containing data or manually populate the .data "
87
87
"attribute with a dataframe before calling .setup()"
88
88
)
89
89
self .data_collector = ObservationalDataCollector (self .scenario , data )
@@ -128,40 +128,20 @@ def run_json_tests(self, effects: dict, estimators: dict, f_flag: bool = False,
128
128
if "skip" in test and test ["skip" ]:
129
129
continue
130
130
test ["estimator" ] = estimators [test ["estimator" ]]
131
- if "mutations" in test :
131
+ # If we have specified concrete control and treatment value
132
+ if "mutations" not in test :
133
+ failed , msg = self ._run_concrete_metamorphic_test (test , f_flag , effects )
134
+ # If we have a variable to mutate
135
+ else :
132
136
if test ["estimate_type" ] == "coefficient" :
133
- msg = self ._run_coefficient_test (test = test , f_flag = f_flag , effects = effects )
137
+ failed , msg = self ._run_coefficient_test (test = test , f_flag = f_flag , effects = effects )
134
138
else :
135
- msg = self ._run_ate_test (test = test , f_flag = f_flag , effects = effects , mutates = mutates )
136
- self ._append_to_file (msg , logging .INFO )
137
- else :
138
- outcome_variable = next (
139
- iter (test ["expected_effect" ])
140
- ) # Take first key from dictionary of expected effect
141
- base_test_case = BaseTestCase (
142
- treatment_variable = self .variables ["inputs" ][test ["treatment_variable" ]],
143
- outcome_variable = self .variables ["outputs" ][outcome_variable ],
144
- )
145
-
146
- causal_test_case = CausalTestCase (
147
- base_test_case = base_test_case ,
148
- expected_causal_effect = effects [test ["expected_effect" ][outcome_variable ]],
149
- control_value = test ["control_value" ],
150
- treatment_value = test ["treatment_value" ],
151
- estimate_type = test ["estimate_type" ],
152
- )
153
-
154
- failed , _ = self ._execute_test_case (causal_test_case = causal_test_case , test = test , f_flag = f_flag )
155
-
156
- msg = (
157
- f"Executing concrete test: { test ['name' ]} \n "
158
- + f"treatment variable: { test ['treatment_variable' ]} \n "
159
- + f"outcome_variable = { outcome_variable } \n "
160
- + f"control value = { test ['control_value' ]} , treatment value = { test ['treatment_value' ]} \n "
161
- + f"Result: { 'FAILED' if failed else 'Passed' } "
162
- )
163
- print (msg )
164
- self ._append_to_file (msg , logging .INFO )
139
+ failed , msg = self ._run_metamorphic_tests (
140
+ test = test , f_flag = f_flag , effects = effects , mutates = mutates
141
+ )
142
+ test ["failed" ] = failed
143
+ test ["result" ] = msg
144
+ return self .test_plan ["tests" ]
165
145
166
146
def _run_coefficient_test (self , test : dict , f_flag : bool , effects : dict ):
167
147
"""Builds structures and runs test case for tests with an estimate_type of 'coefficient'.
@@ -183,18 +163,45 @@ def _run_coefficient_test(self, test: dict, f_flag: bool, effects: dict):
183
163
estimate_type = "coefficient" ,
184
164
effect_modifier_configuration = {self .scenario .variables [v ] for v in test .get ("effect_modifiers" , [])},
185
165
)
186
- result = self ._execute_test_case (causal_test_case = causal_test_case , test = test , f_flag = f_flag )
166
+ failed , result = self ._execute_test_case (causal_test_case = causal_test_case , test = test , f_flag = f_flag )
187
167
msg = (
188
168
f"Executing test: { test ['name' ]} \n "
189
169
+ f" { causal_test_case } \n "
190
170
+ " "
191
- + ("\n " ).join (str (result [ 1 ] ).split ("\n " ))
171
+ + ("\n " ).join (str (result ).split ("\n " ))
192
172
+ "==============\n "
193
- + f" Result: { 'FAILED' if result [0 ] else 'Passed' } "
173
+ + f" Result: { 'FAILED' if failed else 'Passed' } "
174
+ )
175
+ self ._append_to_file (msg , logging .INFO )
176
+ return failed , result
177
+
178
+ def _run_concrete_metamorphic_test (self , test : dict , f_flag : bool , effects : dict ):
179
+ outcome_variable = next (iter (test ["expected_effect" ])) # Take first key from dictionary of expected effect
180
+ base_test_case = BaseTestCase (
181
+ treatment_variable = self .variables ["inputs" ][test ["treatment_variable" ]],
182
+ outcome_variable = self .variables ["outputs" ][outcome_variable ],
194
183
)
195
- return msg
196
184
197
- def _run_ate_test (self , test : dict , f_flag : bool , effects : dict , mutates : dict ):
185
+ causal_test_case = CausalTestCase (
186
+ base_test_case = base_test_case ,
187
+ expected_causal_effect = effects [test ["expected_effect" ][outcome_variable ]],
188
+ control_value = test ["control_value" ],
189
+ treatment_value = test ["treatment_value" ],
190
+ estimate_type = test ["estimate_type" ],
191
+ )
192
+ failed , msg = self ._execute_test_case (causal_test_case = causal_test_case , test = test , f_flag = f_flag )
193
+
194
+ msg = (
195
+ f"Executing concrete test: { test ['name' ]} \n "
196
+ + f"treatment variable: { test ['treatment_variable' ]} \n "
197
+ + f"outcome_variable = { outcome_variable } \n "
198
+ + f"control value = { test ['control_value' ]} , treatment value = { test ['treatment_value' ]} \n "
199
+ + f"Result: { 'FAILED' if failed else 'Passed' } "
200
+ )
201
+ self ._append_to_file (msg , logging .INFO )
202
+ return failed , msg
203
+
204
+ def _run_metamorphic_tests (self , test : dict , f_flag : bool , effects : dict , mutates : dict ):
198
205
"""Builds structures and runs test case for tests with an estimate_type of 'ate'.
199
206
200
207
:param test: Single JSON test definition stored in a mapping (dict)
@@ -226,7 +233,8 @@ def _run_ate_test(self, test: dict, f_flag: bool, effects: dict, mutates: dict):
226
233
+ f" Number of concrete tests for test case: { str (len (concrete_tests ))} \n "
227
234
+ f" { failures } /{ len (concrete_tests )} failed for { test ['name' ]} "
228
235
)
229
- return msg
236
+ self ._append_to_file (msg , logging .INFO )
237
+ return failures , msg
230
238
231
239
def _execute_tests (self , concrete_tests , test , f_flag ):
232
240
failures = 0
@@ -265,9 +273,13 @@ def _execute_test_case(
265
273
causal_test_result = causal_test_case .execute_test (
266
274
estimator = estimation_model , data_collector = self .data_collector
267
275
)
268
-
269
276
test_passes = causal_test_case .expected_causal_effect .apply (causal_test_result )
270
277
278
+ if "coverage" in test and test ["coverage" ]:
279
+ adequacy_metric = DataAdequacy (causal_test_case , estimation_model , self .data_collector )
280
+ adequacy_metric .measure_adequacy ()
281
+ causal_test_result .adequacy = adequacy_metric
282
+
271
283
if causal_test_result .ci_low () is not None and causal_test_result .ci_high () is not None :
272
284
result_string = (
273
285
f"{ causal_test_result .ci_low ()} < { causal_test_result .test_value .value } < "
@@ -283,7 +295,6 @@ def _execute_test_case(
283
295
f"got { result_string } "
284
296
)
285
297
failed = True
286
- logger .warning (" FAILED- expected %s, got %s" , causal_test_case .expected_causal_effect , result_string )
287
298
return failed , causal_test_result
288
299
289
300
def _setup_test (self , causal_test_case : CausalTestCase , test : Mapping ) -> Estimator :
@@ -294,7 +305,6 @@ def _setup_test(self, causal_test_case: CausalTestCase, test: Mapping) -> Estima
294
305
data. Conditions should be in the query format detailed at
295
306
https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html
296
307
:returns:
297
- - causal_test_engine - Test Engine instance for the test being run
298
308
- estimation_model - Estimator instance for the test being run
299
309
"""
300
310
minimal_adjustment_set = self .causal_specification .causal_dag .identification (causal_test_case .base_test_case )
@@ -370,7 +380,6 @@ def get_args(test_args=None) -> argparse.Namespace:
370
380
parser .add_argument (
371
381
"--log_path" ,
372
382
help = "Specify a directory to change the location of the log file" ,
373
- default = "./json_frontend.log" ,
374
383
)
375
384
parser .add_argument (
376
385
"--data_path" ,
0 commit comments