いざ機械学習!そのための最低限の前処理とは?

前回までで、ようやく、銀行の顧客ターゲティングに機械学習を活用するにあたって、そのビジネス的な目的と、性能を図るに適した指標の整理が、出来ました。

今回からいよいよ、実際に機械学習していってみたいと、思います。

はじめに:前処理ってなに?

機械学習するためには、データの前処理が必要だと、言われています。一口に「前処理」といいますが、私が思うに、実際には二つあるような、気がします。一つ目が、「それをしないと、そもそも機械学習を実行出来ない」前処理で、二つ目が、「それをすることにより、精度が上がる」前処理です。

前者は、最低限必須な前処理です。一方後者は、コンペなどで勝利するためにはある意味、必須な前処理かもしれません。が、ビジネス的には、必ずしも必須ではない可能性も、ありえます。ビジネス上必要な精度に達していなければ、やる必要がありますし、達していれば、あえてやる必要も、ないかもしれません。

本エントリーではまず、最低限必要な前者の前処理のみを行って、銀行の顧客ターゲティングを機械学習してみたいと、思います。

ほんとはよくない?いきなり機械学習

どこかで見ました。「いきなり機械学習するのは、よくない」と。あくまでもデータと向き合い、そのデータがどういうデータであるかを理解するのが、重要だと。確かに、その通りかもしれません。いきなり機械学習して、「〇〇な精度が出ました。」となっても、「で、どうしよ?」で終わるのが、オチかもしれません。

大体の本やサイトだと、データを読み込んで、とりあえずhead()*1して、describe()*2して、相関を調べて・・・なんて、やっているような気が、します。相関とは、超ざっくりいうと、比例関係でしょうか。一方の値が変化すれば、それに連動してもう一方の値も変わる、あれです。

相関を調べ、予測したい値*3と強い相関を持つデータを、入力値として使い、機械学習を実行する・・・それが、王道な気がします。

しかし、です。

基準値が欲しいと、思いませんか?

もしかしたら私だけかもしれませんが、初心者が無い知恵を絞って、入力データを選りすぐる*4より、何も考えずに全量ぶち込んで機械学習した方が、ましな可能性が、あります。初心者が初心者なりに工夫したつもりになって、「やあ、いい精度が出たぞ」と思っても、何もしない方が精度が高いことも、ありうるのです。

・・・というわけで、本エントリーでは、今後、何か工夫してみた時に、効果があった/なかったを判断するための基準値として、まずは最低限の前処理で、機械学習してみることに、しました。

最低限の前処理で機械学習する流れ

当然と言えば当然ですが、まずは「どんな前処理が必要か」を、確認する必要が、あります。それを踏まえ、以下のような流れで、やってみたいと、思います。

  1. 必要な前処理を確認する
  2. 必要な前処理を実施する
  3. モデルを選択する
  4. 機械学習を実行する
  5. 精度を測定する

ではやってみよう!

必要な前処理を確認する

まずそもそも、機械学習は入力データとして、数値しかとることが出来ません。つまり、「最低限必要な前処理」とは具体的に、以下と考えます。

  • 数値項目の非数値データを、処理する。
  • 文字列項目を数値データに、変換する。

前者はいわゆる、「NaN」(Not a Number)というやつでしょうか。間違えて数値項目に文字列が入っていることもありえますが、いわゆるnull(欠損値)が、代表的です。処理の仕方は様々で、NaNのある行を除去したり、代表値(平均値や最頻値など)で埋めたり、高度なものになると、前処理用の機械学習で欠損値を埋めることなども、あるようです。

後者は、カテゴリ項目などが代表的で、「性別」などがあります。例えば、男性⇒1、女性⇒2などに置き換えることもあれば、以下のように、「性別」項目を「男性フラグ」「女性フラグ」に分割することなども、あります。前者の手法を「マッピング」、後者の手法を「one-hot-encoding」という、みたいです。

性別 男性フラグ 女性フラグ
男性 1 0
女性 0 1

