Skip to content

Commit cc355e8

Browse files
committed
Update unit and integration tests
1 parent aaac4c3 commit cc355e8

File tree

6 files changed

+105
-77
lines changed

6 files changed

+105
-77
lines changed

src/sasctl/pzmm/write_json_files.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ def write_model_properties_json(
262262
json_path: Union[str, Path, None] = None,
263263
model_desc: Optional[str] = None,
264264
model_algorithm: Optional[str] = None,
265+
model_function: Optional[str] = None,
265266
modeler: Optional[str] = None,
266267
train_table: Optional[str] = None,
267268
properties: Optional[List[dict]] = None,
@@ -295,7 +296,9 @@ def write_model_properties_json(
295296
model_desc : str, optional
296297
User-defined model description. The default value is an empty string.
297298
model_algorithm : str, optional
298-
User-defined model type. The default value is an empty string.
299+
User-defined model algorithm name. The default value is an empty string.
300+
model_function : str, optional
301+
User-defined model function name. The default value is an empty string.
299302
modeler : str, optional
300303
User-defined value for the name of the modeler. The default value is an
301304
empty string.
@@ -372,11 +375,11 @@ def write_model_properties_json(
372375
output_json = {
373376
"name": model_name,
374377
"description": model_desc if model_desc else "",
375-
"function": model_function if model_function else "",
376378
"scoreCodeType": "python",
377379
"trainTable": train_table if train_table else "",
378380
"trainCodeType": "Python",
379381
"algorithm": model_algorithm if model_algorithm else "",
382+
"function": model_function if model_function else "",
380383
"targetVariable": target_variable if target_variable else "",
381384
"targetEvent": target_event if target_event else "",
382385
"targetLevel": target_level if target_level else "",
@@ -471,7 +474,7 @@ def write_file_metadata_json(
471474
dict_list = [
472475
{"role": "inputVariables", "name": INPUT},
473476
{"role": "outputVariables", "name": OUTPUT},
474-
{"role": "score", "name": model_prefix + "Score.py"},
477+
{"role": "score", "name": f"score_{model_prefix}.py"},
475478
]
476479
if is_h2o_model:
477480
dict_list.append({"role": "scoreResource", "name": model_prefix + ".mojo"})
@@ -914,8 +917,8 @@ def check_for_data(
914917

915918
@staticmethod
916919
def stat_dataset_to_dataframe(
917-
data: Union[DataFrame, List[list], Type["numpy.array"]] = None,
918-
target_value: Union[str, int, float, None] = None,
920+
data: Union[DataFrame, List[list], Type["numpy.array"]],
921+
target_value: Union[str, int, float] = None,
919922
) -> DataFrame:
920923
"""
921924
Convert the user supplied statistical dataset from either a pandas DataFrame,

src/sasctl/pzmm/write_score_code.py

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import re
55
from pathlib import Path
6-
from typing import Union, Optional, Callable, List, Tuple, Any, Generator
6+
from typing import Union, Optional, Callable, List, Tuple, Any, Generator, Type
77
from warnings import warn
88

99
import pandas as pd
@@ -15,8 +15,6 @@
1515
MAS_CODE_NAME = "dmcas_packagescorecode.sas"
1616
CAS_CODE_NAME = "dmcas_epscorecode.sas"
1717

18-
# TODO: add converter for any type of dataset (list, dataframe, numpy array)
19-
2018

2119
class ScoreCode:
2220
score_code: str = ""
@@ -161,7 +159,7 @@ def write_score_code(
161159
else:
162160
raise ValueError(
163161
"Either the binary_string or the model_file_name argument needs to be"
164-
"specified in order to generate the score code."
162+
" specified in order to generate the score code."
165163
)
166164

167165
# Add the core imports to the score code with the specified model serializer
@@ -176,16 +174,16 @@ def write_score_code(
176174
if model_id and not binary_string:
177175
model_load = cls._viya35_model_load(
178176
model_id,
179-
pickle_type,
180177
model_file_name,
178+
pickle_type=pickle_type,
181179
mojo_model="mojo_model" in kwargs,
182180
binary_h2o_model="binary_h2o_model" in kwargs,
183181
)
184182
# As above, but for SAS Viya 4 models
185183
elif not binary_string:
186184
model_load = cls._viya4_model_load(
187-
pickle_type,
188185
model_file_name,
186+
pickle_type=pickle_type,
189187
mojo_model="mojo_model" in kwargs,
190188
binary_h2o_model="binary_h2o_model" in kwargs,
191189
)
@@ -195,7 +193,7 @@ def write_score_code(
195193
model_prefix = cls._check_valid_model_prefix(model_prefix)
196194

197195
# Define the score function using the variables found in input_data
198-
cls.score_code += f"def score{model_prefix}({', '.join(input_var_list)}):\n"
196+
cls.score_code += f"def score_{model_prefix}({', '.join(input_var_list)}):\n"
199197

200198
if not score_metrics:
201199
score_metrics = cls._determine_score_metrics(
@@ -234,9 +232,10 @@ def write_score_code(
234232
)
235233
# Include check for numpy values and a conversion operation as needed
236234
cls.score_code += (
237-
f"{'':4}prediction = [val.item() if type(val)."
238-
f"__module__ == np.__name__ else val for val in "
239-
f"prediction]\n"
235+
f"\n{'':4}# Check for numpy values and convert to a CAS readable "
236+
f"representation\n"
237+
f"{'':4}if isinstance(prediction, np.ndarray):\n"
238+
f"{'':8}prediction = prediction.tolist()[0]\n\n"
240239
)
241240
cls._predictions_to_metrics(
242241
score_metrics,
@@ -252,7 +251,7 @@ def write_score_code(
252251
)
253252

254253
if score_code_path:
255-
py_code_path = Path(score_code_path) / (model_prefix + "_score.py")
254+
py_code_path = Path(score_code_path) / f"score_{model_prefix}.py"
256255
with open(py_code_path, "w") as py_file:
257256
py_file.write(cls.score_code)
258257
if model_id and score_cas:
@@ -263,7 +262,7 @@ def write_score_code(
263262
# noinspection PyUnboundLocalVariable
264263
sas_file.write(cas_code)
265264
else:
266-
output_dict = {model_prefix + "_score.py": cls.score_code}
265+
output_dict = {f"score_{model_prefix}.py": cls.score_code}
267266
if model_id and score_cas:
268267
# noinspection PyUnboundLocalVariable
269268
output_dict[MAS_CODE_NAME] = mas_code
@@ -1000,19 +999,19 @@ def _binary_target(
1000999
f"{'':8}{metrics} = \"{target_values[0]}\"\n"
10011000
f"{'':4}else:\n"
10021001
f"{'':8}{metrics} = \"{target_values[1]}\"\n\n"
1003-
f"return {metrics}"
1002+
f"{'':4}return {metrics}"
10041003
)
10051004
# One return that is the classification
10061005
elif len(returns) == 1 and returns[0]:
1007-
cls.score_code += f"\n\nreturn prediction"
1006+
cls.score_code += f"{'':4}return prediction"
10081007
# One return that is a probability
10091008
elif len(returns) == 1 and not returns[0]:
10101009
cls.score_code += (
10111010
f"{'':4}if prediction > {threshold}:\n"
10121011
f"{'':8}{metrics} = \"{target_values[0]}\"\n"
10131012
f"{'':4}else:\n"
10141013
f"{'':8}{metrics} = \"{target_values[1]}\"\n\n"
1015-
f"return {metrics}"
1014+
f"{'':4}return {metrics}"
10161015
)
10171016
# Two returns from the prediction method
10181017
elif len(returns) == 2 and sum(returns) == 0:
@@ -1022,7 +1021,7 @@ def _binary_target(
10221021
f"{'':8}{metrics} = \"{target_values[0]}\"\n"
10231022
f"{'':4}else:\n"
10241023
f"{'':8}{metrics} = \"{target_values[1]}\"\n\n"
1025-
f"return {metrics}"
1024+
f"{'':4}return {metrics}"
10261025
)
10271026
# Classification and probability returned; return classification value
10281027
elif len(returns) > 1 and sum(returns) == 1:
@@ -1041,7 +1040,7 @@ def _binary_target(
10411040
"score code should output the classification and probability for "
10421041
"the target event to occur."
10431042
)
1044-
cls.score_code += f"\n\nreturn prediction[1][0], prediction[1][2]"
1043+
cls.score_code += f"{'':4}return prediction[1][0], prediction[1][2]"
10451044
# Calculate the classification; return the classification and probability
10461045
elif sum(returns) == 0 and len(returns) == 1:
10471046
warn(
@@ -1054,7 +1053,7 @@ def _binary_target(
10541053
f"{'':8}{metrics[0]} = \"{target_values[0]}\"\n"
10551054
f"{'':4}else:\n"
10561055
f"{'':8}{metrics[0]} = \"{target_values[1]}\"\n\n"
1057-
f"return {metrics[0]}, prediction"
1056+
f"{'':4}return {metrics[0]}, prediction"
10581057
)
10591058
# Calculate the classification; return the classification and probability
10601059
elif sum(returns) == 0 and len(returns) == 2:
@@ -1068,11 +1067,11 @@ def _binary_target(
10681067
f"{'':8}{metrics[0]} = \"{target_values[0]}\"\n"
10691068
f"{'':4}else:\n"
10701069
f"{'':8}{metrics[0]} = \"{target_values[1]}\"\n\n"
1071-
f"return {metrics[0]}, prediction[0]"
1070+
f"{'':4}return {metrics[0]}, prediction[0]"
10721071
)
10731072
# Return classification and probability value
10741073
elif sum(returns) == 1 and len(returns) == 2:
1075-
cls.score_code += f"\n\nreturn prediction[0], prediction[1]"
1074+
cls.score_code += f"{'':4}return prediction[0], prediction[1]"
10761075
elif sum(returns) == 1 and len(returns) == 3:
10771076
warn(
10781077
"Due to the ambiguity of the provided metrics and prediction return"
@@ -1082,17 +1081,17 @@ def _binary_target(
10821081
# Determine which return is the classification value
10831082
class_index = [i for i, x in enumerate(returns) if x][0]
10841083
if class_index == 0:
1085-
cls.score_code += f"\n\nreturn prediction[0], prediction[1]"
1084+
cls.score_code += f"{'':4}return prediction[0], prediction[1]"
10861085
else:
10871086
cls.score_code += (
1088-
f"\n\nreturn prediction[{class_index}], prediction[0]"
1087+
f"{'':4}return prediction[{class_index}], prediction[0]"
10891088
)
10901089
else:
10911090
cls._invalid_predict_config()
10921091
elif len(metrics) == 3:
10931092
if h2o_model:
10941093
cls.score_code += (
1095-
f"\n\nreturn prediction[1][0], prediction[1][1], prediction[1][2]"
1094+
f"{'':4}return prediction[1][0], prediction[1][1], prediction[1][2]"
10961095
)
10971096
elif sum(returns) == 0 and len(returns) == 1:
10981097
warn(
@@ -1105,7 +1104,7 @@ def _binary_target(
11051104
f"{'':8}{metrics[0]} = \"{target_values[0]}\"\n"
11061105
f"{'':4}else:\n"
11071106
f"{'':8}{metrics[0]} = \"{target_values[1]}\"\n\n"
1108-
f"return {metrics[0]}, prediction, 1 - prediction"
1107+
f"{'':4}return {metrics[0]}, prediction, 1 - prediction"
11091108
)
11101109
elif sum(returns) == 0 and len(returns) == 2:
11111110
warn(
@@ -1118,24 +1117,24 @@ def _binary_target(
11181117
f"{'':8}{metrics[0]} = \"{target_values[0]}\"\n"
11191118
f"{'':4}else:\n"
11201119
f"{'':8}{metrics[0]} = \"{target_values[1]}\"\n\n"
1121-
f"return {metrics[0]}, prediction[0], prediction[1]"
1120+
f"{'':4}return {metrics[0]}, prediction[0], prediction[1]"
11221121
)
11231122
# Find which return is the classification, then return probabilities
11241123
elif sum(returns) == 1 and len(returns) == 2:
11251124
# Determine which return is the classification value
11261125
class_index = [i for i, x in enumerate(returns) if x][0]
11271126
if class_index == 0:
11281127
cls.score_code += (
1129-
f"\n\nreturn prediction[0], prediction[1], 1 - prediction[1]"
1128+
f"{'':4}return prediction[0], prediction[1], 1 - prediction[1]"
11301129
)
11311130
else:
11321131
cls.score_code += (
1133-
f"\n\nreturn prediction[1], prediction[0], 1 - prediction[0]"
1132+
f"{'':4}return prediction[1], prediction[0], 1 - prediction[0]"
11341133
)
11351134
# Return all values from prediction method
11361135
elif sum(returns) == 1 and len(returns) == 3:
11371136
cls.score_code += (
1138-
f"\n\nreturn prediction[0], prediction[1], prediction[2]"
1137+
f"{'':4}return prediction[0], prediction[1], prediction[2]"
11391138
)
11401139
else:
11411140
cls._invalid_predict_config()
@@ -1181,20 +1180,20 @@ def _nonbinary_targets(
11811180
f"{'':4}target_values = {target_values}\n{'':4}"
11821181
f"{metrics} = target_values[prediction[1][1:]."
11831182
f"index(max(prediction[1][1:]))]\n{'':4}"
1184-
f"return {metrics}"
1183+
f"{'':4}return {metrics}"
11851184
)
11861185
# One return that is the classification
11871186
elif len(returns) == 1:
11881187
cls.score_code += f"{'':4}{metrics} = prediction\n\nreturn {metrics}"
11891188
elif len(returns) == len(target_values):
11901189
cls.score_code += (
11911190
f"{'':4}target_values = {target_values}\n\n"
1192-
f"return target_values[prediction.index(max(prediction))]"
1191+
f"{'':4}return target_values[prediction.index(max(prediction))]"
11931192
)
11941193
elif len(returns) == (len(target_values) + 1):
11951194
# Determine which return is the classification value
11961195
class_index = [i for i, x in enumerate(returns) if x][0]
1197-
cls.score_code += f"\n\nreturn prediction[{class_index}]"
1196+
cls.score_code += f"{'':4}return prediction[{class_index}]"
11981197
else:
11991198
cls._invalid_predict_config()
12001199
elif len(metrics) == 2:
@@ -1203,19 +1202,19 @@ def _nonbinary_targets(
12031202
f"{'':4}target_values = {target_values}\n{'':4}"
12041203
f"{metrics} = target_values[prediction[1][1:]."
12051204
f"index(max(prediction[1][1:]))]\n{'':4}"
1206-
f"return {metrics}, max(prediction[1][1:])"
1205+
f"{'':4}return {metrics}, max(prediction[1][1:])"
12071206
)
12081207
elif len(returns) == len(target_values):
12091208
cls.score_code += (
12101209
f"{'':4}target_values = {target_values}\n\n"
1211-
f"return target_values[prediction.index(max(prediction))], "
1210+
f"{'':4}return target_values[prediction.index(max(prediction))], "
12121211
f"max(prediction)"
12131212
)
12141213
elif len(returns) == (len(target_values) + 1):
12151214
# Determine which return is the classification value
12161215
class_index = [i for i, x in enumerate(returns) if x][0]
12171216
cls.score_code += (
1218-
f"\n\nreturn prediction[{class_index}], "
1217+
f"{'':4}return prediction[{class_index}], "
12191218
f"max(prediction[:{class_index}] + prediction[{class_index + 1}:])"
12201219
)
12211220
else:
@@ -1224,25 +1223,25 @@ def _nonbinary_targets(
12241223
if h2o_model:
12251224
if len(metrics) == len(target_values):
12261225
h2o_returns = [f"prediction[1][{i+1}]" for i in range(len(metrics))]
1227-
cls.score_code += f"\n\nreturn {', '.join(h2o_returns)}"
1226+
cls.score_code += f"{'':4}return {', '.join(h2o_returns)}"
12281227
elif len(metrics) == (len(target_values) + 1):
12291228
h2o_returns = [f"prediction[1][{i}]" for i in range(len(metrics))]
1230-
cls.score_code += f"\n\nreturn {', '.join(h2o_returns)}"
1229+
cls.score_code += f"{'':4}return {', '.join(h2o_returns)}"
12311230
elif (
12321231
len(metrics) == len(target_values) == len(returns) and sum(returns) == 0
12331232
) or (
12341233
len(metrics) == (len(target_values) + 1) == len(returns)
12351234
and sum(returns) == 1
12361235
):
12371236
proba_returns = [f"prediction[{i}]" for i in range(len(returns))]
1238-
cls.score_code += f"\n\nreturn {', '.join(proba_returns)}"
1237+
cls.score_code += f"{'':4}return {', '.join(proba_returns)}"
12391238
elif (len(metrics) - 1) == len(returns) == len(target_values) and sum(
12401239
returns
12411240
) == 0:
12421241
proba_returns = [f"prediction[{i}]" for i in range(len(returns))]
12431242
cls.score_code += (
12441243
f"{'':4}target_values = {target_values}\n\n"
1245-
f"return target_values[prediction.index(max(prediction))], "
1244+
f"{'':4}return target_values[prediction.index(max(prediction))], "
12461245
f"{', '.join(proba_returns)}"
12471246
)
12481247
else:
@@ -1454,15 +1453,19 @@ def _viya35_score_code_import(
14541453
"""
14551454
files = [
14561455
{
1457-
"name": f"{prefix}_score.py",
1456+
"name": f"score_{prefix}.py",
14581457
"file": cls.score_code,
14591458
"role": "score",
14601459
}
14611460
]
14621461
cls.upload_and_copy_score_resources(model_id, files)
1463-
mr.convert_python_to_ds2(model_id)
1462+
# The typeConversion endpoint is only valid for models with Python score code
1463+
model = mr.get_model(model_id)
1464+
model["scoreCodeType"] = "Python"
1465+
model = mr.update_model(model)
1466+
mr.convert_python_to_ds2(model)
14641467
if score_cas:
1465-
model_contents = mr.get_model_contents(model_id)
1468+
model_contents = mr.get_model_contents(model)
14661469
for file in model_contents:
14671470
if file.name == "score.sas":
14681471
mas_code = mr.get(
@@ -1478,9 +1481,9 @@ def _viya35_score_code_import(
14781481
}
14791482
],
14801483
)
1481-
cas_code = cls.convert_mas_to_cas(mas_code, model_id)
1484+
cas_code = cls.convert_mas_to_cas(mas_code, model)
14821485
cls.upload_and_copy_score_resources(
1483-
model_id,
1486+
model,
14841487
[
14851488
{
14861489
"name": CAS_CODE_NAME,

src/sasctl/pzmm/zip_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _filter_files(file_dir: Union[str, Path], is_viya4: Optional[bool] = False)
2929
file_names = []
3030
file_names.extend(sorted(Path(file_dir).glob("*.json")))
3131
if is_viya4:
32-
file_names.extend(sorted(Path(file_dir).glob("*Score.py")))
32+
file_names.extend(sorted(Path(file_dir).glob("score_*.py")))
3333
file_names.extend(sorted(Path(file_dir).glob("*.pickle")))
3434
# Include H2O.ai MOJO files
3535
file_names.extend(sorted(Path(file_dir).glob("*.mojo")))

0 commit comments

Comments
 (0)