SPSS Modelerの拡張ノードでPythonを利用する ②Spark MLlibを利用したモデル作成

#0. はじめに
SPSS Modelerでは一般的に分析で利用される各種機能が提供されていますが、RやPythonの機能も使いたいケースがあると思います。
SPSS Modelerの拡張ノードを利用することで、SPSS ModelerからRやPythonを実行することができます。

今回はPython連携で、Spark MLlibを使用したモデル作成を試します。
Python連携の前提となる環境セットアップ方法については、[SPSS Modelerの拡張ノードでPythonを利用する ①セットアップと可視化](https://qiita.com/1000aki/private/72c3c9ea0fe00edd6d19) をご覧ください。

R連携を試す場合は、@kawada2017さんの [SPSS Modelerの拡張ノードでRを利用する](https://qiita.com/kawada2017/items/0bb68e0b577e02bb8c6a
) で紹介されています。

■テスト環境
SPSS Modeler 18.2.2
Windows 10
Python 3.7.7

# 1. データの準備

動作確認用のデータセットとして、irisのデータセットをダウンロードします。
https://github.com/mwaskom/seaborn-data/blob/master/iris.csv

SPSS Modeler を起動し、可変長ノードを追加、iris.csvを読み込みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/0282b17e-5d3a-c2e2-8632-542afaac2244.png)

データ型ノード、データ区分ノードを追加し、接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/78560cdc-a248-03db-255f-1b5fa0a77963.png)

データ区分ノードのプロパティを開き、データを全体を**モデル学習用**と**テスト用**に任意の割合で分割する設定を追加します。設定の追加後、適用をクリックします。
例では、学習データとテストデータを、80%と20%の割合に分けています。
分割後、データ区分ノードのプレビューを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/69b94126-8768-9e91-a046-f4422d90f8a9.png)

プレビューされたデータには、データ区分という列が追加されており、「1_学習」と「2_テスト」という値のいずれかが含まれています。
条件抽出ノードで、`データ区分='1_学習'` と指定すれば、学習用データのみを取り出してモデル作成に使用することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/cddbab08-decd-75c9-a8bd-8578f0ebc049.png)

条件抽出ノードを追加し、データ区分ノードから接続します。
ノードのプロパティを開き、下記のように`データ区分='1_学習'`と記載します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/92a6c34a-82a9-fbeb-fdb1-283c0ba1f170.png)

また、ノードの目的をわかりやすくするため、ノード名を変更しておきます。
注釈タブでノードの名前に「学習用データ」を指定し、OKします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/098da1e5-6d3e-d1cf-266e-f1a433895bc3.png)

ここまでの手順で、データの準備は完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/efd02442-1653-761a-3d3a-7eb99417e53a.png)


# 2. 拡張モデルの作成

モデル作成タブから、拡張モデルノードを追加し、学習用データから接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/c2a2fdc7-0f2d-7cf6-f809-0b1faf97b1c6.png)

拡張モデルノードのプロパティを開きます。
プロパティは2つのセクションに分かれています。
モデル作成用シンタックスと、モデルスコアリングシンタックスです。
この後、各シンタックスを作成していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/b51f814a-7b00-f049-a2f7-1765b5d45bf7.png)

## 2-1. モデル作成シンタックスの作成

Python for Spark のシンタックスへ、モデル作成用のシンタックスを記載します。
おおまかな記述の流れは下記となります。
各パートに分けて説明します。

1. 拡張ノード固有の前処理
2. モデル作成（一般的なPysparkとMLlibのシンタックス）
3. モデル保存（拡張ノード固有の記述）