これを踏まえ、Google版Jupyter NotebookであるGoogle Colabを使って、実際に必要な前処理を確認していってみたいと、思います。銀行の顧客ターゲティング用のGoogle Colabの環境については、このあたりのエントリーをご確認いただければと、思います。

それでは早速、Google Colabのお約束からやっていきます。一個一個が、Jupyter(Colab)のセルの、イメージになります。

# まずはGoogle Colabのお約束、Googleドライブをマウント
from google.colab import drive
drive.mount('/content/gdrive/')

# 作業フォルダへ移動
%cd ./gdrive/My\ Drive/colab/bank

続いて、構造化データを扱うためのPythonのライブラリである、pandasをインポート。pandasを使って、銀行の顧客データを、読み込みます。

# pandasをインポート
import pandas as pd

# 銀行の顧客データを読み込み、正しく読み込めていることを確認(head()の実行結果は省略)
train = pd.read_csv("./dataset/train.csv")
train.head()


さて、ここからが本題。先に挙げた二つの前処理(①数値項目の非数値データを、処理する。②文字列項目を数値データに、変換する)を実施するには、どの項目が数値項目で、どの項目が文字項目かを、確認する必要があります。pandasのinfo()関数を、使います。

# 各項目のデータ型を確認
train.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 27128 entries, 0 to 27127
Data columns (total 18 columns):
id 27128 non-null int64
age 27128 non-null int64
job 27128 non-null object
marital 27128 non-null object
education 27128 non-null object
default 27128 non-null object
balance 27128 non-null int64
housing 27128 non-null object
loan 27128 non-null object
contact 27128 non-null object
day 27128 non-null int64
month 27128 non-null object
duration 27128 non-null int64
campaign 27128 non-null int64
pdays 27128 non-null int64
previous 27128 non-null int64
poutcome 27128 non-null object
y 27128 non-null int64
dtypes: int64(9), object(9)
memory usage: 3.7+ MB


結果、「age、balance、day、duration、campaign、pdays、previous」が数値。「job、marital、education、default、housing、loan、contact、month、poutcom」が文字項目*5であることが、分かりました。ちなみに、idは単なるキー項目、yは予測対象の項目(入力データでは、ない)なので、ここでは除きます。

続いて、実際にNaN項目があるかないかを、確認します。isnull()関数とsum()関数を、使います。

train.isnull().sum()

id 0
age 0
job 0
marital 0
education 0
default 0
balance 0
housing 0
loan 0
contact 0
day 0
month 0
duration 0
campaign 0
pdays 0
previous 0
poutcome 0
y 0
dtype: int64

どうやら、NaNはないらしいことが分かりました。素晴らしい!というわけで、最低限の前処理として、今回は、以下のみが必要であることになります。

  • 文字列項目を数値データに、変換する。

必要な前処理を実施する

さて、実際に

  • 文字列項目を数値データに、変換する。

これを行うにあたり、まずはマッピングかone-hot-encodingかを切り分ける必要があります。一般的に、サイズの「S」「M」「L」みたいに、序列関係がありそうなものは「1」「2」「3」みたいにマッピングして、先の性別みたいに序列関係のないものは、各フラグ項目に分けてone-hot-encodingするのが、良いそうです。ざっと見た限り、今回怪しげなのは、monthです。まずは、monthの値を、value_counts()関数で、確認してみます。

train["month"].value_counts()

may 8317
jul 4136
aug 3718
jun 3204
nov 2342
apr 1755
feb 1586
jan 846
oct 439
sep 356
mar 299
dec 130
Name: month, dtype: int64

「may」や「jul」など、英単語の各月の略称が、入っているみたいです。時系列データを考えるなら、「jun」→「1」、「feb」→「2」のように、マッピングするのがよさそうな気がしますが・・・今回の銀行の顧客ターゲティングでいうと、特に1月よりも2月の方が序列が上とか、そういう関係は、直感的に、なさそうな気がします。今回は、精度の基準値を図るのが目的ですし、pythonの場合、マッピングよりone-hot-encodingの方がちょっぴり手間がかからないので、一旦月も一緒くたにone-hot-encodingしたいと、思います。

