Skip to content

Commit c3dea2f

Browse files
author
Jon Walker
authored
Merge pull request #38 from sassoftware/dev-1.3.0
Dev 1.3.0
2 parents 684392d + cdb5574 commit c3dea2f

File tree

128 files changed

+76658
-11930
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+76658
-11930
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dist
3232
*.egg-info
3333
.eggs
3434
.pypirc
35+
*.pyc
3536

3637
## tox testing tool
3738
.tox

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11

22
Unreleased
33
----------
4-
-
4+
-
5+
6+
v1.3 (2019-10-10)
7+
-----------------
8+
**Improvements**
9+
- Added `update_performance` task for easily uploading performance information for a model.
10+
- New (experimental) pyml2sas sub-package provides utilities for generating SAS code from Python gradient boosting models.
11+
- New (experimental) methods for managing workflows added to `model_management` service.
12+
13+
**Changes**
14+
- `register_model` task automatically captures installed Python packages.
15+
- All `list_xxx` methods return all matching items unless a `limit` parameter is specified.
16+
- Improved API documentation.
17+
- Updated `full_lifecycle` example with performance monitoring.
518

619
v1.2.5 (2019-10-10)
720
-------------------

doc/api/sasctl.core.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
sasctl.core
2+
=======================
3+
4+
.. automodule:: sasctl.core
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:
8+

doc/api/sasctl.current_session.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
current_session
2+
===============
3+

doc/api/sasctl.rst

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,18 @@ sasctl package
44
Module contents
55
---------------
66

7-
.. automodule:: sasctl
8-
:members:
9-
:undoc-members:
10-
:show-inheritance:
7+
.. toctree::
8+
9+
sasctl.session
10+
sasctl.current_session
1111

1212
Submodules
1313
----------
1414

15-
sasctl.core module
16-
~~~~~~~~~~~~~~~~~~
17-
18-
.. automodule:: sasctl.core
19-
:members:
20-
:undoc-members:
21-
:show-inheritance:
22-
23-
24-
sasctl.tasks module
25-
~~~~~~~~~~~~~~~~~~~
26-
27-
.. automodule:: sasctl.tasks
28-
:members:
29-
:undoc-members:
30-
:show-inheritance:
31-
15+
.. toctree::
3216

17+
sasctl.core
18+
sasctl.tasks
3319

3420
Subpackages
3521
-----------

doc/api/sasctl.session.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Session
2+
=======
3+
4+
.. autoclass:: sasctl.core.Session
5+
:members:

doc/api/sasctl.tasks.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
sasctl.tasks
2+
=======================
3+
4+
.. automodule:: sasctl.tasks
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

examples/full_lifecycle.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from sklearn.model_selection import train_test_split
1212

1313
from sasctl import Session
14-
from sasctl.tasks import register_model, publish_model
14+
from sasctl.tasks import register_model, publish_model, update_model_performance
1515
from sasctl.services import model_repository as mr
1616
from sasctl.services import model_management as mm
1717

@@ -39,12 +39,10 @@
3939
project=project, # Register in "Iris" project
4040
force=True) # Create project if it doesn't exist
4141

42-
# Update project properties
42+
# Update project properties. Target variable must be set before performance
43+
# definitions can be created.
4344
project = mr.get_project(project)
44-
project['function'] = 'prediction'
45-
project['targetLevel'] = 'interval'
4645
project['targetVariable'] = 'Price'
47-
project['predictionVariable'] = 'var1'
4846
project = mr.update_project(project)
4947

5048
# Instruct the project to look for tables in the "Public" CAS library with
@@ -55,8 +53,8 @@
5553
# Publish the model to the real-time scoring engine
5654
module_lm = publish_model(model_name, 'maslocal')
5755

58-
# Select the first row of training data
59-
x = X.iloc[0, :]
56+
# Select the first row of testing data
57+
x = X_test.iloc[0, :]
6058

6159
# Call the published module and score the record
6260
result = module_lm.score(**x)
@@ -76,5 +74,16 @@
7674
result = module_dt.score(**x)
7775
print(result)
7876

77+
# Model Manager can track model performance over time if provided with
78+
# historical model observations & predictions. SIMULATE historical data by
79+
# repeatedly sampling from the test set.
80+
perf_df = X_test.copy()
81+
perf_df['var1'] = lm.predict(X_test)
82+
perf_df['Price'] = y
83+
84+
# For each (simulated) historical period, upload model results
85+
for period in ('q12019', 'q22019', 'q32019', 'q42019'):
86+
sample = perf_df.sample(frac=0.2)
87+
update_model_performance(sample, model_name, period)
7988

8089

src/sasctl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Copyright © 2019, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
55
# SPDX-License-Identifier: Apache-2.0
66

7-
__version__ = '1.2.5'
7+
__version__ = '1.3'
88
__author__ = 'SAS'
99
__credits__ = ['Yi Jian Ching, Lucas De Paula, James Kochuba, Peter Tobac, '
1010
'Chris Toth, Jon Walker']

src/sasctl/_services/model_management.py

Lines changed: 127 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# SPDX-License-Identifier: Apache-2.0
66

77
from .service import Service
8-
8+
from ..utils.decorators import experimental
99

