2値分類問題を解くAutoMLを自作した（その1）-特徴量選択まで-

　2値分類（Binary Classification）を解くための自作のAutomated Machine Learning（AutoML）プログラムを[Github][1]にアップしたので、その解説を書きます。機械学習を勉強しながら作成したプログラムなので、至らない点があるかもしれません。要改善点を指摘して頂けると嬉しいです。
[1]:https://github.com/bright1998/AutoML_Binary_Classification/blob/master/AML_binary-classification.py

##【経緯】
　昨年JDLAのE資格を受験しました。受験資格を得るためには認定講座を修了する必要があります。私が受講した機械学習講座の最終課題で作成したのが、上記のプログラムです。最近、業務で非常に役に立ったので、備忘録を兼ねて解説を残しておこうと思い立ちました。それにしても、思いもよらないところでE資格受験が仕事に好影響を与えたということで、嬉しい誤算です。さて、ここからが本題の解説です。

##【パラメータ】
AutoMLの中身に行く前に、実行時に設定するパラメータについてです。

```
model_file_name = 'data/train.csv'
score_file_name = 'data/test.csv'
header_ID = 'ID'
header_obj = 'Objective'
ohe_list = ['x1', 'x2']
use_PCA = True
scoring_method = 'roc_auc'
```
__model_file_name__：学習に使うデータファイル（目的変数のデータあり）。
__score_file_name__：予測に使うデータファイル（目的変数のデータなし）。
__header_ID__：データIDの列名（予測には使われません）。
__header_obj__：目的変数の列名。
__ohe_list__：one-hot encodingする変数の列名をリストで指定。
__use_PCA__：主成分分析（PCA）により変数変換するか否か。
__scoring_method__：機械学習手法の優劣を決めるメトリック（選択できるのは、accuracy, precision, recall, f1, roc_auc）。

```
AML_BinaryClassification(model_file_name, score_file_name, header_ID, header_obj, ohe_list, use_PCA, scoring_method, n_GS=3, cls=1)
```
にあるように、他にn_GS, clsというパラメータもあります。
__n_GS__：詳細はあとで解説しますが、今回のプログラムではデフォルトのモデルパラメータで学習した結果で機械学習手法を1次スクリーニングした後に、上位n_GS個の手法に関してGrid Searchでモデルパラメータを調整します。つまり1次スクリーニングでどれだけ手法を絞り込むかを指定するパラメータです。
__cls__：確率値を出力するクラスの番号。今回は2値分類なので、目的変数は0か1のいずれかであることを想定しています。どちらのクラスに所属する確率を出力させたいかを指定します。
　以降、AutoMLの中身の解説に入りますが、ブラックボックスとして使用するのでもよければ、ここまでの説明で実行できると思います（エラーが出たらすみません）。

##【データの読み込み】
```
# ファイル読み込み時のデータ型指定用辞書作成
    dtype_dict = {}
    for item in ohe_list:
        dtype_dict[item] = 'object'

# データファイルの読み込み
# データフレーム
    df_m = pd.read_csv(mfile, dtype=dtype_dict)
    df_s = pd.read_csv(sfile, dtype=dtype_dict)
```
　one-hot encodingする変数をobject型で読み込むために、まず変数名をkeyとして'object'をvalueとした辞書型変数dtype_dictを作成しています。次に、2つのファイルよりデータをDataFrameとして読み込んでいます。