続いて、以下をご覧ください。先にご紹介したone-hot-encodingを、結婚状態(marital)に対して、やってみたイメージです。

結婚状態 未婚フラグ 既婚フラグ 離婚フラグ
未婚 1 0 0
既婚 0 1 0
離婚 0 0 1

実はこれ、下記のようにフラグ列を一個減らしても、情報量は同じです。

結婚状態 既婚フラグ 離婚フラグ
未婚 0 0
既婚 1 0
離婚 0 1

既婚フラグも離婚フラグも立っていなければ、未婚と判断できます。「機械学習のための特徴量エンジニアリング ―その原理とPythonによる実践 (オライリー・ジャパン)」によると、この手法は、「ダミーコーディング」というようです。人によっては、「one-hot-encoding」「ダミー変数」「ダミーコーディング」とごっちゃになっていることがあったりなかったりするようなしないような気がしますが・・・別物みたいです*6

情報量が同じなので結果はさして変わらないような気もしてきますが、実際のところ、one-hot-encoding/ダミーコーディングで精度が変わることが、ままあります。one-hot-encodingのやり方は有名なので、ここではダミーコーディングでいってみます。

最後に、カテゴリ量が膨大すぎて、one-hot-encodingすると膨大なフラグ列が生まれるケースがありえますが・・・先のvalue_counts()関数で確認したところ、今回に関してはそのような項目はなさそうなので、割愛します。

というわけで、いざ、ダミーコーディングです。

# ダミーコーディング実行
train_data = pd.get_dummies(data=train, columns=['job', 'marital', 'education', 'default', 'housing', 'loan', 'contact', 'poutcome', 'month'], drop_first=True)
train_data.columns

Index(['id', 'age', 'balance', 'day', 'duration', 'campaign', 'pdays',
'previous', 'y', 'job_blue-collar', 'job_entrepreneur', 'job_housemaid',
'job_management', 'job_retired', 'job_self-employed', 'job_services',
'job_student', 'job_technician', 'job_unemployed', 'job_unknown',
'marital_married', 'marital_single', 'education_secondary',
'education_tertiary', 'education_unknown', 'default_yes', 'housing_yes',
'loan_yes', 'contact_telephone', 'contact_unknown', 'poutcome_other',
'poutcome_success', 'poutcome_unknown', 'month_aug', 'month_dec',
'month_feb', 'month_jan', 'month_jul', 'month_jun', 'month_mar',
'month_may', 'month_nov', 'month_oct', 'month_sep'],
dtype='object')

pandasのget_dummies()関数を使えば、簡単にダミーコーディングできます。dataにpandasのデータフレームを、columnsにダミーコーディングしたい列の列名を、指定します。また、同じ関数で、「drop_first=True」の指定をなくせば、one-hot-encodingになります

参考までに、もし月をマッピングしてみたい場合は、以下のようなコードで、マッピングできます。当然ながら、一個一個マッピング変数を定義して変換しないといけないので、一気に出来るone-hot-encoding/ダミーコーディングに比べ、数が増えると面倒です。

# 月を数字に変換
month_mapping = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 'jul': 7, 'aug':8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}
train['month'] = train['month'].map(month_mapping)

最低限必要な前処理は、以上になります。

モデルを選択する

続いて、モデルを選択します。一般的には、いくつかのモデルで実際に機械学習してみて、よさそうなものを選んでさらなるチューニングをしていくのが、セオリーみたいですが・・・ここでは、決め打ちでランダムフォレストでやってみます。ランダムフォレストには、以下の特徴が、あります。

  • 安定してそこそこの精度が出る。
  • チューニングするパラメータもそんなに多くなく、初心者に優しい。

これより簡単なモデル、例えば決定木やロジスティック回帰などですと、解釈可能性*7は高いですが、精度はそんなに出ません。一方、これよりも複雑なモデル、例えば勾配ブースティングやディープラーニングですと、ちゃんとチューニングすると精度は出ますが、初心者が適当に使うと、あまり精度が出たりでなかったり。

