Digital Developer Conference: Hybrid Cloud 2021. On Sep 21, gain free hybrid cloud skills from experts and partners. Register now

Building and scoring AI models in Netezza with nzpyida and Watson Studio

In this article, learn how to use Netezza Performance Server, Watson Studio, and Watson Machine Learning as a single platform for pushdown (in-database) execution of key data science activities like data preparation, model building, and model scoring.

The three main components of this solution are:

  • nzpyida: This Python package allows the analytics pushdown to Netezza Performance Server, an advanced data warehouse and analytics platform that leverages a massively parallel processing (MPP) architecture for building and scoring AI models quickly.
  • Watson Studio client – Jupyter Notebook: This web interactive environment lets you create and share documents that contain live code, equations, visualizations, and explanatory text.
  • Watson Studio deployment – Watson Machine Learning: This environment deploys and runs the AI models.

Typically, you follow these steps with the Netezza Performance Server, Watson Studio, and Watson Machine Learning environment.

  1. Use a Watson Studio notebook to author the custom function using your favorite Python libraries. Depending on the requirement, you might write one of the following functions:

    • A data preparation custom function to perform the data transformations inside Netezza and save to the database tables
    • A model building custom function to build models inside Netezza and save the generated models to Watson Machine Learning
    • A model scoring custom function to fetch Watson Machine Learning models and score the test data inside Netezza
  2. Use the nzpyida package to push the custom function from Watson Studio to Netezza. Depending on the requirement, you might select one of the following pushdown APIs to apply a function on the data:

    • NZFunApply: Applies a function on each row of the table
    • NZFunTApply: Applies a function on the entire table
    • NZFunGroupedApply: Applies a function on the subsets of data in the table

This article uses a stock price prediction example to illustrate all the previous scenarios.

Prerequisites

If you would like to follow along with the article, you should have a working Watson Studio and Watson Machine Learning environment. If needed, you can find more documentation on Watson Studio and Watson Machine Learning.

For nzpyida installation

  1. On the client side (in a Python environment), install the nzpyida package using pip install nzpyida.
  2. On the server side (nz server), install any INZA version starting with v11.2.1.0. INZA customizations (AE launcher, custom templates, preregistered SQL functions) to support nzpyida package invocations are available in INZA versions starting with v11.2.1.0.

  3. Set up an ODBC or JDBC connection on the client side.

    For an ODBC connection, set up and configure an ODBC data source connection following the steps in Installing and Configuring ODBC.

    For a JDBC connection, the IBM Knowledge Center includes a description of how to install the Netezza JDBC driver on your client. After you’ve downloaded and installed the nzjdbc3.jar file, you must include its location in the value of the CLASSPATH environment variable with export CLASSPATH=<path-to-nzjdbc3.jar>:$CLASSPATH.

Connecting to database

Connect to a database table with a DataFrame abstraction.

  1. Import the modules.

      from nzpyida import IdaDataBase, IdaDataFrame
    
  2. Connect to a database. In this example, stocks is the data source name.

     idadb = IdaDataBase('stocks', 'admin', 'password')
    
  3. Connect to a table within the database. In this example, STOCKS is the database table name.

     idadf = IdaDataFrame(idadb, 'STOCKS')
    
  4. Check the top 5 rows.

     idadf.head()
    

Output

        DATE  OPEN   HIGH   LOW  CLOSE  ADJCLOSE     VOLUME TICKER        ID
0  2020-05-14  8.77   9.19  8.25   9.15      9.15   72320500    AAL  39238041
1  2020-05-15  8.85   9.36  8.76   9.04      9.04   39560500    AAL  39238042
2  2020-05-07  9.30   9.80  9.29   9.54      9.54   61817000    AAL  39238036
3  2020-05-04  9.41  10.17  9.15   9.82      9.82  125580000    AAL  39238033
4  2020-05-13  9.52   9.53  8.83   9.11      9.11   68704600    AAL  39238040

Note: The following steps show how to load the allstocks.csv file into the stocks table on Netezza Performance Server.

  1. Create a table.

     create table stocks (date varchar(100), open REAL, high REAL, low REAL, close REAL, AdjClose REAL, Volume INTEGER, ticker VARCHAR(100));
    
  2. Load the .csv file into the table.

     nzload -df allstocks.csv -t stocks -db stocks -nullValue NA -boolStyle YES_NO -skipRows 1 -delim , -dateStyle YMD -dateDelim '-' -pw password
    
  3. For tables that don’t have an identity column, create an identity column and set the value with rowid.

     alter table stocks add column id bigint;
     update stocks set id=rowid;
    

