重回帰と制約条件

ビジネスの要請

 多くのビジネスにおいて「この数字は何をいじれば変わるのか」に対する回答を迫られる。この時、因果の検証を目的とした重回帰や、変数への考察から因子分析を行うことが考えられる。しかし、多くの陥穽に気付かず、単純なプログラムのコピペによる結果を解釈することほど哀しいものもない。

 ここで予測ではなく、解釈可能性=特徴量重要度の観点から幾つか検証と理解の整理を試みた

重回帰と信憑性

 予測ではなく、解釈可能性=特徴量重要度の観点から幾つか検証と理解の整理を試みた。ボストン市の住宅価格データを使用する。

# 必須モジュール
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split

# データ読み込み
boston = load_boston()
df = pd.DataFrame(boston.data, columns=boston.feature_names)
df['PRICE'] = boston.target

# 説明変数、目的変数を定義
X, y = boston.data, boston.target

# データ分割
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size =0.8, random_state=0)

 重回帰では、各種特徴量がみられる statsmodels のほうがscikit-learnより好み。定数項を入れるのはやや面倒。

import statsmodels.api as sm

# 回帰モデル作成
mod = sm.OLS(y_train, sm.add_constant(X_train))

# 訓練
result = mod.fit() 

# 統計サマリを表示
print(result.summary())

    OLS Regression Results                            
==============================================================================
Dep. Variable:                  PRICE   R-squared:                       0.773
Model:                            OLS   Adj. R-squared:                  0.765
Method:                 Least Squares   F-statistic:                     102.2
Date:                Sun, 20 Jun 2021   Prob (F-statistic):          9.64e-117
Time:                        21:52:40   Log-Likelihood:                -1171.5
No. Observations:                 404   AIC:                             2371.
Df Residuals:                     390   BIC:                             2427.
Df Model:                          13                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const         38.0917      5.522      6.898      0.000      27.234      48.949
CRIM          -0.1194      0.037     -3.257      0.001      -0.192      -0.047
ZN             0.0448      0.014      3.102      0.002       0.016       0.073
INDUS          0.0055      0.063      0.087      0.931      -0.119       0.130
CHAS           2.3408      0.902      2.595      0.010       0.567       4.115
NOX          -16.1236      4.212     -3.828      0.000     -24.404      -7.843
RM             3.7087      0.458      8.106      0.000       2.809       4.608
AGE           -0.0031      0.014     -0.218      0.828      -0.031       0.025
DIS           -1.3864      0.214     -6.480      0.000      -1.807      -0.966
RAD            0.2442      0.070      3.481      0.001       0.106       0.382
TAX           -0.0110      0.004     -2.819      0.005      -0.019      -0.003
PTRATIO       -1.0459      0.137     -7.636      0.000      -1.315      -0.777
B              0.0081      0.003      2.749      0.006       0.002       0.014
LSTAT         -0.4928      0.054     -9.086      0.000      -0.599      -0.386
==============================================================================
Omnibus:                      141.494   Durbin-Watson:                   1.996
Prob(Omnibus):                  0.000   Jarque-Bera (JB):              629.882
Skew:                           1.470   Prob(JB):                    1.67e-137
Kurtosis:                       8.365   Cond. No.                     1.55e+04
==============================================================================

Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.55e+04. This might indicate that there are
strong multicollinearity or other numerical problems.

 この結果から、決定係数、係数、t値、p値あたりをパッとみつつ説明変数の重要度をみることができる。そしてVIF計算して多重共線性を疑ったり、交絡因子を考えたり、ステップワイズ法なり主成分分析から変数減らしたり、操作変数法云々と、兎に角こねくり回すのが重回帰におけるプロセスになる。

 だがビジネス側の要請として、変数を落としたくない!でも係数はマイナスになる筈がない!特徴量重要度として列挙するんだ!という話があるのではないだろうか(あった)

 その意味の在処はさておき、そんな話が振ってきたらどうすればよいのか。結論として、制約条件をかけた最適化処理が必要となるであろう。ここの備忘録を記す。 


 係数のマイナスを許さない法 

 重回帰における最小二乗法は行列式で比較的容易に表現できる。そこで係数がマイナスなのが嫌ならば、マイナスにしなければ良いのだ。決定係数の行方は如何に。 

# 普通の重回帰
class LinearRegression1:
    def __init__(self):
        self.w_ = None

    def fit(self, X, y):
        # 切片の計算のため先頭列に1を追加
        X = np.insert(X, 0, 1, axis=1)
        self.w_ = np.linalg.inv(X.T @ X) @ X.T @ y

    def predict(self, X):
        X = np.insert(X, 0, 1, axis=1)
        return X @ self.w_
    
    def score(self, X, t):
        #残差の分散を求める
        u = ((t - self.predict(X))**2).sum()
        #実測値の分散を求める
        v = ((t - t.mean())**2).sum()
        #決定係数を返す       
        return 1 - u/v

