scikit-learnのサンプルデータセットと主要OSSを活用したデータ分析のチュートリアル

著者: 株式会社 日立ソリューションズ 柳村 明宏
監修: 株式会社 日立製作所



# はじめに
近年、機械学習をはじめとするAI技術を活用したデータ分析が注目を集めています。
本投稿では、機械学習に馴染みのない方やデータ分析に馴染みのない方が、データ分析に触れていただくことを目的に、手軽に利用可能なOSSを利用したデータ分析の一連の手順とサンプルコードを紹介いたします。



# データ分析の流れ

データ分析環境と、データ分析の流れを以下に示します。
吹き出しはそれぞれのフェーズで利用するOSSです。

![データ分析.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/702183/06f74572-d7c6-b2b7-593b-1a4bb2386300.png)

- 「データの理解」では、データの全体像を把握するためにJupyter Notebook上でデータセットを可視化します。

- 「モデルの作成と評価」では、データセットを利用して機械学習モデルの学習・評価を行います。今回は、再利用を考慮してPythonコードで記載していますが、Jupyter Notebookでも作成可能です。

- 「モデルの利用」では、作成した機械学習モデルをWebサーバにデプロイして、HTTP API経由で利用します。



# 利用するデータセット
データセットには、scikit-learnに同梱されている「Boston housing prices dataset」という、米国ボストン市の住宅価格に関するデータセットを利用します。これは、住宅周辺の環境情報（説明変数）から、住宅価格（目的変数）を予測する、回帰分析向けのサンプルデータです。次の表の通り、ボストンの住宅価格のデータセットは、13項目の説明変数と1項目の目的変数をもつ、506個のデータで構成されています。
なお、本データセットはscikit-learnのバージョン1.0で非推奨となりました。バージョン1.2で削除される予定です。バージョン1.2以降で、今回のサンプルを動作させる場合は、データセットの読込み部分を変更させる必要があると想定されます。詳細は下記のサイトを参考にしてください。
[Boston housing prices datasetに関して](https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_boston.html)


<table style="width=100%; font-size: 10.5pt;">
 <tr align="center">
  <th width="5%">#</th>
  <th width="15%">カラム</th>
  <th width="60%">説明</th>
  <th width="20%">変数分類</th>
 </tr>
 <tr align="center">
  <td>1</td>
  <td>CRIM</td>
  <td align="left">町別の「犯罪率」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>2</td>
  <td>ZN</td>
  <td align="left">25,000平方フィートを超える区画に分類される住宅地の割合＝「広い家の割合」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>3</td>
  <td>INDUS</td>
  <td align="left">町別の「非小売業の割合」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>４</td>
  <td>CHAS</td>
  <td align="left">チャールズ川のダミー変数（区画が川に接している場合は1、そうでない場合は0）＝「川の隣か」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>5</td>
  <td>NOX</td>
  <td align="left">「NOx濃度（0.1ppm単位）」＝一酸化窒素濃度（parts per 10 million単位）。この項目を目的変数とする場合もある</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>6</td>
  <td>RM</td>
  <td align="left">1戸当たりの「平均部屋数」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>7</td>
  <td>AGE</td>
  <td align="left">1940年より前に建てられた持ち家の割合＝「古い家の割合」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>8</td>
  <td>DIS</td>
  <td align="left">5つあるボストン雇用センターまでの加重距離＝「主要施設への距離」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>9</td>
  <td>RAD</td>
  <td align="left">「主要高速道路へのアクセス性」の指数</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>10</td>
  <td>TAX</td>
  <td align="left">10,000ドル当たりの「固定資産税率」</td>
  <td align="left">説明変数</td>
 </tr>
 </tr> <tr align="center">
  <td>11</td>
  <td>PTRATIO</td>
  <td align="left">町別の「生徒と先生の比率」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>12</td>
  <td>B</td>
  <td align="left">「1000(Bk - 0.63)」の二乗値。Bk＝「町ごとの黒人の割合」を指す</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>13</td>
  <td>LSTAT</td>
  <td align="left">「低所得者人口の割合」</td>
  <td align="left">説明変数</td>
 </tr>
 <tr align="center">
  <td>14</td>
  <td>MEDV</td>
  <td align="left">「住宅価格」（1,000ドル単位）の中央値。通常はこの数値が目的変数として使われる</td>
  <td align="left">目的変数</td>
 </tr>
</table>