Adding new features to the data

As shown in the idadf.head() output, the stocks table contains basic columns like the closing price, volume, date, and ticker. You can write a custom data preparation function to add more columns to the data. Because the goal is to predict the next day prices, you can use the future_close_price column, which holds the next day stock price. You can also add some technical indicator columns like moving averages that could aid in better predictions.

The interaction between the Watson Studio notebook and Netezza Performance Server in this pushdown scenario could be depicted as follows.

Scenario

For the pushdown API selection, NZFunGroupedApply is chosen because the need is to add the new columns on a ticker basis. The output is saved in the stocks_indicator_features table as specified in the output_table argument.

from nzpyida import IdaDataBase, IdaDataFrame
from nzpyida.ae import NZFunGroupedApply


code_str_host_spus = """def all_stocks_add_features(self,df):


        imputed_df = df.copy()
        imputed_df['DATE'] = pd.to_datetime(imputed_df['DATE'])
        imputed_df = imputed_df.sort_values(by='DATE')
        imputed_df['DATE'] = imputed_df['DATE'].dt.date


        #how many days in future you need to predict
        future_days = -1

        #add the future close price column and shift by the required days

        imputed_df['FUTURE_CLOSE_PRICE'] = imputed_df['ADJCLOSE'].shift(future_days)


        # add technical indicators
        for n in [14,30,50,200]:

          # create the moving average indicator
          imputed_df['MA'+str(n)] = imputed_df['ADJCLOSE'].rolling(window=n).mean()


        def print_output(x):
                row = [x['ID'], x['FUTURE_CLOSE_PRICE'], x['MA14'], x['MA30'], x['MA50'], x['MA200']]
                self.output(row)


        imputed_df.apply(print_output, axis=1)


"""
output_signature = {'ID': 'int', 'FUTURE_CLOSE_PRICE': 'float', 'MA14':'float', 'MA30':'float', 'MA50':'float','MA200':'float'}


nz_tapply = NZFunGroupedApply(df=idadf, code_str=code_str_host_spus, fun_name='all_stocks_add_features', index='TICKER', output_table='stocks_indicator_features', output_signature=output_signature, merge_output_with_df=True)
result = nz_tapply.get_result()
print(result.head())

The output is as follows.

FUTURE_CLOSE_PRICE       MA14       MA30       MA50      MA200        DATE  \
0                9.04  10.202143  10.572333  11.863200  23.650406  2020-05-14   
1                9.11  10.366428  10.726334  12.225600  23.863167  2020-05-12   
2                9.15  10.285000  10.623667  12.050800  23.756613  2020-05-13   
3                9.25  10.815715  11.479000  13.330800  24.431896  2020-05-05   
4                9.39  12.733571  15.988000  20.897549  26.790159  2020-04-02   

    OPEN   HIGH    LOW  CLOSE  ADJCLOSE    VOLUME TICKER        ID  
0   8.77   9.19   8.25   9.15      9.15  72320500    AAL  39238041  
1  10.01  10.20   9.60   9.65      9.65  46833100    AAL  39238039  
2   9.52   9.53   8.83   9.11      9.11  68704600    AAL  39238040  
3  10.26  10.38   9.50   9.51      9.51  86943900    AAL  39238034  
4  10.61  11.03  10.00  10.06     10.06  65534600    AAL  39238012

Compute the training data set

Because this is time series data, you can split the training test data based on the sorted timelines. The training data is the first 90% in the timeline, and test data is the last 10% in the timeline.

from nzpyida import IdaDataBase, IdaDataFrame
from nzpyida.ae import NZFunGroupedApply

idadf = IdaDataFrame(idadb, 'stocks_indicator_features')

if (idadb.exists_table("stocks_features_train")):
    idadb.drop_table("stocks_features_train")
code_str_host_spus = """def stocks_train_data(self,df):


        imputed_df = df.copy()
        imputed_df['DATE'] = pd.to_datetime(imputed_df['DATE'])
        imputed_df = imputed_df.sort_values(by='DATE')
        imputed_df['DATE'] = imputed_df['DATE'].dt.date


        #name = imputed_df.TICKER[0]
        train_size = int(0.9*imputed_df.shape[0])
        train_data = imputed_df[0:train_size]

        def print_output(x):
                row = [x['ID']]
                self.output(row)


        train_data.apply(print_output, axis=1)
"""

