LightGBMをSPSS Modelerで使用する

SPSS Modelerの拡張モデルノードを使って、LightGBMを使ってモデリングとスコアリングを行いました。


■環境
Modeler 18.2.2
python 3.7.7(非Anaconda)
lightgbm 3.1.1
Windows 10

# サンプルストリーム
https://github.com/hkwd/210224PythonExtension/raw/master/lightgbm/210323lightgbm.str

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/a1ec7b03-8f28-dd8c-049f-503a8932da56.png)

# 事前作業
以下の@1000akiさんの記事を参考にSPSSの拡張ノードでPythonを利用できるようにしておきます。
pythonを導入して、options.cfgの設定まで行っておきます。

https://qiita.com/1000aki/items/72c3c9ea0fe00edd6d19

# LightGBMの導入
個人フォルダに作っている場合は普通にpip installできますが、上の手順はAll Userで利用できるPythonを"C:\Program Files\Python37"に導入しているので、管理者権限でpip installを使う必要があります。

管理者権限でコマンドプロンプトを起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/0e40c675-ba2d-4b40-e4fa-0c864b0e6646.png)


```console:コマンド
cd "C:\Program Files\Python37"
python -m pip install lightgbm
python -m pip list
```
```console:結果
C:\Program Files\Python37>python -m pip install lightgbm
Collecting lightgbm
  Using cached https://files.pythonhosted.org/packages/80/28/fecd02e7856e36afcdc71ee968b1b3859b3bc784e042991d5520e4d7be2c/lightgbm-3.1.1-py2.py3-none-win_amd64.whl
Collecting wheel (from lightgbm)
  Using cached https://files.pythonhosted.org/packages/65/63/39d04c74222770ed1589c0eaba06c05891801219272420b40311cd60c880/wheel-0.36.2-py2.py3-none-any.whl
Requirement already satisfied: numpy in c:\program files\python37\lib\site-packages (from lightgbm) (1.20.1)
Collecting scikit-learn!=0.22.0 (from lightgbm)
  Downloading https://files.pythonhosted.org/packages/16/33/e0b09b2810e355b667cd3b28850c36963735a77a431efdb2c2ca1c1c5cea/scikit_learn-0.24.1-cp37-cp37m-win_amd64.whl (6.8MB)
     |████████████████████████████████| 6.8MB 3.3MB/s
Collecting scipy (from lightgbm)
  Downloading https://files.pythonhosted.org/packages/06/c8/78d94bf4d039fd1ef878363491f2925e29b7ccfafa3dcd9dddfaeb0f4608/scipy-1.6.1-cp37-cp37m-win_amd64.whl (32.6MB)
     |████████████████████████████████| 32.6MB 3.2MB/s
Collecting joblib>=0.11 (from scikit-learn!=0.22.0->lightgbm)
  Downloading https://files.pythonhosted.org/packages/55/85/70c6602b078bd9e6f3da4f467047e906525c355a4dacd4f71b97a35d9897/joblib-1.0.1-py3-none-any.whl (303kB)
     |████████████████████████████████| 307kB 3.3MB/s
Collecting threadpoolctl>=2.0.0 (from scikit-learn!=0.22.0->lightgbm)
  Using cached https://files.pythonhosted.org/packages/f7/12/ec3f2e203afa394a149911729357aa48affc59c20e2c1c8297a60f33f133/threadpoolctl-2.1.0-py3-none-any.whl
Installing collected packages: wheel, joblib, scipy, threadpoolctl, scikit-learn, lightgbm
  WARNING: The script wheel.exe is installed in 'C:\Program Files\Python37\Scripts' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed joblib-1.0.1 lightgbm-3.1.1 scikit-learn-0.24.1 scipy-1.6.1 threadpoolctl-2.1.0 wheel-0.36.2
WARNING: You are using pip version 19.2.3, however version 21.0.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.

C:\Program Files\Python37>python -m pip list
Package         Version
--------------- -------
joblib          1.0.1
lightgbm        3.1.1
numpy           1.20.1
pandas          1.2.3
pip             19.2.3
python-dateutil 2.8.1
pytz            2021.1
scikit-learn    0.24.1
scipy           1.6.1
setuptools      41.2.0
six             1.15.0
threadpoolctl   2.1.0
wheel           0.36.2
WARNING: You are using pip version 19.2.3, however version 21.0.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.

```