1010
class ModelManagement(Service):
1111
"""The Model Management API provides basic resources for monitoring
@@ -20,7 +20,8 @@ class ModelManagement(Service):
2020
'performance tasks')
2121

2222
# TODO: set ds2MultiType
23-
def publish_model(self,
23+
@classmethod
24+
def publish_model(cls,
2425
model,
2526
destination,
2627
name=None,
@@ -80,15 +81,16 @@ def publish_model(self,
8081
# Publishes a model that has already been registered in the model
8182
# repository.
8283
# Unlike model_publish service, does not require Code to be specified.
83-
r = self.post('/publish',
84-
json=request,
85-
params=dict(force=force,
84+
r = cls.post('/publish',
85+
json=request,
86+
params=dict(force=force,
8687
reloadModelTable=reload_model_table),
87-
headers={'Content-Type':
88+
headers={'Content-Type':
8889
'application/vnd.sas.models.publishing.request.asynchronous+json'})
8990
return r
9091

91-
def create_performance_definition(self,
92+
@classmethod
93+
def create_performance_definition(cls,
9294
model,
9395
library_name,
9496
table_prefix,
@@ -156,7 +158,7 @@ def create_performance_definition(self,
156158
"""
157159
from .model_repository import ModelRepository
158160

159-
if '_' in table_prefix:
161+
if not scoring_required and '_' in table_prefix:
160162
raise ValueError(
161163
"Parameter 'table_prefix' cannot contain underscores."
162164
" Received a value of '%s'.") % table_prefix
@@ -210,11 +212,12 @@ def create_performance_definition(self,
210212
project.get('variables', []) if
211213
v.get('role') == 'output']
212214

213-
return self.post('/performanceTasks', json=request,
214-
headers={
215+
return cls.post('/performanceTasks', json=request,
216+
headers={
215217
'Content-Type': 'application/vnd.sas.models.performance.task+json'})
216218

217-
def execute_performance_definition(self, definition):
219+
@classmethod
220+
def execute_performance_definition(cls, definition):
218221
"""Launches a job to run a performance definition.
219222
220223
Parameters
@@ -228,7 +231,118 @@ def execute_performance_definition(self, definition):
228231
The executing job
229232
230233
"""
231-
definition = self.get_performance_definition(definition)
234+
definition = cls.get_performance_definition(definition)
235+
236+
return cls.post('/performanceTasks/%s' % definition.id)
237+
238+
@classmethod
239+
@experimental
240+
def list_model_workflow_definition(cls):
241+
"""List all enabled Workflow Processes to execute on Model Project.
242+
243+
Returns
244+
-------
245+
RestObj
246+
The list of workflows
247+
248+
"""
249+
from .workflow import Workflow
250+
wf = Workflow()
251+
252+
return wf.list_enabled_definitions()
253+
254+
@classmethod
255+
@experimental
256+
def list_model_workflow_prompt(cls, workflowName):
257+
"""List prompt Workflow Processes Definitions.
258+
259+
Parameters
260+
----------
261+
workflowName : str
262+
Name or ID of an enabled workflow to retrieve inputs
263+
264+
Returns
265+
-------
266+
list
267+
The list of prompts for specific workflow
268+
269+
"""
270+
from .workflow import Workflow
271+
wf = Workflow()
272+
273+
return wf.list_workflow_prompt(workflowName)
274+
275+
@classmethod
276+
@experimental
277+
def list_model_workflow_executed(cls, projectName):
278+
"""List prompt Workflow Processes Definitions.
279+
280+
Parameters
281+
----------
282+
projectName : str
283+
Name of the Project list executed workflow
284+
285+
Returns
286+
-------
287+
RestObj
288+
List of workflows associated to project
289+
290+
"""
291+
from .model_repository import ModelRepository
292+
mr = ModelRepository()
293+
294+
project = mr.get_project(projectName)
295+
296+
return cls.get('/workflowProcesses?filter=eq(associations.solutionObjectId,%22' + project['id'] + '%22)')
297+
298+
@classmethod
299+
@experimental
300+
def execute_model_workflow_definition(cls, project_name, workflow_name, input=None):
301+
"""Runs specific Workflow Processes Definitions.
302+
303+
Parameters
304+
----------
305+
project_name : str
306+
Name of the Project that will execute workflow
307+
workflow_name : str
308+
Name or ID of an enabled workflow to execute
309+
input : dict, optional
310+
Input values for the workflow for initial workflow prompt
311+
312+
Returns
313+
-------
314+
RestObj
315+
The executing workflow
316+
317+
"""
318+
from .model_repository import ModelRepository
319+
from .workflow import Workflow
320+
321+
mr = ModelRepository()
322+
wf = Workflow()
323+
324+
project = mr.get_project(project_name)
325+
326+
workflow = wf.run_workflow_definition(workflow_name, input=input)
327+
328+
# Associations running workflow to model project, note workflow has to be running
329+
# THINK ABOUT: do we do a check on status of the workflow to determine if it is still running before associating?
330+
331+
input = {"processName": workflow['name'],
332+
"processId": workflow['id'],
333+
"objectType": "MM_Project",
334+
"solutionObjectName": project_name,
335+
"solutionObjectId": project['id'],
336+
"solutionObjectUri": "/modelRepository/projects/" + project['id'],
337+
"solutionObjectMediaType": "application/vnd.sas.models.project+json"}
338+
339+
#Note, you can get a HTTP Error 404: {"errorCode":74052,"message":"The workflow process for id <> cannot be found.
340+
# Associations can only be made to running processes.","details":["correlator:
341+
# e62c5562-2b11-45db-bcb7-933200cb0f0a","traceId: 3118c0fb1eb9702d","path:
342+
# /modelManagement/workflowAssociations"],"links":[],"version":2,"httpStatusCode":404}
343+
# Which is fine and expected like the Visual Experience.
344+
return cls.post('/workflowAssociations',
345+
json=input,
346+
headers={'Content-Type': 'application/vnd.sas.workflow.object.association+json'})
232347

233-
return self.post('/performanceTasks/%s' % definition.id)
234348

0 commit comments

Comments
 (0)