##【データIDと目的変数を分離】
```
# IDの抽出
    ID_m = df_m[[header_ID]]
    ID_s = df_s[[header_ID]]

# 説明変数と目的変数の抽出
    y_m = df_m[header_obj]
    X_m = df_m.drop([header_ID, header_obj], axis=1)

    try:
        y_s = df_s[header_obj]
        X_s = df_s.drop([header_ID, header_obj], axis=1)
    except:
        X_s = df_s.drop([header_ID], axis=1)

##### 必要に応じて実行 #####
# 目的変数の数値化
#    class_mapping = {'N':1, 'Y':0}
#    y_m = y_m.map(class_mapping)
#####################
```
　データIDは学習や予測には用いないので別のDataFrame（ID_m, ID_s）にしておきます。学習用のデータからは目的変数もy_mというSeriesとして抽出します。そしてデータIDと目的変数を除いた説明変数をX_mとします（try, exceptの部分の処理は、予測用のデータにも目的変数が含まれていた場合を考えた、念のための処理です）。
　目的変数が数値ではなく、例えばY, N（Yes, No）などで書かれている場合は、数値に変換するためにmapメソッドを使います。変換のルールは辞書型で記述します。

##【データ前処理】
```
# モデル用
    X_ohe_m = pd.get_dummies(X_m, dummy_na=True, columns=ohe_list)
    imp = SimpleImputer(missing_values=np.nan, strategy='mean')
    imp.fit(X_ohe_m)
    X_ohe2_m = pd.DataFrame(data=imp.transform(X_ohe_m), columns=X_ohe_m.columns.values)
```
　学習用データの処理です。まずpd.get_dummiesによりohe_listで指定した変数のone-hot encodingをしています。そしてSimpleImputerで欠損を平均値で埋めています（最新のバージョンのscikit-learnには、SimpleImputerはないかもしれません）。

```
# スコア用
    X_ohe_s = pd.get_dummies(X_s, dummy_na=True, columns=ohe_list)
    X_ohe2_s = pd.DataFrame(data=None, columns=X_ohe2_m.columns.values)
    X_ohe2_s = pd.concat([X_ohe2_s, X_ohe_s])
    X_ohe2_s.loc[:, list(set(X_ohe_m.columns) - set(X_ohe_s.columns))] = X_ohe2_s.loc[:, list(set(X_ohe_m.columns) - set(X_ohe_s.columns))].fillna(value=0, axis=1)
    X_ohe3_s = X_ohe2_s.drop(list(set(X_ohe_s.columns) - set(X_ohe_m.columns)), axis=1)
    X_ohe4_s = X_ohe3_s.reindex(columns=X_ohe2_m.columns.values)
    X_ohe5_s = pd.DataFrame(data=imp.transform(X_ohe4_s),　columns=X_ohe4_s.columns.values)
```
　予測用データの処理です。学習用データと同様、まずはone-hot encodingです。ここで注意すべきは、学習用データと予測用データでone-hot encodingする変数が取る値の種類が完全に一致しているとは限らない点です。もし一致していないと、学習用データと予測用データで異なる変数が生成されます。厳密に処理しようと思うならば、学習用データと予測用データを結合してからone-hot encodingを行い、その結果を分離するのがよいでしょう。今回は、X_ohe_sにだけ存在してX_ohe_mには存在しない変数を削除することで対応しています（X_ohe2_s.drop(list...)の部分）。最後に欠損を埋めていますが、注意すべきは学習用データでfitしたSimpleImputerをそのまま使用していることです。学習用データと予測用データともに同じ平均値で欠損を埋めています。

## 【特徴量選択】
```
    selector = RFECV(RandomForestClassifier(random_state=1), step=0.05, scoring=scoring_method)
    selector.fit(X_ohe2_m.values, y_m.values)
    X_fin_m = X_ohe2_m.loc[:, X_ohe2_m.columns.values[selector.support_]]
    X_fin_s = X_ohe5_s.loc[:, X_ohe5_s.columns.values[selector.support_]]
```
　交差検証しながらRecursive Feature Elimination（RFE）で変数選択を行っています。反復的に変数を減らしていきますが、一回あたり5%ずつ変数を減らしていくようにstepというパラメータで指定しています。目的変数（y_m）と相関があるものを残すので、学習用データを用いてfitします。その結果を予測用データにも適用します。

##【続き】
実際に学習を行い、予測結果を出力する処理については、その2に書きます。