# モデルを保存するパスを作っておきます。
モデルを保存するパスを作っておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/114ade4b-3381-4dcc-2b4c-b8176f1ae3db.png)

#ストリームの作成
Demosフォルダにあるtree_credit.savを使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/cfc68b54-2743-7e40-6573-9d965faa3605.png)

置換ノードで全列を整数型にしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/73dcf3c0-3653-dcba-0584-9c48c5c0944e.png)

データ型ノードでCredit_ratingをフラグ型にして、ロールを対象にします。
IncomeからCar_loansは名義型にしておきます。
AgeやIncomeなどからCredit_ratingを予測するモデルを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/c9f4411c-dbf2-2070-1d3c-95ddd6f8a031.png)

データ区分ノードで学習データとテストデータに分けます。
なお、Python連携でつかうPysparkが日本語列をうまく扱えないのでフィールド名を"Partition"に変更しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/f08dbdea-5baf-6b31-4086-05e6b3085526.png)

拡張モデルノードでPython for Sparkを選びます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/716e38ca-6e9a-5131-9183-9ec3cb87f474.png)

以下のlightGBMのモデリングとスコアリングのスクリプトを入力します。

```python:Pythonモデル作成シンタックス
#モデルファイルの保存パス
modelpath='c:/temp/modelpath/'
modelfile='lgbModel.pkl'

# Analytics Server 対話用のライブラリのインポート
import spss.pyspark.runtime

# Analytics Serverコンテキストオブジェクト定義
ascontext = spss.pyspark.runtime.getContext()
# スキーマ読み込み
inputSchema = ascontext.getSparkInputSchema()

#Roleから説明変数名と目的変数名を取得
predictors =[]
for col  in inputSchema:
    print(col.metadata['role'])
    #説明変数列の取得
    if col.metadata['role']=='input':
        predictors.append(col.name)
    #目的変数列の取得
    elif col.metadata['role']=='target':
        target=col.name
        targetType=col.dataType 
        targetMeta={}
        targetMeta['measure']=col.metadata['measure']
        prediction_field = "$L-" + col.name
        confidence_field = "$LC-" + col.name
    #データ区分列の取得
    elif col.metadata['role']=='partition':
        partition=col.name

# データ読込
sdf = ascontext.getSparkInputData()
#データ区分が学習のデータのみに絞る
print(sdf.filter(partition+" like '1%'").count())
if partition:
    sdf=sdf.filter(partition+" like '1%'")
#Spark dataframeをPandasに変換
df = sdf.toPandas()

#lighgbmでモデル作成
import lightgbm as lgb
model = lgb.LGBMClassifier()
model.fit(df[predictors], df[target]) 

#モデルをファイルシステムに保存
import pickle
pickle.dump(model, open(modelpath+modelfile, 'wb'))
```

```python:Pythonモデルスコアリングシンタックス
#モデルファイルの保存パス
modelpath='c:/temp/modelpath/'
modelfile='lgbModel.pkl'

# Analytics Server 対話用のライブラリのインポート
import spss.pyspark.runtime
from pyspark.sql.types import FloatType, StructField 
from pyspark.sql.context import SQLContext

# Analytics Server 対話用のライブラリのインポート
import spss.pyspark.runtime

# Analytics Serverコンテキストオブジェクト定義
ascontext = spss.pyspark.runtime.getContext()
# スキーマ読み込み
inputSchema = ascontext.getSparkInputSchema()

#Roleから説明変数名と目的変数名を取得
predictors =[]
for col  in inputSchema:
    #説明変数列の取得
    if col.metadata['role']=='input':
        predictors.append(col.name)
    #目的変数列の取得
    elif col.metadata['role']=='target':
        target=col.name
        targetType=col.dataType 
        targetMeta={}
        targetMeta['measure']=col.metadata['measure']
        prediction_field = "$L-" + col.name
        confidence_field = "$LC-" + col.name

#予測値のフィールドを出力スキーマに追加
outputSchema = inputSchema
outputSchema.fields.append(StructField(prediction_field, targetType, nullable=True,metadata=targetMeta))
outputSchema.fields.append(StructField(confidence_field, FloatType(), nullable=True))

ascontext.setSparkOutputSchema(outputSchema)

if not ascontext.isComputeDataModelOnly():
        #スコアリングデータの読み込み
        indf = ascontext.getSparkInputData()
        df = indf.toPandas()

        #ファイルシステム上のモデルを読み込む 
        import pickle
        loaded_model = pickle.load(open(modelpath+modelfile, 'rb'))
        
        #スコアリング
        df[prediction_field] =  loaded_model.predict(df[predictors])
        df[confidence_field] =  loaded_model.predict_proba(df[predictors])[:,1]
        print(df)

        #Sparkコンテキストの取得
        sc = ascontext.getSparkContext()
        #データの出力
        sqlCtx = SQLContext(sc)
        outdf = sqlCtx.createDataFrame(df,schema=outputSchema)

        ascontext.setSparkOutputData(outdf)
```