output_signature = {'ID': 'int'}


nz_tapply = NZFunGroupedApply(df=idadf, code_str=code_str_host_spus, fun_name='stocks_train_data', index='TICKER', output_table='stocks_features_train', output_signature=output_signature, merge_output_with_df=True)
result = nz_tapply.get_result()
print(result.head())

The outut is as follows.

FUTURE_CLOSE_PRICE       MA14       MA30       MA50      MA200        DATE  \
0                9.04  10.202143  10.572333  11.863200  23.650406  2020-05-14   
1                9.11  10.366428  10.726334  12.225600  23.863167  2020-05-12   
2                9.15  10.285000  10.623667  12.050800  23.756613  2020-05-13   
3                9.25  10.815715  11.479000  13.330800  24.431896  2020-05-05   
4                9.39  12.733571  15.988000  20.897549  26.790159  2020-04-02   

    OPEN   HIGH    LOW  CLOSE  ADJCLOSE    VOLUME TICKER        ID  
0   8.77   9.19   8.25   9.15      9.15  72320500    AAL  39238041  
1  10.01  10.20   9.60   9.65      9.65  46833100    AAL  39238039  
2   9.52   9.53   8.83   9.11      9.11  68704600    AAL  39238040  
3  10.26  10.38   9.50   9.51      9.51  86943900    AAL  39238034  
4  10.61  11.03  10.00  10.06     10.06  65534600    AAL  39238012

Compute the test data set

To compute the test data set:

from nzpyida import IdaDataBase, IdaDataFrame
from nzpyida.ae import NZFunGroupedApply

idadf = IdaDataFrame(idadb, 'stocks_indicator_features')

if (idadb.exists_table("stocks_features_test")):
    idadb.drop_table("stocks_features_test")
code_str_host_spus = """def stocks_test_data(self,df):


        imputed_df = df.copy()
        imputed_df['DATE'] = pd.to_datetime(imputed_df['DATE'])
        imputed_df = imputed_df.sort_values(by='DATE')
        imputed_df['DATE'] = imputed_df['DATE'].dt.date

        #name = imputed_df.TICKER[0]
        train_size = int(0.9*imputed_df.shape[0])


        test_data = imputed_df[train_size:]

        def print_output(x):
                row = [x['ID']]
                self.output(row)


        test_data.apply(print_output, axis=1)
"""

output_signature = {'ID': 'int'}


nz_tapply = NZFunGroupedApply(df=idadf, code_str=code_str_host_spus, fun_name='stocks_test_data', index='TICKER', output_table='stocks_features_test', output_signature=output_signature, merge_output_with_df=True)
result = nz_tapply.get_result()
print(result.head())

The output is as follows.

FUTURE_CLOSE_PRICE       MA14       MA30     MA50      MA200        DATE  \
0               11.74  11.653571  12.242000  12.4992  14.109396  2020-11-11   
1               12.04  11.732857  12.250333  12.5204  14.182702  2020-11-10   
2               12.24  11.592143  12.214000  12.4694  14.030005  2020-11-12   
3               12.38  11.759286  12.246000  12.5338  14.254807  2020-11-09   
4               12.53  12.080714  12.160000  12.4014  13.648150  2020-11-19   

    OPEN   HIGH    LOW  CLOSE  ADJCLOSE     VOLUME TICKER        ID  
0  12.40  12.46  11.93  12.04     12.04   81359600    AAL  39238167  
1  12.85  13.02  12.34  12.38     12.38  127529300    AAL  39238166  
2  11.97  12.23  11.65  11.74     11.74   75511700    AAL  39238168  
3  14.33  14.41  12.75  13.20     13.20  231326200    AAL  39238165  
4  12.73  13.04  12.63  12.79     12.79   58809400    AAL  39238173

Delete the Watson Machine Learning models, if any.

from ibm_watson_machine_learning import APIClient

wml_credentials = {
                   'url': 'xxx',
                   'apikey':'xxx'
                  }

 client = APIClient(wml_credentials)
 client.set.default_space('xxx')
 #delete the previous models
 model_dict = client.repository.get_model_details()


 for key, value in model_dict.items():

    if (key == 'resources'):
        for res_element in value:
            metadata_dic = res_element.get('metadata')
            res = client.repository.delete(metadata_dic.get('id'))