```python:モデル作成用シンタックス全体
# Analytics Server 対話用のライブラリのインポート
import spss.pyspark.runtime

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

# Sparkコンテキスト定義
sc = ascontext.getSparkContext()

# データ読込
df = ascontext.getSparkInputData()

# pysparkおよびmllibモジュール等のインポート
from pyspark.context import SparkContext
from pyspark.sql.context import SQLContext
from pyspark.ml.feature import StringIndexer, VectorIndexer, IndexToString
from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.classification import RandomForestClassifier
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
from pyspark.sql import Row
from pyspark.sql.types import DoubleType
from pyspark.sql.functions import udf
from pyspark.ml.linalg import Vectors
import numpy
import json

# 入力用フィールド（説明変数）の指定
inputs = ["sepal_length", "sepal_width", "petal_length", "petal_width"]

# 予測対象フィールド（目的変数）の指定
target = "species"

# モデル作成前処理の定義
# 文字列のラベルを数値にエンコード
labelIndexer = StringIndexer(inputCol=target, outputCol="label").fit(df)

# 文字列とエンコード後の数値の対応関係を出力しておく. スコアリング結果デコード用の処理作成時に必要
print(labelIndexer.labels)

# MLlibモデル作成 入力用データのベクトル化処理. 複数の変数がある場合、各行単位にベクトル化しておくことが必要
assembler = VectorAssembler(inputCols=inputs, outputCol="features")

# ランダムフォレストの分類器を定義
rf=RandomForestClassifier(labelCol="label", featuresCol="features")

# パイプラインとして各ステップを定義
pipeline = Pipeline(stages=[labelIndexer, assembler, rf])

# パイプラインモデルの構築 
model = pipeline.fit(df)

# モデルの精度を確認 
results = model.transform(df)
evaluator = MulticlassClassificationEvaluator(
    labelCol="label", predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(results)
print("Test Error = %g" % (1.0 - accuracy))

# モデルの保存
modelpath = ascontext.createTemporaryFolder()
model.save(modelpath)
ascontext.setModelContentFromPath("model", modelpath)

print("Save model completed.")
```

**1. 拡張ノード共通の前処理**

拡張ノードでPythonを取り扱う際、常に記述が必要な内容となります。

Analytics Server対話用のライブラリ spss.pyspark.runtimeをインポートします。
変数 ascontext へ、Analytics Serverコンテキストオブジェクトを定義します。
変数 scへ、Analytics Serverコンテキストを経由して、Sparkコンテキストを定義します。

変数 dfへ、前のノードからのデータが格納されます。
今回の場合、条件抽出ノードで抽出した学習用データが、Spark Dataframe形式で格納されます。

```python:1.拡張ノード共通の前処理
# Analytics Server 対話用のライブラリのインポート
import spss.pyspark.runtime

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

# Sparkコンテキスト定義
sc = ascontext.getSparkContext()

# データ読込
df = ascontext.getSparkInputData()
```

**2. モデル作成（一般的なPysparkとMLlibのシンタックス）**

モデル作成処理は、基本的には一般的なPysparkとMLlibのシンタックスそのままです。
そのため、MLlibのガイドに従って作成することが可能です。

- 機械学習ライブラリ(MLlib)ガイド
    - http://mogile.web.fc2.com/spark/spark211/ml-guide.html

今回は、irisデータセットに含まれるフィールドのうち、sprcies（品種）を分類するモデルを作成します。
入力データとして、品種フィールド以外の、数値フィールド4項目を利用します。
また、分類モデルのアルゴリズムとして、ランダムフォレストを使用します。

- 入力データ（説明変数）
    - sepal_length　：単位はcm
    - sepal_width　 ：単位はcm
    - petal_length　：単位はcm
    - petal_width　 ：単位はcm

- 予測対象（目的変数）
    - species　　：setosa, versicolor, virginica の3カテゴリが存在
    
    Spark MLlibでは、文字列のカテゴリ名をそのまま取り扱うことができません。
    そのため、StringIndexerを利用して、数値のラベルへエンコードを行います。
    この時、カテゴリ名と数値ラベルの対応関係を控えておくことが必要です。
    例では、数値ラベルにエンコードされた順序でspecies(品種)をprintするようにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/381bdb55-4637-a12f-d514-2eeb1461bbe9.png)


