Pythonでロジスティック回帰①

# はじめに

データの基礎分析を学ぶために、[default of credit card clients Data Set](https://archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients)に対して、[Rで基礎分析結果をCSV出力①全体編](https://qiita.com/honda28/items/6b378ec1c51366f3ab21)、[Rで基礎分析結果をCSV出力②量的変数編](https://qiita.com/honda28/items/4aa14ddcd0832946313f)、[Rで基礎分析結果をCSV出力③質的変数編](https://qiita.com/honda28/items/3b0bec2baad61570eee5)にて基礎分析を行ってみた。本記事では、分析を行ってきたデータを用いて、クレジットカードのデフォルト（債務不履行）が起きるかどうかの分類モデルをPythonで扱ってみる。試行する中で気づいたこと、知ったことなどを書きながら進める。知見が乏しいので、間違っている部分や改善すべき部分などあれば、ぜひコメントお願いいたします。

# ロジスティック回帰

今回のデータからデフォルトするかどうかの判定を行いたい。そこで、このような目的変数が二値になるようなモデルでよく使われるロジスティック回帰を試してみる。

## ライブラリのインポート

まず今回使用するライブラリをインポートする。ロジスティック回帰を行うためのライブラリは scikit-learn に含まれている。

```python:importLibrary.py
import numpy as np
import pandas as pd
# For Logistic Regression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve, roc_auc_score
from sklearn.linear_model import LogisticRegression
```

## データの読み込み

[default of credit card clients Data Set](https://archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients)のデータを読み込む。また量的変数と質的変数で前処理が異なるので、それぞれのカラム名を取得しておく。

```python:readCsv.py
df = pd.read_csv("c:/Users/t_honda/Desktop/Default/data/default_of_credit_card_clients.csv", header=1, index_col=0)

col = df.columns.values
quantity_col = col[[0, 4, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]]
quality_col  = col[[1, 2, 3, 5, 6, 7, 8, 9, 10]]
```

## データの前処理

基礎分析は終えているので、それらに基づいてデータを処理する。

### 量的変数の前処理

量的データのスケールが異なるので、標準化を行う。StandardScaler は出力が numpy.array形式となるが、あとで前処理をした質的データと結合して扱いたいので、出力を DataFrame形式で受け取れるようにしている。
（本題とは異なるので詳細は書かないが、プログラムを書いている際にSettingWithCopyWarningというエラーに悩まされた。SettingWithCopyWarningの対応に関しては、[pandas の SettingWithCopyWarning で苦労した話](https://qiita.com/HEM_SP/items/56cd62a1c000d342bd70)を参考にした。下記プログラムのように、.copy()を使えば問題ない。）

```python:quantity.py
from sklearn.preprocessing import StandardScaler

df_quantity = df[quantity_col].copy()                                   # SettingWithCopyWarning を避けるためにコピー
df_quantity[quantity_col] = StandardScaler().fit_transform(df_quantity) # 標準化後のデータを numpy.array ではなく DataFrame で受け取る
```

### 質的変数の前処理

質的データをダミー変数化する。基礎分析の結果、未定義のカテゴリが複数ある項目があるためそれらの処理を行う。具体的には、EDUCATIONはカテゴリとして  1: graduate school, 2: university, 3: high school, 4: others とデータの説明には書いてあるが、0, 5, 6 のデータも存在する。それらをまとめて 4: others に入れてもよいかもしれないが、ここではそれら未定義のものを1つのカテゴリとした。そのため、5, 6 というデータを 0 に置換している。

```python:replaceEducation.py
df_quality = df[quality_col].copy()
df_quality["EDUCATION"] = df_quality["EDUCATION"].replace({5: 0}).replace({6: 0}) # 0, 5, 6 の3種類未定義カテゴリがあるため、それらは 0 に統合
```

PAY_0 - 6 というデータも同様に未定義カテゴリ（-2, 0）が存在するため、-2 を 0 に置換している。またこれらデータは -1 から 8 までの値をとるが、PAY_5, PAY_6 は 1 というカテゴリに属するデータを持っていないため、そのまま処理するとカテゴリが一つ少なくなり、かつずれてしまう。そこでこれらのカテゴリを pd.Categorical() でカテゴリを統一したうえで、ダミー変数にする。

```python:replacePay.py
# -2, 0 の2種類未定義カテゴリがあるため、それらは 0 に統合
df_quality["PAY_0"] = df_quality["PAY_0"].replace({-2: 0})
df_quality["PAY_2"] = df_quality["PAY_2"].replace({-2: 0})
df_quality["PAY_3"] = df_quality["PAY_3"].replace({-2: 0})
df_quality["PAY_4"] = df_quality["PAY_4"].replace({-2: 0})
df_quality["PAY_5"] = df_quality["PAY_5"].replace({-2: 0})
df_quality["PAY_6"] = df_quality["PAY_6"].replace({-2: 0})

pay_categories = set(df_quality["PAY_0"].unique().tolist()
                     + df_quality["PAY_2"].unique().tolist()
                     + df_quality["PAY_3"].unique().tolist()
                     + df_quality["PAY_4"].unique().tolist()
                     + df_quality["PAY_5"].unique().tolist()
                     + df_quality["PAY_6"].unique().tolist())

df_quality["PAY_0"] = pd.Categorical(df_quality["PAY_0"], categories=pay_categories)
df_quality["PAY_2"] = pd.Categorical(df_quality["PAY_2"], categories=pay_categories)
df_quality["PAY_3"] = pd.Categorical(df_quality["PAY_3"], categories=pay_categories)
df_quality["PAY_4"] = pd.Categorical(df_quality["PAY_4"], categories=pay_categories)
df_quality["PAY_5"] = pd.Categorical(df_quality["PAY_5"], categories=pay_categories)
df_quality["PAY_6"] = pd.Categorical(df_quality["PAY_6"], categories=pay_categories)

df_quality_dummies = pd.get_dummies(df_quality, columns=quality_col, drop_first=True)
```

## 前処理したデータを統合し学習

これまでで量的変数と質的変数の前処理を実行したので、それらを統合して訓練用データとテスト用データに分割する。

```python:concat.py
X = pd.concat([df_quantity, df_quality_dummies], axis=1)
Y = df["default payment next month"]
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=0)
```

ロジスティック回帰モデルを使って学習・予測。AUCスコアを出す。

```python:train.py
lr = LogisticRegression(max_iter=2000)
lr.fit(X_train, Y_train)

THRESHOLD = 0.50
Y_pred = np.where(lr.predict_proba(X_test)[:,1] > THRESHOLD, 1, 0)
print("Prediction:", np.count_nonzero(Y_pred==1))
print("      Test:", np.count_nonzero(np.array(Y_test)==1))
print("AUC Score:", roc_auc_score(Y_test, Y_pred))
# Prediction: 673
#       Test: 1297
# AUC Score: 0.6571462858317605
```

通常ロジスティック回帰は0.50をしきい値として判定をするが、上記コードではTHRESHOLDを変更することでしきい値を変えることができる。しきい値0.50では実際と比べてデフォルト判定数が非常に少ないため、ためしにTHRESHOLD=0.30とした場合の結果を以下に示す。

```python:threshold.py
THRESHOLD = 0.30
Y_pred = np.where(lr.predict_proba(X_test)[:,1] > THRESHOLD, 1, 0)
print("Prediction:", np.count_nonzero(Y_pred==1))
print("      Test:", np.count_nonzero(np.array(Y_test)==1))
print("AUC Score:", roc_auc_score(Y_test, Y_pred))
# Prediction: 1115
#       Test: 1297
# AUC Score: 0.6957317062174753
```

しきい値を変えることで、実際の判定とほぼ同じ数の判定をした。しかし、AUCスコア自体はそこまで大きく変動していないため、精度に関してはあまり良いとは言えない。次回の記事では、ロジスティック回帰の精度向上に努めてみようと思う。