8
8
from collections .abc import Iterable , Mapping
9
9
from dataclasses import dataclass
10
10
from pathlib import Path
11
+ from statistics import StatisticsError
11
12
12
13
import pandas as pd
13
14
import scipy
@@ -43,14 +44,15 @@ class JsonUtility:
43
44
:attr {CausalSpecification} causal_specification:
44
45
"""
45
46
46
- def __init__ (self , log_path ):
47
- self .paths = None
47
+ def __init__ (self , output_path : str , output_overwrite : bool = False ):
48
+ self .input_paths = None
48
49
self .variables = None
49
50
self .data = []
50
51
self .test_plan = None
51
52
self .scenario = None
52
53
self .causal_specification = None
53
- self .setup_logger (log_path )
54
+ self .output_path = Path (output_path )
55
+ self .check_file_exists (self .output_path , output_overwrite )
54
56
55
57
def set_paths (self , json_path : str , dag_path : str , data_paths : str ):
56
58
"""
@@ -59,14 +61,14 @@ def set_paths(self, json_path: str, dag_path: str, data_paths: str):
59
61
:param dag_path: string path representation to the .dot file containing the Causal DAG
60
62
:param data_paths: string path representation to the data files
61
63
"""
62
- self .paths = JsonClassPaths (json_path = json_path , dag_path = dag_path , data_paths = data_paths )
64
+ self .input_paths = JsonClassPaths (json_path = json_path , dag_path = dag_path , data_paths = data_paths )
63
65
64
66
def setup (self , scenario : Scenario ):
65
67
"""Function to populate all the necessary parts of the json_class needed to execute tests"""
66
68
self .scenario = scenario
67
69
self .scenario .setup_treatment_variables ()
68
70
self .causal_specification = CausalSpecification (
69
- scenario = self .scenario , causal_dag = CausalDAG (self .paths .dag_path )
71
+ scenario = self .scenario , causal_dag = CausalDAG (self .input_paths .dag_path )
70
72
)
71
73
self ._json_parse ()
72
74
self ._populate_metas ()
@@ -104,12 +106,16 @@ def generate_tests(self, effects: dict, mutates: dict, estimators: dict, f_flag:
104
106
abstract_test = self ._create_abstract_test_case (test , mutates , effects )
105
107
106
108
concrete_tests , dummy = abstract_test .generate_concrete_tests (5 , 0.05 )
107
- logger .info ("Executing test: %s" , test ["name" ])
108
- logger .info (abstract_test )
109
- logger .info ([abstract_test .treatment_variable .name , abstract_test .treatment_variable .distribution ])
110
- logger .info ("Number of concrete tests for test case: %s" , str (len (concrete_tests )))
111
109
failures = self ._execute_tests (concrete_tests , estimators , test , f_flag )
112
- logger .info ("%s/%s failed for %s\n " , failures , len (concrete_tests ), test ["name" ])
110
+ msg = (
111
+ f"Executing test: { test ['name' ]} \n "
112
+ + "abstract_test \n "
113
+ + f"{ abstract_test } \n "
114
+ + f"{ abstract_test .treatment_variable .name } ,{ abstract_test .treatment_variable .distribution } \n "
115
+ + f"Number of concrete tests for test case: { str (len (concrete_tests ))} \n "
116
+ + f"{ failures } /{ len (concrete_tests )} failed for { test ['name' ]} "
117
+ )
118
+ self ._append_to_file (msg , logging .INFO )
113
119
114
120
def _execute_tests (self , concrete_tests , estimators , test , f_flag ):
115
121
failures = 0
@@ -122,9 +128,9 @@ def _execute_tests(self, concrete_tests, estimators, test, f_flag):
122
128
123
129
def _json_parse (self ):
124
130
"""Parse a JSON input file into inputs, outputs, metas and a test plan"""
125
- with open (self .paths .json_path , encoding = "utf-8" ) as f :
131
+ with open (self .input_paths .json_path , encoding = "utf-8" ) as f :
126
132
self .test_plan = json .load (f )
127
- for data_file in self .paths .data_paths :
133
+ for data_file in self .input_paths .data_paths :
128
134
df = pd .read_csv (data_file , header = 0 )
129
135
self .data .append (df )
130
136
self .data = pd .concat (self .data )
@@ -141,7 +147,7 @@ def _populate_metas(self):
141
147
fitter .fit ()
142
148
(dist , params ) = list (fitter .get_best (method = "sumsquare_error" ).items ())[0 ]
143
149
var .distribution = getattr (scipy .stats , dist )(** params )
144
- logger . info (var .name + f" { dist } ({ params } )" )
150
+ self . _append_to_file (var .name + f" { dist } ({ params } )" , logging . INFO )
145
151
146
152
def _execute_test_case (self , causal_test_case : CausalTestCase , test : Iterable [Mapping ], f_flag : bool ) -> bool :
147
153
"""Executes a singular test case, prints the results and returns the test case result
@@ -169,12 +175,13 @@ def _execute_test_case(self, causal_test_case: CausalTestCase, test: Iterable[Ma
169
175
)
170
176
else :
171
177
result_string = f"{ causal_test_result .test_value .value } no confidence intervals"
172
- if f_flag :
173
- assert test_passes , (
174
- f"{ causal_test_case } \n FAILED - expected { causal_test_case .expected_causal_effect } , "
175
- f"got { result_string } "
176
- )
178
+
177
179
if not test_passes :
180
+ if f_flag :
181
+ raise StatisticsError (
182
+ f"{ causal_test_case } \n FAILED - expected { causal_test_case .expected_causal_effect } , "
183
+ f"got { result_string } "
184
+ )
178
185
failed = True
179
186
logger .warning (" FAILED- expected %s, got %s" , causal_test_case .expected_causal_effect , result_string )
180
187
return failed
@@ -218,15 +225,34 @@ def _setup_test(self, causal_test_case: CausalTestCase, test: Mapping) -> tuple[
218
225
219
226
return causal_test_engine , estimation_model
220
227
228
+
229
+
230
+ def _append_to_file (self , line : str , log_level : int = None ):
231
+ """Appends given line(s) to the current output file. If log_level is specified it also logs that message to the
232
+ logging level.
233
+ :param line: The line or lines of text to be appended to the file
234
+ :param log_level: An integer representing the logging level as specified by pythons inbuilt logging module. It
235
+ is possible to use the inbuilt logging level variables such as logging.INFO and logging.WARNING
236
+ """
237
+ with open (self .output_path , "a" , encoding = "utf-8" ) as f :
238
+ f .write (
239
+ line + "\n " ,
240
+ )
241
+ if log_level :
242
+ logger .log (level = log_level , msg = line )
243
+
221
244
@staticmethod
222
- def setup_logger (log_path : str ):
223
- """Setups up logging instance for the module and adds a FileHandler stream so all stdout prints are also
224
- sent to the logfile
225
- :param log_path: Path specifying location and name of the logging file to be used
245
+ def check_file_exists (output_path : Path , overwrite : bool ):
246
+ """Method that checks if the given path to an output file already exists. If overwrite is true the check is
247
+ passed.
248
+ :param output_path: File path for the output file of the JSON Frontend
249
+ :param overwrite: bool that if true, the current file can be overwritten
226
250
"""
227
- setup_log = logging .getLogger (__name__ )
228
- file_handler = logging .FileHandler (Path (log_path ))
229
- setup_log .addHandler (file_handler )
251
+ if output_path .is_file ():
252
+ if overwrite :
253
+ output_path .unlink ()
254
+ else :
255
+ raise FileExistsError (f"Chosen file output ({ output_path } ) already exists" )
230
256
231
257
@staticmethod
232
258
def get_args (test_args = None ) -> argparse .Namespace :
@@ -242,6 +268,12 @@ def get_args(test_args=None) -> argparse.Namespace:
242
268
help = "if included, the script will stop if a test fails" ,
243
269
action = "store_true" ,
244
270
)
271
+ parser .add_argument (
272
+ "-w" ,
273
+ help = "Specify to overwrite any existing output files. This can lead to the loss of existing outputs if not "
274
+ "careful" ,
275
+ action = "store_true" ,
276
+ )
245
277
parser .add_argument (
246
278
"--log_path" ,
247
279
help = "Specify a directory to change the location of the log file" ,
0 commit comments