アセットマネージャーのためのファイナンス機械学習:特徴量の重要度分析 クラスタ特徴量
直交化による特徴量重要度分析では、線形な特徴量同士の組み合わせによる代替効果は抑制できても、依存関係が非線形である場合の代替効果は残る。また、変換後の特徴量の重要度を、変換前の特徴量に換算するのは難しい。
よって、PCA変換なしに、類似の特徴量をクラスタにまとめ、クラスタ同士は相互に非類似であることから、代替効果が抑制されるクラスタレベルでの特徴量分析を行う。
特徴量をクラスタに分ける
特徴量を、相関に基づく測度、または情報距離測度の測度空間に射影し、測度行列$${\{X_f\},f=1,\dots F}$$を作成する。
ONCアルゴリズムでクラスタ数と構成を決定する。
ただし、ONCアルゴリズムでは、一つの特徴量を複数のクラスタに割り当てないため、複数のクラスタにまたがる特徴量と相関関係にある特徴量が存在で、シルエット係数が低くなることがある。
この時には、ONC適用後、全てのクラスタについて、そこに含まれる特徴量を、その補集合の特徴量、残差特徴量に入れ替える。
全クラスタ$${k=1,\dots K}$$に含まれる特徴量を$${{\bf D}_k}$$とすると、この入れ替えにより、
$${{\bf D}_k \subset D}$$であり、
全てのクラスタ上で、$${||{\bf D}_k|| > 0}$$となる。
また、非同一クラスタ間$${k,l, k \ne l}$$では、$${{\bf D}_k\cap {\bf D}_l \ne \emptyset}$$、全クラスタで$${\cup^{K}_{k=1}{\bf D}_k ={\bf D} }$$となる。
特徴量$${X_i, i\in {\bf D}_k}$$の残差特徴量$${\hat{\epsilon}_i}$$は、その観測値のインデックスを$${n, n=1,\dots,N}$$として、以下の式にフィットして得られる。
$${X_{n,i} = \alpha_i + \sum_{j\in\{ \cup_{l < k}{\bf D}_l\}}\beta_{i,j}X_{n,j} + \epsilon_{n,i} }$$
この残差の特性の一つに、残差は回帰子に直交することが挙げられる。$${\hat{\epsilon}_i}$$に置き換えることによって、クラスタは互いに独立となる。
ただし、この入れ替えは、シルエット係数が十分高い場合には行う必要はない。
特徴量重要度分析
類似性を用いてクラスタにまとめられた特徴量の重要度分析は、個々の特徴量ではなくそのクラスタで、MDIやMDAを適用する。この方法の分析は、分割的クラスタだけでなく、階層的クラスタにも適用できる。
クラスタMDI : そのクラスタを構成する特徴量のMDIの合計値がそのクラスタのMDI値となる。ランダムフォレストや決定木のアンサブルでは、各木は一つのクラスタMDIを持ち、特徴量MDIと同様に、クラスタMDIの平均値と分散が計算できる。この実装はスニペット6.4で行われている。
def groupMeanStd(df0, clstrs):
out = pd.DataFrame(columns=['mean', 'std'])
for i, j in clstrs.items():
df1 = df0[j].sum(axis=1)
out.loc['C_'+str(i), 'mean'] = df1.mean()
out.loc['C_'+str(i), 'std'] = df1.std() * df1.shape[0]**-.5
return out
def featImpMDI_Clustered(fit, featNames, clstrs):
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) #because max_features=1
imp = groupMeanStd(df0, clstrs)
imp /= imp['mean'].sum()
return imp
クラスタMDA: 一つの特徴量をシャッフルするのではなく、クラスタを構成する全ての特徴量をシャッフルして、性能を比較する。実装は、スニペット6.5で行われている。
def featImpMDA_Clustered(clf, X, y, clstrs, n_splits=10):
cvGen = KFold(n_splits=n_splits)
scr0, scr1 = pd.Series(dtype='float64'), pd.DataFrame(columns=clstrs.keys())
for i, (train, test) in enumerate(cvGen.split(X=X)):
X0, y0, = X.iloc[train,:], y.iloc[train]
X1, y1 = X.iloc[test, :], y.iloc[test]
fit = clf.fit(X=X0, y=y0)
prob=fit.predict_proba(X1)
scr0.loc[i] = -log_loss(y1, prob, labels=clf.classes_)
for j in scr1.columns:
X1_=X1.copy(deep=True)
for k in clstrs[j]:
np.random.shuffle(X1_[k].values) # shuffle clusters
prob=fit.predict_proba(X1_)
scr1.loc[i,j]=-log_loss(y1, prob, labels=clf.classes_)
imp=(-1*scr1).add(scr0,axis=0)
imp = imp/(-1*scr1)
imp = pd.concat({'mean':imp.mean(), 'std':imp.std()*imp.shape[0]**-.5}, axis=1)
imp.index=['C_'+str(i) for i in imp.index]
return imp