# データ分析のチュートリアル
[利用するデータセット](#利用するデータセット)で紹介したデータセットを利用して、データ分析を行っていきます。実現する内容を次の表に示します。

<table style="width=100%; font-size: 10.5pt;">
 <tr align="center">
  <th width="5%">#</th>
  <th width="20%">項目</th>
  <th width="45%">内容</th>
  <th width="30%">利用するライブラリ</th>
 </tr>
 <tr align="center">
  <td>1</td>
  <td>データの理解</td>
  <td align="left">データの全体像を把握するために、Jupyter Notebook上でデータセットを可視化します。</td>
  <td align="left">
   <ul>
    <li>scikit-learn</li>
    <li>numpy</li>
    <li>pandas</li>
    <li>seaborn</li>
   </ul>
  </td>
 </tr>
 <tr align="center">
  <td>2</td>
  <td>モデルの作成と評価</td>
  <td align="left">データセットを利用して、機械学習モデルの学習・評価を行います。</td>
  <td align="left">
   <ul>
    <li>scikit-learn</li>
    <li>optuna</li>
    <li>pandas</li>
   </ul>
  </td>
 </tr>
 <tr align="center">
  <td>3</td>
  <td>モデルの利用</td>
  <td align="left">#2 で作成した機械学習モデルをWebサーバにデプロイして、HTTP API経由で利用します。</td>
  <td align="left">
   <ul>
    <li>flask</li>
   </ul>
  </td>
 </tr>
</table>


## データの理解
今回は、データの全体像を把握するために、Jupyter Notebook上でデータセットを可視化します。データセットを読込み、データセットの内容の参照と、説明変数と目的変数のデータの相関を可視化します。データの理解に使用したノートブック の内容を以下に示します。
ライブラリをimportし、データセットを読み込みます。

```python:Visualization.ipynb
import pandas as pd
import numpy as np
from pandas import DataFrame
from sklearn.datasets import load_boston

# ボストン住宅価格データセットの読み込み
boston = load_boston()

# 説明変数
X_array = boston.data
# 目的変数
y_array = boston.target
```

データセットを表形式で参照します。今回は、先頭と末尾の行を表示します。

```python:Visualization.ipynb
df = DataFrame(X_array, columns = boston.feature_names).assign(MEDV=np.array(y_array))

# ヘッダ出力
df.head
```

出力結果は次の通りです。

![df_head_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/702183/73998dc5-3fff-2c01-b657-5b2b0964568f.png)

[利用するデータセット](#利用するデータセット)で紹介したカラムで、506個のデータが存在することがわかりました。
次に、説明変数と目的変数の相関関係を可視化してみます。
可視化用のライブラリとしてseabornをimportします。


```python:Visualization.ipynb
import seaborn as sns
```


CRIM（犯罪率）と、MEDV（住宅価格）の相関を見ます。

```python:Visualization.ipynb
sns.regplot(x=df["CRIM"], y=df["MEDV"], data = df)
```

出力結果は次の通りです。

![seaborn_CRIM_MEDV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/702183/68cc9f3b-a217-5f29-d848-b594472ae9d4.png)

犯罪率が低い地域の住宅は、住宅価格が高いことがわかります。

最後に、RM（1戸当たりの「平均部屋数」）と、MEDV（住宅価格）の相関を見ます。

```python:Visualization.ipynb
sns.regplot(x=df["RM"], y=df["MEDV"], data = df)
```
![seaborn_RM_MEDV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/702183/09af5392-d893-3694-1ad9-9e8f924f1d41.png)

部屋数が多い住宅は、住宅価格が高いことがわかります。


## モデルの作成と評価
本項では、ボストンの住宅価格データセットを用いて、住宅周辺の環境情報から住宅価格を予測するモデルを作成します。また、作成したモデルの予測精度を評価します。
初めに学習・検証・評価データに分割します。次に学習・検証データを利用して、Optunaによるハイパーパラメータチューニングを行いつつ、モデルを学習します。最後に評価データを利用して、モデルの予測精度（評価指標）を確認します。
検証用と評価用のデータを分ける理由は、もし評価データを学習時のハイパーパラメータチューニングに利用してしまうと、評価データに最適化されたモデルが選択されてしまうためです。評価ではモデルの本番導入後（＝サービング時）に全く知らない新しいデータが来た際の性能を知りたいため、一度も学習に使ったことのないデータで評価する必要があります。
本項の実施内容を以下の表に示します。

<table style="width=100%; font-size: 10.5pt;">
 <tr align="center">
  <th width="5%">#</th>
  <th width="45%">内容</th>
  <th width="50%">ポイント</th>
 </tr>
 <tr align="left">
  <td align="center">1</td>
  <td>データセットを読込み学習、評価、検証用にデータを分割します。</td>
  <td>分割数は任意ですが、今回は以下のように分割しました。
   <ul>
    <li>学習用：303個(60%)</li>
    <li>検証用：102個(20%)</li>
    <li>評価用：101個(20%)</li>
   </ul>
  </td>
 </tr>
 <tr align="left">
  <td align="center">2</td>
  <td>Optunaでモデルのハイパーパラメータチューニングを行い、モデルを学習します。</td>
  <td>
   <ul>
    <li>利用したモデル： Ridge回帰</li>
    <li>探索するモデルのハイパーパラメータ：正則化係数（今回の探索範囲：0.01～100.00）</li>
    <li>最適化する評価指標： 平均絶対誤差(MAE)</li>
   </ul>
  </td>
 </tr>
</table>


Ridge回帰に関しては次のサイトを参考にしてください。

- [Ridge回帰とは](https://scikit-learn.org/stable/modules/linear_model.html#ridge-regression)
- [scikit-learnのRidge回帰の関数仕様](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Ridge.html)

平均絶対誤差 (MAE, Mean Absolute Error) は、実際の値と予測値の絶対値を平均したものです。MAE が小さいほど誤差が少なく、予測モデルが正確に予測できていることを示しています。回帰分析に対する評価指標に関しては、次のサイトを参考にしてください。

- [MAEとは](https://scikit-learn.org/stable/modules/model_evaluation.html#mean-absolute-error)
- [scikit-learnのMAEの関数仕様](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.mean_absolute_error.html)


モデルの作成と評価に使用したPythonコードの内容を以下に示します。

```python:train.py
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.linear_model import Ridge

import optuna
import pickle
import pandas as pd
import sklearn.metrics

boston = load_boston()
# xが説明変数
# yが目的変数(MEDV)
x = boston.data
y = boston.target

# 訓練データ：6、検証データ：2、評価データ：2に分割
train_x, valid_test_x, train_y, valid_test_y = train_test_split(x, y, train_size = 0.6, random_state = 1)
test_x, valid_x, test_y, valid_y = train_test_split(valid_test_x, valid_test_y, test_size = 0.5, random_state = 1)

# Optunaでtrial回数分のモデルをregister_modelで保持する
# 試行が完了したら、get_modelで最良の試行回数をキーにモデルを取得する
class Callback:
    def __init__(self):
        self.models = {}

    def __call__(self, study, trial):
        pass

    def register_model(self, trial_number, model):
        self.models[str(trial_number)] = model

    def get_model(self, trial_number):
        return self.models[str(trial_number)]

def objective(trial):
    # ハイパーパラメータとして指定範囲の乱数を生成
    param_alpha = trial.suggest_float("alpha", 0.01, 100.00)

    # リッジ回帰モデルを作成
    model = Ridge(alpha = param_alpha)

    # モデルの訓練を実行
    model.fit(train_x, train_y)

    # 訓練済みモデルを登録
    callback.register_model(trial.number, model)

    # 訓練済みモデルを使用して、検証データによる予測を実行
    y_pred = model.predict(valid_x)

    # 訓練データの正解値と予測結果から、MAE(Mean Absolute Error)を算出
    return sklearn.metrics.mean_absolute_error(valid_y, y_pred)

callback = Callback()
# セッションの作成
study = optuna.create_study()

# 試行
study.optimize(objective,n_trials=100, callbacks=[callback])
# 最良のモデルの取得
best_model = callback.get_model(study.best_trial.number)

# 評価データによる予測を実行
pred = best_model.predict(test_x)
# 評価データの正解値と予測結果から、MAEを算出
print("MAE : " + str(sklearn.metrics.mean_absolute_error(test_y, pred)))

# 学習済みモデルをファイルに出力
filename = "model.pickle"
pickle.dump(best_model, open(filename, 'wb'))
```


作成したPythonコードを実行してモデルの作成と評価を行います。

```powershell
C:\work>python train.py
# --- 実行結果例 ここから -------------------------------------------------
# [I 2021-09-13 16:47:35,285] A new study created in memory with name: no-name-063072d1-55ec-433c-b92e-de199a5af387
# [I 2021-09-13 16:47:35,298] Trial 0 finished with value: 3.4490167509024525 and parameters: {'alpha': 30.18482525879907}. Best is trial 0 with value: 3.4490167509024525.
# [I 2021-09-13 16:47:35,300] Trial 1 finished with value: 3.5562661263791084 and parameters: {'alpha': 70.35330894918852}. Best is trial 0 with value: 3.4490167509024525.
#                                              ：
# [I 2021-09-13 16:47:35,690] Trial 68 finished with value: 3.301292598752645 and parameters: {'alpha': 0.1044170092688274}. Best is trial 68 with value: 3.301292598752645.
# [I 2021-09-13 16:47:35,694] Trial 69 finished with value: 3.356478806393828 and parameters: {'alpha': 6.2616723436725525}. Best is trial 68 with value: 3.301292598752645.
#                                              ：
# [I 2021-09-13 16:47:35,867] Trial 99 finished with value: 3.3576048354298553 and parameters: {'alpha': 6.4807971462797465}. Best is trial 68 with value: 3.301292598752645.
# MAE : 3.6333021709099773
# --- 実行結果例 ここまで--------------------------------------------------
```

trial 68が最良のモデルとなったため、trial 68の学習済モデルを出力します。学習済みモデルは、上述の実装にある通り、`~/model.pickle`で出力されます。


## モデルの利用
### Webサーバの起動

[モデルの作成と評価](#モデルの作成と評価)で作成した学習済みモデルをWebサーバにロードして、HTTP API経由で利用してみます。

Flaskで実装したHTTP APIをもつWebサーバ のPythonコードの内容を以下に示します。

```python:web_server.py
import pickle
import pandas as pd
from flask import Flask, request, jsonify

# 学習済みパイプラインをロードする
filename = "model.pickle"
model = pickle.load(open(filename, 'rb'))

# Flaskサーバを作成
app = Flask(__name__)

# エンドポイントを定義 http:/host:port/predict, POSTメソッドのみ受け付ける
@app.route('/predict', methods=['POST'])
def get_predict():
    # Jsonリクエストから値取得
    X_dict = request.json

    # DataFrame化
    X_df = pd.DataFrame(X_dict)

    # 予測実行
    prediction = model.predict(X_df)

    # 予測結果をJson化して返信
    result = {"MEDV": float(prediction[0])}
    return jsonify(result)

# Flaskサーバをポート番号5060で起動
if __name__ == "__main__":
    app.run(port=5060)
```


Webサーバを起動します。

```powershell
C:\work>python web_server.py
# --- 実行結果例 ここから -------------------------------------------------
# * Serving Flask app "web_server" (lazy loading)
# * Environment: production
#   WARNING: This is a development server. Do not use it in a production deployment.
#   Use a production WSGI server instead.
# * Debug mode: off
# * Running on http://127.0.0.1:5060/ (Press CTRL+C to quit)
# --- 実行結果例 ここまで--------------------------------------------------
```


### 推論実行
住宅価格を予測したい住宅の周辺情報のデータをJSONファイルで作成し、HTTPリクエストで送信します。入力データのJSONファイルの例を示します。今回各項目の値は、データセットの先頭のデータを利用します。

```json:request.json
[
  {
    "CRIM":"0.00632",
    "ZN":"18.0",
    "INDUS":"2.31",
    "CHAS":"0.0",
    "NOX":"0.538",
    "RM":"6.575",
    "AGE":"65.2",
    "DIS":"4.0900",
    "RAD":"1.0",
    "TAX":"296.0",
    "PTRATIO":"15.3",
    "B":"396.90",
    "LSTAT":"4.98"
  }
]
```

curl コマンドを利用して、HTTPリクエストを送信します。前述で作成したJSONファイルを入力データとします。

```powershell
# HTTPリクエストの送信
C:\work>curl -v http://127.0.0.1:5060/predict -H "Content-Type: application/json" -d @request.json
# --- 実行結果例 ここから -------------------------------------------------
# *   Trying 127.0.0.1:5060...
# * Connected to 127.0.0.1 (127.0.0.1) port 5060 (#0)
# > POST /predict HTTP/1.1
# > Host: 127.0.0.1:5060
# > User-Agent: curl/7.71.1
# > Accept: */*
# > Content-Type: application/json
# > Content-Length: 242
# >
# * upload completely sent off: 242 out of 242 bytes
# * Mark bundle as not supporting multiuse
# * HTTP 1.0, assume close after body
# < HTTP/1.0 200 OK
# < Content-Type: application/json
# < Content-Length: 28
# < Server: Werkzeug/1.0.1 Python/3.8.8
# < Date: Mon, 13 Sep 2021 09:39:02 GMT
# <
# {"MEDV":29.493047836307362} 
# * Closing connection 0
# --- 実行結果例 ここまで -------------------------------------------------
```

HTTPレスポンスとして、MEDVの値が`{"MEDV": 29.493047836307362}`のような形式で返却されます。 MEDVは千ドル単位の住宅価格なので、この場合は住宅価格が29,493ドルと予測されたことを示しています。

Webサーバのログには次のようなログが出力されます。

```
# ---出力例 ここから ------------------------------------------------------
# 127.0.0.1 - - [13/Sep/2021 18:39:02] "[37mPOST /predict HTTP/1.1[0m" 200 - 
# --- 出力例 ここまで -----------------------------------------------------
```

# おわりに
本投稿では、Python製の機械学習フレームワークであるscikit-learnとハイパーパラメータの最適化を自動化するOptunaとWebサーバであるFlaskを活用して、データの理解・モデルの開発・モデルの利用（サービング）の一連の手順を紹介しました。機械学習に馴染みのない方や、簡単にデータ分析を行ってみたいという方は、是非試してみてください。