```python:2.モデル作成
# pysparkおよびmllibモジュール等のインポート
from pyspark.context import SparkContext
from pyspark.sql.context import SQLContext
from pyspark.ml.feature import StringIndexer, VectorIndexer, IndexToString
from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.classification import RandomForestClassifier
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
from pyspark.sql import Row
from pyspark.sql.types import DoubleType
from pyspark.sql.functions import udf
from pyspark.ml.linalg import Vectors
import numpy
import json

# 入力用フィールド（説明変数）の指定
inputs = ["sepal_length", "sepal_width", "petal_length", "petal_width"]

# 予測対象フィールド（目的変数）の指定
target = "species"

# モデル作成前処理の定義
# 文字列のラベルを数値にエンコード
labelIndexer = StringIndexer(inputCol=target, outputCol="label").fit(df)

# 文字列とエンコード後の数値の対応関係を出力しておく. スコアリング結果デコード用の処理作成時に必要
print(labelIndexer.labels)

# MLlibモデル作成 入力用データのベクトル化処理. 複数の変数がある場合、各行単位にベクトル化しておくことが必要
assembler = VectorAssembler(inputCols=inputs, outputCol="features")

# ランダムフォレストの分類器を定義
rf=RandomForestClassifier(labelCol="label", featuresCol="features")

# パイプラインとして各ステップを定義
pipeline = Pipeline(stages=[labelIndexer, assembler, rf])

# パイプラインモデルの構築 
model = pipeline.fit(df)

# モデルの精度を確認 
results = model.transform(df)
evaluator = MulticlassClassificationEvaluator(
    labelCol="label", predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(results)
print("Test Error = %g" % (1.0 - accuracy))
```

**3. モデル保存（拡張ノード固有の記述）**

最後に学習したモデルを保存します。
今回はモデルの保存先パスとして、Analytics ServerコンテキストオブジェクトのcreateTemporaryFolder()を使用して一時保管領域を作成して利用します。
この一時保管領域は、SPSS Modelerのtempフォルダ配下に作成されます。

```python:3.モデル保存
# モデルの保存
modelpath = ascontext.createTemporaryFolder()
model.save(modelpath)
ascontext.setModelContentFromPath("model", modelpath)

print("Save model completed.")
```

モデルの作成に関して、より詳細な情報はSPSS Modelerのマニュアルをご覧ください。

- Python for Spark を使用したスクリプト > モデルの作成
    - https://www.ibm.com/support/knowledgecenter/ja/SS3RA7_18.2.2/modeler_r_nodes_ddita/clementine/r_pyspark_api.html


## 2-2. モデルスコアリングシンタックスの作成

Python for Spark のシンタックスへ、モデル作成用のシンタックスを記載します。
おおまかな記述の流れは下記となります。
各パートに分けて説明します。

1. 拡張ノード共通の前処理
2. スコアリング結果出力の準備
3. スコアリング実行