Build models with training data set on Netezza and save to Watson Machine Learning

The custom model building function shown below imputes the missing values and does a label encoding of categorical variables before fitting a scikit-learn random forest algorithm for the training data set. The model generated is then saved to the Watson Machine Learning service through the Watson Machine Learning interfaces.

The interaction between the Watson Studio notebook, Netezza Performance Server, and Watson Machine Learning in this pushdown scenario could be depicted as follows.

Figure 2

For the pushdown API selection, NZFunGroupedApply is chosen because the need is to build the models on a ticker basis.

idadf = IdaDataFrame(idadb, 'stocks_features_train')


code_str_host_spus = """def stocks_rf_ml(self, df):

    import numpy as np

    from sklearn.impute import SimpleImputer
    from sklearn.metrics import mean_squared_error
    from sklearn.ensemble import RandomForestRegressor
    from ibm_watson_machine_learning import APIClient
    from sklearn.model_selection import train_test_split

    wml_credentials = {
                   'url': 'xxx',
                   'apikey':'xxx'
                  }

    client = APIClient(wml_credentials)
    client.set.default_space('xxx')

    imputed_df = df.copy()

    imputed_df['DATE'] = pd.to_datetime(imputed_df['DATE'])
    imputed_df = imputed_df.sort_values(by='DATE')

    imputed_df['DATE']=imputed_df['DATE'].dt.date
    name = imputed_df.TICKER[0]

    from sklearn.preprocessing import LabelEncoder

    temp_dict = dict()


    imputed_df.dropna(inplace=True)


    columns = imputed_df.columns
    for column in columns:

        if column=='ID':
            continue

        #impute missing values
        # mean for numerical and 'missing' for categorical
        if (imputed_df[column].dtype == 'float64' or imputed_df[column].dtype == 'int64'):
           if imputed_df[column].isnull().sum()==len(imputed_df):

                imputed_df[column] = imputed_df[column].fillna(0)


           else :

                imp = SimpleImputer(missing_values=np.nan, strategy='mean')

                transformed_column = imp.fit_transform(imputed_df[column].values.reshape(-1, 1))

                imputed_df[column] = transformed_column


        if (imputed_df[column].dtype == 'object'):
            # impute missing values for categorical variables


            imp = SimpleImputer(missing_values=None, strategy='constant', fill_value='missing')
            imputed_df[column] = imp.fit_transform(imputed_df[column].values.reshape(-1, 1))
            imputed_df[column] = imputed_df[column].astype('str')

            le = LabelEncoder()

            le.fit(imputed_df[column])
                    # print(le.classes_)
            imputed_df[column] = le.transform(imputed_df[column])
            temp_dict[column] = le

    # Create a random forest regressor
    rf = RandomForestRegressor(n_estimators=200)

    X = imputed_df.drop(['FUTURE_CLOSE_PRICE'], axis=1)
    y = imputed_df['FUTURE_CLOSE_PRICE']


    rf.fit(X,y)

    sw_spec_id = client.software_specifications.get_id_by_name('default_py3.7')
    metadata = {

       client.repository.ModelMetaNames.NAME: 'rf_sklearn_wml_'+name,
       client.repository.ModelMetaNames.SOFTWARE_SPEC_UID: sw_spec_id,
       client.repository.ModelMetaNames.TYPE: 'scikit-learn_0.23'
       }
    model_dic = client.repository.store_model(rf, meta_props=metadata)
    metadata_dic = model_dic.get('metadata')
    model_id = metadata_dic.get('id')
    model_name = metadata_dic.get('name')
    self.output([model_id, model_name])

"""

output_signature = {'model_id': 'str', 'model_name': 'str'}


nz_groupapply = NZFunGroupedApply(df=idadf, code_str=code_str_host_spus, index='TICKER', fun_name="stocks_rf_ml",
                                  output_signature=output_signature, merge_output_with_df=False)

result = nz_groupapply.get_result()
result = result.as_dataframe()

print(result)

The output is as follows:

model_id                           model_name