というわけで、初心者がとりあえず基準値を図るには、ランダムフォレストが最適かと、考えてみました。

機械学習を実行する

いよいよ機械学習です。ここでは、ホールドアウト法でやってみたいと、思います。ホールドアウト法とは、以下の図1のイメージのように、入力データを2分割し、一方を訓練用、他方を検証用にする手法です。

f:id:tatsu_mk2:20190429120939p:plain
図1 ホールドアウト法イメージ

機械学習では、未知のデータに対して、可能な限り正確な予測をしたいわけですが、未知のデータは文字通り未知、手にはいりません。そこで、手元のデータを2分割し、片方のみで学習。もう片方は学習せずにとっておくことにより、仮想の未知のデータとします。仮想の未知のデータで精度を図ることにより、本物の未知のデータに対して、どれくらい精度が出そうかを推測するのが、ホールドアウト法の基本コンセプトと、なります。

pythonでは、train_test_sprit()という関数を使えば、簡単にホールドアウト法を実現できるので、やってみたいと思います。まずは、入力データを、予測対象の項目である「y」と、予測に使用するデータx(yとid以外)に、分割します。

# 予測対象列の定義
target_col = 'y'


# 予測に使用しない列の定義
exclude_cols = [target_col, 'id']


# 予測に使用する列の定義
feature_cols = [col for col in train_data.columns if col not in exclude_cols]


# 予測対象列と予測に使用する列を分割
y = train_data[target_col]
x = train_data[feature_cols]
x.head()

「予測に使用する列の定義」のところ、Pythonになじみがないと(私もないですが)分かりにくいですが、「リスト内法表記」という、やつです。「train_data.columns」という変数に、train_dataが持っている列の列名が、リストで定義されています。その列定義のリストから、「exclude_cols」で定義した、予測に使用しない列に合致しない列のみを、取り出しています。

続いて、ホールド法を実行するために、xとyを2分割します。

# データ分割用の関数をインポート
from sklearn.model_selection import train_test_split

# 分割実行
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.1, random_state=1234)


「test_size」を指定することにより、どれくらいを学習に使用し、どれくらいを仮想の未知のデータとして使用するか、指定できます。所説あるみたいですが・・・事前に色々やってみたところ、今回の銀行の顧客ターゲティングに関しては、9割を学習に使用するのが、よさそうでした。random_stateは、乱数のシードです。設定しておくことにおり、再現性のある分割結果を得られるため、精度を図る際は、固定しておく方が、望ましいです。

では、学習を実行してみます。

# ランダムフォレストをインポート
from sklearn.ensemble import RandomForestClassifier

# 学習を実行
rcf = RandomForestClassifier(random_state=1234)
rcf.fit(x_train, y_train)

# 分類結果を取得
y_pred = rcf.predict(x_test)


# 確率を取得
y_pred_proba = rcf.predict_proba(x_test)[:, 1]

今回は、予測をしたい数値が離散値の、分類問題*8なので、ランダムフォレストのうち、「RandomForestClassifier」を使います。同じく、再現性を持たせるため、random_stateには何かしらの値を設定しておきますが、それ以外のチューニングパラメータは、今回は指定しません(まずは、素でやります)。

そして学習を実行し、予測結果を取得します。pythonの機械学習ライブラリであるscikit-learnを使用すれば、どのモデルを使用しても、関数は以下のように統一されています。

  • 学習を実行する場合は、fit()
  • 分類結果を得る場合は、predict()
  • 各分類項の確率(予測値が0である確率/1である確率)を得る場合は、predict_proba()

最後のところの[:, 1]の部分ですが、predict_proba()の実行結果(リスト)から、サブリスト(特定の行/列)を、切り出しています。まず行ですが、全行切り出すので「:」(特定の行だけ切り出すには、0:10とか指定します)、列は、0番目の列にFalseの確率、1番目の列にはTrueの確率が入っているので、今回は1番目の列のみを、取り出しています。