```python:スコアリング用シンタックス
# Analytics Server 対話用のライブラリのインポート
import spss.pyspark.runtime

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

# Sparkコンテキスト定義
sc = ascontext.getSparkContext()

# pysparkおよびmllibモジュール等のインポート
from pyspark.context import SparkContext
from pyspark.sql.context import SQLContext
from pyspark.ml.feature import StringIndexer, VectorIndexer, IndexToString
from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.classification import RandomForestClassifier, RandomForestClassificationModel
from pyspark.ml import PipelineModel
from pyspark.sql import Row
from pyspark.sql.types import DoubleType, StructField
from pyspark.sql.functions import udf
from pyspark.ml.linalg import Vectors
import numpy
import json

# 出力する列定義（=スキーマ)
outputSchema = ascontext.getSparkInputSchema()# 入力データの列定義を取得
outputSchema.fields.append(StructField('prediction',DoubleType(), nullable=True))# スコアリング結果用の列を追加
outputSchema.fields.append(StructField('probability_0',DoubleType(), nullable=True))# ラベル1の所属確率の格納用に列を追加
outputSchema.fields.append(StructField('probability_1',DoubleType(), nullable=True))# ラベル2の所属確率の格納用に列を追加
outputSchema.fields.append(StructField('probability_2',DoubleType(), nullable=True))# ラベル3の所属確率の格納用に列を追加

# 出力する列定義をAnalytics Serverコンテキストに登録
ascontext.setSparkOutputSchema(outputSchema)
print(outputSchema)

# 配列から値を取りだす関数
udf_0 = udf(lambda vector: float(vector[0]), DoubleType())
udf_1 = udf(lambda vector: float(vector[1]), DoubleType())
udf_2 = udf(lambda vector: float(vector[2]), DoubleType())

if not ascontext.isComputeDataModelOnly():

	# データ読込
	indf = ascontext.getSparkInputData()
	
	# モデル読込
	model_path = ascontext.getModelContentToPath("model")
	model = PipelineModel.load(model_path)
	
	# スコアリング実行
	r1 = model.transform(indf)
	
	# スコアリング結果の整形
	# predictionに分類されるクラス、probabilityに各クラスへの所属確率が配列で含まれる
	# probabilityの配列から、各クラスの確率を取り出し、それぞれの列に格納
	r2 = (r1.select(
			r1["sepal_length"], 
			r1["sepal_width"], 
			r1["petal_length"], 
			r1["petal_width"],
			r1["species"], 
			r1["prediction"],
			r1["probability"]
		).withColumn('probability_0', udf_0(r1.probability)
		).withColumn('probability_1', udf_1(r1.probability)
		).withColumn('probability_2', udf_2(r1.probability)
		).drop("probability")
	)
	
	# 整形後のSpark Dataframe名をわかりやすいものに変更
	outdf=r2
	
	# 整形後のSpark DataframeをAnalytics Serverコンテキストに登録。後続のノードでスコアリング結果が利用可能となる。
	ascontext.setSparkOutputData(outdf)
```

**1. 拡張ノード共通の前処理**
モデル作成用シンタックスと同じ内容のため割愛します。

**2. スコアリング結果出力の準備**

MllibのRandomForestでは、スコアリング結果が下記の形式で返されます。
probabilityの配列含まれる分類確率の数は、予測対象の列に含まれるカテゴリ数に対応します。

- prediction [分類確率が最も高いカテゴリラベル]
- probaility [カテゴリ0への分類確率, カテゴリ1への分類確率, .... , カテゴリNへの分類確率]

今回の場合、予測対象の列speciesには、setosa, versicolor, virginica の3カテゴリが存在しています。
従って、probaility は　[カテゴリ0への分類確率, カテゴリ1への分類確率, カテゴリ2への分類確率] となります。

SPSS Modelerでスコアリング結果を利用できるように、スコアリング結果格納用の列定義を作成します。
入力データに含まれる列に追加する形で、スコアリング結果格納用の列定義を作成します。

追加で作成する列は4つです。
このうち、probability_* は予測対象に含まれるカテゴリ数に対応して作成が必要です。
今回の予測対象のspeciesには、setosa/versicolor/virginica の3カテゴリが存在しています。
そのため、probability_0 ~ probability_2 まで、3列を作成しています。

- prediction　  :スコアリング結果ラベル用の列. モデルによって予測されたカテゴリ0/1/2のいずれかが入ります。
- probability_0 :カテゴリ0への分類確率
- probability_1 :カテゴリ1への分類確率
- probability_2 :カテゴリ2への分類確率

スコアリング結果格納用の列定義を、出力する列定義をAnalytics Serverコンテキストに登録します。
また、スコアリング結果のうち、各クラスへの分類確率のprobabilityは配列で返却されるため、配列から値を取り出す関数も定義しておきます。

