Introduction
모델들의 다양한 특성에 따라서 독립 변수들 (input x값)의 영향도
그리고 왜 이런 결정이 이루어 졌는지를 모델에 따라서 다양한 방법으로 분석을 할 수 있습니다.
본문에서는 Decision Tree가 어떻게 의사결정을 하여서 y값을 유추해냈고, 그 영향도를 분석하고자 합니다.
Titanic Survival Dataset
먼저 데이터를 다운받고, raw data 전처리를 합니다.
def get_raw_dataset ():
# Preprocessing Raw Dataset
train_df = pd . read_csv ( 'train.csv' )
# train_df.dropna(inplace=True)
columns = [ 'Sex' , 'Age' , 'SibSp' , 'Parch' , 'Fare' , 'Embarked' ]
y_train = np . array ( train_df [ 'Survived' ])
x_train = train_df [ columns ]
# Convert Datatype
# x_train['Sex'].astype('category', inplace=True)
# x_train['Embarked'].astype('category', inplace=True)
return x_train , y_train
x_train , y_train = get_raw_dataset ()
# Missing Value
print ( 'Missing Values' )
display ( x_train . isna (). sum ())
print ( 'shape:' , x_train . shape )
x_train . head ()
Decision Tree Model
Preprocessing
Sex
Age
Embarked
Missing value: mode 사용 (가장 빈도수가 많은 값)
from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import LabelEncoder
x_train , y_train = get_raw_dataset ()
# Missing Values
x_train [ 'Age' ]. loc [ x_train [ 'Age' ]. isna ()] = x_train [ 'Age' ]. mean ()
x_train [ 'Embarked' ]. loc [ x_train [ 'Embarked' ] == None ] = x_train [ 'Embarked' ]. mode ()
# Encoding
label_encoder = LabelEncoder ()
x_train = pd . get_dummies ( x_train , columns = [ 'Embarked' ])
x_train [ 'Sex' ] = label_encoder . fit_transform ( x_train [ 'Sex' ])
Model & Training
clf = DecisionTreeClassifier ( max_depth = None )
clf = clf . fit ( x_train , y_train )
# Validation on Train data
y_pred = clf . predict ( x_train )
print ( '[Classification Report]' )
print ( classification_report ( y_train , y_pred ))
Tree Analysis
Visualization
from io import StringIO
from sklearn.tree import export_graphviz , plot_tree
f = StringIO ()
export_graphviz ( clf , out_file = f ,
feature_names = x_train . columns . tolist ())
graph = pydot . graph_from_dot_data ( f . getvalue ())[ 0 ]
graph . write_png ( 'titanic_decision_tree.png' )
Image ( filename = 'titanic_decision_tree.png' )
Decision Path
어떤 데이터가 모델의 input으로 들어왔을때, 어떤 의사결정으로 tree를 traversal했는지 알기 위해서는 다음과 같이 합니다.
1은 traversal 한 노드이고, 0은 지나가지 않은 자리 입니다.
x_sample = x_train . sample ( 1 )
dp = clf . decision_path ( x_sample ). todense ()
dp
사용한 Features 얻는 방법
트리 구조 안에서의 features 정보를 얻는 방법은 다음과 같이 합니다.
from sklearn.tree._tree import TREE_UNDEFINED
columns = x_train . columns
features = np . array ( columns [ clf . tree_ . feature ])
features [ clf . tree_ . feature == TREE_UNDEFINED ] = None
features
dp 그리고 features 값을 사용해서 어떤 features를 사용했는지는 다음의 코드로 알 수 있습니다.
마지막의 None값은 leaf node이기 때문에 None값이 나옵니다.
해석을 다양하게 할 수 있습니다.
앞쪽에 있을수록 종속 변수 y값에 가장 크게 영향을 미치는 feature
많이 사용된 features일수록 tree구조 안에서 y값에 영향을 주는 정도가 커진다
2개중에서 1번의 중요도가 더 중요합니다.
Feature Importance
Feature Importance를 봐도 어떤것을 중요하게 봐야 할지 알 수 있습니다.
columns = x_train . columns
iters = zip ( clf . feature_importances_ , columns [ np . argsort ( clf . feature_importances_ )[:: - 1 ]])
df = pd . DataFrame ([[ c , sum ( features == c ), fi ] for fi , c in iters ],
columns = [ 'feature' , 'n_use' , 'importance' ])
df . sort_values ( 'importance' , ascending = False , inplace = True )
df
Cause Analysis by Decision Path
아래의 코드를 사용해서, 특정 데이터의 sample에 대해서 어떻게 의사 결정을 내렸는지의 tree traversal 정보를 알 수 있습니다.
from sklearn.tree._tree import TREE_UNDEFINED
from pprint import pprint
def get_decision_path ( data , columns ):
features = np . array ( columns [ clf . tree_ . feature ])
features [ clf . tree_ . feature == TREE_UNDEFINED ] = None
dp = clf . decision_path ( data )
n = dp . shape [ 0 ]
response = []
for i in range ( n ):
node_indices = dp . indices [ dp . indptr [ i ]: dp . indptr [ i + 1 ]]
node_features = features [ node_indices ][: - 1 ]
node_thresholds = clf . tree_ . threshold [ node_indices ][: - 1 ]. round ( 2 )
causes = list ( zip ( node_features , node_thresholds ))
causes = [[ f , t , data [ f ]. values [ 0 ] <= t ] for f , t in causes ]
response . append ( causes )
return response
columns = np . array ( x_train . columns )
x_sample = x_train . sample ( 1 )
y_sample = y_train [ x_sample . index ]
decision_pathes = get_decision_path ( x_sample , columns )
display ( list ( np . where ( y_sample , '살았음' , '죽었음' )))
pprint ( decision_pathes )
위의 내용만 갖고서는 사실 요점을 잘 이해하기 어려울수 있습니다.
아래의 코드는 threshold의 기준점을 잘라서 보여주는 코드입니다.
def _agg_decision_path ( df ):
df1 = df [ df [ 'is_lower' ]]. groupby ( 'feature' ). min ()
df2 = df [ ~ df [ 'is_lower' ]]. groupby ( 'feature' ). max ()
df3 = df . groupby ( 'feature' ). count ()[[ 'priority' ]]
df3 = df2 . join ( df1 , how = 'outer' , rsuffix = '_b' ). join (
df3 , how = 'outer' , rsuffix = '_c' )
df3 [ 'priority' ] = np . nanmin (
df3 [[ 'priority' , 'priority_b' , 'priority_c' ]]. values [ 0 ])
df3 [ 'value' ] = np . nanmin ( df3 [[ 'value' , 'value_b' ]]. values [ 0 ])
df3 = df3 [[ 'priority' , 'priority_c' , 'threshold' , 'value' , 'threshold_b' ]]
df3 . columns = [ 'priority' , 'count' ,
'threshold_less' , 'value' , 'threshold_more' ]
df3 . reset_index ( drop = True , inplace = True )
return df3
def summary_decision_path ( decision_pathes , x_data , y_data ):
response = []
# Set Feature Importances
arg_feature_importances = np . argsort ( clf . feature_importances_ )[:: - 1 ]
f_impt = pd . DataFrame ({ 'feature' : np . array ( columns [ arg_feature_importances ]),
'importance' : clf . feature_importances_ [ arg_feature_importances ]})
f_impt . set_index ( 'feature' , inplace = True )
for i , dp in enumerate ( decision_pathes ):
# Preprocess X data
x_sample = x_data . iloc [ i : i + 1 ]. T
x_sample . reset_index ( inplace = True )
idx = x_sample . columns [ 1 ]
x_sample . columns = [ 'feature' , 'value' ]
# Preprocess Decision Path
df = pd . DataFrame ( dp , columns = [ 'feature' , 'threshold' , 'is_lower' ])
df = df . merge ( x_sample , left_on = 'feature' , right_on = 'feature' )
df . reset_index ( inplace = True )
df . rename ({ 'index' : 'priority' }, axis = 1 , inplace = True )
df = df . groupby ( 'feature' ). apply ( _agg_decision_path )
df . index = df . index . get_level_values ( 0 )
df = df . join ( f_impt , how = 'left' )
df = df . sort_values ( 'importance' , ascending = False )
df . insert ( 0 , 'idx' , idx )
if y_data is not None :
df . insert ( 1 , 'y_true' , y_data [ i ])
response . append ( df )
return pd . concat ( response )
summary_decision_path ( decision_pathes , x_sample , y_sample )
위의 내용으로 설명을 하면 다음과 같습니다.
일단 해당 데이터의 사람 (이하 A군 이라 하겠음)은 죽었음
A군은 남자였으며, 생존에서 가장 중요한 요소가 성별
운임요금 (Fare)는 7.8달러로 비교적 싼 가격이었으며 7.75달러 이상 7.8달러 이하 구간 에 속함
나이는 괘 젊은편이며, 21.5세 이상 그리고 26.5세 구간 으로 판별됨
형제자매/배우자 (SibSp) 는 전혀 없었으며 0.5 이하 가 구간으로 판별됨
C 항구에서 승선하지 않았음
대략 이런식으로 어떤 중요도와 구간 정도를 파악해 볼 수 있습니다.
What IF Analysis
what if 툴이 있고, 여기서 영감을 받아서 what if analysis라고 이름을 지었습니다.
어떤 특정 변수를 변화 시켰을때의 확률 값을 계산하는 방법으로,
어떤 값을 변화시켰을때 정말로 큰 요인이 되는지 알아보는 방법입니다.
예를 들어서 성별이 가장 큰 요소 인데, 이것을 변경했을때 예측값이 어떻게 변화되는지 볼 수 있습니다.
실제 남자의 경우 577명중 109명이 생존(18%)했고, 여자의 경우 314명중에서 233명이 생존(74%)했습니다.
만약 죽은 남자들이 여자였다면, 또는 죽은 여자들중에 남자였다면 그 결과가 어떻게 변화 될까요?
일단 사고 희생자중의 성별은 다음과 같습니다.
df = x_train . copy ()
df [ 'y' ] = y_train
df = df [ df [ 'y' ] == 0 ]
y_pred1 = clf . predict ( df . drop ( 'y' , axis = 1 ))
df [ 'Sex' ] = 1 - df [ 'Sex' ]
y_pred2 = clf . predict ( df . drop ( 'y' , axis = 1 ))
df2 = pd . DataFrame ({ 'Sex' : 1 - df [ 'Sex' ], 'before' : y_pred1 , 'after' : y_pred2 })
df2 . groupby ( 'Sex' ). agg ([ 'count' , np . mean , np . sum ])
위의 코드를 보면 변화된 부분을 알 수 있습니다.
여자 -> 남자
여자 81명중 39명이 생존자라고 예측했으나 -> 남자로 변수를 변경시킨 이후 8명으로 줄었습니다.
즉 모두 같은 조건에서 성별이 바뀌었다면, -79% 정도 생존확률이 감소하게 됩니다.
남자 -> 여자
남자 468명중 생존자는 2명으로 예측했으나 -> 여자로 변수를 변경시킨 이후 343명으로 생존자가 급격하게 늘었습니다.
즉 모두 같은 조건에서 성별이 바뀌었다면 약 170%정도 생존확률이 늘어나게 됩니다.