精度を測定する

それでは最後に、精度を測定してみたいと思います。前回整理した通り、コンペにおける指標値はAUCですが、ここでは参考までに、正解率・Recall・Presicionも、出してみたいと思います。

# 精度測定用関数のインポート
from sklearn.metrics import roc_auc_score
from sklearn.metrics import accuracy_score
from sklearn.metrics import recall_score
from sklearn.metrics import precision_score

# AUC測定
auc = roc_auc_score(y_test, y_pred_proba)
print('AUC: ', auc)


# 正解率測定
acy = accuracy_score(y_test, y_pred)
print('Accuracy: ', acy)


# Recall測定
rc = recall_score(y_test, y_pred)
print('Recall: ', rc)


# Presicion
pr = precision_score(y_test, y_pred)
print('Precision: ', pr)

AUC: 0.8896970529161214
Accuracy: 0.903059343899742
Recall: 0.34951456310679613
Precision: 0.6352941176470588

注意点として、AUCの測定には、predict_proba()の実行結果を使用する必要があります。正解率/Recall/Precisionは、基本的に、「一致する率」を見るので、predict_proba()を閾値で0/1に切り上げた、predict()の値を使用します。こちらでも整理した通り、AUCは、閾値を変化させつつ確率を0/1に切り上げ/切り下げし、真陽性率・偽陽性率を図るものです。なので、閾値で切り上げ/切り下げ済みのpredict()の値を使用すると・・・ちょっと変なことになります。最初は私、間違ってpredict()の値でAUCを測定し、あまりの低さに愕然としました

それはさておき、結果ですが・・・AUC0.889だと、SIGNATEのコンペ上は、大体1062/5034位くらいです(2019年4月29日現在)。上位2割くらいには、入っていますね。

一方、仮にこのモデルに沿って、ビジネスを行ったとすると・・・Recallは大体0.35くらい。顧客データは、全部で4万人くらいあったので、およそ1.4万人くらいの優良顧客を、発掘できる計算になります。一方、Precisionは、0.64くらい。モデルが「True」と予測した顧客に絞って営業した場合、成約率は65%とみることが、出来ます。何も考えずにテレマーケティングした場合、よほど魅力的でタイムリーな商品でもない限り、成約率は30%もあればいいところのような気がするので・・・悪くない数字だと、思います。

終わりに

というわけで、何もせずに機械学習をした場合の基準値は、大体AUC0.889くらいであることが、分かりました。次回は、ランダムフォレストのパラメータをチューニングして、精度を上げていってみたいと、思います。

最後まで読んでいただいて、ありがとうございました。「とりあえず機械学習をやってみたいけど、どうやればいいか分からない」と言う方の、何かしらの参考にでもなれば、幸いでございます。

参考資料一覧

機械学習のための特徴量エンジニアリング ―その原理とPythonによる実践 (オライリー・ジャパン)

機械学習のための特徴量エンジニアリング ―その原理とPythonによる実践 (オライリー・ジャパン)

  • 作者: Alice Zheng,Amanda Casari,株式会社ホクソエム
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2019/02/23
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る
フリーライブラリで学ぶ機械学習入門

フリーライブラリで学ぶ機械学習入門

*1: 読み込んだデータの先頭5行を表示する、pythonの関数です。

*2: 読み込んだデータの平均や標準偏差といった、基礎統計を表示する。pythonの関数です。

*3: 今回の銀行の顧客ターゲティングの場合、その顧客が優良顧客(True)か否(False)か。もっと言えば、優良顧客である確率(%)。

*4: 専門用語で言えば、特長量選択でしょうか?

*5:info()関数の場合、文字項目はStringじゃなく、Objectと出るみたいです。

*6:one-hot-encoding/ダミーコーディング総称して、ダミー変数と呼んでいるのかも、しれません。後述しますが、関数がget_dummies()ですし。

*7: 「何故そのような予測結果になったのか」を、人が理解しやすいかどうか。

*8: 今回のケースは、True/Falseや0/1などの、2項分類問題にあたります。