```python:2.スコアリング結果出力の準備
# pysparkおよびmllibモジュール等のインポート
from pyspark.context import SparkContext
from pyspark.sql.context import SQLContext
from pyspark.ml.feature import StringIndexer, VectorIndexer, IndexToString
from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.classification import RandomForestClassifier, RandomForestClassificationModel
from pyspark.ml import PipelineModel
from pyspark.sql import Row
from pyspark.sql.types import DoubleType, StructField
from pyspark.sql.functions import udf
from pyspark.ml.linalg import Vectors
import numpy
import json

# 出力する列定義（=スキーマ)
outputSchema = ascontext.getSparkInputSchema()# 入力データの列定義を取得
outputSchema.fields.append(StructField('prediction',DoubleType(), nullable=True))# スコアリング結果ラベル用の列を追加
outputSchema.fields.append(StructField('probability_0',DoubleType(), nullable=True))# カテゴリ0の所属確率の格納用に列を追加
outputSchema.fields.append(StructField('probability_1',DoubleType(), nullable=True))# カテゴリ1の所属確率の格納用に列を追加
outputSchema.fields.append(StructField('probability_2',DoubleType(), nullable=True))# カテゴリ2の所属確率の格納用に列を追加

# 出力する列定義をAnalytics Serverコンテキストに登録
ascontext.setSparkOutputSchema(outputSchema)
print(outputSchema)

# 配列から値を取りだす関数
udf_0 = udf(lambda vector: float(vector[0]), DoubleType())
udf_1 = udf(lambda vector: float(vector[1]), DoubleType())
udf_2 = udf(lambda vector: float(vector[2]), DoubleType())
```

**3. スコアリング実行**

変数indfへ、入力データを読み込みます。
保存したモデルを読み込みます。
変数r1へ、スコアリング実行結果を含むspark dataframeを格納します。
r1を整形し、後続ノードで利用できるようにAnalytics Serverコンテキストに登録します。

```python:スコアリング用シンタックス
if not ascontext.isComputeDataModelOnly():

	# データ読込
	indf = ascontext.getSparkInputData()
	
	# モデル読込
	model_path = ascontext.getModelContentToPath("model")
	model = PipelineModel.load(model_path)
	
	# スコアリング実行
	r1 = model.transform(indf)
	
	# スコアリング結果の整形
	# predictionに分類されるクラス、probabilityに各クラスへの所属確率が配列で含まれる
	# probabilityの配列から、各クラスの確率を取り出し、それぞれの列に格納
	r2 = (r1.select(
			r1["sepal_length"], 
			r1["sepal_width"], 
			r1["petal_length"], 
			r1["petal_width"],
			r1["species"], 
			r1["prediction"],
			r1["probability"]
		).withColumn('probability_0', udf_0(r1.probability)
		).withColumn('probability_1', udf_1(r1.probability)
		).withColumn('probability_2', udf_2(r1.probability)
		).drop("probability")
	)
	
	# 整形後のSpark Dataframe名をわかりやすいものに変更
	outdf=r2
	
	# 整形後のSpark DataframeをAnalytics Serverコンテキストに登録。後続のノードでスコアリング結果が利用可能となる。
	ascontext.setSparkOutputData(outdf)
```

シンタックスの作成は以上となります。

最後に、拡張モデルノードのノード名を変更しておきます。
注釈タブでノードの名前に「RandomForestClassifier」を指定し、OKします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/c4a99c5e-d0c5-2701-41df-27326e3ce2ad.png)


## 2-3. 拡張モデルノードの実行

各シンタックスの記載が完了したことを確認し、拡張モデルノード実行します。
実行完了までに数分かかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/142f1666-6ec9-e51c-427b-ca38a8a57888.png)

実行が完了すると、モデルナゲットが追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/5d49a80d-7294-82fb-e610-e3d49f788b81.png)

モデルナゲットをダブルクリックすると、中身を確認することができます。
テキスト出力タブを開き、1行目の出力内容を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/2b8c09a8-e4f0-43b2-4425-54aeaea815bc.png)

1行目の配列には、品種名が含まれています。この内容を控えておきます。
この配列には、品種名を数値のラベルにエンコードした時の順序で品種名が含まれています。
数値ラベルを品種名に逆変換する際に必要となります。
例では、['versicolor', 'virginica', 'setosa']　となっているため、それぞれ [0, 1, 2] として数値ラベルにエンコードされていることがわかります。