実行するとモデルナゲットができるので、テーブルノードを繋ぎます。
テーブルノードを実行すると＄L-Credit_ratingに予測値、$LC-Credit_ratingに確信度が出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/e59ce4a5-8d70-6b55-066c-5f56cb82222e.png)


モデルナゲットにデータ型ノード＋精度分析ノードも繋ぎます。データ型ノードで値を読み込みます。拡張ノードのモデルナゲットはインスタンス化した値を保持できないようなので、この作業をしないと後続の精度分析ノードがうまく動きませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/96845eee-f7cb-c37f-62a2-43797ddf36fd.png)


精度分析ノードで「次を使用する予測済み/予測フィールドの検出」で「フィールド名の形式」を選択します。これで＄L-の接頭辞があるフィールドを予測結果列と認識します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/b912c4ca-f770-a909-b99e-1b3b8cdc9417.png)

以下のように通常のモデルのように精度を確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/e5666c82-761f-929a-c83b-7e1b29b219bd.png)

#モデリングスクリプトの解説
最初にモデルの保存パスを定義しています。
本来はascontext.setModelContentFromStringでモデルナゲット内にモデルが保存できると思うのですが、どうしても失敗するために、ファイルシステム内に保存しています。
スコアリング時にはこのファイルが必要になります。特にModeler ServerやCADSと組み合わせて実行する場合など、別のマシンでスコアリングを行う場合には、同じ場所にモデルファイルがないと動きませんので注意が必要です。


```python:Pythonモデル作成シンタックス-モデルの保存パスを定義
#モデルファイルの保存パス
modelpath='c:/temp/modelpath/'
modelfile='lgbModel.pkl'
```
以下でModelerとのやり取りを行うためのコンテキストオブジェクトを作っています。


```python:Pythonモデル作成シンタックス-AnalyticsServerコンテキストオブジェクト定義
# Analytics Server 対話用のライブラリのインポート
import spss.pyspark.runtime
# Analytics Serverコンテキストオブジェクト定義
ascontext = spss.pyspark.runtime.getContext()
```

以下でノードへの入力データから説明変数名群と目的変数名、データ区分列名を取得するために、スキーマを読んでいます。

```python:Pythonモデル作成シンタックス-スキーマ読み込み
# スキーマ読み込み
inputSchema = ascontext.getSparkInputSchema()

#Roleから説明変数名群と目的変数名、データ区分列名を取得
predictors =[]
for col  in inputSchema:
```

スキーマ情報には、以下のような各列の列名、データ型、尺度、ロールなどの情報が含まれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/2d07fbae-769d-dca5-e6ca-7ceb26372ab7.png)

詳しくは[マニュアル](https://www.ibm.com/support/knowledgecenter/ja/SS3RA7_18.2.2/modeler_r_nodes_ddita/clementine/r_pyspark_api_metadata.html)に書いてあります。




以下でロールが入力、つまり説明変数の列名のリストを作っています。

```python:Pythonモデル作成シンタックス-説明変数列名リストの取得
    #説明変数列名リストの取得
    if col.metadata['role']=='input':
        predictors.append(col.name)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/1fc7384e-0cc2-b4a8-95bb-d166c9b00c02.png)

以下でロールが対象、つまり目的変数の列名を取得しています。
なお、対象列が複数あった場合は後からでてきたものが採用されます。

```python:Pythonモデル作成シンタックス-目的変数列名の取得
    #目的変数列名の取得
    elif col.metadata['role']=='target':
        target=col.name
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/a2b454f8-9b12-ef7d-62a5-67c39c25fda6.png)

