Skip to content

Commit 33c0b2c

Browse files
committed
modular unit tests
1 parent c9d1d1e commit 33c0b2c

25 files changed

+549
-426
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
- name: Test with pytest
4141
run: |
4242
cd tests
43-
python global-unit-test.py
43+
python -m pytest . -s --disable-warnings
4444
linting:
4545
needs: unit-tests
4646

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
test:
2-
cd tests && python global-unit-test.py
2+
cd tests && python -m pytest . -s --disable-warnings
33

44
lint:
55
python -m pylint chefboost/ --fail-under=10

chefboost/Chefboost.py

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def fit(
2424
config: Optional[dict] = None,
2525
target_label: str = "Decision",
2626
validation_df: Optional[pd.DataFrame] = None,
27+
silent: bool = False,
2728
) -> Dict[str, Any]:
2829
"""
2930
Build (a) decision tree model(s)
@@ -55,6 +56,9 @@ def fit(
5556
if nothing is passed to validation data frame, then the function validates
5657
built trees for training data frame
5758
59+
silent (bool): set this to True if you do not want to see
60+
any informative logs
61+
5862
Returns:
5963
chefboost model
6064
"""
@@ -139,7 +143,8 @@ def fit(
139143

140144
if enableParallelism == True:
141145
num_cores = config["num_cores"]
142-
logger.info(f"[INFO]: {num_cores} CPU cores will be allocated in parallel running")
146+
if silent is False:
147+
logger.info(f"[INFO]: {num_cores} CPU cores will be allocated in parallel running")
143148

144149
from multiprocessing import set_start_method, freeze_support
145150

@@ -169,7 +174,8 @@ def fit(
169174
config["algorithm"] = "Regression"
170175

171176
if enableGBM == True:
172-
logger.info("Gradient Boosting Machines...")
177+
if silent is False:
178+
logger.info("Gradient Boosting Machines...")
173179
algorithm = "Regression"
174180
config["algorithm"] = "Regression"
175181

@@ -184,7 +190,8 @@ def fit(
184190

185191
# -------------------------
186192

187-
logger.info(f"{algorithm} tree is going to be built...")
193+
if silent is False:
194+
logger.info(f"{algorithm} tree is going to be built...")
188195

189196
# initialize a dictionary. this is going to be used to check features numeric or nominal.
190197
# numeric features should be transformed to nominal values based on scales.
@@ -212,7 +219,13 @@ def fit(
212219

213220
if enableAdaboost == True:
214221
trees, alphas = adaboost_clf.apply(
215-
df, config, header, dataset_features, validation_df=validation_df, process_id=process_id
222+
df,
223+
config,
224+
header,
225+
dataset_features,
226+
validation_df=validation_df,
227+
process_id=process_id,
228+
silent=silent,
216229
)
217230

218231
elif enableGBM == True:
@@ -224,6 +237,7 @@ def fit(
224237
dataset_features,
225238
validation_df=validation_df,
226239
process_id=process_id,
240+
silent=silent,
227241
)
228242
# classification = True
229243

@@ -235,12 +249,19 @@ def fit(
235249
dataset_features,
236250
validation_df=validation_df,
237251
process_id=process_id,
252+
silent=silent,
238253
)
239254
# classification = False
240255

241256
elif enableRandomForest == True:
242257
trees = randomforest.apply(
243-
df, config, header, dataset_features, validation_df=validation_df, process_id=process_id
258+
df,
259+
config,
260+
header,
261+
dataset_features,
262+
validation_df=validation_df,
263+
process_id=process_id,
264+
silent=silent,
244265
)
245266
else: # regular decision tree building
246267
root = 1
@@ -264,22 +285,23 @@ def fit(
264285
main_process_id=process_id,
265286
)
266287

267-
logger.info("-------------------------")
268-
logger.info(f"finished in {time.time() - begin} seconds")
288+
if silent is False:
289+
logger.info("-------------------------")
290+
logger.info(f"finished in {time.time() - begin} seconds")
269291

270292
obj = {"trees": trees, "alphas": alphas, "config": config, "nan_values": nan_values}
271293

272294
# -----------------------------------------
273295

274296
# train set accuracy
275297
df = base_df.copy()
276-
evaluate(obj, df, task="train")
298+
trainset_evaluation = evaluate(obj, df, task="train", silent=silent)
299+
obj["evaluation"] = {"train": trainset_evaluation}
277300

278301
# validation set accuracy
279302
if isinstance(validation_df, pd.DataFrame):
280-
evaluate(obj, validation_df, task="validation")
281-
282-
# -----------------------------------------
303+
validationset_evaluation = evaluate(obj, validation_df, task="validation", silent=silent)
304+
obj["evaluation"]["validation"] = validationset_evaluation
283305

284306
return obj
285307

@@ -455,31 +477,38 @@ def restoreTree(module_name) -> Any:
455477
return functions.restoreTree(module_name)
456478

457479

458-
def feature_importance(rules: Union[str, list]) -> pd.DataFrame:
480+
def feature_importance(rules: Union[str, list], silent: bool = False) -> pd.DataFrame:
459481
"""
460482
Show the feature importance values of a built model
461483
Args:
462-
rules (str or list): e.g. decision_rules = "outputs/rules/rules.py"
484+
rules (str or list): e.g. decision_rules = "outputs/rules/rules.py"
463485
or this could be retrieved from built model as shown below.
464486
465-
decision_rules = []
466-
for tree in model["trees"]:
467-
rule = .__dict__["__spec__"].origin
468-
decision_rules.append(rule)
487+
```python
488+
decision_rules = []
489+
for tree in model["trees"]:
490+
rule = .__dict__["__spec__"].origin
491+
decision_rules.append(rule)
492+
```
493+
silent (bool): set this to True if you do want to see
494+
any informative logs.
469495
Returns:
470496
feature importance (pd.DataFrame)
471497
"""
472498

473499
if not isinstance(rules, list):
474500
rules = [rules]
475-
logger.info(f"rules: {rules}")
501+
502+
if silent is False:
503+
logger.info(f"rules: {rules}")
476504

477505
# -----------------------------
478506

479507
dfs = []
480508

481509
for rule in rules:
482-
logger.info("Decision rule: {rule}")
510+
if silent is False:
511+
logger.info(f"Decision rule: {rule}")
483512

484513
with open(rule, "r", encoding="UTF-8") as file:
485514
lines = file.readlines()
@@ -564,17 +593,23 @@ def feature_importance(rules: Union[str, list]) -> pd.DataFrame:
564593

565594

566595
def evaluate(
567-
model: dict, df: pd.DataFrame, target_label: str = "Decision", task: str = "test"
568-
) -> None:
596+
model: dict,
597+
df: pd.DataFrame,
598+
target_label: str = "Decision",
599+
task: str = "test",
600+
silent: bool = False,
601+
) -> dict:
569602
"""
570603
Evaluate the performance of a built model on a data set
571604
Args:
572605
model (dict): built model which is the output of fit function
573606
df (pandas data frame): data frame you would like to evaluate
574607
target_label (str): target label
575608
task (string): set this to train, validation or test
609+
silent (bool): set this to True if you do not want to see
610+
any informative logs
576611
Returns:
577-
None
612+
evaluation results (dict)
578613
"""
579614

580615
# --------------------------
@@ -598,4 +633,4 @@ def evaluate(
598633
df["Decision"] = df["Decision"].astype(str)
599634
df["Prediction"] = df["Prediction"].astype(str)
600635

601-
cb_eval.evaluate(df, task=task)
636+
return cb_eval.evaluate(df, task=task, silent=silent)

chefboost/commons/evaluate.py

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
11
import math
2+
import pandas as pd
23
from chefboost.commons.logger import Logger
34

45
# pylint: disable=broad-except
56

67
logger = Logger(module="chefboost/commons/evaluate.py")
78

89

9-
def evaluate(df, task="train"):
10+
def evaluate(df: pd.DataFrame, task: str = "train", silent: bool = False) -> dict:
11+
"""
12+
Evaluate results
13+
Args:
14+
df (pd.DataFrame): data frame
15+
task (str): train, test
16+
silent (bool): set this to True if you do not want to
17+
see any informative logs
18+
Returns:
19+
evaluation results (dict)
20+
"""
1021
if df["Decision"].dtypes == "object":
1122
problem_type = "classification"
1223
else:
1324
problem_type = "regression"
1425

15-
# -------------------------------------
16-
26+
evaluation_results = {}
1727
instances = df.shape[0]
1828

19-
logger.info("-------------------------")
20-
logger.info(f"Evaluate {task} set")
21-
logger.info("-------------------------")
29+
if silent is False:
30+
logger.info("-------------------------")
31+
logger.info(f"Evaluate {task} set")
32+
logger.info("-------------------------")
2233

2334
if problem_type == "classification":
2435
idx = df[df["Prediction"] == df["Decision"]].index
2536
accuracy = 100 * len(idx) / df.shape[0]
26-
logger.info(f"Accuracy: {accuracy}% on {instances} instances")
37+
if silent is False:
38+
logger.info(f"Accuracy: {accuracy}% on {instances} instances")
2739

40+
evaluation_results["Accuracy"] = accuracy
41+
evaluation_results["Instances"] = instances
2842
# -----------------------------
2943

3044
predictions = df.Prediction.values
@@ -48,8 +62,12 @@ def evaluate(df, task="train"):
4862
confusion_row.append(item)
4963
confusion_matrix.append(confusion_row)
5064

51-
logger.info(f"Labels: {labels}")
52-
logger.info(f"Confusion matrix: {confusion_matrix}")
65+
if silent is False:
66+
logger.info(f"Labels: {labels}")
67+
logger.info(f"Confusion matrix: {confusion_matrix}")
68+
69+
evaluation_results["Labels"] = labels
70+
evaluation_results["Confusion matrix"] = confusion_matrix
5371

5472
# -----------------------------
5573
# precision and recall
@@ -79,11 +97,19 @@ def evaluate(df, task="train"):
7997
accuracy = round(100 * (tp + tn) / (tp + tn + fp + fn + epsilon), 4)
8098

8199
if len(labels) >= 3:
82-
logger.info(f"Decision {decision_class}")
83-
logger.info(f"Accuray: {accuracy}")
100+
if silent is False:
101+
logger.info(f"Decision {decision_class}")
102+
logger.info(f"Accuracy: {accuracy}")
103+
104+
evaluation_results[f"Decision {decision_class}'s Accuracy"] = accuracy
84105

85-
logger.info(f"Precision: {precision}%, Recall: {recall}%, F1: {f1_score}%")
86-
logger.debug(f"TP: {tp}, TN: {tn}, FP: {fp}, FN: {fn}")
106+
if silent is False:
107+
logger.info(f"Precision: {precision}%, Recall: {recall}%, F1: {f1_score}%")
108+
logger.debug(f"TP: {tp}, TN: {tn}, FP: {fp}, FN: {fn}")
109+
110+
evaluation_results["Precision"] = precision
111+
evaluation_results["Recall"] = recall
112+
evaluation_results["F1"] = f1_score
87113

88114
if len(labels) < 3:
89115
break
@@ -99,13 +125,17 @@ def evaluate(df, task="train"):
99125

100126
if instances > 0:
101127
mae = df["Absolute_Error"].sum() / instances
102-
logger.info(f"MAE: {mae}")
103-
104128
mse = df["Absolute_Error_Squared"].sum() / instances
105-
logger.info(f"MSE: {mse}")
106-
107129
rmse = math.sqrt(mse)
108-
logger.info(f"RMSE: {rmse}")
130+
131+
evaluation_results["MAE"] = mae
132+
evaluation_results["MSE"] = mse
133+
evaluation_results["RMSE"] = rmse
134+
135+
if silent is False:
136+
logger.info(f"MAE: {mae}")
137+
logger.info(f"MSE: {mse}")
138+
logger.info(f"RMSE: {rmse}")
109139

110140
rae = 0
111141
rrse = 0
@@ -122,12 +152,26 @@ def evaluate(df, task="train"):
122152
except Exception as err:
123153
logger.error(str(err))
124154

125-
logger.info(f"RAE: {rae}")
126-
logger.info(f"RRSE {rrse}")
155+
if silent is False:
156+
logger.info(f"RAE: {rae}")
157+
logger.info(f"RRSE {rrse}")
158+
159+
evaluation_results["RAE"] = rae
160+
evaluation_results["RRSE"] = rrse
127161

128162
mean = df["Decision"].mean()
129-
logger.info(f"Mean: {mean}")
163+
164+
if silent is False:
165+
logger.info(f"Mean: {mean}")
166+
167+
evaluation_results["Mean"] = mean
130168

131169
if mean > 0:
132-
logger.info(f"MAE / Mean: {100 * mae / mean}%")
133-
logger.info(f"RMSE / Mean: {100 * rmse / mean}%")
170+
if silent is False:
171+
logger.info(f"MAE / Mean: {100 * mae / mean}%")
172+
logger.info(f"RMSE / Mean: {100 * rmse / mean}%")
173+
174+
evaluation_results["MAE / Mean"] = 100 * mae / mean
175+
evaluation_results["RMSE / Mean"] = 100 * rmse / mean
176+
177+
return evaluation_results

0 commit comments

Comments
 (0)