# 結果出力
clf1 = LinearRegression1()
clf1.fit(X_train, y_train)
print(clf1.score(X_test,y_test))

# 横軸がy(実際値)、縦軸がy-y_pred(誤差)
y_pred1 = clf1.predict(X)
plt.figure(figsize=(6, 6))
plt.hlines([0], min(y)-5, max(y) , color='blue')
plt.scatter(y, y - y_pred1, color='red', marker='o', alpha=0.2)
plt.xlabel('y')
plt.ylabel('y - y_pred1');

# 係数確認
coef_df = pd.DataFrame(clf1.w_[1:],
                       index=boston.feature_names,
                       columns=['clf1'])
coef_df

 Scipyは、Non-negative least squaresを有している。sklearnで作っておく。

# NLS 係数非負 重回帰
from scipy.optimize import nnls

class LinearRegression2:
    def __init__(self):
        self.w_ = None

    def fit(self, X, y):
        # 先頭列に1を追加
        # 追加の理由は先ほどの説明を参照(切片の計算のため)
        X = np.insert(X, 0, 1, axis=1)
        #self.w_ = np.linalg.inv(X.T @ X) @ X.T @ y
        self.w_ = nnls(X,y)[0]
        
    def predict(self, X):
        X = np.insert(X, 0, 1, axis=1)
        return X @ self.w_
    def score(self, X, t):
        #残差の分散を求める
        u = ((t - self.predict(X))**2).sum()
        #実測値の分散を求める
        v = ((t - t.mean())**2).sum()
        #決定係数を返す       
        return 1 - u/v

clf2 = LinearRegression2()
clf2.fit(X_train, y_train)
print(clf1.score(X_test,y_test))

coef_df['clf2'] = clf2.w_[1:]
coef_df.round(2)

# 横軸がy(実際値)、縦軸がy-y_pred(誤差)
y_pred2= clf2.predict(X)
plt.figure(figsize=(6, 6))
plt.hlines([0], min(y)-5, max(y) , color='blue')
plt.scatter(y, y - y_pred2, color='red', marker='o', alpha=0.2)
plt.xlabel('y')
plt.ylabel('y - y_pred2');

 Scipyは、完全な0も困る場合は、lsq_linearで。

from scipy.sparse import rand
from scipy.optimize import lsq_linear

class LinearRegression3:
    def __init__(self):
        self.w_ = None

    def fit(self, X, y):
        # 先頭列に1を追加
        # 追加の理由は先ほどの説明を参照(切片の計算のため)
        X = np.insert(X, 0, 1, axis=1)
        #self.w_ = np.linalg.inv(X.T @ X) @ X.T @ y
   # 0.01 ~ 100 にする場合はboundsで指定
        res = lsq_linear(X, y, bounds=(0.01, 100), lsmr_tol='auto', verbose=1)
        self.w_ =res.x
        
    def predict(self, X):
        X = np.insert(X, 0, 1, axis=1)
        return X @ self.w_


 ちな決定係数は準に、0.76, 0.41, 0.17という悲惨な結果。しかし良いのだ。。問題は係数を負にしないことだけなのだから、、、


ところでランダムフォレストでお茶は濁せないのか?

 ランダムフォレストつまりは決定木における特徴量重要度(Gain)は、多重共線性の影響を受けないわけでは勿論ない。だが、係数として顕現しないので説明時にはごまかせるのではないか…?

qiita.com


# 自由度調整済みr2を算出
def adjusted_r2(X,Y,model):
    from sklearn.metrics import r2_score
    import numpy as np
    r_squared = r2_score(Y, model.predict(X))
    adjusted_r2 = 1 - (1-r_squared)*(len(Y)-1)/(len(Y)-X.shape[1]-1)
    #yhat = model.predict(X) \ #SS_Residual = sum((Y-yhat)**2) \ #SS_Total = sum((Y-np.mean(Y))**2)
    #r_squared = 1 - (float(SS_Residual))/ SS_Total
    return adjusted_r2

# 予測モデルの精度確認の各種指標を算出
def get_model_evaluations(X_train,Y_train,X_test,Y_test,model):
    from sklearn.metrics import explained_variance_score
    from sklearn.metrics import mean_absolute_error
    from sklearn.metrics import mean_squared_error
    from sklearn.metrics import mean_squared_log_error
    from sklearn.metrics import median_absolute_error

   # 評価指標確認
   # 参考: https://funatsu-lab.github.io/open-course-ware/basic-theory/accuracy-index/
    yhat_test = model.predict(X_test)
    return "adjusted_r2(train)     :" + str(adjusted_r2(X_train,Y_train,model)) \
         , "adjusted_r2(test)      :" + str(adjusted_r2(X_test,Y_test,model)) \
         , "平均誤差率(test)       :" + str(np.mean(abs(Y_test / yhat_test - 1))) \
         , "MAE(test)              :" + str(mean_absolute_error(Y_test, yhat_test)) \
         , "MedianAE(test)         :" + str(median_absolute_error(Y_test, yhat_test)) \
         , "RMSE(test)             :" + str(np.sqrt(mean_squared_error(Y_test, yhat_test))) \
         , "RMSE(test) / MAE(test) :" + str(np.sqrt(mean_squared_error(Y_test, yhat_test)) / mean_absolute_error(Y_test, yhat_test)) #better if result = 1.253