以下でロールがpartition、つまりデータ区分列名を取得しています。
なお、対象列が複数あった場合は後からでてきたものが採用されます。

```python:Pythonモデル作成シンタックス-データ区分列名の取得
    #データ区分列名の取得
    elif col.metadata['role']=='partition':
        partition=col.name
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/e16e86e4-df14-61db-3431-63a12b47143e.png)

以下でデータを読み込んでいます。
データ区分列名がある場合は、sdf.filter(partition+" like '1%'")で、1から始まる行、つまり学習データだけに絞り込んでいます。
Modelerから取得されるデータフレームはSpark DataFrameなので、sdf.toPandas()
でpandasのDataFrameにして取り扱いやすくしています。

```python:Pythonモデル作成シンタックス-データ読込
# データ読込
sdf = ascontext.getSparkInputData()
#データ区分が学習のデータのみに絞る
if partition:
    sdf=sdf.filter(partition+" like '1%'")
#Spark dataframeをPandasに変換
df = sdf.toPandas()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/a04ce898-05d2-2fa3-1f51-a22040571c6d.png)

以下でLightGBMのモデルを作っています。
LightGBMには様々なパラメーターがありますが、ここではデフォルトで動かしています。

```python:Pythonモデル作成シンタックス-モデル作成
#lighgbmでモデル作成
import lightgbm as lgb
model = lgb.LGBMClassifier()
model.fit(df[predictors], df[target]) 
```
最後に、生成されたモデルをpickleを使ってシリアライズして、スクリプトの先頭で定義したmodelpath+modelfileに保存しています。

```python:Pythonモデル作成シンタックス-モデルをファイルシステムに保存
#モデルをファイルシステムに保存
import pickle
pickle.dump(model, open(modelpath+modelfile, 'wb'))
```
# スコアリングスクリプトの解説

モデリングスクリプトで指定したものと同じモデルファイル保存パスを指定します。

```python:Pythonモデルスコアリングシンタックス-モデルファイル保存パス指定
#モデルファイルの保存パス
modelpath='c:/temp/modelpath/'
modelfile='lgbModel.pkl'
```

モデリングスクリプトでも行ったように、Modelerとのやり取りを行うためのコンテキストオブジェクトを作っています。
モデリングスクリプトに加えて、ここでは新しいSparkDataFrameを作成するためにSQLContextを、また新しい列をつくるためにFloatType, StructFieldもインポートしています。

```python:Pythonモデルスコアリングシンタックス-AnalyticsServerコンテキストオブジェクト定義
# Analytics Server 対話用のライブラリのインポート
import spss.pyspark.runtime
from pyspark.sql.types import FloatType, StructField 
from pyspark.sql.context import SQLContext

# Analytics Serverコンテキストオブジェクト定義
ascontext = spss.pyspark.runtime.getContext()
```

以下でノードへの入力データから説明変数名群と目的変数名を取得するために、スキーマを読んでいます。モデリングスクリプトと同様です。

```python:Pythonモデルスコアリングシンタックス-スキーマ読み込み
# スキーマ読み込み
inputSchema = ascontext.getSparkInputSchema()

#Roleから説明変数名と目的変数名を取得
predictors =[]
for col  in inputSchema:
```
以下でロールが入力、つまり説明変数の列名のリストを作っています。ここもモデリングスクリプトと同様です。

```python:Pythonモデルスコアリングシンタックス-説明変数列名リストの取得

    #説明変数列の取得
    if col.metadata['role']=='input':
        predictors.append(col.name)
```

以下でロールが対象、つまり目的変数の列名を取得しています。
ここはモデリングスクリプトでのスクリプトに加えて、予測値列（"\$L-"）や確信度列（"\$LC-"）を追加するために列名を定義したり、データタイプ（col.dataType）や尺度（col.metadata['measure']）も取得しています。

