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