0  f658235e-34e0-4de0-abdc-ddd6b3953ac9   rf_sklearn_wml_CRM
1  b7d8fa84-d430-4e43-865f-499ec4d03a62  rf_sklearn_wml_TSLA
2  fe438306-9b71-47df-815e-9e2de23f1f67   rf_sklearn_wml_AAL
3  40b1b6b6-7c29-41de-ad98-e5239687ac64   rf_sklearn_wml_CVS
4  cb52f694-6318-49c3-a936-1098a1aee29f   rf_sklearn_wml_IBM
5  2fc23d83-9b8d-4374-b7dd-b46ca151417e   rf_sklearn_wml_XOM

Retrieve the models from Watson Machine Learning and score the test data set on Netezza

The custom model scoring function in this article fetches the models from Watson Machine Learning, loads the ticker-specific model, and scores the test data set. Interaction between the Watson Studio notebook, Netezza Performance Server, and Watson Machine Learning in this pushdown scenario could be depicted as follows.

Interaction scenario

For the pushdown API selection, NZFunGroupedApply is chosen because the need is to score the test data on the stock/ticker basis.

idadf = IdaDataFrame(idadb, 'stocks_features_test')


code_str_host_spus = """def stocks_rf_ml(self, df):

    import numpy as np


    from sklearn.impute import SimpleImputer
    from sklearn.metrics import mean_squared_error
    from sklearn.ensemble import RandomForestRegressor
    from ibm_watson_machine_learning import APIClient


    imputed_df = df.copy()
    imputed_df.dropna(inplace=True)
    name = imputed_df.TICKER[0]


    from sklearn.preprocessing import LabelEncoder

    temp_dict = dict()


    columns = imputed_df.columns
    for column in columns:

        if column=='ID':
            continue

        #impute missing values
        # mean for numerical and 'missing' for categorical
        if (imputed_df[column].dtype == 'float64' or imputed_df[column].dtype == 'int64'):
           if imputed_df[column].isnull().sum()==len(imputed_df):

                imputed_df[column] = imputed_df[column].fillna(0)


           else :

                imp = SimpleImputer(missing_values=np.nan, strategy='mean')

                transformed_column = imp.fit_transform(imputed_df[column].values.reshape(-1, 1))

                imputed_df[column] = transformed_column


        if (imputed_df[column].dtype == 'object'):
            # impute missing values for categorical variables


            imp = SimpleImputer(missing_values=None, strategy='constant', fill_value='missing')
            imputed_df[column] = imp.fit_transform(imputed_df[column].values.reshape(-1, 1))
            imputed_df[column] = imputed_df[column].astype('str')

            le = LabelEncoder()

            le.fit(imputed_df[column])
                    # print(le.classes_)
            imputed_df[column] = le.transform(imputed_df[column])
            temp_dict[column] = le

    wml_credentials = {
                   'url': 'xxx',
                   'apikey':'xxx'
                  }

    client = APIClient(wml_credentials)
    client.set.default_space('xxx')
    model_dict= client.repository.get_model_details()


    rf=None


    for key, value in model_dict.items():

        if (key=='resources'):
            for res_element in value:
                metadata_dic = res_element.get('metadata')
                #print(metadata_dic)
                model_name ='rf_sklearn_wml_'+name
                if (metadata_dic.get('name')==model_name):
                   rf = client.repository.load(metadata_dic.get('id'))



    X_test = imputed_df.drop(['FUTURE_CLOSE_PRICE'], axis=1)
    y_test = imputed_df['FUTURE_CLOSE_PRICE']

    test_results_df = X_test.copy()

    y_pred = rf.predict(X_test)
    test_results_df['FUTURE_CLOSE_PRICE_PRED'] =y_pred
    test_results_df['FUTURE_CLOSE_PRICE'] =y_test


    accuracy  = rf.score(X_test, y_test)    


    rms = mean_squared_error(y_test, y_pred)

    ds_size = len(X_test)
    test_results_df['DATASET_SIZE'] = ds_size
    test_results_df['ACCURACY']=round(accuracy,2)
    test_results_df['MEAN_SQUARE_ERROR']=round(rms)

    #for all the columns that had label encoders, do an inverse transform

    original_columns = test_results_df.columns

    for column in original_columns:

     if column in temp_dict:   
      test_results_df[column] = temp_dict[column].inverse_transform(test_results_df[column])



    def print_output(x):
                row = [int(x['ID']), x['FUTURE_CLOSE_PRICE_PRED'], x['DATASET_SIZE'], x['ACCURACY']]


                self.output(row)


    test_results_df.apply(print_output, axis=1)
"""