# 必須モジュール
import xgboost as xgb
import matplotlib.pylab as plt
from sklearn.ensemble import RandomForestRegressor


# 説明変数、目的変数を定義
X = df.drop('PRICE', axis=1)
y = df['PRICE']

# データ分割
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size =0.8, random_state=1)

# sklearnによるランダムフォレスト
rf_reg = RandomForestRegressor(n_estimators=10)
rf_reg = rf_reg.fit(X_train, y_train)
fti = rf_reg.feature_importances_

print('Feature Importances:')
for i, feat in enumerate(boston['feature_names']):
    print('\t{0:10s} : {1:>.6f}'.format(feat, fti[i]))

# 結果評価
get_model_evaluations(X_train,y_train,X_test,y_test,rf_reg)

######################################
# xgboost による回帰①

# Dmatrix作りつつ、説明変数は入れておく
dtrain = xgb.DMatrix(X_train, label=y_train,feature_names=boston['feature_names'])
dtest = xgb.DMatrix(X_test, label=y_test,feature_names=boston['feature_names'])

# 回帰パラメタ設定
xgb_params = {"objective": "reg:linear", "eta": 0.1, "max_depth": 6, "silent": 1}
num_rounds = 100

# 学習
gbdt = xgb.train(xgb_params, dtrain, num_rounds)

# 特徴量重要度を、weight と gainで出したい
fig = plt.figure(figsize=(13, 5))
ax1 = plt.subplot(121)
ax1 = xgb.plot_importance(gbdt, ax=ax1,importance_type='weight')
ax2 = plt.subplot(122)
ax2 = xgb.plot_importance(gbdt, ax=ax2,importance_type='gain')


######################################
# xgboost による回帰②
model_A = xgb.XGBRegressor(feature_names=boston['feature_names'])
model_A.fit(X_train, y_train)
get_model_evaluations(X_train,y_train,X_test,y_test,model_A)


y_train_pred = model_A.predict(X_train)
y_test_pred = model_A.predict(X_test)

from sklearn.metrics import r2_score
from sklearn.metrics import mean_squared_error

print('MSE   train: %.3f,  test: %.3f' % (
        mean_squared_error(y_train, y_train_pred),
        mean_squared_error(y_test, y_test_pred)))
print('RMSE  train: %.3f,  test: %.3f' %(
        np.sqrt(mean_squared_error(y_train, y_train_pred)),
        np.sqrt(mean_squared_error(y_test, y_test_pred))))
print('R^2   train: %.3f,  test: %.3f' % (
        r2_score(y_train, y_train_pred),
        r2_score(y_test, y_test_pred)))



# feature importance のプロット
import pandas as pd
import matplotlib.pyplot as plt
importances = pd.Series(model_A.feature_importances_, index = boston.feature_names)
importances = importances.sort_values()
importances.plot(kind = 'barh')
plt.title('XGBRegressorの重要度')
plt.show()


fig = plt.figure(figsize=(13, 5))
ax1 = plt.subplot(121)
ax1 = xgb.plot_importance(model_A, ax=ax1,importance_type='weight')
ax2 = plt.subplot(122)
ax2 = xgb.plot_importance(model_A, ax=ax2,importance_type='gain')

import shap
shap.initjs()
explainerA = shap.TreeExplainer(model = model_A)
shap_valuesA = explainerA.shap_values(X = X_train)

とくに、単調整制約という概念を学んだ。

entre-temps.hatenablog.com

決定木で別れるノードの重みを、親兄弟の平均を境界として決める事で木全体の単調整を担保できる模様。Xgboostだと、parameterで指定可能というのもそそる仕様だ。

# 単調性制約を課した XGBoost で回帰モデルを構築します.
print(X_train.shape)
model_B = xgb.XGBRegressor(monotone_constraints=(1,1,1,1,1,1,1,1,1,1,1,1,1))
#model_B = xgb.XGBRegressor(monotone_constraints=(0,0,0,0,0,0,0,0,0,0,0,0,0))
model_B.fit(X_train, y_train)
get_model_evaluations(X_train,y_train,X_test,y_test,model_B)


shap.initjs()
explainerB = shap.TreeExplainer(model = model_B)
shap_valuesB = explainerB.shap_values(X = X_train)
shap.summary_plot(shap_valuesB, X_train, plot_type = "bar")



結果に対する解釈については、もう少し時間を取ってゆっくり考えることとしたい。一旦。