[Python] 선형회귀모형, 로지스틱 회귀모형에 대한 각 관측치 별 변수별 기여도(민감도) 분석 (Sensitivity analysis of linear regression & Logistic regression per each variables and each observations)
Python 분석과 프로그래밍/Python 통계분석 2020. 1. 12. 20:05이번 포스팅에서는 선형회귀모형에 대한 각 관측치별 변수별 기여도 분석 (each variable contribution per each observations)에 대해서 소개하겠습니다.
변수 중요도 (variable importance, feature importance)가 전체 관측치를 사용해 적합한 모델 단위의 변수별 (상대적) 중요도를 나타내는 것이라면, 이번 포스팅에서 소개하려는 관측치별 변수별 기여도(민감도)는 개별 관측치 단위에서 한개의 칼럼이 모델 예측치에 얼마나 기여를 하는지를 분석해보는 것입니다.
선형회귀 모형을 예로 들면, 변수 중요도는 회귀모형의 회귀 계수라고 할 수 있겠구요, 관측치별 변수별 기여도는 특정 관측치의 특정 칼럼값만 그대로 사용하고 나머지 변수값에는 '0'을 대입해서 나온 예측치라고 할 수 있겠습니다. 변수 중요도가 Global 하게 적용되는 model weights 라고 한다면, 관측치별 변수별 기여도는 specific variable's weight * value 라고 할 수 있겠습니다.
변수 중요도를 분석했으면 됐지, 왜 관측치별 변수별 기여도 분석을 할까 궁금할 수도 있을 텐데요, 관측치별 변수별 기여도 분석은 특정 관측치별로 개별적으로 어떤 변수가 예측치에 크게 영향을 미쳤는지 파악하기 위해서 사용합니다. (동어반복인가요? ^^;)
모델의 변수별 가중치(high, low)와 각 관측치별 변수별 값(high, low)의 조합별로 아래와 같이 관측치별 변수별 예측치에 대한 기여도가 달라지게 됩니다. 가령, 관측치별 변수별 예측치에 대한 기여도가 높으려면 모델의 가중치(중요도)도 높아야 하고 동시에 개별 관측치의 해당 변수의 관측값도 역시 높아야 합니다.
관측치별 변수별 기여도 분석은 복잡하고 어려운 이론에 기반을 둔 분석이라기 보다는 단순한 산수를 반복 연산 프로그래밍으로 계산하는 것이므로 아래의 예를 따라가다 보면 금방 이해할 수 있을 것이라고 생각합니다.
예를 들어보기 위해 공개된 전복(abalone) 데이터를 사용하여 전체 무게(whole weight)를 예측하는 선형회귀모형을 적합하고, 관측치별로 각 변수별 예측치에 기여한 정도를 분석해보겠습니다.
1. 선형회귀모형에 대한 관측치별 변수별 기여도(민감도) 분석 (Sensitivity analysis for linear regression model) |
(1) abalone dataset 을 pandas DataFrame으로 불러오기
import numpy as np import pandas as pd url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data' abalone = pd.read_csv(url , sep=',' , header=None , names=['sex', 'length', 'diameter', 'height', 'whole_weight', 'shucked_weight', 'viscera_weight', 'shell_weight', 'rings'] , index_col=None) abalone.head() [Out]:
|
(2) X, y 변수 데이터셋 생성 (creating X and y dataset)
예측하려는 목표변수 y 로는 '전체 무게(whole weight)' 변수를 사용하겠으며, 설명변수 X 에는 'sex', 'length', 'diameter', 'height', 'shell_weight', 'rings' 의 6개 변수를 사용하겠습니다. 이중에서 'sex' 변수는 범주형 변수이므로 가변수(dummy variable)로 변환하였습니다.
# transformation of categorical variable to dummy variable abalone['sex'].unique()
abalone['sex_m'] = np.where(abalone['sex'] == 'M', 1, 0) abalone['sex_f'] = np.where(abalone['sex'] == 'F', 1, 0) # get X variables X = abalone[["sex_m", "sex_f", "length", "diameter", "height", "shell_weight", "rings"]] import statsmodels.api as sm X = sm.add_constant(X) # add a constant to model print(X) [Out]: Index(['const', 'sex_m', 'sex_f', 'length', 'diameter', 'height',
'shell_weight', 'rings'],
dtype='object')
const sex_m sex_f length diameter height shell_weight rings
0 1.0 1 0 0.455 0.365 0.095 0.1500 15
1 1.0 1 0 0.350 0.265 0.090 0.0700 7
2 1.0 0 1 0.530 0.420 0.135 0.2100 9
3 1.0 1 0 0.440 0.365 0.125 0.1550 10
4 1.0 0 0 0.330 0.255 0.080 0.0550 7
... ... ... ... ... ... ... ... ...
4172 1.0 0 1 0.565 0.450 0.165 0.2490 11
4173 1.0 1 0 0.590 0.440 0.135 0.2605 10
4174 1.0 1 0 0.600 0.475 0.205 0.3080 9
4175 1.0 0 1 0.625 0.485 0.150 0.2960 10
4176 1.0 1 0 0.710 0.555 0.195 0.4950 12
[4177 rows x 8 columns] # get y value y = abalone["whole_weight"]
|
Train:Test = 8:2 의 비율로 데이터셋을 분할 (Split train and test set)하겠습니다.
# train, test set split from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=2004) print('Shape of X_train:', X_train.shape) print('Shape of X_test:', X_test.shape) print('Shape of y_train:', y_train.shape) print('Shape of y_test:', y_test.shape)
|
(3) 선형회귀모형 적합 (fitting linear regression model)
이번 포스팅이 선형회귀모형에 대해서 설명하는 것이 목적이 아니기 때문에 회귀모형을 적합하는데 필요한 가정사항 진단 및 조치는 생략하고, 그냥 statsmodels.api 라이브러리를 이용해서 회귀모델을 적합해서 바로 민감도 분석(Sensitivity analysis)으로 넘어가겠습니다.
이번 포스팅의 주제인 '관측치별 변수별 기여도 분석'에서 사용하는 모델은 어떤 알고리즘도 전부 가능하므로 개별 관측치별 변수별 영향력을 해석을 하는데 유용하게 사용할 수 있습니다. (가령, 블랙박스 모형인 Random Forest, Deep Learning 등도 가능)
# multivariate linear regression model import statsmodels.api as sm lin_reg = sm.OLS(y_train, X_train).fit() lin_reg.summary()
# prediction predicted = lin_reg.predict(X_test) actual = y_test act_pred_df = pd.DataFrame({'actual': actual , 'predicted': predicted , 'error': actual - predicted}) act_pred_df.head() [Out]:
# Scatter Plot: Actual vs. Predicted import matplotlib.pyplot as plt plt.plot(act_pred_df['actual'], act_pred_df['predicted'], 'o') plt.xlabel('actual', fontsize=14) plt.ylabel('predicted', fontsize=14) plt.show() # RMSE (Root Mean Squared Error) from sklearn.metrics import mean_squared_error from math import sqrt rmse = sqrt(mean_squared_error(actual, predicted)) rmse
|
(4) 관측치별 변수별 예측모델 결과에 대한 기여도 분석 (contribution per each variables from specific observation)
아래 예에서는 전체 836개의 test set 관측치 중에서 '1번 관측치 (1st observation)' 의 8개 변수별 기여도를 분석해보겠습니다.
X_test.shape [Out]: (836, 8) # get 1st observation's value as an example X_i = X_test.iloc[0, :] X_i
|
관측치별 변수별 기여도(민감도, variable's contribution & sensitivity) 분석의 핵심 개념은 전체 데이터를 사용해서 적합된 모델에 특정 관측치의 변수별 값을 변수별로 순서대로 돌아가면서 해당 변수 측정값은 그대로 사용하고 나머지 변수들의 값은 '0'으로 대체해서 예측을 해보는 것입니다. 아래 코드의 for i, j in enumerate(X_i): X_mat[i, i] = j 부분을 유심히 살펴보시기 바랍니다.
# all zeros matrix with shape of [col_num, col_num] X_mat = np.zeros(shape=[X_i.shape[0], X_i.shape[0]]) X_mat [Out]: array([[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.]]) # fill only 1 variable's value and leave '0' for the others for i, j in enumerate(X_i): X_mat[i, i] = j X_mat [Out]:
|
바로 위에서 만든 X_mat 행렬 (1번 관측치의 각 변수별로 하나씩 실제 값을 사용하고, 나머지는 '0'으로 대체)을 사용해서 (3)에서 적합한 선형회귀모형으로 y값을 예측(lin_reg.predict(X_mat))을 하면 우리가 원하는 '1번 관측치의 개별 변수별 y값 예측에 대한 기여도'를 구할 수 있습니다.
개별 변수별 y 예측값에 대한 기여도를 기준으로 내림차순 정렬해서 DataFrame을 만들고, 가로로 누운 막대그래프도 그려보았습니다.
# sensitivity analysis sensitivity_df = pd.DataFrame({'x': X_test.iloc[0, :] , 'contribution_x': lin_reg.predict(X_mat)}).\ sort_values(by='contribution_x', ascending=False) sensitivity_df [Out]:
# horizontal bar plot by column's contribution sensitivity_df['contribution_x'].plot(kind='barh', figsize=(10, 5)) plt.title('Sensitivity Analysis', fontsize=16) plt.xlabel('Contribution', fontsize=16) plt.ylabel('Variable', fontsize=16) plt.yticks(fontsize=14) plt.show() | |||||||||||||||||||||||||||
물론, 당연하게 관측치별 변수별 기여도 (민감도) 분석에서 나온 결과를 전부 다 합치면 애초의 모형에 해당 관측치의 전체 변수별 값을 넣어서 예측한 값과 동일한 결과가 나옵니다.
# result from sum of contribution analysis sum(sensitivity_df['contribution_x'])
# result from linear regression model's prediction lin_reg.predict(X_test.iloc[0, :].to_numpy())
|
(5) 관측치별 변수별 예측 기여도 (민감도) 분석을 위한 사용자 정의 함수
관측치별 변수별 기여도 분석 결과를 --> pandas DataFrame으로 저장하고, --> 기여도별로 정렬된 값을 기준으로 옆으로 누운 막대그래프를 그려주는 사용자 정의함수를 만들어보겠습니다.
# UDF for contribution(sensitivity) analysis per each variables def sensitivity_analysis(model, X, idx, bar_plot_yn):
import numpy as np import pandas as pd import matplotlib.pyplot as plt import statsmodels.api as sm pd.options.mode.chained_assignment = None
# get one object's X values X_i = X.iloc[idx, :]
# make a matrix with zeros with shape of [num_cols, num_cols] X_mat = np.zeros(shape=[X_i.shape[0], X_i.shape[0]])
# fil X_mat with values from one by one columns, leaving the ohters zeros for i, j in enumerate(X_i): X_mat[i, i] = j
# data frame with contribution of each X columns in descending order sensitivity_df = pd.DataFrame({'idx': idx , 'x': X_i , 'contribution_x': model.predict(X_mat)}).\ sort_values(by='contribution_x', ascending=True)
# if bar_plot_yn == True then display it col_n = X_i.shape[0] if bar_plot_yn == True: sensitivity_df['contribution_x'].plot(kind='barh', figsize=(10, 0.7*col_n)) plt.title('Sensitivity Analysis', fontsize=18) plt.xlabel('Contribution', fontsize=16) plt.ylabel('Variable', fontsize=16) plt.yticks(fontsize=14) plt.show()
return sensitivity_df # check UDF sensitivity_df = sensitivity_analysis(model=lin_reg, X=X_test, idx=0, bar_plot_yn=True) sensitivity_df |
아래는 위에서 정의한 sensitivity_analysis() 사용자 정의 함수에서 bar_plot_yn=False 로 설정을 해서 옆으로 누운 막대그래프는 그리지 말고 기여도 분석 결과 DataFrame만 반환하라고 한 경우입니다.
# without bar plot (bar_plot_yn=False) sensitivity_df = sensitivity_analysis(model=lin_reg, X=X_test, idx=0, bar_plot_yn=False) sensitivity_df [Out]:
| ||||||||||||||||||||||||||||||||||||
(6) 다수의 관측치에 대해 '개별 관측치별 변수별 예측 기여도 분석'
위에서 부터 10개의 관측치를 선별해서, 개별 관측치별 각 변수별로 위의 (5)번에서 정의한 sensitivity_analysis() 사용자정의함수와 for loop 반복문을 사용해서 변수 기여도를 분석해보겠습니다. (단, 변수 기여도 막대 그래프는 생략)
# calculate sensitivity of each columns of the first 10 objects using for loop # blank DataFrame to save the sensitivity results together sensitivity_df_all = pd.DataFrame() to_idx = 10 for idx in range(0, to_idx): sensitivity_df_idx = sensitivity_analysis(model=lin_reg , X=X_test , idx=idx , bar_plot_yn=False)
sensitivity_df_all = pd.concat([sensitivity_df_all, sensitivity_df_idx], axis=0)
print("[STATUS]", idx+1, "/", to_idx, "(", 100*(idx+1)/to_idx, "%) is completed...") [STATUS] 1 / 10 ( 10.0 %) is completed...
[STATUS] 2 / 10 ( 20.0 %) is completed...
[STATUS] 3 / 10 ( 30.0 %) is completed...
[STATUS] 4 / 10 ( 40.0 %) is completed...
[STATUS] 5 / 10 ( 50.0 %) is completed...
[STATUS] 6 / 10 ( 60.0 %) is completed...
[STATUS] 7 / 10 ( 70.0 %) is completed...
[STATUS] 8 / 10 ( 80.0 %) is completed...
[STATUS] 9 / 10 ( 90.0 %) is completed...
[STATUS] 10 / 10 ( 100.0 %) is completed...
|
결과를 한번 확인해볼까요?
sensitivity_df_all[:20] [Out]:
|
2. 로지스틱 회귀모형에 대한 관측치별 변수별 민감도 분석 (Sensitivity analysis for Logistic Regression model) |
제가 서두에서 민감도분석에 어떤 모델도 가능하도고 말씀드렸습니다. 그러면 이번에는 같은 abalone 데이터셋에 대해 목표변수로 전체무게(whole weight)가 평균보다 크면 '1', 평균보다 작으면 '0' 으로 범주를 구분한 후에, 이를 이진 분류(binary classification)할 수 있는 로지스틱 회귀모형(Logistic Regression)을 적합한 후에 --> 민감도 분석을 적용해보겠습니다.
먼저, y_cat 이라는 범주형 변수를 만들고, train/ test set으로 분할을 한 후에, 로지스틱 회귀모형(Logistic Regression)을 적합후, 이진분류를 할 수 있는 확률을 계산(확률이 0.5보다 크면 '1'로 분류)해보겠습니다.
# make a y_category variable: if y is greater or equal to mean, then 1 y_cat = np.where(abalone["whole_weight"] >= np.mean(abalone["whole_weight"]), 1, 0) y_cat[:20]
cat_class, counts = np.unique(y_cat, return_counts=True) dict(zip(cat_class, counts)) [Out]: {0: 2178, 1: 1999} # train, test set split from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y_cat, test_size=0.2, random_state=2004) # fitting logistic regression import statsmodels.api as sm pd.options.mode.chained_assignment = None logitreg = sm.Logit(y_train, X_train) logitreg_fit = logitreg.fit() print(logitreg_fit.summary()) [Out]:
# prediction test_prob_logitreg = logitreg_fit.predict(X_test) test_prob_logitreg.head() [Out]: 3473 9.341757e-12
3523 2.440305e-10
1862 1.147617e-02
2966 9.999843e-01
659 9.964952e-01
dtype: float64 |
다음으로 위의 선형회귀모형에 대한 민감도 분석 사용자정의 함수를 재활용하여 로지스틱 회귀모형에도 사용가능하도록 일부 수정해보았습니다.
아래의 사용자 정의 함수는 로지스틱 회귀모형 적합 시 statsmodels 라이브러리를 사용한 것으로 가정하고 작성하였습니다. 만약 로지스틱 회귀모형의 model 적합에 from sklearn.linear_model import LogisticRegression 의 라이브러리를 사용하였다면 '#'으로 비활성화해놓은 부분을 해제하여 사용하면 됩니다. (predict_proba(X_mat)[:, 1] 의 부분이 달라서 그렇습니다)
# UDF for contribution(sensitivity) analysis per each variables # task: "LinearReg" or "LogitReg" def sensitivity_analysis_LinearReg_LogitReg(task, model, X, idx, bar_plot_yn):
import numpy as np import pandas as pd import matplotlib.pyplot as plt import statsmodels.api as sm pd.options.mode.chained_assignment = None
# get one object's X values X_i = X.iloc[idx, :]
# make a matrix with zeros with shape of [num_cols, num_cols] X_mat = np.zeros(shape=[X_i.shape[0], X_i.shape[0]])
# fil X_mat with values from one by one columns, leaving the ohters zeros for i, j in enumerate(X_i): X_mat[i, i] = j
# data frame with contribution of each X columns in descending order sensitivity_df = pd.DataFrame({ 'idx': idx , 'task': task , 'x': X_i , 'contribution_x': model.predict(X_mat) })
# # ==== Remark ===== # # if you used LogisticRegressionsklearn from sklearn.linear_model # # then use codes below # if task == "LinearReg": # sensitivity_df = pd.DataFrame({ # 'idx': idx # , 'task': task # , 'x': X_i # , 'contribution_x': model.predict(X_mat) # })
# elif task == "LogitReg": # sensitivity_df = pd.DataFrame({ # 'idx': idx # , 'task': task # , 'x': X_i # , 'contribution_x': model.predict_proba(X_mat)[:,1] # }) # else: # print('Please choose task one of "LinearReg" or "LogitReg"...')
sensitivity_df = sensitivity_df.sort_values(by='contribution_x', ascending=True)
# if bar_plot_yn == True then display it col_n = X_i.shape[0] if bar_plot_yn == True: sensitivity_df['contribution_x'].plot(kind='barh', figsize=(10, 0.7*col_n)) plt.title('Sensitivity Analysis', fontsize=18) plt.xlabel('Contribution', fontsize=16) plt.ylabel('Variable', fontsize=16) plt.yticks(fontsize=14) plt.show()
return sensitivity_df.sort_values(by='contribution_x', ascending=False) # apply sensitivity analysis function on 1st observation for Logistic Regression sensitivity_analysis_LinearReg_LogitReg(task="LogitReg" , model=logitreg_fit , X=X_test , idx=0 , bar_plot_yn=True) |
많은 도움이 되었기를 바랍니다.
이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)