ネイティブ Python拡張モデルノード：モデル作成OneClassSVM


SPSS Modeler18.5の新機能の [ネイティブ Python API](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis)のテストをしてみました。これは、「拡張ノード」のなかでpandasを使ってModelerのデータを読んだり書いたりすることができる機能です。

この記事ではネイティブ Python APIで「拡張モデル」が、どのように利用可能かをテストしてみました。
マニュアルに[サンプル・ストリーム](https://www.ibm.com/docs/ja/SS3RA7_18.5.0/modeler_r_nodes_ddita/clementine/python-extension-demo.zip)がついていましたので、これを修正してテストしてみています。

サンプルストリームは以下です。

https://github.com/hkwd/231229NativePythonExtension/raw/master/231229NativePythonExtensionSample/python-extension-demo_hkwd.str

* テスト環境
    * Windows 11 64bit
    * Modeler 18.5
    * Python 3.10.7


# 拡張モデル
## OneClassSVMのモデル作成とスコアリング
あらかじめ、以下のコマンドを[管理者権限で実行してscikit-learnを導入しておきます](https://qiita.com/kawada2017/items/9252f93cf59948dd2810)。
```powershell
"C:\Program Files\IBM\SPSS\Modeler\18.5\python_venv\Scripts\python.exe" -m pip install scikit-learn
```

「Python」を選択し、「Python model building Syntax」の欄と「Python model scoring Syntax」の欄に以下のスクリプトを記入します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/340b0f4d-310c-69d7-4f1a-a9b557d21b83.png)

## モデルの作成(Python model building Syntax)
```python:#OneClassSVMのモデル作成
###Use this code to import data from Modeler to Python
# ネイティブ Python APIのパッケージ
import modelerpy

import numpy
from sklearn import svm

#前のノードからデータの読み込み
modelerData = modelerpy.readPandasDataframe()
#Modelerのデータモデルを取得
#modelerDataModel = modelerpy.getDataModel()
modelerModel = None
modelName = None

#    build model here
# モデルの作成
#カテゴリ変数をLabelEcodingして数値化、numpyの配列化
encoded_data = modelerData.replace({'Sex': {'M': 0, 'F': 1}, 'BP': {'HIGH':3, 'NORMAL':2, 'LOW':1}, 'Cholesterol': {'HIGH':3, 'NORMAL':2}, 'Drug':{'drugA':1, 'drugB':2, 'drugC':3, 'drugX':4,  'drugY':5}})
training_data = encoded_data.to_numpy()

#OneClassSVMでモデル作成。教師なし学習
modelerModel = svm.OneClassSVM()
modelerModel.set_params(nu=0.1)
modelerModel.fit(training_data)

#モデルをストリーム内に保存
modelName = 'svmmodel'
modelerpy.saveModel(modelerModel, modelName)
```

「モデルの作成(Python model building Syntax)」のスクリプトを解説します。

まず、[modelerpy.readPandasDataframe()](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis#extension_nativepython_api__title__4)で前ノードからのデータを読み込んでいます。

前ノードのデータから、カテゴリ変数の「Sex」「BP」「Cholesterol」「Drug」をLabelEcodingして数値化し、numpyの配列化して学習データを準備しています。
```python:カテゴリ変数をLabelEcodingして数値化、numpyの配列化
encoded_data = modelerData.replace({'Sex': {'M': 0, 'F': 1}, 'BP': {'HIGH':3, 'NORMAL':2, 'LOW':1}, 'Cholesterol': {'HIGH':3, 'NORMAL':2}, 'Drug':{'drugA':1, 'drugB':2, 'drugC':3, 'drugX':4,  'drugY':5}})
training_data = encoded_data.to_numpy()
```

OneClassSVMでモデル作成を作成しています。ここは普通のscikitlearnのモデリングです。
```python:OneClassSVMでモデル作成。教師なし学習
modelerModel = svm.OneClassSVM()
modelerModel.set_params(nu=0.1)
modelerModel.fit(training_data)
```
モデルの名前をつけて、[saveModel](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis#extension_nativepython_api__title__3)のAPIでストリーム内に保存しています。
Python for Sparkでは、scikitlearnのモデルはストリーム内に保存できなかったために、どこかのフォルダにpickleで保存していましたが、そういう作業が不要です。
```python:モデルをストリーム内に保存
modelName = 'svmmodel'
modelerpy.saveModel(modelerModel, modelName)
```

## モデルのスコアリング(Python model scoring Syntax)
```python:#OneClassSVMのスコアリング
###Use this code to transfer data between Modeler and Python
# ネイティブ Python APIのパッケージ
import modelerpy

import numpy
from sklearn import svm

field_outlier = 'Outlier'
field_dist_hp = 'DIST_HP'

#データモデルの参照時
if modelerpy.isComputeDataModelOnly():
    modelerDataModel = modelerpy.getDataModel()
    outputDataModel = None

    #Compute output data model here
    #出力データモデルの設定。スコアリング結果を出力するOutlier列とDIST_HP列を追加
    outputDataModel = modelerDataModel
    outputDataModel.addField(modelerpy.Field(field_outlier, "real", measure="flag"))
    outputDataModel.addField(modelerpy.Field(field_dist_hp, "real", measure="continuous"))
    modelerpy.setOutputDataModel(outputDataModel)

#データ出力時
else:
    modelerData = modelerpy.readPandasDataframe()
    modelName = None

    #モデルをストリームから読み出し
    modelName = 'svmmodel'
    modelerModel = modelerpy.loadModel(modelName)
    outputData = None

    #データの出力
    outputData = modelerData
    #カテゴリ変数をLabelEcodingして数値化、numpyの配列化
    encoded_data = modelerData.replace({'Sex': {'M': 0, 'F': 1}, 'BP': {'HIGH':3, 'NORMAL':2, 'LOW':1}, 'Cholesterol': {'HIGH':3, 'NORMAL':2}, 'Drug':{'drugA':1, 'drugB':2, 'drugC':3, 'drugX':4,  'drugY':5}})
    score_data = encoded_data.to_numpy()

    #スコアリング
    outlier = modelerModel.predict(score_data)
    dist_hp = modelerModel.decision_function(score_data)
    outputData[field_outlier] = outlier
    outputData[field_dist_hp] = dist_hp
    #データの出力
    modelerpy.writePandasDataframe(outputData)
```

「モデルのスコアリング(Python model scoring Syntax)」のスクリプトを解説します。
以下でスコアリングで追加される列名を定義しています。
```python:
field_outlier = 'Outlier'
field_dist_hp = 'DIST_HP'
```

大きくはデータモデルの処理とデータ自体の処理の二つに分かれています。
### 処理の分岐
「データモデルの処理」と「データ自体の処理」の分岐を以下のif文で行っています。
```python:
if modelerpy.isComputeDataModelOnly()
```
[modelerpy.isComputeDataModelOnly()](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis#extension_nativepython_api__title__2):のif文は、後続のノードがデータモデルを参照する場合に実データを用意しなくていいための分岐になります。Modelerは、後続のノードを接続した際に前のノードのデータモデルを参照します。例えば「フィルター」ノードを接続した場合にはデータは必要ありませんが、「列名」の情報は必要になります。このif文をなくしても動作はしますが、列名などのデータモデルを参照したいだけなのにデータの読み込みが行われてしまうため、無駄な処理が動いてしまいます。
なお、このif文は、「拡張の変換」ノードと「拡張モデル」ノードといった後続ノードが必要なノードでも、同様に使われます。

### データモデルの処理
modelerDataModel = [modelerpy.getDataModel()](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis#extension_nativepython_api__title__2)で前のノードからデータモデルを取得しています。

以下でスコアリングで追加される列のメタデータを[addField](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis#extension_nativepython_api__title__6)で追加しています。
```python:出力データモデルの設定。スコアリング結果を出力するOutlier列とDIST_HP列を追加
outputDataModel = modelerDataModel
outputDataModel.addField(modelerpy.Field(field_outlier, "real", measure="flag"))
outputDataModel.addField(modelerpy.Field(field_dist_hp, "real", measure="continuous"))
```
[modelerpy.setOutputDataModel(outputDataModel)](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis#extension_nativepython_api__title__2)で後続ノードに渡すデータモデルを設定しています。

### スコアリングの処理
[modelerpy.readPandasDataframe()](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis#extension_nativepython_api__title__4)で前ノードからのデータを読み込んでいます。

以下で、ストリーム内に保存したOneClassSVMのモデルを読み出しています。
```python:モデルをストリームから読み出し
modelName = 'svmmodel'
modelerModel = modelerpy.loadModel(modelName)
```

以下で、前ノードのデータから、カテゴリ変数をLabelEcodingして数値化、numpyの配列化してスコアリングデータを準備しています
```python:カテゴリ変数をLabelEcodingして数値化、numpyの配列化
encoded_data = modelerData.replace({'Sex': {'M': 0, 'F': 1}, 'BP': {'HIGH':3, 'NORMAL':2, 'LOW':1}, 'Cholesterol': {'HIGH':3, 'NORMAL':2}, 'Drug':{'drugA':1, 'drugB':2, 'drugC':3, 'drugX':4,  'drugY':5}})
score_data = encoded_data.to_numpy()
```

以下で、スコアリングを行っています。
[predict](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html#sklearn.svm.SVC.predict)で外れ値化どうかのスコアを算出し、[decision_function](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html#sklearn.svm.SVC.decision_function)で距離のスコアを算出しています。
```python:スコアリング
outlier = modelerModel.predict(score_data)
dist_hp = modelerModel.decision_function(score_data)
outputData[field_outlier] = outlier
outputData[field_dist_hp] = dist_hp
```

[modelerpy.writePandasDataframe(outputData)](https://www.ibm.com/docs/ja/spss-modeler/18.5.0?topic=spark-native-python-apis#extension_nativepython_api__title__4)で後続のノードにデータを出力しています。

### モデリングノードの実行結果
モデリングノードを実行すると、以下のように、「拡張モデル」のスコアリングノードが自動生成されます。また、「シンタックス」には「拡張モデル」のモデリングノードの「Python model scoring Syntax」に記述したPythonスクリプトがそのままコピーされます。
一応「編集」は可能ですが、「拡張モデリング」ノードとコードが食い違っていると後で混乱すると思いますので、開発・テスト用と考えるのがよいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/21599c0c-255b-9cf8-1312-753751419123.png)

### スコアリングノードの実行結果
「テーブル」ノードを接続し、実行すると、スコアリング結果が「Outlier」、「DIST_HP」の列に書き出されていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/c7a1245f-611a-acb3-ac59-d7e875c057e2.png)