今さら聞けないPython - scikit-learnを用いた機械学習

こちらのウェビナーで説明した内容の抜粋です。最初のセクションの「pandasを用いたデータ分析」をカバーしています。[今さら聞けないPython - pandasを用いたデータ分析](https://qiita.com/taka_yayoi/items/3bb88b204f47bdf091eb)の続きです。

https://pages.databricks.com/202303-APJ-VE-Fundamental-Series-Python.html

全体構成は以下の通りです。
- [今さら聞けないPython \- Pythonの基礎](https://qiita.com/taka_yayoi/items/bb96e1be88959902cb17)
- [今さら聞けないPython \- pandasを用いたデータ分析](https://qiita.com/taka_yayoi/items/3bb88b204f47bdf091eb)
- [今さら聞けないPython \- scikit\-learnを用いた機械学習](https://qiita.com/taka_yayoi/items/08d3166714b53b9acc7b)
- [今さら聞けないPython \- Sparkのご紹介](https://qiita.com/taka_yayoi/items/5415db284b96c7c60d44)

ウェビナーで使用したノートブックはこちらにあります。

https://github.com/taka-yayoi/public_repo_2/tree/master/python_webinar

本記事でカバーしているノートブックはこちらです。

https://github.com/taka-yayoi/public_repo_2/blob/master/python_webinar/3.%20scikit-learn%E3%82%92%E7%94%A8%E3%81%84%E3%81%9F%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92.py

* 機械学習とは何か？
  * 機械学習のタイプ
* トレーニング - テストデータセットの分割
* 線形回帰モデルを構築するために `sklearn` を活用
* ワンホットエンコーディング
* パイプライン
* 評価メトリクス

必要に応じて[scikit-learnのドキュメント](https://scikit-learn.org/stable/user_guide.html)や[pandasのドキュメント](https://pandas.pydata.org/pandas-docs/stable/index.html)を参照します。そして、 https://github.com/nytimes/covid-19-data にあるNew York Times COVID-19 US Statesデータセットのデータを分析します。

:::note
**免責**
このデータセットにおいては線形回帰が最適のアルゴリズムではありませんが、ここではscikit-learnの使い方を説明するために線形回帰を使用します。
:::

# 機械学習とは何か？

* 明示的なプログラミングを行うことなしにデータからパターンを学習します。
* 特徴量をアウトプットにマッピングする関数です。

![](https://brookewenig.com/img/DL/al_ml_dl.png)

**参考資料**
- [今さら聞けない機械学習](https://qiita.com/taka_yayoi/items/51583a581ce5a6ba6558)
- [今さら聞けないシリーズ 機械学習とMLOpsとは – Databricks](https://www.databricks.com/p/webinar/jp-mlops-beginner-series)

## 機械学習のタイプ
* 教師あり学習
  * 回帰<br><img src="https://miro.medium.com/max/640/1*LEmBCYAttxS6uI6rEyPLMQ.png" style="height: 250px; padding: 10px"/>
  * 分類
    <img src="https://cdn3-www.dogtime.com/assets/uploads/2018/10/puppies-cover.jpg" style="height: 250px; padding: 10px"/>
    <img src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&w=1000&q=80" style="height: 250px; padding: 10px"/>
* 教師なし学習
<img src="https://www.iotforall.com/wp-content/uploads/2018/01/Screen-Shot-2018-01-17-at-8.10.14-PM.png" style="height: 250px; padding: 10px"/>

* 強化学習<br>
<img src="https://brookewenig.com/img/ReinforcementLearning/Rl_agent.png" style="height: 250px; padding: 10px"/>

ここではシンプルにスタートし、教師あり学習(回帰)問題にフォーカスします。ここでは、COVID-19による死者数を予測する線形回帰モデルを使用します。

```
%fs ls databricks-datasets/COVID/covid-19-data/us-states.csv
```
![Screenshot 2023-03-31 at 11.05.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/edcf979a-a680-140d-3820-c8edd4aea4fe.png)

```py:Python
import pandas as pd

df = pd.read_csv("/dbfs/databricks-datasets/COVID/covid-19-data/us-states.csv")
df.head()
df.date.max()
```
```
Out[21]: '2021-03-11'
```

```py:Python
df.shape
```
```
Out[22]: (20584, 5)
```

# 感染者数と死者数の関係

```py:Python
# ノートブックでプロットを出力できるようにします
%matplotlib inline
```

```py:Python
# 2020-05-01のデータにフィルタリングします
df_05_01 = df[df["date"] == "2020-05-01"]

ax = df_05_01.plot(x="cases", y="deaths", kind="scatter", 
                   figsize=(12,8), s=100, title="Deaths vs Cases on 2020-05-01 - All States")

df_05_01[["cases", "deaths", "state"]].apply(lambda row: ax.text(*row), axis=1);
```
![Screenshot 2023-03-31 at 11.07.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/542ef8ac-e6a1-7c9b-418f-8b2e9fc1fde5.png)

# ニューヨークとニュージャージーは外れ値と言えます

```py:Python
# New YorkとNew Jersey以外の州にフィルタリングします
not_ny = df[(df["state"] != "New York") & (df["state"] != "New Jersey")]
not_ny.head()
```
![Screenshot 2023-03-31 at 11.08.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e187fa51-7da6-2549-29a1-03129654797a.png)

```py:Python
# 2020-05-01のデータにフィルタリングします
not_ny_05_01 = not_ny[not_ny["date"] == "2020-05-01"]

ax = not_ny_05_01.plot(x="cases", y="deaths", kind="scatter", 
                   figsize=(12,8), s=50, title="Deaths vs Cases on 2020-05-01 - All States but NY and NJ")

not_ny_05_01[["cases", "deaths", "state"]].apply(lambda row: ax.text(*row), axis=1);
```
![Screenshot 2023-03-31 at 11.08.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/1545ca8b-a107-25ee-03e5-52b7796672e1.png)

# New YorkとCaliforniaにおけるCOVID-19の死者数の比較

```py:Python
df_ny_cali = df[(df["state"] == "New York") | (df["state"] == "California")]

# 両方の州における死者数の時間変換をプロットできるように、df_ny_caliデータフレームをピボットしましょう
df_ny_cali_pivot = df_ny_cali.pivot(index='date', columns='state', values='deaths').fillna(0)
df_ny_cali_pivot
```
![Screenshot 2023-03-31 at 11.09.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/3b3835d6-20d3-8a04-fe5c-8d3239b2911b.png)

```py:Python
df_ny_cali_pivot.plot.line(title="Deaths 2020-01-25 to 2020-05-01 - CA and NY", figsize=(12,8))
```
![Screenshot 2023-03-31 at 11.10.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/c15aa9f2-970d-b5a2-c379-e3f137d59683.png)

# トレーニング - テストデータセットの分割

![](https://brookewenig.com/img/IntroML/trainTest.png)

これは時系列データなのでランダムに分割するのではなく、モデルのトレーニングには3/1から4/7のデータを使い、4/8から4/14の値を予測することでモデルをテストします。

```py:Python
train_df = df[(df["date"] >= "2020-03-01") & (df["date"] <= "2020-04-07")]
test_df = df[df["date"] > "2020-04-07"]

X_train = train_df[["cases"]]
y_train = train_df["deaths"]

X_test = test_df[["cases"]]
y_test = test_df["deaths"]
```

# 線形回帰

* ゴール: 最もフィットする直線を見つけ出す
$$\hat{y} = w_0 + w_1x$$

$$\{y} ≈ \hat{y} + ϵ$$
* *x*: 特徴量
* *y*: ラベル

![](https://miro.medium.com/max/640/1*LEmBCYAttxS6uI6rEyPLMQ.png)

ここでは、scikit-learnの[LinearRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html)モデルをフィッティングします。

```py:Python
from sklearn.linear_model import LinearRegression

lr = LinearRegression().fit(X_train, y_train)
print(f"num_deaths = {lr.intercept_:.4f} + {lr.coef_[0]:.4f}*cases")
```
```
num_deaths = -16.6442 + 0.0359*cases
```

むむむ...感染者数が`0`の場合、COVID-19の死者はいないはずですので切片は`0`であるべきです。`fit_intercept=False`を指定します。

```py:Python
lr = LinearRegression(fit_intercept=False).fit(X_train, y_train)
print(f"num_deaths = {lr.coef_[0]:.4f}*cases")
```
```
num_deaths = 0.0355*cases
```
これによって、使用しているデータセットにおいては死亡率が3.5%であることを示唆しています。しかし、我々はいくつかの州においてはさらに高い死亡率になっていることを知っています。それでは州を特徴量として追加しましょう！

その前に一点補足を。Databricksでは機械学習モデルをトレーニングすると、MLflowによってモデルが自動でトラッキングされます。
![Screenshot 2023-03-31 at 11.14.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/b5649784-8b9c-0286-89d7-2311b347e399.png)
これによって、「どれがどのモデルかわからない」という状況を避けることができます。
![Screenshot 2023-03-31 at 11.14.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/027319e0-7b4d-20b0-f75e-09b056e706b9.png)

# ワンホットエンコーディング

州のように数値では無い特徴量をどのように扱ったらいいのでしょうか？

あるアイデア:
* 非数値を表現する単一の数値特徴量を作成する
* カテゴリー変数の特徴量を作成する:
  * state = {'New York', 'California', 'Louisiana'}
  * 'New York' = 1, 'California' = 2, 'Louisiana' = 3
  
しかし、これではCaliforniaがNew Yorkの2倍ということになってしまいます！

より良いアイデア:
* カテゴリーごとの`ダミー`特徴量を作成する
* 'New York' => [1, 0, 0], 'California' => [0, 1, 0], 'Louisiana' => [0, 0, 1]

このテクニックは["One Hot Encoding"](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html)として知られるものです。

```py:Python
from sklearn.preprocessing import OneHotEncoder

X_train = train_df[["cases", "state"]]
X_test = test_df[["cases", "state"]]

enc = OneHotEncoder(handle_unknown='ignore', sparse=False)
enc.fit(X_train).transform(X_train)
```
```
Out[12]: array([[1., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 1., 0., ..., 0., 0., 0.],
       ...,
       [0., 0., 0., ..., 1., 0., 0.],
       [0., 0., 0., ..., 0., 1., 0.],
       [0., 0., 0., ..., 0., 0., 1.]])
```

形状を確認してみましょう。

```py:Python
enc.fit(X_train).transform(X_train).shape
```
```
Out[13]: (1754, 915)
```

うわっ、感染者数の変数もワンホットエンコーディングしてしまいました。

```py:Python
enc.categories_
```
```
Out[14]: [array([     1,      2,      3,      4,      5,      6,      7,      8,
             9,     10,     11,     12,     13,     14,     15,     16,
            17,     18,     19,     20,     21,     22,     23,     24,
            25,     26,     27,     28,     29,     30,     31,     32,
            33,     34,     35,     36,     37,     38,     39,     40,
            41,     42,     43,     44,     45,     46,     47,     48,
            49,     50,     51,     52,     53,     54,     55,     56,
            57,     58,     59,     60,     61,     62,     63,     64,
            65,     66,     67,     68,     69,     70,     71,     72,
            73,     74,     75,     76,     77,     78,     79,     80,
            81,     82,     83,     84,     85,     86,     87,     88,
            89,     90,     91,     92,     93,     94,     95,     96,
            97,     98,     99,    100,    101,    102,    103,    104,
```

特定のカラムにのみワンホットエンコーディングを適用するように、[column transformer](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html)が必要となります。

```py:Python
from sklearn.compose import ColumnTransformer

ct = ColumnTransformer([("enc", enc, ["state"])], remainder="passthrough")
ct.fit_transform(X_train).shape
```
```
Out[15]: (1754, 56)
```

# パイプライン

[パイプライン](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html)を用いることで、一連のデータ変換処理をチェーンすることができます。また、このようにすることで、トレーニングセットに適用したすべてのオペレーションが、同じ順序でテストセットに適用されることを保証できます。

```py:Python
from sklearn.pipeline import Pipeline

pipeline = Pipeline(steps=[("ct", ct), ("lr", lr)])
pipeline_model = pipeline.fit(X_train, y_train)

y_pred = pipeline_model.predict(X_test)
```

# 州が異なるとどうなるのか？

特徴量を追加することで、感染者数特徴量の係数も変化していることに気づきます。

```py:Python
print(f"num_deaths = {pipeline_model.steps[1][1].coef_[-1]:.4f}*cases + state_coef")
```
```
num_deaths = 0.0381*cases + state_coef
```

```py:Python
import pandas as pd
pd.set_option('display.float_format', '{:.2f}'.format)

categories = pipeline_model.steps[0][1].transformers[0][1].categories_[1]

pd.DataFrame(zip(categories, pipeline_model.steps[1][1].coef_[:-1]), columns=["State", "Coefficient"])
```
![Screenshot 2023-03-31 at 11.19.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/120efe94-652d-129a-14fa-854d4f0377d4.png)

# 評価メトリクス

![](https://brookewenig.com/img/IntroML/RMSE.png)

[sklearn.metrics](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.mean_squared_error.html?highlight=mean_squared_error)を用いて、データセットのMSEとRMSEを計算しましょう。

```py:Python
from sklearn.metrics import mean_squared_error
import numpy as np

mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
print(f"MSE is {mse:.1f}, RMSE is {rmse:.1f}")
```
```
MSE is 78694639.4, RMSE is 8871.0
```

# 予測結果の可視化

```py:Python
pred = pd.concat([test_df.reset_index(drop=True), pd.DataFrame(y_pred, columns=["predicted_deaths"])], axis=1)
pred
```
![Screenshot 2023-03-31 at 11.20.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/41ebab47-7726-4319-755e-8c0c307c472d.png)

やりました！scikit-learnを用いて機械学習パイプラインを構築することができました！

scikit-learnを探索し続ける場合には、[UCI ML Repository](https://archive.ics.uci.edu/ml/index.php)や[Kaggle](https://www.kaggle.com/)のデータセットをチェックしてみてください！

### Databricksクイックスタートガイド

[Databricksクイックスタートガイド](https://www.amazon.co.jp/dp/B09V1YXFVQ/)


### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)