ファイナンス機械学習:特徴量の重要度 MDI、MDA、SFI

 パージ付きのk-Fold交差検証を用いて、汎化誤差をバリアンス/バイアス/ノイズで評価し、さらにそのモデルのパフォーマンスに寄与する特徴量を知ることにより、戦略やリサーチの手法を発展させることができる。
 ここで、重要なのは、ある特徴量同士が互いに相関している場合、この特徴量同士で互いにその特徴量自身の重要度を下げていることである。これを代替効果、もしくは多重共線性と呼ぶ。重要度分析では、この代替効果に注意を払う必要がある。

平均不純度減少量(MDI)

ランダムフォレストのようなツリーベイスにおいて、インサンプルで分析を行う。
 決定木のノードの不純度は、そのノード内の異なるクラスのデータポイントが混ざっている度合いで与えられている。各決定木の各ノードにおいて、ランダムに選択された特徴量で、不純度が削減されるように、ノード内のデータポイント集合を分割する。従って、決定木毎に、各特徴量によって削減された不純度量が計算できる。最後に、この決定木の集合に対し、それぞれの木で、各特徴量毎に削減された減少率を平均化し、ランク付けする。
 ランダムフォレストでは、分割判断で使用する特徴量数をmax_featuresで指定できる。MDIを測定する場合は、これを1にする(max_features=1)。
 平均化する時には、重要度ゼロの特徴量を平均に含めてはいけない。不純度によるランク付けであるから、予測力が全くない特徴量でも重要度はゼロではないことに注意する。
 MDIはツリーベース以外の分類器には適用されず、また代替効果が存在していることを忘れてはならない。
 scikit-learnはツリーベイスの分類器に、MDIをGini不純度から導出するfeature_importances_を実装している。tree.feature_importances_は、各木の特徴量の重要度を返し、これを平均して全体の決定木集合の特徴量の重要度を計算できる。
 ランダムフォレストのフィッティングと同時に計算できるので、計算機負荷は軽い。
 このMDIの実装はスニペット8.2で与えられている。

def getFeatImpMDI(fit,featNames):
    # features importance based on IS mean impurity reduction
    # only works with tree based classifiers
    df0={i:tree.feature_importances_ for i,tree
         in enumerate(fit.estimators_)}
    df0=pd.DataFrame.from_dict(df0,orient='index')
    df0.columns=featNames
    df0=df0.replace(0,np.nan) 
    imp=(pd.concat({'mean':df0.mean(),
                    'std':df0.std()*df0.shape[0]**-0.5},
                   axis=1))
    imp/=imp['mean'].sum()
    return imp

平均正解減少量(MDA)

Out-of-Sample、もしくはOut-of-Bagを用いて、予測における重要度を測定する。テストデータで分類器を学習させ、テストデータでパフォーマンス(正解率、neg-log-loss、F1)を算出する。さらに、テストデータでの特徴量を一度に一つづつシャッフルし、再びパフォーマンスを測り、並び替えによるパフォーマンスの低下で特徴量の重要度を推定する。
 この方法は、MDIと違い計算機負荷は大きいが、ツリーベース以外の分類器全般で使うことができる。ただし、インサンプルでの測定ではないので、結果として全ての特徴量が重要でないと判断される場合がある。インサンプルとアウトオブサンプルは独立であることを求められるので、エンバーゴとパージングが必須となる。
 特徴量を一つづつシャッフルしていく計算方法のため、代替効果が結果に含まれている。
 MDAの実装はスニペット8.3で与えられている。

def getFeatImpMDA(clf,X,y,cv,sample_weight,t1,pctEmbargo,scoring='neg_log_loss'):
    # features imporant based on OOS score reduction
    import FinCV as fcv
    if scoring not in ['neg_log_loss','accuracy']:
        raise ValueError('wrong scoring method.')
    from sklearn.metrics import log_loss, accuracy_score
    cvGen=fcv.PurgedKFold(n_splits=cv,t1=t1,pctEmbargo=pctEmbargo) # purged cv
    scr0,scr1=pd.Series(), pd.DataFrame(columns=X.columns)

    for i,(train,test) in enumerate(cvGen.split(X=X)):
        X0,y0,w0=X.iloc[train,:],y.iloc[train],sample_weight.iloc[train]
        X1,y1,w1=X.iloc[test,:],y.iloc[test],sample_weight.iloc[test]
        fit=clf.fit(X=X0,y=y0,sample_weight=w0.values)
        if scoring=='neg_log_loss':
            prob=fit.predict_proba(X1)
            scr0.loc[i]=-log_loss(y1,prob,sample_weight=w1.values,
                                  labels=clf.classes_)
        else:
            pred=fit.predict(X1)
            scr0.loc[i]=accuracy_score(y1,pred,sample_weight=w1.values)

    for j in X.columns:
        X1_=X1.copy(deep=True)
        np.random.shuffle(X1_[j].values) # permutation of a single column
        if scoring=='neg_log_loss':
            prob=fit.predict_proba(X1_)
            scr1.loc[i,j]=-log_loss(y1,prob,sample_weight=w1.values,
                                    labels=clf.classes_)
        else:
            pred=fit.predict(X1_)
            scr1.loc[i,j]=accuracy_score(y1,pred,sample_weight=w1.values)
    imp=(-scr1).add(scr0,axis=0)
    if scoring=='neg_log_loss':imp=imp/-scr1
    else: imp=imp/(1.-scr1)
    imp=(pd.concat({'mean':imp.mean(),
                    'std':imp.std()*imp.shape[0]**-0.5},
                   axis=1))
  return imp,scr0.mean()

代替効果を除いた特徴量重要度 SFI

MDIとMDAは、代替効果によって、本来は重要度が高い特徴量が破棄されている可能性がある。この欠点を補うために、Single Feature Importanceでは、Out-of-Sampleで一つの特徴量だけを用いて予測し、その交差検証の結果を全特徴量での予測と比べ、特徴量の重要度を測定する。
 代替効果は、予測に使う特徴量は一つだけのために起こらないが、Out-of-Sampleでの計測のため、全ての特徴量が重要でないと判断される可能性はある。この方法の計算機負荷は、MDI,MDAと比べて高い。
 実装はスニペット8.4で与えられている。

def getAuxFeatImpSFI(featNames,clf,trnsX,cont,scoring,cvGen):
    imp=pd.DataFrame(columns=['mean','std'])
    for featName in featNames:
        df0=cvScore(clf,X=trnsX[[featName]],y=cont['bin'],
                    sample_weight=cont['w'],scoring=scoring,cvGen=cvGen)
        imp.loc[featName,'mean']=df0.mean()
        imp.loc[featName,'std']=df0.std()*df0.shape[0]**-0.5
    return imp


この記事が気に入ったらサポートをしてみませんか?