output_signature = {'ID': 'int', 'FUTURE_CLOSE_PRICE_PRED': 'double',
                    'DATASET_SIZE': 'int', 'ACCURACY': 'float'}


nz_groupapply = NZFunGroupedApply(df=idadf, code_str=code_str_host_spus, index='TICKER', fun_name="stocks_rf_ml",
                                  output_signature=output_signature, output_table='stocks_predictions_ws', merge_output_with_df=True)

result = nz_groupapply.get_result()
result = result.as_dataframe()
print(result)

test_pred_groups = result.groupby('TICKER')

for name, group in test_pred_groups:
    input = group.copy()
    input["DATE"] = pd.to_datetime(input["DATE"])

    input = input.sort_values(by="DATE")
    input['DATE'] = input['DATE'].dt.date
    input.plot(x="DATE", y=["FUTURE_CLOSE_PRICE", "FUTURE_CLOSE_PRICE_PRED",], kind="line", title=name)

The output is as follows.

[750 rows x 1 columns]
     FUTURE_CLOSE_PRICE_PRED  DATASET_SIZE  ACCURACY  FUTURE_CLOSE_PRICE  \
0                  13.540444           125      0.87           11.740000   
1                  14.041644           125      0.87           12.740000   
2                  14.606994           125      0.87           14.820000   
3                  15.115144           125      0.87           14.270000   
4                  16.838494           125      0.87           17.209999   
..                       ...           ...       ...                 ...   
745                51.201630           125      0.75           56.660000   
746                51.026211           125      0.75           55.270000   
747                51.046427           125      0.75           58.110001   
748                52.186832           125      0.75           59.189999   
749                57.990582           125      0.75           62.580002   

          MA14       MA30       MA50      MA200        DATE       OPEN  \
0    11.653571  12.242000  12.499200  14.109396  2020-11-11  12.400000   
1    11.860000  12.183333  12.412000  13.807500  2020-11-17  12.480000   
2    12.341429  12.159000  12.399000  13.492750  2020-11-23  12.750000   
3    13.170000  12.479000  12.501200  13.189100  2020-11-30  14.920000   
4    14.114285  12.824333  12.768600  12.930750  2020-12-04  16.400000   
..         ...        ...        ...        ...         ...        ...   
745  56.559284  57.771332  55.683941  43.055321  2021-04-15  57.419998   
746  56.330715  57.237667  56.282600  43.344662  2021-04-21  54.500000   
747  56.052856  56.487335  56.699001  43.657146  2021-04-27  56.009998   
748  56.769287  56.531666  57.175598  43.995815  2021-05-03  57.980000   
749  57.962143  57.214668  57.622200  44.396042  2021-05-07  61.070000   

          HIGH        LOW  CLOSE  ADJCLOSE     VOLUME TICKER        ID  
0    12.460000  11.930000  12.04     12.04   81359600    AAL  39238167  
1    12.810000  12.230000  12.70     12.70   61956500    AAL  39238171  
2    13.580000  12.690000  13.56     13.56  100764000    AAL  39238175  
3    14.960000  13.930000  14.13     14.13   97536300    AAL  39238179  
4    16.930000  16.120001  16.40     16.40  117387600    AAL  39238183  
..         ...        ...    ...       ...        ...    ...       ...  
745  57.419998  56.779999  56.98     56.98   23250300    XOM  39239530  
746  56.130001  54.299999  56.00     56.00   16625600    XOM  39239534  
747  56.630001  55.810001  56.41     56.41   19278600    XOM  39239538  
748  58.990002  57.740002  58.82     58.82   20509600    XOM  39239542  
749  62.470001  60.849998  62.43     62.43   33555200    XOM  39239546  

[750 rows x 17 columns]

AAL

CRM

CVS

IBM

TSLA

XoM

Summary

In this article, you learned how to use Watson Studio, Netezza Performance Server, and Watson Machine Learning as a single platform for pushdown execution of data preparation, model building, and model scoring steps. With the nzpyida package, you can build models inside Netezza Performance Server and save models to Watson Machine Learning, then fetch the stored models from Watson Machine Learning and score inside Netezza Performance Server. You can also use Netezza Performance Server for model scoring on a stand-alone basis if there are pre-existing models available outside Netezza Performance Server. Netezza support for such a wide variety of pushdown scenarios makes it flexible for users to truly benefit from in-database analytics when compared to other database platforms that support AI and machine learning.