```python:Pythonモデルスコアリングシンタックス-目的変数列名の取得
    #目的変数列の取得
    elif col.metadata['role']=='target':
        target=col.name
        targetType=col.dataType 
        targetMeta={}
        targetMeta['measure']=col.metadata['measure']
        prediction_field = "$L-" + col.name
        confidence_field = "$LC-" + col.name
```

以下で入力データのスキーマに予測値列と確信度列を追加して、出力スキーマとしています。予測値列は目的変数列のデータ型やメタデータをコピーしています。確信度列はFloatType()を指定しています。

```python:Pythonモデルスコアリングシンタックス-予測値列と確信度列を出力スキーマに追加
#予測値列と確信度列を出力スキーマに追加
outputSchema = inputSchema
outputSchema.fields.append(StructField(prediction_field, targetType, nullable=True,metadata=targetMeta))
outputSchema.fields.append(StructField(confidence_field, FloatType(), nullable=True))

ascontext.setSparkOutputSchema(outputSchema)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/711d0379-a1c4-8bd2-d8ef-bfaa45a97e75.png)


以下は実際にスコアリングを行う場合に以降のスクリプトを実行するという条件になります。
ここは少しややこしいところです。
ここまでのスクリプトはsetSparkOutputSchemaで出力スキーマを定義するものでした。モデルナゲットの後ろにノードを追加する場合、DataModelをComputeしてスキーマを定義することは必要ですが、実際にスコアリングしたデータは必要ありません。
ですので無駄なスコアリング処理をしないために、「スコアリングデータがいる場合にのみ」（Not isComputeDataModelOnly）、スコアリングをするスクリプトを実行するという条件を入れています。
マニュアルに[DataModelOnly モード](https://www.ibm.com/support/knowledgecenter/ja/SS3RA7_18.2.2/modeler_r_nodes_ddita/clementine/r_pyspark_api.html)という解説があります。

```python:Pythonモデルスコアリングシンタックス-スコアリング実行判定
if not ascontext.isComputeDataModelOnly():
```

以下で入力データを読み込んでPandasのDataFrameに変換しています。

```python:Pythonモデルスコアリングシンタックス-スコアリングデータの読み込み
        #スコアリングデータの読み込み
        indf = ascontext.getSparkInputData()
        df = indf.toPandas()
```

以下で、スクリプトの先頭で定義したmodelpath+modelfileからモデルをファイルシステムから読み込みます。

```python:Pythonモデルスコアリングシンタックス-モデルをファイルシステムから読み込む
        #ファイルシステム上のモデルを読み込む 
        import pickle
        loaded_model = pickle.load(open(modelpath+modelfile, 'rb'))
```
以下で、スコアリングをしています。predictは予測値を得ています。predict_probaは確信度を得ています。確信度はそれぞれのクラス、ここではCredit_ratingが0，1で値を返します。[:,1]でCredit_rating＝1の確信度のみを取得しています。

```python:Pythonモデルスコアリングシンタックス-スコアリング
        #スコアリング
        df[prediction_field] =  loaded_model.predict(df[predictors])
        df[confidence_field] =  loaded_model.predict_proba(df[predictors])[:,1]
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/c0a8710a-e597-314c-6081-45301da0d070.png)

以下で予測値列と確信度列を追加したPandas DataFrameであるdfを、sqlCtx.createDataFrame(df,schema=outputSchema)でSparkDataFrameに変換しなおして、setSparkOutputDataでModelerに返しています。

```python:Pythonモデルスコアリングシンタックス-データ出力
        #Sparkコンテキストの取得
        sc = ascontext.getSparkContext()
        #データの出力
        sqlCtx = SQLContext(sc)
        outdf = sqlCtx.createDataFrame(df,schema=outputSchema)

        ascontext.setSparkOutputData(outdf)
```

# 参考

https://qiita.com/1000aki/items/72c3c9ea0fe00edd6d19

https://qiita.com/1000aki/items/aefa0007128b72e21d89

https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMClassifier.html

Python for Spark を使用したスクリプト

https://www.ibm.com/support/knowledgecenter/ja/SS3RA7_18.2.2/modeler_r_nodes_ddita/clementine/r_pyspark_api.html