# 3. テストの実行

レコード設定タブより、条件抽出ノードを追加し、データ区分ノードから接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/4470ca29-7b5c-a341-b1b2-5fdb9ecfbfa0.png)

条件抽出ノードのプロパティを開き、下記のように`データ区分='2_テスト'`と記載します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/3ccd5355-c2b1-4148-30d6-7f5d0b8220c1.png)

また、ノードの目的をわかりやすくするため、ノード名を変更しておきます。
注釈タブでノードの名前に「テストデータ」を指定し、OKします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/5aa2d60e-63ee-529f-57cb-c91039523644.png)

フィールド設定タブより、フィルターノードを追加し、データ区分フィールドをフィルターします。
スコアリング時、モデルに不要な列を除外することが目的です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/982e963b-f4bb-e774-ca1f-e99dabc9de72.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/01bb2b4a-c823-4c55-b413-5d140a2df51b.png)

モデルナゲットをコピーし&ペーストし、フィルターノードから接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/8391945c-73a1-8145-b3f0-f39dd998197f.png)

レコード設定タブより、拡張の変換ノードを追加し、モデルナゲットから接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/fa8102d3-4163-65b2-6d70-f913932b9c42.png)

拡張の変換ノードのプロパティを開き、数値で出力されたラベルを品種名に変換するための処理を記載します。
my_labelsには、モデル作成後に控えておいた配列を指定します。
この処理によって、prediction_speciesという列に予測結果の品種名が格納されます。

```python:予測結果のラベルを品種名に変換
import spss.pyspark.runtime

ascontext = spss.pyspark.runtime.getContext()
sc = ascontext.getSparkContext()

my_labels=['versicolor', 'virginica', 'setosa']

from pyspark.ml.feature import IndexToString
from pyspark.sql.types import StringType, StructField
from pyspark.sql.types import StringType,DoubleType
import numpy

outputSchema = ascontext.getSparkInputSchema()
outputSchema.fields.append(StructField('prediction_species', StringType(), nullable=True))
ascontext.setSparkOutputSchema(outputSchema)

if not ascontext.isComputeDataModelOnly():
        indf = ascontext.getSparkInputData()
        converter = IndexToString(
                inputCol="prediction", 
                outputCol="prediction_species", 
                labels=my_labels
        )
        outdf = converter.transform(indf)

        # return the output DataFrame as the result
        ascontext.setSparkOutputData(outdf)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/56293134-136b-2cb1-ee50-1a38f3336761.png)

出力タブより、クロス集計ノードを追加し、拡張の変換ノードに接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/4aff2acc-85ce-a258-85cc-6d3c163d32e2.png)

クロス集計ノードのプロパティを設定します。

- 行: species
- 列: prediction_species

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/7bb11da9-d095-c7f4-0ce0-58cde58babb8.png)

クロス集計ノードの外観タブで、下記をチェックします。

- 度数
- 列のパーセンテージ
- 行および列合計を含める

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/5d376699-4db2-19ee-3d0b-c952960dea7d.png)

クロス集計ノードを実行します。
テストデータの混同行列を出力し、実際の品種と予測結果がどの程度一致しているか確認します。
例では、versicolorのデータ1件について、virginicaと誤分類していますが、他のデータは正しい品種への分類に成功しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/5f115620-ff47-db7e-cc17-e55178b75ead.png)

手順は以上です。

# 参考
Analytics Serverコンテキストに関する詳細は、下記をご確認ください。

Python for Spark を使用したスクリプト
https://www.ibm.com/support/knowledgecenter/ja/SS3RA7_18.2.2/modeler_r_nodes_ddita/clementine/r_pyspark_api.html

Analytic Server コンテキスト
https://www.ibm.com/support/knowledgecenter/ja/SS3RA7_18.2.2/modeler_r_nodes_ddita/clementine/r_pyspark_api_context.html