Posts IBM HR Data로 해보는 퇴사자 예측
Post
Cancel

IBM HR Data로 해보는 퇴사자 예측

Predicting Employee Attrition


순서

  1. 패키지 import
  2. 데이터 설명 및 전처리
  3. EDA
  4. 가설확인
  5. Feature Engineering
  6. 예측을 위한 데이터 처리
  7. 머신러닝 알고리즘을 이용한 퇴사자 예측
  8. 이후의 방향

0. 패키지 import


0.1 필요한 패키지 import

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from collections import OrderedDict

# Data preprocessing
from sklearn.preprocessing import OneHotEncoder, StandardScaler, LabelEncoder, RobustScaler
from sklearn.model_selection import train_test_split, GridSearchCV, learning_curve
from sklearn.metrics import accuracy_score, roc_curve, roc_auc_score, recall_score, f1_score, confusion_matrix, precision_score
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from lightgbm import LGBMClassifier


# 모델 import
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression

import xgboost as xgb
import statsmodels.formula.api as smf
import statsmodels.api as sm
import graphviz
import pydotplus

# EDA package
import pandas as pd
import numpy as np
import missingno
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
import matplotlib as mpl


# warnings 끄기
warnings.filterwarnings('ignore')

# pandas display option view row & columns
pd.set_option('display.max_row', 500)
pd.set_option('display.max_columns', 100)
pd.set_option('display.max_colwidth', 1000)

# # matplotlib set
plt.rc('font', family='DejaVu Sans')  # For MacOS
plt.rc('axes', unicode_minus=False)

%matplotlib inline


1. 데이터 설명 및 전처리


1.1 데이터 로드 및 체크

1
2
data = pd.read_csv('WA_Fn-UseC_-HR-Employee-Attrition.csv')
data.tail()
AgeAttritionBusinessTravelDailyRateDepartmentDistanceFromHomeEducationEducationFieldEmployeeCountEmployeeNumberEnvironmentSatisfactionGenderHourlyRateJobInvolvementJobLevelJobRoleJobSatisfactionMaritalStatusMonthlyIncomeMonthlyRateNumCompaniesWorkedOver18OverTimePercentSalaryHikePerformanceRatingRelationshipSatisfactionStandardHoursStockOptionLevelTotalWorkingYearsTrainingTimesLastYearWorkLifeBalanceYearsAtCompanyYearsInCurrentRoleYearsSinceLastPromotionYearsWithCurrManager
146536NoTravel_Frequently884Research & Development232Medical120613Male4142Laboratory Technician4Married2571122904YNo173380117335203
146639NoTravel_Rarely613Research & Development61Medical120624Male4223Healthcare Representative1Married9991214574YNo15318019537717
146727NoTravel_Rarely155Research & Development43Life Sciences120642Male8742Manufacturing Director2Married614251741YYes20428016036203
146849NoTravel_Frequently1023Sales23Medical120654Male6322Sales Executive2Married5390132432YNo143480017329608
146934NoTravel_Rarely628Research & Development83Medical120682Male8242Laboratory Technician3Married4404102282YNo12318006344312
  1. Age : 해당 직원의 나이
  2. Attrition : 퇴직 여부 Target값 (종속변수)
  3. BusinessTravel : 출장의 빈도
  4. DailyRate : 일 대비 급여의 수준
  5. Department : 업무분야
  6. DistanceFromHome : 집과의 거리
  7. Education : 교육의 정도
    • 1 : ‘Below College’ : 대학 이하
    • 2 : ‘College’ : 전문대
    • 3 : ‘Bachelor’ : 학사
    • 4 : ‘Master’ : 석사
    • 5 : ‘Doctor’ : 박사
  8. EducationField : 전공
  9. EmployeeCount : 직원 숫자
  10. EmployeeNumber : 직원 ID
  11. EnvironmentSatisfaction : 업무 환경에 대한 만족도
    • 1 : ‘Low’
    • 2 : ‘Medium’
    • 3 : ‘High’
    • 4 : ‘Very High’
  12. Gender : 성별
  13. HourlyRate : 시간 대비 급여의 수준
  14. JobInvolvement : 업무 참여도
    • 1 : ‘Low’
    • 2 : ‘Medium’
    • 3 : ‘High’
    • 4 : ‘Very High’
  15. JobLevel : 업무의 수준
  16. JobRole : 업무 종류
  17. JobSatisfaction : 업무 만족도
    • 1 : ‘Low’
    • 2 : ‘Medium’
    • 3 : ‘High’
    • 4 : ‘Very High’
  18. MaritalStatus : 결혼 여부
  19. MonthlyIncome : 월 소득
  20. MonthlyRate : 월 대비 급여 수준
  21. NumCompaniesWorked : 일한 회사의 수
  22. Over18 : 18세 이상
  23. OverTime : 규정외 노동시간
  24. PercentSalaryHike : 급여의 증가분 백분율
  25. PerformanceRating : 업무 성과
    • 1 : ‘Low’
    • 2 : ‘Good’
    • 3 : ‘Excellent’
    • 4 : ‘Outstanding’
  26. RelationshipSatisfaction : 대인관계 만족도
    • 1 : ‘Low’
    • 2 : ‘Medium’
    • 3 : ‘High’
    • 4 : ‘Very High’
  27. StandardHours : 표준 시간
  28. StockOptionLevel : 스톡옵션 정도
  29. TotalWorkingYears : 경력 기간
  30. TrainingTimesLastYear : 교육 시간
  31. WorkLifeBalance : 일과 생활의 균형 정도
    • 1 : ‘Bad’
    • 2 : ‘Good’
    • 3 : ‘Better’
    • 4 : ‘Best’
  32. YearsAtCompany : 근속 연수
  33. YearsInCurrentRole : 현재 역할의 년수
  34. YearsSinceLastPromotion : 마지막 프로모션
  35. YearsWithCurrManager : 현재 관리자와 함께 보낸 시간
  • 총 독립변수 : 34개, 종속변수 1개 확인 됩니다.


1
data.info()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1470 entries, 0 to 1469
Data columns (total 35 columns):
 #   Column                    Non-Null Count  Dtype 
---  ------                    --------------  ----- 
 0   Age                       1470 non-null   int64 
 1   Attrition                 1470 non-null   object
 2   BusinessTravel            1470 non-null   object
 3   DailyRate                 1470 non-null   int64 
 4   Department                1470 non-null   object
 5   DistanceFromHome          1470 non-null   int64 
 6   Education                 1470 non-null   int64 
 7   EducationField            1470 non-null   object
 8   EmployeeCount             1470 non-null   int64 
 9   EmployeeNumber            1470 non-null   int64 
 10  EnvironmentSatisfaction   1470 non-null   int64 
 11  Gender                    1470 non-null   object
 12  HourlyRate                1470 non-null   int64 
 13  JobInvolvement            1470 non-null   int64 
 14  JobLevel                  1470 non-null   int64 
 15  JobRole                   1470 non-null   object
 16  JobSatisfaction           1470 non-null   int64 
 17  MaritalStatus             1470 non-null   object
 18  MonthlyIncome             1470 non-null   int64 
 19  MonthlyRate               1470 non-null   int64 
 20  NumCompaniesWorked        1470 non-null   int64 
 21  Over18                    1470 non-null   object
 22  OverTime                  1470 non-null   object
 23  PercentSalaryHike         1470 non-null   int64 
 24  PerformanceRating         1470 non-null   int64 
 25  RelationshipSatisfaction  1470 non-null   int64 
 26  StandardHours             1470 non-null   int64 
 27  StockOptionLevel          1470 non-null   int64 
 28  TotalWorkingYears         1470 non-null   int64 
 29  TrainingTimesLastYear     1470 non-null   int64 
 30  WorkLifeBalance           1470 non-null   int64 
 31  YearsAtCompany            1470 non-null   int64 
 32  YearsInCurrentRole        1470 non-null   int64 
 33  YearsSinceLastPromotion   1470 non-null   int64 
 34  YearsWithCurrManager      1470 non-null   int64 
dtypes: int64(26), object(9)
memory usage: 402.1+ KB
  • Education, EnvironmentSatisfaction, JobInvolvement, JobSatisfaction, PerformanceRating, RelationshipSatisfaction, WorkLifeBalance, JobLevel, StockOptionLevel, NumCompaniesWorked
  • 위의 컬럼들이 실제론 Category column인데, int형으로 되어있습니다.
  • int형 컬럼들을 Category 컬럼으로 바꿔주어야 EDA 할때 조금더 편합니다. <int형 26개, category형 9개>


1.2 EDA를 쉽게하기 위해 데이터값을 Object로 변경

1
2
3
4
# Education
change_dict = {1: 'Below College', 2: 'College', 3: 'Bachelor', 4: 'Master', 5: 'Doctor'}
data.replace({'Education': change_dict}, inplace=True)
data['Education'].unique()
1
2
array(['College', 'Below College', 'Master', 'Bachelor', 'Doctor'],
      dtype=object)
1
2
3
4
# EnvironmentSatisfaction
change_dict = {1: 'Low', 2: 'Medium', 3: 'High', 4: 'Very High'}
data.replace({'EnvironmentSatisfaction': change_dict}, inplace=True)
data['EnvironmentSatisfaction'].unique()
1
array(['Medium', 'High', 'Very High', 'Low'], dtype=object)
1
2
3
4
# JobInvolvement
change_dict = {1: 'Low', 2: 'Medium', 3: 'High', 4: 'Very High'}
data.replace({'JobInvolvement': change_dict}, inplace=True)
data['JobInvolvement'].unique()
1
array(['High', 'Medium', 'Very High', 'Low'], dtype=object)
1
2
3
4
# JobSatisfaction
change_dict = {1: 'Low', 2: 'Medium', 3: 'High', 4: 'Very High'}
data.replace({'JobSatisfaction': change_dict}, inplace=True)
data['JobSatisfaction'].unique()
1
array(['Very High', 'Medium', 'High', 'Low'], dtype=object)
1
2
3
4
# PerformanceRating
change_dict = {1: 'Low', 2: 'Good', 3: 'Excellent', 4: 'Outstanding'}
data.replace({'PerformanceRating': change_dict}, inplace=True)
data['PerformanceRating'].unique()
1
array(['Excellent', 'Outstanding'], dtype=object)
1
2
3
4
# RelationshipSatisfaction
change_dict = {1: 'Low', 2: 'Medium', 3: 'High', 4: 'Very High'}
data.replace({'RelationshipSatisfaction': change_dict}, inplace=True)
data['RelationshipSatisfaction'].unique()
1
array(['Low', 'Very High', 'Medium', 'High'], dtype=object)
1
2
3
4
# WorkLifeBalance
change_dict = {1: 'Bad', 2: 'Good', 3: 'Better', 4: 'Best'}
data.replace({'WorkLifeBalance': change_dict}, inplace=True)
data['WorkLifeBalance'].unique()
1
array(['Bad', 'Better', 'Good', 'Best'], dtype=object)
1
2
3
# JobLevel, StockOptionLevel, TrainingTimesLastYear, NumCompaniesWorked, TotalWorkingYears
data = data.astype({'JobLevel': object, 'StockOptionLevel': object, 'NumCompaniesWorked': object})
data
AgeAttritionBusinessTravelDailyRateDepartmentDistanceFromHomeEducationEducationFieldEmployeeCountEmployeeNumberEnvironmentSatisfactionGenderHourlyRateJobInvolvementJobLevelJobRoleJobSatisfactionMaritalStatusMonthlyIncomeMonthlyRateNumCompaniesWorkedOver18OverTimePercentSalaryHikePerformanceRatingRelationshipSatisfactionStandardHoursStockOptionLevelTotalWorkingYearsTrainingTimesLastYearWorkLifeBalanceYearsAtCompanyYearsInCurrentRoleYearsSinceLastPromotionYearsWithCurrManager
041YesTravel_Rarely1102Sales1CollegeLife Sciences11MediumFemale94High2Sales ExecutiveVery HighSingle5993194798YYes11ExcellentLow80080Bad6405
149NoTravel_Frequently279Research & Development8Below CollegeLife Sciences12HighMale61Medium2Research ScientistMediumMarried5130249071YNo23OutstandingVery High801103Better10717
237YesTravel_Rarely1373Research & Development2CollegeOther14Very HighMale92Medium1Laboratory TechnicianHighSingle209023966YYes15ExcellentMedium80073Better0000
333NoTravel_Frequently1392Research & Development3MasterLife Sciences15Very HighFemale56High1Research ScientistHighMarried2909231591YYes11ExcellentHigh80083Better8730
427NoTravel_Rarely591Research & Development2Below CollegeMedical17LowMale40High1Laboratory TechnicianMediumMarried3468166329YNo12ExcellentVery High80163Better2222
............................................................................................................
146536NoTravel_Frequently884Research & Development23CollegeMedical12061HighMale41Very High2Laboratory TechnicianVery HighMarried2571122904YNo17ExcellentHigh801173Better5203
146639NoTravel_Rarely613Research & Development6Below CollegeMedical12062Very HighMale42Medium3Healthcare RepresentativeLowMarried9991214574YNo15ExcellentLow80195Better7717
146727NoTravel_Rarely155Research & Development4BachelorLife Sciences12064MediumMale87Very High2Manufacturing DirectorMediumMarried614251741YYes20OutstandingMedium80160Better6203
146849NoTravel_Frequently1023Sales2BachelorMedical12065Very HighMale63Medium2Sales ExecutiveMediumMarried5390132432YNo14ExcellentVery High800173Good9608
146934NoTravel_Rarely628Research & Development8BachelorMedical12068MediumMale82Very High2Laboratory TechnicianHighMarried4404102282YNo12ExcellentLow80063Best4312

1470 rows × 35 columns

1
data.info()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1470 entries, 0 to 1469
Data columns (total 35 columns):
 #   Column                    Non-Null Count  Dtype 
---  ------                    --------------  ----- 
 0   Age                       1470 non-null   int64 
 1   Attrition                 1470 non-null   object
 2   BusinessTravel            1470 non-null   object
 3   DailyRate                 1470 non-null   int64 
 4   Department                1470 non-null   object
 5   DistanceFromHome          1470 non-null   int64 
 6   Education                 1470 non-null   object
 7   EducationField            1470 non-null   object
 8   EmployeeCount             1470 non-null   int64 
 9   EmployeeNumber            1470 non-null   int64 
 10  EnvironmentSatisfaction   1470 non-null   object
 11  Gender                    1470 non-null   object
 12  HourlyRate                1470 non-null   int64 
 13  JobInvolvement            1470 non-null   object
 14  JobLevel                  1470 non-null   object
 15  JobRole                   1470 non-null   object
 16  JobSatisfaction           1470 non-null   object
 17  MaritalStatus             1470 non-null   object
 18  MonthlyIncome             1470 non-null   int64 
 19  MonthlyRate               1470 non-null   int64 
 20  NumCompaniesWorked        1470 non-null   object
 21  Over18                    1470 non-null   object
 22  OverTime                  1470 non-null   object
 23  PercentSalaryHike         1470 non-null   int64 
 24  PerformanceRating         1470 non-null   object
 25  RelationshipSatisfaction  1470 non-null   object
 26  StandardHours             1470 non-null   int64 
 27  StockOptionLevel          1470 non-null   object
 28  TotalWorkingYears         1470 non-null   int64 
 29  TrainingTimesLastYear     1470 non-null   int64 
 30  WorkLifeBalance           1470 non-null   object
 31  YearsAtCompany            1470 non-null   int64 
 32  YearsInCurrentRole        1470 non-null   int64 
 33  YearsSinceLastPromotion   1470 non-null   int64 
 34  YearsWithCurrManager      1470 non-null   int64 
dtypes: int64(16), object(19)
memory usage: 402.1+ KB
  • EDA 과정을 하기 위해 Category Columns는 object 형식으로 변환함
  • <int형 16개, Category형 19개>


1.3 결측치 확인

1
2
missingno.matrix(data)
plt.show()

  • Missingno 패키지를 통해 Null 데이터가 있는지 시각화 해보았습니다.
  • 총 1470 데이터 중에 Null 데이터는 없는것으로 확인됩니다.
  • 만일 있다면 중간값, 삭제, 평균 값 등으로 채워주거나 혹은 해당 데이터 행 자체를 삭제 해야합니다.
  • 만일 Null data를 임의적으로 0혹은 999와 같이 일괄적인 값으로 채워넣었다면 여기서 확인은 어렵습니다.


2. EDA


2.1 가설 설정 - 누가 퇴사를 할것인가?

  • 일단 퇴사를 할것 같은 사람들 간단한 도메인 지식을 활용하여 가설설정하였음.
  • 가설1 : 집과 회사의 거리가 먼 사람들이 퇴사를 많이 할것이다.
  • 가설2 : 월급여가 낮은 사람이 퇴사를 많이 할것이다.
  • 가설3 : 업무환경이 안좋은 사람이 퇴사를 할것이다.
  • 가설4 : 워라벨이 안좋은 사람들이 퇴사를 할것이다.
  • 가설5 : 근무부서에 따른 퇴사의 비율이 다를것이다. 즉, 특정부서가 퇴사율이 높을것이다.
  • 가설6 : 초기 경력자들이 퇴직을 많이 할것이다.


2.2 Target 확인

1
2
3
print('Attrition 비율')
print(f'{data.Attrition.value_counts().index[0]} : {round(data.Attrition.value_counts()[0] / len(data), 2) * 100}%')
print(f'{data.Attrition.value_counts().index[1]} : {round(data.Attrition.value_counts()[1] / len(data), 2) * 100}%')
1
2
3
Attrition 비율
No : 84.0%
Yes : 16.0%
1
2
3
4
plt.figure(figsize = (12,12))
sns.countplot(x = data['Attrition'])
plt.title('Attrition의 분포')
plt.show()

  • 퇴사자의 분포는 전체 데이터의 약 16%를 차지하는것을 알수 있었습니다.


2.3 전체 컬럼 분포 확인

2.3.1 카테고리형 컬럼

1
2
3
4
5
6
7
8
9
10
11
12
# category column
cate_cols = []
for column in data.columns:
    if data[column].dtype == object:
        cate_cols.append(column)
        print('=============================================================================================')
        print(f'{column} : {data[column].unique()}')
        print(f'{data[column].value_counts()}')
        print()
        
print()
print(f'object column의 갯수 : {len(cate_cols)} 개')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
=============================================================================================
Attrition : ['Yes' 'No']
No     1233
Yes     237
Name: Attrition, dtype: int64

=============================================================================================
BusinessTravel : ['Travel_Rarely' 'Travel_Frequently' 'Non-Travel']
Travel_Rarely        1043
Travel_Frequently     277
Non-Travel            150
Name: BusinessTravel, dtype: int64

=============================================================================================
Department : ['Sales' 'Research & Development' 'Human Resources']
Research & Development    961
Sales                     446
Human Resources            63
Name: Department, dtype: int64

=============================================================================================
Education : ['College' 'Below College' 'Master' 'Bachelor' 'Doctor']
Bachelor         572
Master           398
College          282
Below College    170
Doctor            48
Name: Education, dtype: int64

=============================================================================================
EducationField : ['Life Sciences' 'Other' 'Medical' 'Marketing' 'Technical Degree'
 'Human Resources']
Life Sciences       606
Medical             464
Marketing           159
Technical Degree    132
Other                82
Human Resources      27
Name: EducationField, dtype: int64

=============================================================================================
EnvironmentSatisfaction : ['Medium' 'High' 'Very High' 'Low']
High         453
Very High    446
Medium       287
Low          284
Name: EnvironmentSatisfaction, dtype: int64

=============================================================================================
Gender : ['Female' 'Male']
Male      882
Female    588
Name: Gender, dtype: int64

=============================================================================================
JobInvolvement : ['High' 'Medium' 'Very High' 'Low']
High         868
Medium       375
Very High    144
Low           83
Name: JobInvolvement, dtype: int64

=============================================================================================
JobLevel : [2 1 3 4 5]
1    543
2    534
3    218
4    106
5     69
Name: JobLevel, dtype: int64

=============================================================================================
JobRole : ['Sales Executive' 'Research Scientist' 'Laboratory Technician'
 'Manufacturing Director' 'Healthcare Representative' 'Manager'
 'Sales Representative' 'Research Director' 'Human Resources']
Sales Executive              326
Research Scientist           292
Laboratory Technician        259
Manufacturing Director       145
Healthcare Representative    131
Manager                      102
Sales Representative          83
Research Director             80
Human Resources               52
Name: JobRole, dtype: int64

=============================================================================================
JobSatisfaction : ['Very High' 'Medium' 'High' 'Low']
Very High    459
High         442
Low          289
Medium       280
Name: JobSatisfaction, dtype: int64

=============================================================================================
MaritalStatus : ['Single' 'Married' 'Divorced']
Married     673
Single      470
Divorced    327
Name: MaritalStatus, dtype: int64

=============================================================================================
NumCompaniesWorked : [8 1 6 9 0 4 5 2 7 3]
1    521
0    197
3    159
2    146
4    139
7     74
6     70
5     63
9     52
8     49
Name: NumCompaniesWorked, dtype: int64

=============================================================================================
Over18 : ['Y']
Y    1470
Name: Over18, dtype: int64

=============================================================================================
OverTime : ['Yes' 'No']
No     1054
Yes     416
Name: OverTime, dtype: int64

=============================================================================================
PerformanceRating : ['Excellent' 'Outstanding']
Excellent      1244
Outstanding     226
Name: PerformanceRating, dtype: int64

=============================================================================================
RelationshipSatisfaction : ['Low' 'Very High' 'Medium' 'High']
High         459
Very High    432
Medium       303
Low          276
Name: RelationshipSatisfaction, dtype: int64

=============================================================================================
StockOptionLevel : [0 1 3 2]
0    631
1    596
2    158
3     85
Name: StockOptionLevel, dtype: int64

=============================================================================================
WorkLifeBalance : ['Bad' 'Better' 'Good' 'Best']
Better    893
Good      344
Best      153
Bad        80
Name: WorkLifeBalance, dtype: int64


object column의 갯수 : 19 개
  • 총 19개의 Category형 컬럼의 값들을 확인해 보았습니다.
  • 그중 Over18 컬럼이 Y값 하나만을 가지고 있을것을 알수 있었으며, 그 외 다른 컬럼은 큰 이상이 없어 보입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
# category column 그래프로 보기
fig, ax = plt.subplots(5, 4, figsize=(28, 28), constrained_layout=True)
ax = ax.flatten()
fig.suptitle('Attrition과 Category Column들의 분포', fontsize=16)

for i in range(len(cate_cols)):
    sns.countplot(x=cate_cols[i], data=data,
                      hue='Attrition', ax=ax[i]).set(xlabel = None)
    ax[i].set(title = cate_cols[i])
    
    if data[cate_cols[1]].nunique() >= 3:
        ax[i].tick_params(labelrotation=20)
        
plt.show()

  • 요약 데이터를 그래프로 시각화 해보았습니다.
  • Department(근무부서) : 근무부서에 따라 퇴사 여부가 달라짐이 보입니다. 일단 눈으로 볼땐 HR부서가 가장 적어보이지만, 모수가 적기 때문에 자세히 확인해볼 필요가 있습니다.
  • EnvironmentSatisfaction(근무환경 만족도) : 근무환경 만족도에 따라서 퇴사 여부가 확인될듯 싶었는데, 아래에서 자세히 확인해봐야할듯 싶습니다.
  • JobSatisfaction(직업 만족도) : 직업 만족에 따른 퇴사 여부도 확인해 보아야겠습니다.
  • StockOptionLevel(스톡옵션 레벨) : 스톡옵션이 없거나 낮은 직원이 많이 떠나는것으로 보입니다. 확인해볼 필요가 있어 보입니다.
  • WorkLifeBalance(워라벨의 정도) : 워라벨이 중요한 사람들은 퇴사를 많이하는지 확인이 필요합니다. 만일 그렇다면 해당 회사는 워라벨이 좋지 않은 회사 인듯 합니다.


2.3.2 연속형 컬럼 확인

1
2
3
4
5
6
7
8
9
# continuous column
cont_cols = []
for column in data.columns:
    if data[column].dtype != object:
        cont_cols.append(column)
        print(f'{column} : {data[column].nunique()}')
        print('==============================')
print()
print(f'연속형 column의 갯수 : {len(cont_cols)} 개')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Age : 43
==============================
DailyRate : 886
==============================
DistanceFromHome : 29
==============================
EmployeeCount : 1
==============================
EmployeeNumber : 1470
==============================
HourlyRate : 71
==============================
MonthlyIncome : 1349
==============================
MonthlyRate : 1427
==============================
PercentSalaryHike : 15
==============================
StandardHours : 1
==============================
TotalWorkingYears : 40
==============================
TrainingTimesLastYear : 7
==============================
YearsAtCompany : 37
==============================
YearsInCurrentRole : 19
==============================
YearsSinceLastPromotion : 16
==============================
YearsWithCurrManager : 18
==============================

연속형 column의 갯수 : 16 개
  • int형 컬럼을 요약해보니, EmployeeCount, StandardHours는 값이 1개로 되어 있어서, 삭제가 필요합니다.
  • EmployeeNumber는 값이 1470개로 모든 Row마다 값이 유니크함으로, 삭제가 필요합니다.
  • 눈으로 쉽게 보기 위해 그래프로 그려보겠습니다.


1
2
3
4
5
6
7
8
9
10
fig, ax = plt.subplots(4, 4, figsize=(28, 14), constrained_layout=True)
ax = ax.flatten()
fig.suptitle('Attrition과 continuous Column들의 분포', fontsize=16)

for i in range(len(cont_cols)):
    sns.distplot(data[data['Attrition'] == 'Yes'][cont_cols[i]], color='Red', ax=ax[i], hist = False).set(xlabel = None, ylabel = None)
    sns.distplot(data[data['Attrition'] == 'No'][cont_cols[i]], ax=ax[i],hist = False).set(xlabel = None, ylabel = None)
    ax[i].set(title = cont_cols[i])
    
plt.show()

  • 밀도 그래프를 그려보았습니다. 빨간색은 퇴사한 사람들에 대한 그래프이고 파란색은 반대입니다.
  • 밀도 그래프만으로는 부족해보여 박스그래프도 그려보겠습니다.


1
2
3
4
5
6
7
8
9
10
# boxplot
fig, ax = plt.subplots(4, 4, figsize=(28, 14), constrained_layout=True)
ax = ax.flatten()
fig.suptitle('Boxplot of Attrition', fontsize=16)

for i in range(len(cont_cols)):
    ax[i].set(title = cont_cols[i])
    sns.boxplot(x=data['Attrition'], y = data[cont_cols[i]], ax=ax[i]).set(xlabel=None,  ylabel=None)
    
plt.show()

  • int형 컬럼에 대해 밀도 그래프와 박스 그래프를 그려보니 이상한 컬럼이 눈에 확실히 띕니다.(위에서 이야기했던 3가지 컬럼)
  • Age (나이) : 나이가 어릴때 퇴사를 많이하는것으로 보입니다.
  • MonthlyIncome (월수입) : 월 급여가 적으면 퇴사합니다. 많아도 퇴사를 하는 극단치가 보입니다.
  • DistanceFromHome (집과 회사의 거리) : 집과 거리가 멀면 퇴사를 하는 경향이 보입니다.
  • YearsInCurrentRole (현재 역할의 연수) : 장기간 같은 역할을 할때 그대로 있고, 초창기에 퇴직을 많이 합니다. 이는 승진을 하고 퇴사를 한다는 이야기 같습니다.
  • PercentSalaryHike (연봉 상승률) : 연봉 상승률이 낮은 사람은들은 그렇지 않은 사람들에 비해 더 많이 퇴사하는것으로 봉비니다.
  • YearsWithCurrManager (현재 관리자와 같이 일한 연도) : 관리자와 오래일하면 퇴직하진 않지만 중간에 퇴사하는 사람이 들쭉날쭉합니다. 진짜 관리자 때문에 퇴사를 하는 것일지 궁금합니다.
  • TotalWorkingYears (총 경력) : 경력이 짧을때 퇴사를 많이 합니다. 경력이 많은 사람들이 퇴사를 하는것으로 보이는데, 아마 정년퇴임이 아닐까 싶긴 합니다.


2.4 컬럼 삭제

1
2
3
4
5
6
7
8
# EmployeeCount, StandardHours, Over18, EmployeeNumber 
print('Over18 :', data['Over18'].unique()[0])
print('EmployeeCount :', data['EmployeeCount'].unique()[0])
print('StandardHours :', data['StandardHours'].unique()[0])
print('EmployeeNumber :', data['EmployeeNumber'].unique()[0])
data.drop(['EmployeeCount', 'StandardHours', 'Over18', 'EmployeeNumber'], axis = 1, inplace = True)
print(data.shape)
data.tail()
1
2
3
4
5
Over18 : Y
EmployeeCount : 1
StandardHours : 80
EmployeeNumber : 1
(1470, 31)
AgeAttritionBusinessTravelDailyRateDepartmentDistanceFromHomeEducationEducationFieldEnvironmentSatisfactionGenderHourlyRateJobInvolvementJobLevelJobRoleJobSatisfactionMaritalStatusMonthlyIncomeMonthlyRateNumCompaniesWorkedOverTimePercentSalaryHikePerformanceRatingRelationshipSatisfactionStockOptionLevelTotalWorkingYearsTrainingTimesLastYearWorkLifeBalanceYearsAtCompanyYearsInCurrentRoleYearsSinceLastPromotionYearsWithCurrManager
146536NoTravel_Frequently884Research & Development23CollegeMedicalHighMale41Very High2Laboratory TechnicianVery HighMarried2571122904No17ExcellentHigh1173Better5203
146639NoTravel_Rarely613Research & Development6Below CollegeMedicalVery HighMale42Medium3Healthcare RepresentativeLowMarried9991214574No15ExcellentLow195Better7717
146727NoTravel_Rarely155Research & Development4BachelorLife SciencesMediumMale87Very High2Manufacturing DirectorMediumMarried614251741Yes20OutstandingMedium160Better6203
146849NoTravel_Frequently1023Sales2BachelorMedicalVery HighMale63Medium2Sales ExecutiveMediumMarried5390132432No14ExcellentVery High0173Good9608
146934NoTravel_Rarely628Research & Development8BachelorMedicalMediumMale82Very High2Laboratory TechnicianHighMarried4404102282No12ExcellentLow063Best4312
  • EmployeeCount와 EmployeeNumber는 1, StandardHours는 80, Over18은 Y로 각각 하나의 값만 가지므로 분석 및 예측에 필요 없기에 삭제하였습니다.


2.5 상관관계

1
2
3
4
5
6
# 상관계수 구하기
data_cp = data.copy()
data_cp = pd.get_dummies(data_cp, drop_first= True)
data_cp = data_cp[['Attrition_Yes'] + [column for column in data_cp.columns if column != 'Attrition_Yes']]
data_corr = data_cp.corr()
print(data_corr.shape)
1
(71, 71)

2.5.1 상관계수 히트맵으로 확인

1
2
3
4
5
6
7
8
9
plt.figure(figsize=(36, 18))
plt.title('Corr Heatmap')

# 실제 히트맵 그리는 코드
sns.set(font_scale=1.2)
sns.heatmap(data_corr, annot=True, annot_kws={
    "size": 90 / np.sqrt(len(data_corr))}, fmt='.2f', cmap='RdYlBu_r', linewidths=0.5,)
plt.savefig('corrmap.png')
plt.show()

  • 변수가 너무많아서 상관계수를 히트맵으로 표현해도 잘 볼수가 없습니다.
  • 물론 볼수는 있지만, 이럴땐 다른 방법으로 확인해야겠습니다.


2.5.2 상관관계가 있는것들만 따로 보기

1
2
3
4
5
6
7
8
temps = data_corr[(data_corr > 0.4) | (data_corr < -0.4)]
high_corr = []
for c in temps.columns: 
    temp = temps[c].dropna()

    if len(temp) == 1:
        continue
    high_corr.append([temp.name, temp.to_dict()])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fig, ax = plt.subplots(6,5, figsize = (36, 36), constrained_layout=True)
fig.suptitle('Corr', fontsize=16)
ax = ax.flatten()

for i, c in enumerate(high_corr):
    ordered_d = OrderedDict(sorted(high_corr[i][1].items(), key=lambda t:t[1], reverse=True))
    title = ordered_d.popitem(0)
    
    sns.barplot(x = list(ordered_d.keys()), y = list(ordered_d.values()), ax = ax[i])
    ax[i].set(title = title[0])
    
    if len(ordered_d.keys()) > 2:
        ax[i].tick_params(labelrotation=20)
plt.savefig('corrbar.png')
plt.show()

  • 월급여, 경력, 업무의 수준, 관리자, 나이에 상관계수가 높았습니다., 아무래도 경력이 쌓이고, 관리자의 직급, 어려운 업무일수록 급여를 많이주는것으로 파악됩니다.
  • 만일 퇴사의 여부가 월 급여와 관련이 있다면 경력, 업무의 수준, 나이 등이 급여와 상관관계가 있으므로 같이 보아야 할듯 합니다.


2.5.3 종속변수와의 상관관계

1
2
3
4
# 잘안보여서 일단 종속변수 상관관계만 확인
plt.title('Attrition of Corr')
data_cp.drop('Attrition_Yes', axis = 1).corrwith(data_cp.Attrition_Yes).sort_values().plot(kind='barh', figsize = (10, 24))
plt.show()

  • 위에서 확인해보았던 히트맵에서는 종속변수는 다른 독립변수들과 비교하여 강한 상관관계를 가지는 변수는 없었습니다.
  • 그래서 따로 확인을 해보았는데, 약하게 나마 OverTime, TotalWorkingYea, MonthlyIncome과 관계가 있어 보입니다.


2.5.4 VIF 확인

1
2
3
4
5
6
from statsmodels.stats.outliers_influence import variance_inflation_factor
# 피처마다의 VIF 계수를 출력합니다.
vif = pd.DataFrame()
vif["VIF Factor"] = [variance_inflation_factor(data_corr.values, i) for i in range(data_corr.shape[1])]
vif["features"] = data_corr.columns
vif.sort_values(by='VIF Factor', ascending = False)
VIF Factorfeatures
1713640.716455Department_Sales
1613522.448575Department_Research & Development
223238.702786EducationField_Life Sciences
53212.315104MonthlyIncome
242882.516493EducationField_Medical
231749.979398EducationField_Marketing
441229.895130JobRole_Sales Executive
26816.753726EducationField_Technical Degree
37740.090127JobLevel_5
36721.256599JobLevel_4
38580.597267JobRole_Human Resources
25503.944369EducationField_Other
35344.522261JobLevel_3
45249.398879JobRole_Sales Representative
8235.109044TotalWorkingYears
40196.264859JobRole_Manager
34162.649949JobLevel_2
10161.956363YearsAtCompany
43107.302581JobRole_Research Scientist
3991.089185JobRole_Laboratory Technician
6988.607126WorkLifeBalance_Better
5073.095522MaritalStatus_Single
7065.434059WorkLifeBalance_Good
4253.287587JobRole_Research Director
1140.420495YearsInCurrentRole
6538.927425StockOptionLevel_1
1337.935563YearsWithCurrManager
6829.813928WorkLifeBalance_Best
5121.270629NumCompaniesWorked_1
120.328417Age
719.126971PercentSalaryHike
6118.554877PerformanceRating_Outstanding
4116.536042JobRole_Manufacturing Director
1515.969357BusinessTravel_Travel_Rarely
1415.703952BusinessTravel_Travel_Frequently
5314.656369NumCompaniesWorked_3
6614.637033StockOptionLevel_2
5212.851189NumCompaniesWorked_2
4912.089123MaritalStatus_Married
5412.066410NumCompaniesWorked_4
1210.986051YearsSinceLastPromotion
678.399376StockOptionLevel_3
566.210026NumCompaniesWorked_6
576.093062NumCompaniesWorked_7
555.382364NumCompaniesWorked_5
594.785775NumCompaniesWorked_9
04.706463Attrition_Yes
584.495559NumCompaniesWorked_8
483.975349JobSatisfaction_Very High
643.799682RelationshipSatisfaction_Very High
213.694006Education_Master
293.634595EnvironmentSatisfaction_Very High
623.628168RelationshipSatisfaction_Low
463.556749JobSatisfaction_Low
273.444139EnvironmentSatisfaction_Low
473.402401JobSatisfaction_Medium
283.310664EnvironmentSatisfaction_Medium
633.220096RelationshipSatisfaction_Medium
193.059469Education_College
182.625621Education_Below College
601.823511OverTime_Yes
311.796165JobInvolvement_Low
331.726490JobInvolvement_Very High
201.706147Education_Doctor
321.687609JobInvolvement_Medium
31.580221DistanceFromHome
21.447858DailyRate
61.379119MonthlyRate
41.349891HourlyRate
301.328611Gender_Male
91.318633TrainingTimesLastYear
  • 다중공선성(vif) : 통계학의 회귀분석에서 독립변수들 간에 강한 상관관계가 나타나는 문제입니다.
  • 보통은 10 미만이면 다중공선성이 없다고 하는데, HR 데이터에는 상당히 높은 특성들이 많습니다.
  • 일단, vif가 13000으로 너무 높은 Department 컬럼은 제외시키겠습니다.
  • 물론 vif가 높다고 다 드랍 시킬순 없으니(Department 제외, 압도적으로 높음), 어떤 특성을 드랍시켜야하는지는 후에 고민을 해봐야 할듯하다.

3. 가설 확인


3.1 가설

  • 가설1) 집과 회사의 거리가 먼 사람들이 퇴사를 많이 할것이다.
  • 가설2) 월급여가 낮은 사람이 퇴사를 많이 할것이다.
  • 가설3) 업무환경이 안좋은 사람이 퇴사를 할것이다.
  • 가설4) 워라벨이 안좋은 사람들이 퇴사를 할것이다.
  • 가설5) 근무부서에 따른 퇴사의 비율이 다를것이다. 즉, 특정부서가 퇴사율이 높을것이다.


1
2
3
4
5
6
# 비율 확인할 pivot 테이블 만드는 함수
def make_pivot(data, x, y, func):
    table = pd.pivot_table(data = data, values = 'Age', index = x, columns= y, aggfunc=func)
    table['total'] = table['No'] + table['Yes']
    table['Attrition_rate'] = table['Yes'] / table['total'] * 100
    return table

3.2 가설1) 집과 회사의 거리가 먼 사람들이 퇴사를 많이 할것이다.

1
2
3
4
5
6
7
rate = make_pivot(data, 'DistanceFromHome', 'Attrition', func=len)

# plt.rc('font', family='AppleGothic') 
plt.figure(figsize=(12,8))
plt.title('DistanceFromHome', fontsize=16)
sns.barplot(rate.index, rate.Attrition_rate)
plt.show()

  • 확실히 집에서 먼 사람이 집에서 가까운 사람들보다 많이 퇴사를 합니다.
  • 가장 높은 비율인 거리 24는 전체 28명중에, 12 퇴사하여 비율로는 42.8%이고 전체 퇴사인원의 5%를 차지합니다.
  • 전체 퇴사인원중에서 집과의 거리가 가까운 사람의 비율이 제일 많지만, 사실 집과의 거리가 가까운 인원의 비율이 전체 비율에서 제일 많아서 그렇습니다.


3.2.1 거리가 먼 사람 컬럼 추가

1
round(data[data.DistanceFromHome >= 22]['DistanceFromHome'].count() / len(data),2)
1
0.13
1
2
data['FarFromHome'] = np.where(data.DistanceFromHome >= 22 , 1, 0)
data['FarFromHome'] = data['FarFromHome'].astype(object)
  • 그래프 상에서 거리가 멀어서 퇴사비율이 많아보이는 22부터 집과 거리가 먼 컬럼을 새로 생성하고, 나중을 위해 type을 object로 변경


3.3 가설2) 월급여가 낮은 사람이 퇴사를 많이 할것이다.

1
2
3
4
plt.figure(figsize = (12,8))
plt.title('월급여에 따른 퇴사율')
sns.boxplot(x = 'Attrition', y = 'MonthlyIncome', data = data)
plt.show()

  • 박스그래프를 보면 월급여가 낮은 사람들이 퇴사가 있는것으로 보입니다.
  • 또한 퇴사를 하지 않은 사람들의 중앙값이 퇴사를 한 사람들보다 위에 위치하고, 박스의 크기가 더 큰것으로 보아 분포도 넓은것으로 보입니다.
  • 이는 월급여가 퇴사에 영향을 준다고 볼수 있다.


3.3.1 월급여가 낮은 사람 추가

1
data[data['Attrition'] == 'Yes']['MonthlyIncome'].median()
1
3202.0
1
2
data['LowMonthlyIncome'] = np.where(data.MonthlyIncome <= 3202 , 1, 0)
data['LowMonthlyIncome'] = data['LowMonthlyIncome'].astype(object)
  • 박스그래프상에 퇴사한 사람들의 급여의 중앙값을 기준으로 작은 급여를 책성하여 0,1로 나누었습니다.
  • 또한 추후 처리를 쉽게하기 위해 object형식으로바꿔주었습니다.

3.4 가설3) 업무환경이 안좋은 사람이 퇴사를 할것이다.

1
2
3
4
5
6
rate = make_pivot(data, 'JobInvolvement', 'Attrition', func=len)

plt.figure(figsize=(12, 8))
plt.title('업무 환경에 따른 퇴사인원 비율', fontsize=16)
sns.barplot(rate.index, rate.Attrition_rate, order=['Low', 'Medium', 'High', 'Very High'])
plt.show()

  • 업무환경이 Low인 사람들은 총 83명이었고, 그 중 28명이 퇴사를 하였습니다. Low 인원의 비율로는 33%로 가장 높은 비율을 차지 합니다.
  • 따라서 업무환경이 낮은 사람들이 더 많이 퇴사를 하는것으로 알수 있습니다.
  • Onehot Encoding을하면 자동으로 Low, Medium, High, Very High는 구별되기에 따로 컬럼을 만들진 않았습니다.


3.5 가설4) 워라벨이 안좋은 사람들이 퇴사를 할것이다.

1
2
3
4
5
6
rate = make_pivot(data, 'WorkLifeBalance', 'Attrition', func= len)

plt.figure(figsize = (12,8))
plt.title('워라벨 정도에 따른 퇴사 인원 비율')
sns.barplot(rate.index, rate.Attrition_rate, order=['Bad', 'Good', 'Better', 'Best'])
plt.show()

  • 워라벨이 Bad인 인원은 80명이고 그 중에 퇴사한 인원은 25명으로 총 비율로는 31%로 가장 높은 퇴사 비율을 보입니다.
  • 의외인것은 Best인 인원도 꽤 많은 퇴사율을 보인다는 것입니다.
  • 일단, 워라벨이 Bad인 사람들의 다른 사람들에 비해 퇴사율이 높습니다.


3.6 가설5) 근무부서에 따른 퇴사의 비율이 다를것이다. 즉, 특정부서가 퇴사율이 높을것이다.

1
2
3
4
5
6
7
8
9
10
rate = make_pivot(data, 'Department', 'Attrition', func= len)
fig, ax = plt.subplots(1,2, figsize = (16,8))

sns.countplot(data['Department'], hue = data['Attrition'], ax = ax[0], order = ['Sales', 'Human Resources', 'Research & Development'])
ax[0].set(title = 'Attrition of Department')

sns.barplot(rate.index, rate.Attrition_rate, ax = ax[1], order = ['Sales', 'Human Resources', 'Research & Development'])
ax[1].set(title = 'Attrition of Department rate')

plt.show()

  • 근무부서가 HR인곳은 전체 인력도 많지 않은데, 퇴사자가 생각보다 많음을 알수 있다, HR에 근무하는 인원 대비 약 18%정도가 퇴사를 하였습니다.
  • 그렇다면 근무부서별로 월수입의 차이가 나서 HR 부서에서 퇴사 인력이 많은것 일까?


1
2
3
4
plt.figure(figsize=(12, 8))
plt.title('MonthlyIncome of Department')
sns.boxplot(x='Department', y='MonthlyIncome', data=data)
plt.show()

  • 근무 부서별로 월 수입이 차이가 많이 나는지 확인을 해보았으나 (HR이 가장 낮을까?) 사실살 Sales를 제외하고는 나머지 2부서는 큰 차이가 없어 보입니다.
  • 또한 가장 많은 월소득 구간은 세개의 부서는 큰 차이가 없는것으로 보입니다.
  • 전체적으로 Sale의 급여가 조금 더 높게 형성되어 있음을 알수 있습니다.


1
2
temp = data.groupby('Department', axis=0).agg(['min', 'median', 'mean', 'max', 'std'])['MonthlyIncome']
temp
minmedianmeanmaxstd
Department
Human Resources15553886.06654.507937197175788.732921
Research & Development10094374.06281.252862199994895.835087
Sales10525754.56959.172646198474058.739322
  • 혹시 몰라 부서별 월수입에 대한 간단한 통계요약치를 보았습니다.
  • 위에 박스그래프에서 적은 내용과 큰 차이는 없습니다.


3.7 가설6) 초기 경력자들이 퇴직을 많이 할것이다.

1
2
3
4
5
6
rate = make_pivot(data, 'TotalWorkingYears', 'Attrition', func= len).fillna(0)

plt.figure(figsize = (12,8))
plt.title('Attrition of TotalWorkingYears rate')
sns.barplot(rate.index, rate.Attrition_rate)
plt.show()

  • 예상대로 초기 경력자 (0년 ~ 2년사이)의 인원이 가장 많은 퇴직율을 보였습니다.
  • 아마 처음 경력을 쌓고 다른곳으로 이직을 하기 위해 퇴사를 하는것이 아닐까 생각해 보았습니다.


3.7.1 초기경력자 컬럼 생성

1
2
data['LowWorkingYears'] = np.where(data.TotalWorkingYears <= 2 , 1, 0)
data['LowWorkingYears'] = data['LowWorkingYears'].astype(object)

3.8 가설확인 결론

  • 가설1) 집과 회사의 거리가 먼 사람들이 퇴사를 많이 할것이다. - 맞음 -> 해당 컬럼 생성
  • 가설2) 월급여가 낮은 사람이 퇴사를 많이 할것이다. - 맞음 -> 해당 컬럼 생성
  • 가설3) 업무환경이 안좋은 사람은 퇴사를 할것이다. - 맞음
  • 가설4) 워라벨이 안좋은 사람들이 퇴사를 할것이다. - 맞음, 하지만 워라벨이 좋아도 퇴사를 함
  • 가설5) 근무부서에 따른 퇴사의 비율이 다를것이다. 즉, 특정부서가 퇴사율이 높을것이다. - 틀림
  • 가설6) 초기 경력자들이 퇴직을 많이 할것이다. - 맞음 -> 해당 컬럼 생성


4. Feature Engineering


4.1 Age

1
2
3
4
plt.figure(figsize=(12,8))
plt.title('Age histogram')
data['Age'].plot(kind = 'hist')
plt.show()

1
2
3
# [(17.958, 26.4] : 0,  (26.4, 34.8] : 1, (34.8, 43.2] : 2, (43.2, 51.6] : 3,  (51.6, 60.0] : 4]
# (미포함, 포함) 임
data['Age_cut'] = pd.cut(data['Age'], 5, labels=[0, 1, 2, 3, 4]).astype(object)
  • 나이는 연속적이긴하지만, 5개 구간으로 나누는 컬럼을 생성하였습니다. 이후 나이 컬럼은 삭제합니다.

4.2 NumCompaniesWorked

1
2
3
plt.figure(figsize=(12,8))
sns.countplot(data.NumCompaniesWorked)
plt.show()

  • 일한 회사의 수가 0인게 이상합니다. 일한 회사의 수가 없을수가 없으니까요
  • 만약 전체 일한 회사의수가 현재 IBM을 제외시킨거라면 이해는 갑니다.
  • 그렇다면 일한 회사의 수가 0인 사람들은 총 경력기간과 회사의 근속년수가 같아야 합니다.


1
data[(data.NumCompaniesWorked == 0) & (data.TotalWorkingYears == data.YearsAtCompany)][['NumCompaniesWorked','TotalWorkingYears','YearsAtCompany']]
NumCompaniesWorkedTotalWorkingYearsYearsAtCompany
  • 일한 회사의 수가 0개인 사람들 중 전체 경력과 근속년수가 같은 인원은 한명도 없습니다.
  • 데이터의 오류인지 더 확인해봐야겠습니다.
  • 이번엔 일한 회사가 0개인 사람들 중에 경력과 근속년수가 다른 사람들을 확인하겠습니다.
1
2
print(data[(data.NumCompaniesWorked == 0) & (data.TotalWorkingYears != data.YearsAtCompany)][['NumCompaniesWorked','TotalWorkingYears','YearsAtCompany']].shape)
data[(data.NumCompaniesWorked == 0) & (data.TotalWorkingYears != data.YearsAtCompany)][['NumCompaniesWorked','TotalWorkingYears','YearsAtCompany']].head()
1
(197, 3)
NumCompaniesWorkedTotalWorkingYearsYearsAtCompany
5087
80109
10065
110109
13032
  • 총 197개의 데이터가 있고, 경력과 근속년수가 1년씩 차이납니다.
  • 그렇다면 경력과 근속년수가 같은 사람들은 일한 회사의 수가 몇개로 나오는지 확인하겠습니다.
1
data[(data.TotalWorkingYears == data.YearsAtCompany)][['NumCompaniesWorked','TotalWorkingYears','YearsAtCompany']].head()
NumCompaniesWorkedTotalWorkingYearsYearsAtCompany
111010
3188
7111
12155
1511010
  • 경력과 근속년수가 같은 사람들은 모두 일한회사의 수가 1개로 나옵니다.
  • 그렇다면 일한회사의 수가 2개 이상인데 경력과 근속년수가 같은 사람도 있는지 확인해봐야겠습니다
1
data[(data.NumCompaniesWorked >= 2) & (data.TotalWorkingYears == data.YearsAtCompany)][['NumCompaniesWorked','TotalWorkingYears','YearsAtCompany']].head()
NumCompaniesWorkedTotalWorkingYearsYearsAtCompany
  • 일한회사의 수개 2개 이상 인데, 근속년수가 같은 사람은 없습니다.
1
data[(data.NumCompaniesWorked >= 1) & ((data.TotalWorkingYears - data.YearsAtCompany) == 1)][['NumCompaniesWorked','TotalWorkingYears','YearsAtCompany']].head()
NumCompaniesWorkedTotalWorkingYearsYearsAtCompany
35165
4512322
9411211
109110
12911615
  • 일한 회사가 1개고, 총 경력과 근속년수가 1년 차이나는 사람은 있습니다.
  • 하지만 일한 회사가 2개 이상이며, 총 경력과 근속년수가 1년 차이나는 사람은 없습니다.
  • 정리하자면 일한회사가 0개의 의미는
    • 1) 현재 근속하는 회사를 제외시킨 수치여서 그렇다. (일한회사가 0 = IBM이 첫회사이다)
      • 그렇다면 일한회사가 1개인 사람들은 총 경력과 현재 근속년수가 같으면 안된다. (IBM에 들어오기전 직장이 있기 때문에)
      • 하지만 일한회사가 1개인 사람들 중 총 경력과 현재 근속년수가 같은 사람이 있다. -> 오류
    • 2) 일한회사가 0개는 오류이다.
      • 일한 회사가 없을수는 없으니 무조건 1개부터 시작한다,
      • 0으로 넣은 사람들은 오류이고, 1개를 더해주면 된다.(IBM 회사)
  • 결론을 이야기하자면, 데이터 상으로 일한회사가 0개가 나올수는 없으니, 0인 사람들은 모두 +1을 해주면 된다.
1
2
3
4
data.NumCompaniesWorked.replace(0, 1, inplace = True)
plt.figure(figsize=(12,8))
sns.countplot(data.NumCompaniesWorked)
plt.show()

  • 다시 일한회사의 수가 0인 사람들을 1로 바꾸고 보니, 압도적으로 IBM에서만 일한 사람이 많다.
1
data.NumCompaniesWorked = data.NumCompaniesWorked.astype(object)
  • int형으로 변경되었기에 다시 object로 변경

4.3 Total Satisfaction

1
2
3
4
5
point = {'Low' : 0, 'Medium' : 1, 'High' : 2 , 'Very High' : 3}
EnvironmentSatisfaction = data['EnvironmentSatisfaction'].map(point)
RelationshipSatisfaction = data['RelationshipSatisfaction'].map(point)
JobSatisfaction = data['JobSatisfaction'].map(point)
data['TotalSatisfaction'] = (EnvironmentSatisfaction + RelationshipSatisfaction + JobSatisfaction).astype(object)
1
2
3
4
plt.figure(figsize=(12, 8))
plt.title('Total Satisfaction')
sns.countplot(data['TotalSatisfaction'])
plt.show()

  • EnvironmentSatisfaction, RelationshipSatisfaction, JobSatisfaction는 만족이라는 키워드를 가지는 컬럼입니다.
  • 위 컬럼의 만족도에 대한 점수들을 합산하여 포탈만족도라는 컬럼을 생성하였습니다.
  • 따라서, 개별 만족도 컬럼은 삭제 합니다.

4.4 Outlier 확인

1
2
3
4
5
6
cont_cols = [i for i in data.columns if data[i].dtype != object]
fig, ax = plt.subplots(2, 7, figsize=(36, 12))
ax = ax.flatten()

for index, col in enumerate(cont_cols):
    sns.boxplot(data[col], ax=ax[index])

1
2
3
4
5
6
7
# 아웃라이어 확인 함수 생성
def outlier(data, col):
    q1 = np.percentile(data[col], 25) 
    q3 = np.percentile(data[col], 75)
    IQR = q3 - q1
    outlier_step = 1.5 * IQR
    return data[(data[col] < q1 - outlier_step) | (data[col] > q3 + outlier_step)]

4.4.1 MonthlyIncom

1
2
3
4
5
6
monthlyincom_outlier = outlier(data, 'MonthlyIncome')
print(f'MonthlyIncome의 아웃라이어 갯수 {monthlyincom_outlier.shape[0]}개')
print(f'전체 데이터의 {round(monthlyincom_outlier.shape[0] / data.shape[0],2)}% 차지')
print(monthlyincom_outlier.Attrition.value_counts())
sns.countplot(monthlyincom_outlier.Attrition)
plt.show()
1
2
3
4
5
MonthlyIncome의 아웃라이어 갯수 114개
전체 데이터의 0.08% 차지
No     109
Yes      5
Name: Attrition, dtype: int64

  • 컬럼 삭제 합니다. 아웃라이어가 너무 크고 많으며 VIF 계수도 굉장히 높았습니다.

4.4.2 TotalWorkingYears

1
2
3
4
5
6
workingyear_outlier = outlier(data, 'TotalWorkingYears')
print(f'TotalWorkingYears의 아웃라이어 갯수 {workingyear_outlier.shape[0]}개')
print(f'전체 데이터의 {round(workingyear_outlier.shape[0] / data.shape[0],2)}% 차지')
print(workingyear_outlier.Attrition.value_counts())
sns.countplot(workingyear_outlier.Attrition)
plt.show()
1
2
3
4
5
TotalWorkingYears의 아웃라이어 갯수 63개
전체 데이터의 0.04% 차지
No     58
Yes     5
Name: Attrition, dtype: int64

  • 아웃라이어의 갯수가 많지는 않으나, VIF가 높았었기에 컬럼을 삭제 합니다.

4.4.3 TrainingTimesLastYear

1
2
3
4
5
6
traininigtimes_outlier = outlier(data, 'TrainingTimesLastYear')
print(f'TrainingTimesLastYear의 아웃라이어 갯수 {traininigtimes_outlier.shape[0]}개')
print(f'전체 데이터의 {round(traininigtimes_outlier.shape[0] / data.shape[0],2)}% 차지')
print(traininigtimes_outlier.Attrition.value_counts())
sns.countplot(traininigtimes_outlier.Attrition)
plt.show()
1
2
3
4
5
TrainingTimesLastYear의 아웃라이어 갯수 238개
전체 데이터의 0.16% 차지
No     203
Yes     35
Name: Attrition, dtype: int64

  • VIF 계수도 낮으며, 아웃라이어가 전체 데이터에 많은 양을 자지해서 컬럼 및 아웃라이어도 그대로 유지합니다.

4.4.4 YearsAtCompany

1
2
3
4
5
6
yearsatcompany_outlier = outlier(data, 'YearsAtCompany')
print(f'YearsAtCompany의 아웃라이어 갯수 {yearsatcompany_outlier.shape[0]}개')
print(f'전체 데이터의 {round(yearsatcompany_outlier.shape[0] / data.shape[0],2)}% 차지')
print(yearsatcompany_outlier.Attrition.value_counts())
sns.countplot(yearsatcompany_outlier.Attrition)
plt.show()
1
2
3
4
5
YearsAtCompany의 아웃라이어 갯수 104개
전체 데이터의 0.07% 차지
No     94
Yes    10
Name: Attrition, dtype: int64

  • 아웃라이어가 전체 데이터에 0.07% 차지합니다. 근속년수가 퇴사 여부에 영향을 미칠듯 싶어서 아웃라이어만 삭제 합니다.

4.4.5 YearsInCurrentRole

1
2
3
4
5
6
yearsincurrentrole_outlier = outlier(data, 'YearsInCurrentRole')
print(f'YearsInCurrentRole의 아웃라이어 갯수 {yearsincurrentrole_outlier.shape[0]}개')
print(f'전체 데이터의 {round(yearsincurrentrole_outlier.shape[0] / data.shape[0],2)}% 차지')
print(yearsincurrentrole_outlier.Attrition.value_counts())
sns.countplot(yearsincurrentrole_outlier.Attrition)
plt.show()
1
2
3
4
5
YearsInCurrentRole의 아웃라이어 갯수 21개
전체 데이터의 0.01% 차지
No     19
Yes     2
Name: Attrition, dtype: int64

  • 아웃라이어가 전체 데이터에 0.01%밖에 차지를 안합니다. VIF 계수가 낮기에 아웃라이어만 삭제 결정

4.4.6 YearsSinceLastPromotion

1
2
3
4
5
6
yearssincelastpromotion_outlier = outlier(data, 'YearsSinceLastPromotion')
print(f'YearsSinceLastPromotion의 아웃라이어 갯수 {yearssincelastpromotion_outlier.shape[0]}개')
print(f'전체 데이터의 {round(yearssincelastpromotion_outlier.shape[0] / data.shape[0],2)}% 차지')
print(yearssincelastpromotion_outlier.Attrition.value_counts())
sns.countplot(yearssincelastpromotion_outlier.Attrition)
plt.show()
1
2
3
4
5
YearsSinceLastPromotion의 아웃라이어 갯수 107개
전체 데이터의 0.07% 차지
No     94
Yes    13
Name: Attrition, dtype: int64

  • 아웃라이어가 전체 데이터에 0.07% 차지하여 삭제하긴 어렵고, 컬럼은 VIF가 낮으니 삭제하기 어려워 유지

4.4.7 YearsWithCurrManager

1
2
3
4
5
6
yearswithcurrmanager_outlier = outlier(data, 'YearsWithCurrManager')
print(f'YearsWithCurrManager의 아웃라이어 갯수 {yearswithcurrmanager_outlier.shape[0]}개')
print(f'전체 데이터의 {round(yearswithcurrmanager_outlier.shape[0] / data.shape[0],2)}% 차지')
print(yearswithcurrmanager_outlier.Attrition.value_counts())
sns.countplot(yearswithcurrmanager_outlier.Attrition)
plt.show()
1
2
3
4
YearsWithCurrManager의 아웃라이어 갯수 14개
전체 데이터의 0.01% 차지
No    14
Name: Attrition, dtype: int64

  • VIF 도 상대적으로 낮고, 아웃라이어가 몇개 없긴 하지만, 모두 No라는 특성을 가지고 있기에 아웃라이어가 실제 특성을 가지고 있을수 있어서 유지 결정

4.4.8 Outlier 및 컬럼 삭제

1
2
3
4
5
6
data_copy = data.copy()
outlier_df = pd.concat([yearsatcompany_outlier, yearsincurrentrole_outlier]).drop_duplicates()
data_copy.drop(index=outlier_df.index, inplace=True)
data_copy.drop(labels=['MonthlyIncome', 'Age', 'TotalWorkingYears', 'YearsAtCompany', 'Department',
                       'EnvironmentSatisfaction', 'RelationshipSatisfaction', 'JobSatisfaction'], axis=1, inplace=True)
data_copy.shape
1
(1361, 28)
  • Outlier 및 Column을 삭제할때는 하나의 지표를 절대적으로 참고하는것이 아닌 여러가지의 지표를 사용해야 합니다.
  • 일단, 해당 데이터에 도메인 지식이 있어서 도메인 지식을 활용하여 컬럼 및 데이터 삭제를 할수 있으면 좋습니다.
  • 그 외에는 VIF, 아웃라이어를 가진 컬럼의 시각화, 아웃라이어 데이터만 보기 등이 있습니다.
  • 만일 아웃라이어데이터가 이진분류에서 하나의 클래스(All 0 or all 1)만 가지고 있다면 해당 아웃라이어는 클래스를 설명하는 지표가 될수 있기에 삭제를 하지 않는쪽으로 생각 해야합니다.
  • 반대로 두개의 클래스에 50:50으로 분포가 되어있다면, 삭제를 고려하여도 됩니다.


5. 예측을 위한 데이터 처리


5.1 Label Encoder 및 Scaler 적용

1
2
3
4
5
numeric_features = [column for column in data_copy.columns if data_copy[column].dtype != object]
categorical_features = [column for column in data_copy.columns if data_copy[column].dtype == object]

data_copy[categorical_features] = data_copy[categorical_features].apply(LabelEncoder().fit_transform)
data_copy[numeric_features] = RobustScaler().fit_transform(data_copy[numeric_features])
  • 컴퓨터는 Male, HR 이런 단어들을 인식하지 못합니다. 해당 단어들을 숫자로 바꿔주는것이 Label Encoder 입니다.
  • 예를들어 Department의 ‘Sales’, ‘Human Resources’, ‘Research & Development’ 를 컴퓨터가 알아볼수 있게 0, 1, 2로 바꿔주는 것입니다.

5.2 X,y 분리

1
2
X = data_copy.drop(labels= ['Attrition'], axis = 1)
y = data_copy.Attrition
  • 데이터를 종속변수와 독립변수로 나눠줍니다.

5.3 train, test 분리

1
2
X_train, X_test, y_train, y_test = train_test_split(
    X, y, stratify=y, random_state=87, test_size=0.2)
1
2
3
4
train_rate = round(y_train.sum() / len(y_train),2)
test_rate = round(y_test.sum() / len(y_test),2)
print(f'학습 데이터에서의 Target 비율 : {train_rate}')
print(f'테스트 데이터에서의 Target 비율 : {test_rate}')
1
2
학습 데이터에서의 Target 비율 : 0.17
테스트 데이터에서의 Target 비율 : 0.17

5.4 함수 생성

5.4.1 Learning Curve 함수 준비

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# Plot learning curve
def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,
                        n_jobs=-1, train_sizes=np.linspace(.1, 1.0, 5), scoring = 'accuracy'):
    """
    Generate a simple plot of the test and traning learning curve.

    Parameters
    ----------
    estimator : object type that implements the "fit" and "predict" methods
        An object of that type which is cloned for each validation.

    title : string
        Title for the chart.

    X : array-like, shape (n_samples, n_features)
        Training vector, where n_samples is the number of samples and
        n_features is the number of features.

    y : array-like, shape (n_samples) or (n_samples, n_features), optional
        Target relative to X for classification or regression;
        None for unsupervised learning.

    ylim : tuple, shape (ymin, ymax), optional
        Defines minimum and maximum yvalues plotted.

    cv : integer, cross-validation generator, optional
        If an integer is passed, it is the number of folds (defaults to 3).
        Specific cross-validation objects can be passed, see
        sklearn.cross_validation module for the list of possible objects

    n_jobs : integer, optional
        Number of jobs to run in parallel (default 1).

    x1 = np.linspace(0, 10, 8, endpoint=True) produces
        8 evenly spaced points in the range 0 to 10
    """

    plt.figure(figsize = (12,8))
    plt.title(title)
    if ylim is not None:
        plt.ylim(*ylim)

    plt.xlabel("Training examples")
    plt.ylabel("Score")
    train_sizes, train_scores, test_scores = learning_curve(
        estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes, scoring=scoring)
    train_scores_mean = np.mean(train_scores, axis=1)
    train_scores_std = np.std(train_scores, axis=1)
    test_scores_mean = np.mean(test_scores, axis=1)
    test_scores_std = np.std(test_scores, axis=1)
    plt.grid()

    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
                     train_scores_mean + train_scores_std, alpha=0.1,
                     color="r")
    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
                     test_scores_mean + test_scores_std, alpha=0.1, color="g")
    plt.plot(train_sizes, train_scores_mean, 'o-', color="r",
             label="Training score")
    plt.plot(train_sizes, test_scores_mean, 'o-', color="g",
             label="Cross-validation score")

    plt.legend(loc="best")
    return plt.show()
  • 학습이 과적합인지 과소적합인지 확인하는 Learning Curve function 생성
  • Gridsearch와 title, x, y값을 넣으면 Learning Curve를 볼수 있는 함수
  • Learning Curve로 과적합을 판단함

5.4.2 Model 성능 함수 생성

1
2
3
4
5
6
7
8
def model_score(y_test, predict):
    print('Accuracy : ',round(accuracy_score(y_test, predict), 3))
    print('Recall : ',round(recall_score(y_test, predict), 3))
    print('Precision : ',round(precision_score(y_test, predict), 3))
    print('F1_Score : ',round(f1_score(y_test, predict), 3))
    print()
    print('Confusion_matrix :')
    print(confusion_matrix(y_test, predict))

6. 머신러닝 알고리즘을 이용한 퇴사자 예측


6.1 의사결정나무

  • 데이터를 분석하여 이들 사이에 존재하는 패턴을 예측 가능한 규칙들의 조합으로 나타내며, 그 모양이 ‘나무’와 같다고 해서 의사결정나무라 불립니다.
  • 분류(classification)와 회귀(regression) 모두 가능합니다.
  • 종속변수를 잘 나타내주는 독립변수를 지니불순도나 엔트로피를 이용하여 분류합니다.
  • 장점
    • 모델을 쉽게 시각화하여 비전문가도 이해하기 쉽습니다.
    • 데이터의 스케일에 구애받지 않음, 전처리(정규화,표준화)가 필요 없습니다.
    • 각 특성의 스케일이 다르거나 이진특성, 연속특성이 혼합되어 있을 때도 잘 작동 합니다.
  • 단점
    • 사전 가지치기를 사용해도 과대적합되는 경향이 있어 일반화 성능이 좋지 않습니다.
    • 외삽(extrapolation) = 훈련 데이터의 범위 밖의 포인트는 예측할 수 없습니다.

6.1.1 기본 모델 생성 및 학습

1
2
3
4
5
6
7
clf_model = DecisionTreeClassifier(random_state=87)

params_grid = [{}]

gridsearch = GridSearchCV(
    estimator=clf_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
GridSearchCV(cv=5, estimator=DecisionTreeClassifier(random_state=87), n_jobs=-1,
             param_grid=[{}], return_train_score=True)
  • 의사결정나무에 하이퍼파라미터 튜닝을 하지 않고 기본 설정으로 모델을 생성하였습니다.
  • Cross Validation(CV)를 5회 진행하였습니다.


6.1.2 학습 결과 저장

1
2
3
4
result = []
result.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'mean_train_score': float(gridsearch.cv_results_['mean_train_score']),
           'mean_test_score': float(gridsearch.cv_results_['mean_test_score'])})
  • 학습한 결과를 추후에 모델별로 시각화 할수 있게 저장을 해둡니다.


6.1.3 예측 및 검증

1
2
3
4
5
6
clf_best_model = gridsearch.best_estimator_
clf_best_model.fit(X_train, y_train)
clf_predict = clf_best_model.predict(X_test)

model_score(y_test, clf_predict)
plot_learning_curve(clf_best_model, 'DecisionTree Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.799
Recall :  0.37
Precision :  0.395
F1_Score :  0.382

Confusion_matrix :
[[201  26]
 [ 29  17]]

  • 학습이 잘 되고 있는지, Learning Curve를 그려보았습니다.
  • 학습은 Accuracy가 1로 잘 맞추지만, 실제 검증을 하면 0.8을 넘지못합니다.
  • 과대적합이 있습니다.


6.1.4 중요변수 확인

1
2
3
4
important = clf_best_model.feature_importances_

pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title ='Important Feature')
plt.show()

  • 의사결정나무모델이 사용한 변수들중 중요하다고 판단한 Feature들을 확인해보았습니다.
  • 가장 마지막에 있는 점수가 없는 Feature들은 중요하지 않다고해서 삭제를 할순 없습니다.


6.1.5 결정나무 시각화

1
2
3
4
5
6
7
8
9
10
11
12
dot_data = export_graphviz(clf_best_model, out_file=None, 
                feature_names = X_train.columns,
                class_names = data.Attrition.unique(),
                max_depth = 5, # 표현하고 싶은 최대 depth
                precision = 3, # 소수점 표기 자릿수
                filled = True, # class별 color 채우기
                rounded=True, # 박스의 모양을 둥글게
               )
pydot_graph = pydotplus.graph_from_dot_data(dot_data)
pydot_graph.set_size("\"24\"")
gvz_graph = graphviz.Source(pydot_graph.to_string())
gvz_graph

  • 의사결정나무의 장점은 알고리즘을 시각화 할수 있다는 뜻입니다.
  • 맨위의 Feature인 Overtiem이 0.5 이상이면 True, 아니면 False이며, 지니불순도를 이용하여 클래스들의 섞임 정도를 확인합니다.(하나의 클래스에 몰려있다면 지니계수가 0과 가깝게 나옴)
  • 그러다 마지막에 지니계수가 0이 되거나, 미리 설정한 파라미터에 의해 마지막 Depth가 되면 멈추게 되고, 마지막 클래스를 결정합니다.
  • Class : 0혹은 1로 이루어진 종속변수값


6.2 랜덤포레스트

  • 분류, 회귀 분석 등에 사용되는 앙상블 학습 방법의 일종으로 훈련 과정에서 구성한 다수의 결정트리로부터 분류 또는 평균 회귀 분석을 실행합니다.
  • 여러개의 의사 결정 나무를 생성한 후에 다수결 또는 평균에 따라 출력 변수를 예측하는 알고리즘입니다. 즉 의사 결정 나무와 bagging을 혼합한 형태라고 볼 수 있습니다.
  • Bagging은 샘플을 여러 번 뽑아 각 모델을 학습시켜 결과를 집계(Aggregating) 하는 방법입니다.
  • 장점
    • 여러개의 의사결정트리의 결과를 가지고 최종결과를 결정하기에 과적합을 방지합니다.
    • 따라서, 결측치의 비율이 높아져도 높은 정확도를 나타냅니다.
    • 변수의 중요성을 파악할 수 있습니다.
  • 단점
    • 데이터의 수가 많아지면 의사 결정나무에 비해 속도가 크게 떨어집니다.
    • 여러개의 의사결정나무에서 나온 결과에 대해 최종 결과를 도출하므로, 최종 결과에 대한 해석이 어려운 단점이 있습니다.

6.2.1 기본 모델 생성 및 학습

1
2
3
4
5
6
rf_model = RandomForestClassifier(random_state=87)
params_grid = [{}]

gridsearch = GridSearchCV(
    estimator=rf_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
GridSearchCV(cv=5, estimator=RandomForestClassifier(random_state=87), n_jobs=-1,
             param_grid=[{}], return_train_score=True)
  • 랜덤포레스트의 모델을 기본 설정을 사용하여 생성하고 학습하였습니다.
  • 의사결정나무와 마찬가지로 CV는 5회 하였습니다.


6.2.2 학습 결과 저장

1
2
3
result.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'mean_train_score': float(gridsearch.cv_results_['mean_train_score']),
           'mean_test_score': float(gridsearch.cv_results_['mean_test_score'])})
  • 추후 전체 결과에 대한 시각화를 위해 학습 결과를 저장합니다.


6.2.3 예측 및 검증

1
2
3
4
5
6
rf_best_model = gridsearch.best_estimator_
rf_best_model.fit(X_train, y_train)
rf_predict = rf_best_model.predict(X_test)

model_score(y_test, rf_predict)
plot_learning_curve(rf_best_model, 'RandomForest Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.861
Recall :  0.196
Precision :  0.9
F1_Score :  0.321

Confusion_matrix :
[[226   1]
 [ 37   9]]

  • 의사결정나무와 마찬가지로 Learning Curve를 그려보았을때, 과대적합이 나타납니다.
  • 하지만 검증 Accuracy는 의사결정나무보다 높습니다.
  • 보통 랜덤포레스트가 의사결정나무보다는 더 나은 성능을 보입니다. 하지만 결과에 대한 해석이 어렵다는 단점이 있습니다.


6.2. 4 중요 변수 확인

1
2
3
4
important = rf_best_model.feature_importances_

pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title ='Important Feature')
plt.show()

  • 랜덤포레스트에서 중요하다고 나온 Feature들을 확인해보았습니다.
  • 다행히 의사결정나무와는 다르게 중요하지 않다는 변수는 없습니다.
  • 위와 같은 사유로 하나의 모델을 보고 변수를 삭제하는것은 바람직 하지 않을수 있습니다.


6.2.5 랜덤포레스트 시각화

1
2
3
4
5
6
7
8
9
10
11
12
dot_data = export_graphviz(rf_best_model[0], out_file=None, 
                feature_names = X_train.columns,
                class_names = data.Attrition.unique(),
                max_depth = 5, # 표현하고 싶은 최대 depth
                precision = 3, # 소수점 표기 자릿수
                filled = True, # class별 color 채우기
                rounded=True, # 박스의 모양을 둥글게
               )
pydot_graph = pydotplus.graph_from_dot_data(dot_data)
pydot_graph.set_size("\"24\"")
gvz_graph = graphviz.Source(pydot_graph.to_string())
gvz_graph

  • 랜덤포레스트도 트리기반이기 때문에 결정나무를 시각화하여 볼수 있습니다.
  • 다만 Estimator의 갯수가 1개가 아닌, 많은 의사결정나무의 모임이기 때문에 대표되는 하나의 의사결정나무만 확인합니다.


6.3 로지스틱 회귀

  • 독립 변수의 선형 결합을 이용하여 사건의 발생 가능성을 예측하는데 사용되는 통계 기법입니다.
  • 일반적인 회귀 분석의 목표와 동일하게 종속 변수와 독립 변수간의 관계를 구체적인 함수로 나타내어 향후 예측 모델에 사용합니다.
  • 장점
    • 결과가 ‘0 또는 1’과 같이 이산 분포일 때 선형 회귀 모형의 문제점을 보완합니다.
    • 선형회귀와 다르게 바로 ‘값’이 아닌 ‘확률’로서 분류합니다. 시그모이드 함수를 사용하여 설정하는 확률값(0.5)에따라서 0과 1으로 분류 할수 있습니다.
  • 단점
    • 선형회귀와 마찬가지로 독립변수와 종속변수가 선형 상관관계를 가지고 있다는 가정이 있어야 합니다.
    • 언더피팅되는 경향이 있습니다.


6.3.1 기본 모델 생성 및 학습

1
2
3
4
5
6
lr_model = LogisticRegression(random_state=87)
params_grid = [{}]

gridsearch = GridSearchCV(
    estimator=lr_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
GridSearchCV(cv=5, estimator=LogisticRegression(random_state=87), n_jobs=-1,
             param_grid=[{}], return_train_score=True)
  • 로지스틱회귀 모델을 생성하고 학습하였습니다.
  • 모델의 파라미터는 기본으로 설정하고, CV는 5회 진행합니다.


6.3.2 학습 결과 저장

1
2
3
result.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'mean_train_score': float(gridsearch.cv_results_['mean_train_score']),
           'mean_test_score': float(gridsearch.cv_results_['mean_test_score'])})
  • 추후 전체 결과를 시각화하여 표현하기 위해 결과를 저장합니다.


6.3.3 예측 및 검증

1
2
3
4
5
6
7
lr_best_model = gridsearch.best_estimator_
lr_best_model.fit(X_train, y_train)
lr_predict = lr_best_model.predict(X_test)

model_score(y_test, lr_predict)
plot_learning_curve(lr_best_model,
                    'Logistic Regression Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.89
Recall :  0.457
Precision :  0.808
F1_Score :  0.583

Confusion_matrix :
[[222   5]
 [ 25  21]]

  • 생성된 최적의 모델로 검증을 진행합니다.
  • Accuracy도 높게 나오고, 과적합이 일어나지 않습니다. 생각보다 괜찮은것 같습니다


6.3.4 중요 변수 확인

1
2
3
coefs = np.abs(lr_best_model.coef_[0])
pd.DataFrame(coefs, X_train.columns, columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'Important Feature')
plt.show()

  • 로지스틱 회귀에서 중요한 변수들을 확인합니다.
  • 로지스틱 회귀의 중요도 점수는 음수를 가질수 있기에, np.abs를 사용하여 절대값으로 비교하였습니다.
  • 현재의 그래프에서는 중요한 순서와 점수만 알수 있을뿐, 절대값을 주었기에 음수인지 양수인지는 확인 불가합니다.
  • 음수와 양수를 나누려면 절대값코드를 제거하면 됩니다.


6.4 XGBoost

  • Decision tree를 기반으로 한 Ensemble 방법으로 Boosting을 기반으로 하는 머신러닝 방법입니다.
  • Boosting은 여러개의 알고리즘이 순차적으로 학습을 하되 앞에 학습한 알고리즘 예측이 틀린 데이터에 대해 올바르게 예측할수 있도록 그 다음번 알고리즘에 가중치를 부여하여 학습과 예측을 진행하는 방식입니다.
  • 장점
    • GBM 기반의 알고리즘의 느린속도를 다양한 규제를 통해 해결하여 속도가 빠릅니다.
    • 병렬 학습이 가능하도록 설계됨
    • XGBoost는 반복 수행시 마다 내부적으로 학습데이터와 검증데이터를 교차검증으로 수행합니다.
    • 교차검증을 통해 최적화되면 반복을 중단하는 조기 중단 기능이 있습니다.
  • 단점
    • GBM보다는 빠르지만, 여전히 느립니다.

6.4.1 기본 모델 생성 및 학습

1
2
3
4
5
6
7
xgb_model = xgb.XGBClassifier(random_state = 87)

params_grid = [{}]

gridsearch = GridSearchCV(
    estimator=xgb_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GridSearchCV(cv=5,
             estimator=XGBClassifier(base_score=None, booster=None,
                                     colsample_bylevel=None,
                                     colsample_bynode=None,
                                     colsample_bytree=None, gamma=None,
                                     gpu_id=None, importance_type='gain',
                                     interaction_constraints=None,
                                     learning_rate=None, max_delta_step=None,
                                     max_depth=None, min_child_weight=None,
                                     missing=nan, monotone_constraints=None,
                                     n_estimators=100, n_jobs=None,
                                     num_parallel_tree=None, random_state=87,
                                     reg_alpha=None, reg_lambda=None,
                                     scale_pos_weight=None, subsample=None,
                                     tree_method=None, validate_parameters=None,
                                     verbosity=None),
             n_jobs=-1, param_grid=[{}], return_train_score=True)
  • XGBoost를 기본 파라미터를 사용하여 생성합니다.
  • 마찬가지로 CV는 5회 합니다.


6.4.2 학습 결과 및 저장

1
2
3
result.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'mean_train_score': float(gridsearch.cv_results_['mean_train_score']),
           'mean_test_score': float(gridsearch.cv_results_['mean_test_score'])})
  • 추후 전체 결과를 시각화하기 위해 학습 결과를 저장합니다.


6.4.3 예측 및 검증

1
2
3
4
5
6
7
xgb_best_model = gridsearch.best_estimator_
xgb_best_model.fit(X_train, y_train)
xgb_predict = xgb_best_model.predict(X_test)

model_score(y_test, xgb_predict)
plot_learning_curve(xgb_best_model,
                    'XGBoosting Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.883
Recall :  0.435
Precision :  0.769
F1_Score :  0.556

Confusion_matrix :
[[221   6]
 [ 26  20]]

  • Learning Curve를 확인한 결과 학습은 잘되었지만, 검증과는 차이가 많이 납니다.
  • 역시나 과대 적합 같습니다.


6.4.4 중요 변수 확인

1
2
3
4
important = xgb_best_model.feature_importances_

pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'Important Feature')
plt.show()

  • XGBoost에서의 중요 Feature들을 확인해 보았습니다.
  • Overtime이 모든 모델에 있어 중요한 변수로 확인됩니다.

6.5 LightGBM

  • LightGBM은 XGBoost와 함께 부스팅 계열에서 가장 각광받는 알고리즘으로, XGBoost에서 속도 및 편의 등을 개선하여 나온 알고리즘입니다.
  • 장점
    • LGBM의 큰 장점은 속도입니다. GBM 계열중에 제일 빠릅니다.
    • XGBoost보다 학습에 걸리는 시간이 훨씬 적으며, 메모리 사용량도 상대적으로 적다.
    • XGBoost와 마찬가지로 대용량 데이터에 대한 뛰어난 성능 및 병렬컴퓨팅 기능을 제공하고 최근에는 추가로GPU까지 지원한다.
  • 단점
    • 적은 수의 데이터에는 어울리지 않으며, 데이터가 적으면 과적합에 쉽게 걸린다. (일반적으로 10000건 이상의 데이터가 필요하다고 함)

6.5.1 기본 모델 생성 및 학습

1
2
3
4
5
6
lgbm_model = LGBMClassifier(random_state = 87)
params_grid = [{}]

gridsearch = GridSearchCV(
    estimator=lgbm_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
GridSearchCV(cv=5, estimator=LGBMClassifier(random_state=87), n_jobs=-1,
             param_grid=[{}], return_train_score=True)
  • LightGBM 모델을 기본 파라미터로 생성 합니다.
  • 마찬가지로 CV는 5회 합니다.


6.5.2 학습 결과 저장

1
2
3
result.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'mean_train_score': float(gridsearch.cv_results_['mean_train_score']),
           'mean_test_score': float(gridsearch.cv_results_['mean_test_score'])})
  • 전체 모델에 대한 시각화를 위해 학습 결과를 저장합니다.


6.5.3 예측 및 검증

1
2
3
4
5
6
lgbm_best_model = gridsearch.best_estimator_
lgbm_best_model.fit(X_train, y_train)
lgbm_predict = lgbm_best_model.predict(X_test)

model_score(y_test, lgbm_predict)
plot_learning_curve(lgbm_best_model, 'Light LGBM Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.864
Recall :  0.348
Precision :  0.696
F1_Score :  0.464

Confusion_matrix :
[[220   7]
 [ 30  16]]

  • Learning Curve를 확인한 결과 역시나 과대적합입니다.
  • 트리기반의 모델은 대부분 과대적합을 가지는 단점을 가지고 있습니다.


6.5.4 중요 변수 확인

1
2
3
4
important = lgbm_best_model.feature_importances_

pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'Important Feature')
plt.show()

  • LightGBM의 중요변수를 확인하였습니다.
  • Rate의 변수들이 모두 중요하다고 한것이 특이한 점입니다.


6.6 ROC Curve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
clf_pred_proba = clf_best_model.predict_proba(X_test)[:, 1]
clf_fpr, clf_tpr, thresholds = roc_curve(y_test, clf_pred_proba)
clf_roc_auc = roc_auc_score(y_test, clf_pred_proba)

rf_pred_proba = rf_best_model.predict_proba(X_test)[:, 1]
rf_fpr, rf_tpr, thresholds = roc_curve(y_test, rf_pred_proba)
rf_roc_auc = roc_auc_score(y_test, rf_pred_proba)

lr_pred_proba = lr_best_model.predict_proba(X_test)[:, 1]
lr_fpr, lr_tpr, thresholds = roc_curve(y_test, lr_pred_proba)
lr_roc_auc = roc_auc_score(y_test, lr_pred_proba)

xgb_pred_proba = xgb_best_model.predict_proba(X_test)[:, 1]
xgb_fpr, xgb_tpr, thresholds = roc_curve(y_test, xgb_pred_proba)
xgb_roc_auc = roc_auc_score(y_test, xgb_pred_proba)

lgbm_pred_proba = gridsearch.best_estimator_.predict_proba(X_test)[:, 1]
lgbm_fpr, lgbm_tpr, thresholds = roc_curve(y_test, lgbm_pred_proba)
lgbm_roc_auc = roc_auc_score(y_test, lgbm_pred_proba)



plt.figure(figsize=(12, 8))
plt.title('ROC Curve')

plt.plot([0,1], [0,1])

plt.plot(clf_fpr, clf_tpr, label='DecisionTree Roc Auc Score (area = %0.2f)' % clf_roc_auc)
plt.plot(rf_fpr, rf_tpr, label='RandomForest Roc Auc Score (area = %0.2f)' % rf_roc_auc)
plt.plot(lr_fpr, lr_tpr, label='LogisticRegression Roc Auc Score (area = %0.2f)' % lr_roc_auc)
plt.plot(xgb_fpr, xgb_tpr, label='XGBoosting Roc Auc Score (area = %0.2f)' % xgb_roc_auc)
plt.plot(lgbm_fpr, lgbm_tpr, label='Light LGBM Roc Auc Score (area = %0.2f)' % lgbm_roc_auc)


plt.grid()
plt.legend(loc="lower right")
plt.show()

  • 전체 모델의 ROC Curve와 AUC점수를 확인해 보았습니다.
  • ROC Curve는 그래프가 반대 ㄱ모양으로 생기는게 가장 이상적인 모양입니다.
  • AUC 점수는 ROC Curve의 아래 면적입니다. 1에 가까울수록 높은 수치 입니다.
  • 역시나 의사결정나무가 가장 낮고, 로지스틱회귀가 가장 높으며 나머지는 비슷합니다.


6.7 Accuracy 비교

1
2
3
4
5
6
df = pd.DataFrame(result)

plt.figure(figsize=(12, 8))
g = sns.barplot('mean_test_score','Estimator', data = df)
g.set_xlabel("Mean Accuracy")
plt.show()

  • 전체 모델의 Validation Accuracy를 시각화 하였습니다.
  • 의사결정나무를 제외하고는 다들 비슷한 성능을 보입니다.


7. Hyperparameter Tuning


7.1 Decision Tree Hyperparameter Tuning

7.1.1 튜닝 모델 생성 및 학습

1
2
3
4
5
6
7
8
9
10
11
clf_model = DecisionTreeClassifier(random_state=87)

params_grid = [{
    'max_depth': [5, 7, 9, 11], # 얼마나 깊게 들어가는지.
    'min_samples_split': [1, 2, 3, 4, 5], # 노드를 분할하기 위한 최소한의 샘플 데이터수 → 과적합을 제어하는데 사용
}]


gridsearch = GridSearchCV(
    estimator=clf_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
3
4
GridSearchCV(cv=5, estimator=DecisionTreeClassifier(random_state=87), n_jobs=-1,
             param_grid=[{'max_depth': [5, 7, 9, 11],
                          'min_samples_split': [1, 2, 3, 4, 5]}],
             return_train_score=True)
  • 의사결정나무의 파라미터를 튜닝하여 생성합니다.
  • 파라미터는 그리드서치를 사용하여 최적의 파라미터를 찾게 하였습니다.
  • 사용한 파라미터는 max_depth와 min_samples_split 입니다.


7.1.2 학습 결과 저장

1
2
3
4
result_hyper = []
result_hyper.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'hyper_mean_train_score': np.nanmean(gridsearch.cv_results_['mean_train_score']).astype(float),
           'hyper_mean_test_score': np.nanmean(gridsearch.cv_results_['mean_test_score']).astype(float)})
  • 추후에 전체 모델에 대한 학습한 결과를 시각화하기 위해 저장합니다.


7.1.3 예측 및 검증

1
2
3
4
5
6
7
8
9
clf_hyper_model = gridsearch.best_estimator_
clf_hyper_model.fit(X_train, y_train)
clf_hyper_predict = clf_hyper_model.predict(X_test)

model_score(y_test, clf_predict)
plot_learning_curve(clf_best_model, 'DecisionTree Learning Curve', X_train, y_train, cv=5)

model_score(y_test, clf_hyper_predict)
plot_learning_curve(clf_hyper_model, 'DecisionTree Hyperparameter Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.799
Recall :  0.37
Precision :  0.395
F1_Score :  0.382

Confusion_matrix :
[[201  26]
 [ 29  17]]

1
2
3
4
5
6
7
8
Accuracy :  0.832
Recall :  0.37
Precision :  0.5
F1_Score :  0.425

Confusion_matrix :
[[210  17]
 [ 29  17]]

  • 파라미터를 튜닝한뒤 결과를 확인합니다.
  • 확실히 학습에 규제를 주었기 때문에 Learning Curve가 서로 만나려고 합니다.
  • 튜닝전과 후의 결과도 전체적으로 나은 모습을 보입니다.


1
clf_hyper_model
1
DecisionTreeClassifier(max_depth=5, random_state=87)

7.1.4 중요 변수 확인

1
2
3
4
5
6
important = clf_best_model.feature_importances_
important_hyper = clf_hyper_model.feature_importances_

pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'Before Important Feature')
pd.DataFrame(important_hyper, X_train.columns,columns=['important_hyper']).sort_values(by = 'important_hyper', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'After Important Feature')
plt.show()

  • 파라미터를 튜닝하기 전과 튜닝 후의 중요변수를 확인해보았습니다.
  • 튜닝전에는 Rate 계열의 변수가 중요했다면, 지금은 Overtime과 같은 변수가 중요해지는 등의 변화가 보입니다.


7.1.5 결정나무 시각화

1
2
3
4
5
6
7
8
9
10
11
12
dot_data = export_graphviz(clf_hyper_model, out_file=None, 
                feature_names = X_train.columns,
                class_names = data.Attrition.unique(),
                max_depth = 5, # 표현하고 싶은 최대 depth
                precision = 3, # 소수점 표기 자릿수
                filled = True, # class별 color 채우기
                rounded=True, # 박스의 모양을 둥글게
               )
pydot_graph = pydotplus.graph_from_dot_data(dot_data)
pydot_graph.set_size("\"24\"")
gvz_graph = graphviz.Source(pydot_graph.to_string())
gvz_graph

  • 이번에도 결정나무를 시각화 하였습니다.
  • Max depth에 대해 규제를 주었기에 모든 결과를 볼수 있었습니다.


7.2 RandomForest Hyperparameter Tuning

7.2.1 튜닝 모델 생성 및 학습

1
2
3
4
5
6
7
8
9
10
11
12
rf_model = RandomForestClassifier(random_state=87)

params_grid = [{'n_estimators': [10, 50, 150], # 의사결정나무의 갯수
                'max_features': [0.3, 0.7], # 선택할 특성의 수
                'max_depth': [2, 4, 6, 8], # 얼마나 깊게 들어가는지
                'min_samples_split': [2, 4, 6, 8], # 노드를 분할하기 위한 최소 샘플 수
                'min_samples_leaf': [2, 4, 6, 8], # 한 노드에서 가지고 있어야 하는 최소 샘플 수
                }]

gridsearch = GridSearchCV(
    estimator=rf_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
3
4
5
6
GridSearchCV(cv=5, estimator=RandomForestClassifier(random_state=87), n_jobs=-1,
             param_grid=[{'max_depth': [2, 4, 6, 8], 'max_features': [0.3, 0.7],
                          'min_samples_leaf': [2, 4, 6, 8],
                          'min_samples_split': [2, 4, 6, 8],
                          'n_estimators': [10, 50, 150]}],
             return_train_score=True)
  • 그리드 서치를 이용하여 최적의 파라미터를 가진 모델을 찾습니다.
  • 파라미터는 n_estimators, max_features, max_depth, min_samples_split, min_samples_leaf를 사용하였습니다.


7.2.2 학습 결과 저장

1
2
3
result_hyper.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'hyper_mean_train_score': np.nanmean(gridsearch.cv_results_['mean_train_score']).astype(float),
           'hyper_mean_test_score': np.nanmean(gridsearch.cv_results_['mean_test_score']).astype(float)})
  • 전체 모델의 결과를 시각화하기 위해 학습 결과를 저장합니다.


7.2.3 예측 및 검증

1
rf_hyper_model
1
2
RandomForestClassifier(max_depth=8, max_features=0.3, min_samples_leaf=2,
                       n_estimators=50, random_state=87)
1
2
3
4
5
6
7
8
9
10
rf_hyper_model = gridsearch.best_estimator_
rf_hyper_model.fit(X_train, y_train)
rf_hyper_predict = rf_hyper_model.predict(X_test)

model_score(y_test, rf_predict)
plot_learning_curve(rf_best_model,
                    'RandomForest Learning Curve', X_train, y_train, cv=5)
model_score(y_test, rf_hyper_predict)
plot_learning_curve(rf_hyper_model,
                    'RandomForest Hyperparameter Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.861
Recall :  0.196
Precision :  0.9
F1_Score :  0.321

Confusion_matrix :
[[226   1]
 [ 37   9]]

1
2
3
4
5
6
7
8
Accuracy :  0.864
Recall :  0.196
Precision :  1.0
F1_Score :  0.327

Confusion_matrix :
[[227   0]
 [ 37   9]]

  • 파라미터 튜닝을 하여 찾은 최적의 모델을 사용하여 Learning Curve를 확인하였습니다.
  • 확실히, 이전보다 성능도 좋아지고, 학습곡선도 잘 나옵니다.


7.2.4 중요 변수 확인

1
2
3
4
5
6
important = rf_best_model.feature_importances_
important_hyper = rf_hyper_model.feature_importances_

pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'Before Important Feature')
pd.DataFrame(important_hyper, X_train.columns,columns=['important_hyper']).sort_values(by = 'important_hyper', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'After Important Feature')
plt.show()

  • 파라미터를 튜닝하기전과 하고난 뒤의 중요 Feature들을 비교합니다.
  • 마찬가지로 Overtime이 상위로 올라왔습니다.


7.2.5 결정나무 시각화

1
2
3
4
5
6
7
8
9
10
11
12
dot_data = export_graphviz(rf_hyper_model[0], out_file=None, 
                feature_names = X_train.columns,
                class_names = data.Attrition.unique(),
                max_depth = 5, # 표현하고 싶은 최대 depth
                precision = 3, # 소수점 표기 자릿수
                filled = True, # class별 color 채우기
                rounded=True, # 박스의 모양을 둥글게
               )
pydot_graph = pydotplus.graph_from_dot_data(dot_data)
pydot_graph.set_size("\"24\"")
gvz_graph = graphviz.Source(pydot_graph.to_string())
gvz_graph

  • 랜덤포레스트의 결정나무중 하나를 가져와서 확인해봅니다.
  • 이번에는 LowWorkingYears 변수를 시작하여 노드들이 나갑니다.


7.3 Logistic Regression Hyperparameter Tuning

7.3.1 튜닝 모델 생성 및 학습

1
2
3
4
5
6
7
8
9
10
11
lr_model = LogisticRegression(random_state=87, class_weight='balance')

params_grid = [{
    'solver': ['newton-cg', 'lbfgs', 'liblinear', 'library',  'sag', 'saga'], # 최적화에 사용할 알고리즘
    'C': [0.01, 0.1, 1, 5, 10], # 규칙강도의 역수 값
    'max_iter': list(range(1, 1000, 50)) # solver가 수렴하게 만드는 최대 반복값
}]

gridsearch = GridSearchCV(
    estimator=lr_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
3
4
5
6
7
8
9
10
11
GridSearchCV(cv=5,
             estimator=LogisticRegression(class_weight='balance',
                                          random_state=87),
             n_jobs=-1,
             param_grid=[{'C': [0.01, 0.1, 1, 5, 10],
                          'max_iter': [1, 51, 101, 151, 201, 251, 301, 351, 401,
                                       451, 501, 551, 601, 651, 701, 751, 801,
                                       851, 901, 951],
                          'solver': ['newton-cg', 'lbfgs', 'liblinear',
                                     'library', 'sag', 'saga']}],
             return_train_score=True)
  • 로지스틱 회귀를 마찬가지로 그리드 서치를 사용하여 생성합니다.
  • solver, C, max_iter의 파라미터를 사용하였습니다.


7.3.2 학습 결과 저장

1
2
3
result_hyper.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'hyper_mean_train_score': np.nanmean(gridsearch.cv_results_['mean_train_score']).astype(float),
           'hyper_mean_test_score': np.nanmean(gridsearch.cv_results_['mean_test_score']).astype(float)})
  • 추후 전체 데이터 시각화를 위해 학습결과를 저장합니다.


7.3.3 예측 및 검증

1
2
3
4
5
6
7
8
9
10
11
12
lr_hyper_model = gridsearch.best_estimator_
lr_hyper_model.fit(X_train, y_train)
lr_hyper_predict = lr_hyper_model.predict(X_test)

model_score(y_test, lr_predict)
plot_learning_curve(lr_best_model,
                    'Logistic Regression Learning Curve', X_train, y_train, cv=5)


model_score(y_test, lr_hyper_predict)
plot_learning_curve(lr_hyper_model,
                    'Logistic Regression Hyperparameter Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.89
Recall :  0.457
Precision :  0.808
F1_Score :  0.583

Confusion_matrix :
[[222   5]
 [ 25  21]]

1
2
3
4
5
6
7
8
Accuracy :  0.894
Recall :  0.478
Precision :  0.815
F1_Score :  0.603

Confusion_matrix :
[[222   5]
 [ 24  22]]

  • 트리기반의 모델들 보다는 뚜렷하게 나아진점이 보이진 않지만, 약하게 나마 Learning Curve도 좋아지는 모습을 보입니다.
  • 또한, 성능도 좋은 수준입니다.


7.3.4 중요 변수 확인

1
2
3
4
5
important = np.abs(lr_best_model.coef_[0])
important_hyper = np.abs(lr_hyper_model.coef_[0])
pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'Before Important Feature')
pd.DataFrame(important_hyper, X_train.columns,columns=['important_hyper']).sort_values(by = 'important_hyper', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'After Important Feature')
plt.show()

  • 중요한 변수를 확인해 보았습니다.
  • 트리기반 모델들은 rate 계열의 컬럼을 중요하게 나왔는데, 회귀모델은 overtime이 중요하게 나옵니다.


7.4 XGBoost

7.4.1 튜닝 모델 생성 및 학습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xgb_model = xgb.XGBClassifier(
    random_state=87, objective='binary:logistic', booster='gbtree')

params_grid = [{
    'n_estimators': list(range(10, 110, 10)), # 의사결정나무의 수
    'min_child_weight': list(range(5, 11, 2)), # 자식노드에서 관측되는 최소 가중치의 합
    'gamma': [0.1, 0.5, 0.7, 1, 1.3], # 트리의 노드에서 추가 파티션을 만들기 위해 필요한 최소 손실 감소. 크면 클수록 더 보수적인 알고리즘이 생성됨
    'max_depth': list(range(1, 11, 2)), # 얼마나 깊게 들어가는지

}]

gridsearch = GridSearchCV(
    estimator=xgb_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GridSearchCV(cv=5,
             estimator=XGBClassifier(base_score=None, booster='gbtree',
                                     colsample_bylevel=None,
                                     colsample_bynode=None,
                                     colsample_bytree=None, gamma=None,
                                     gpu_id=None, importance_type='gain',
                                     interaction_constraints=None,
                                     learning_rate=None, max_delta_step=None,
                                     max_depth=None, min_child_weight=None,
                                     missing=nan, monotone_constraints=None,
                                     n_estimators=100, n_jobs=None,
                                     num_parallel_tree=None, random_state=87,
                                     reg_alpha=None, reg_lambda=None,
                                     scale_pos_weight=None, subsample=None,
                                     tree_method=None, validate_parameters=None,
                                     verbosity=None),
             n_jobs=-1,
             param_grid=[{'gamma': [0.1, 0.5, 0.7, 1, 1.3],
                          'max_depth': [1, 3, 5, 7, 9],
                          'min_child_weight': [5, 7, 9],
                          'n_estimators': [10, 20, 30, 40, 50, 60, 70, 80, 90,
                                           100]}],
             return_train_score=True)
  • 파라미터 튜닝을 하여 XGBoost를 생성합니다.
  • 사용한 파라미터는 n_estimators, min_child_weight, gamma, max_depth 입니다.


7.4.2 학습 결과 저장

1
2
3
result_hyper.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'hyper_mean_train_score': np.nanmean(gridsearch.cv_results_['mean_train_score']).astype(float),
           'hyper_mean_test_score': np.nanmean(gridsearch.cv_results_['mean_test_score']).astype(float)})
  • 전체 모델에 대해 결과를 시각화하기 위해 결과 저장


7.4.3 예측 및 검증

1
2
3
4
5
6
7
8
9
10
11
12
xgb_hyper_model = gridsearch.best_estimator_
xgb_hyper_model.fit(X_train, y_train)
xgb_hyper_predict = xgb_hyper_model.predict(X_test)

model_score(y_test, xgb_predict)
plot_learning_curve(xgb_best_model,
                    'XGBoosting Learning Curve', X_train, y_train, cv=5)


model_score(y_test, xgb_hyper_predict)
plot_learning_curve(xgb_hyper_model,
                    'XGBoosting Hyperparameter Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.883
Recall :  0.435
Precision :  0.769
F1_Score :  0.556

Confusion_matrix :
[[221   6]
 [ 26  20]]

1
2
3
4
5
6
7
8
Accuracy :  0.886
Recall :  0.348
Precision :  0.941
F1_Score :  0.508

Confusion_matrix :
[[226   1]
 [ 30  16]]

  • 파라미터를 사용하여 모델을 생성한 결과 많은 수치는 아니지만 꽤 좋은 성능을 내는 모델이 생성되었습니다.
  • Recall의 점수는 떨어졌지만, Precision의 점수는 더욱 올라 F1-score가 좋아졌습니다.


7.4.4 중요 변수 확인

1
2
3
4
5
6
important = xgb_best_model.feature_importances_
important_hyper = xgb_hyper_model.feature_importances_

pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'Before Important Feature')
pd.DataFrame(important_hyper, X_train.columns,columns=['important_hyper']).sort_values(by = 'important_hyper', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'After Important Feature')
plt.show()

  • 파라미터를 튜닝하기 전과 후의 중요변수를 보면 YearWithCurrManager가 갑자기 상승하였습니다.


7.5 LightGBM

7.5.1 튜닝 모델 생성 및 학습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lgbm_model = LGBMClassifier(random_state=87)

params_grid = [{
    'boosting_type': ['gbdt', 'dart', 'rf', 'goss'], # 알고리즘 타입
    'num_leaves': [5, 10, 20], # 잎사귀의 수
    'max_depth': [1, 2, 3, 4, 5, 7, 9, 11], # 깊이의 수
    'reg_alpha': [0.1, 0.5], # L1 정규화
    'lambda_l1': [0, 1, 1.5], # L1 정규화
    'lambda_l2': [0, 1, 1.5], # L2 정규화
}]

gridsearch = GridSearchCV(
    estimator=lgbm_model, param_grid=params_grid, cv=5, n_jobs=-1, return_train_score=True)
gridsearch.fit(X_train, y_train)
1
2
3
4
5
6
7
8
9
10
11
12
13
[LightGBM] [Warning] lambda_l1 is set=1, reg_alpha=0.1 will be ignored. Current value: lambda_l1=1
[LightGBM] [Warning] lambda_l2 is set=1.5, reg_lambda=0.0 will be ignored. Current value: lambda_l2=1.5





GridSearchCV(cv=5, estimator=LGBMClassifier(random_state=87), n_jobs=-1,
             param_grid=[{'boosting_type': ['gbdt', 'dart', 'rf', 'goss'],
                          'lambda_l1': [0, 1, 1.5], 'lambda_l2': [0, 1, 1.5],
                          'max_depth': [1, 2, 3, 4, 5, 7, 9, 11],
                          'num_leaves': [5, 10, 20], 'reg_alpha': [0.1, 0.5]}],
             return_train_score=True)
  • 그리드 서치를 이용하여 LightGBM의 파라미터를 튜닝 및 최적의 모델을 생성하였습니다.
  • 파라미터는 boosting_type, num_leaves, max_depth, reg_alpha, lambda_l1, lambda_l2 를 사용했습니다.
  • 아무래도 트리기반의 모델들은 depth가 중요한 파라미터인듯 합니다.


7.5.2 학습 결과 저장

1
2
3
result_hyper.append({'Estimator': str(gridsearch.best_estimator_).split('(')[0],
           'hyper_mean_train_score': np.nanmean(gridsearch.cv_results_['mean_train_score']).astype(float),
           'hyper_mean_test_score': np.nanmean(gridsearch.cv_results_['mean_test_score']).astype(float)})
  • 전체 모델에 대한 결과를 시각화 하기 위해 결과를 저장합니다.


7.5.3 예측 및 검증

1
2
3
4
5
6
7
8
9
10
11
lgbm_hyper_model = gridsearch.best_estimator_
lgbm_hyper_model.fit(X_train, y_train)
lgbm_hyper_predict = lgbm_hyper_model.predict(X_test)

model_score(y_test, lgbm_predict)
plot_learning_curve(lgbm_best_model,
                    'LightGBM Learning Curve', X_train, y_train, cv=5)

model_score(y_test, lgbm_hyper_predict)
plot_learning_curve(lgbm_hyper_model,
                    'LightGBM Hyperparmeter Learning Curve', X_train, y_train, cv=5)
1
2
3
4
5
6
7
8
Accuracy :  0.864
Recall :  0.348
Precision :  0.696
F1_Score :  0.464

Confusion_matrix :
[[220   7]
 [ 30  16]]

1
2
3
4
5
6
7
8
Accuracy :  0.861
Recall :  0.304
Precision :  0.7
F1_Score :  0.424

Confusion_matrix :
[[221   6]
 [ 32  14]]

  • 이번엔 튜닝을 하여도 성능이 좋아진것 같지 않습니다.
  • Learning Curve만 보면 좋아진것 같지만, 그외의 다른것들의 점수가 다 낮아졌습니다.
  • 사실 LGBM은 데이터가 적으면 큰 성능을 보이지 못합니다. 최소 데이터가 10000개는 있어야 한다고 합니다.


7.5.4 중요 변수 확인

1
2
3
4
5
6
important = lgbm_best_model.feature_importances_
important_hyper = lgbm_hyper_model.feature_importances_

pd.DataFrame(important, X_train.columns,columns=['important']).sort_values(by = 'important', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'Before Important Feature')
pd.DataFrame(important_hyper, X_train.columns,columns=['important_hyper']).sort_values(by = 'important_hyper', ascending=False).plot(kind='bar', figsize = (36, 10), rot = 45, title = 'After Important Feature')
plt.show()

  • 튜닝을 하여도 큰 변화가 없어서 그런지 중요변수도 크게 변화한것 같지 않습니다.
  • 그대로 Rate 계열의 변수가 상위권에 있습니다.


7.6 ROC Curve, AUC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
clf_pred_proba = clf_best_model.predict_proba(X_test)[:, 1]
clf_fpr, clf_tpr, thresholds = roc_curve(y_test, clf_pred_proba)
clf_roc_auc = roc_auc_score(y_test, clf_pred_proba)

clf_hyper_pred_proba = clf_hyper_model.predict_proba(X_test)[:, 1]
clf_hyper_fpr, clf_hyper_tpr, thresholds = roc_curve(y_test, clf_hyper_pred_proba)
clf_hyper_roc_auc = roc_auc_score(y_test, clf_hyper_pred_proba)

####

rf_pred_proba = rf_best_model.predict_proba(X_test)[:, 1]
rf_fpr, rf_tpr, thresholds = roc_curve(y_test, rf_pred_proba)
rf_roc_auc = roc_auc_score(y_test, rf_pred_proba)

rf_hyper_pred_proba = rf_hyper_model.predict_proba(X_test)[:, 1]
rf_hyper_fpr, rf_hyper_tpr, thresholds = roc_curve(y_test, rf_hyper_pred_proba)
rf_hyper_roc_auc = roc_auc_score(y_test, rf_hyper_pred_proba)

###

lr_pred_proba = lr_best_model.predict_proba(X_test)[:, 1]
lr_fpr, lr_tpr, thresholds = roc_curve(y_test, lr_pred_proba)
lr_roc_auc = roc_auc_score(y_test, lr_pred_proba)

lr_hyper_pred_proba = lr_hyper_model.predict_proba(X_test)[:, 1]
lr_hyper_fpr, lr_hyper_tpr, thresholds = roc_curve(y_test, lr_hyper_pred_proba)
lr_hyper_roc_auc = roc_auc_score(y_test, lr_hyper_pred_proba)

###

xgb_pred_proba = xgb_best_model.predict_proba(X_test)[:, 1]
xgb_fpr, xgb_tpr, thresholds = roc_curve(y_test, xgb_pred_proba)
xgb_roc_auc = roc_auc_score(y_test, xgb_pred_proba)

xgb_hyper_pred_proba = xgb_hyper_model.predict_proba(X_test)[:, 1]
xgb_hyper_fpr, xgb_hyper_tpr, thresholds = roc_curve(y_test, xgb_hyper_pred_proba)
xgb_hyper_roc_auc = roc_auc_score(y_test, xgb_hyper_pred_proba)

###

lgbm_pred_proba = gridsearch.best_estimator_.predict_proba(X_test)[:, 1]
lgbm_fpr, lgbm_tpr, thresholds = roc_curve(y_test, lgbm_pred_proba)
lgbm_roc_auc = roc_auc_score(y_test, lgbm_pred_proba)

lgbm_hyper_pred_proba = lgbm_hyper_model.predict_proba(X_test)[:, 1]
lgbm_hyper_fpr, lgbm_hyper_tpr, thresholds = roc_curve(y_test, lgbm_hyper_pred_proba)
lgbm_hyper_roc_auc = roc_auc_score(y_test, lgbm_hyper_pred_proba)



fig = plt.figure(figsize=(24,12))

ax1 = fig.add_subplot(1,2,1)
ax2 = fig.add_subplot(1,2,2)

ax1.plot([0, 1], [0, 1])
ax1.plot(clf_fpr, clf_tpr,
         label='DecisionTree Roc Auc Score (area = %0.2f)' % clf_roc_auc)
ax1.plot(rf_fpr, rf_tpr,
         label='RandomForest Roc Auc Score (area = %0.2f)' % rf_roc_auc)
ax1.plot(lr_fpr, lr_tpr,
         label='LogisticRegression Roc Auc Score (area = %0.2f)' % lr_roc_auc)
ax1.plot(xgb_fpr, xgb_tpr,
         label='XGBoosting Roc Auc Score (area = %0.2f)' % xgb_roc_auc)
ax1.plot(lgbm_fpr, lgbm_tpr,
         label='LightLGBM Roc Auc Score (area = %0.2f)' % lgbm_roc_auc)
ax1.set_title('Before Hyperparameter Tuning')
ax1.grid()
ax1.legend(loc="lower right")

ax2.plot([0, 1], [0, 1])
ax2.plot(clf_hyper_fpr, clf_hyper_tpr,
         label='DecisionTree Roc Auc Score (area = %0.2f)' % clf_hyper_roc_auc)
ax2.plot(rf_hyper_fpr, rf_hyper_tpr,
         label='RandomForest Roc Auc Score (area = %0.2f)' % rf_hyper_roc_auc)
ax2.plot(lr_hyper_fpr, lr_hyper_tpr,
         label='LogisticRegression Roc Auc Score (area = %0.2f)' % lr_hyper_roc_auc)
ax2.plot(xgb_hyper_fpr, xgb_hyper_tpr,
         label='XGBoosting Roc Auc Score (area = %0.2f)' % xgb_hyper_roc_auc)
ax2.plot(lgbm_hyper_fpr, lgbm_hyper_tpr,
         label='LightLGBM Roc Auc Score (area = %0.2f)' % lgbm_hyper_roc_auc)
ax2.set_title('After Hyperparameter Tuning')
ax2.grid()
ax2.legend(loc="lower right")
plt.show()

  • 파라미터 튜닝 후 전체적으로 성능이 좋아진것을 알수 있습니다.


7.7 Validation Score 확인

1
2
3
4
5
6
7
8
df = pd.DataFrame(result)
df2 = pd.DataFrame(result_hyper)
df = pd.merge(df, df2, on='Estimator')

df.plot.bar(x='Estimator', y=[
                   'mean_test_score', 'hyper_mean_test_score'], rot=360, figsize=(16, 8), title = 'Test Score Compare')
plt.legend(loc='lower left')
plt.show()

  • 의사결정나무를 제외하고는 파라미터를 수정하여 Accuracy가 눈에띄게 좋아진것은 없습니다.
  • 하지만 Accuracy만이 모델의 성능을 측정하는 지표는 아니며, Accuracy가 비슷하거나 조금 떨어졌지만, 그외의 지표들이 높아졌으니, 성능에 개선에 있었다고 이야기할수 있습니다.


8. 회고


8.1 회고

  • 만일 데이터가 더 많았다면, 성능을 더 많이 올릴수 있을것 같았는데 아쉽습니다.
  • 해당 내용에는 없지만 SMOTE를 사용하여 오버샘플링을 해보았지만, 오버샘플링의 단점인 과적합에 빠져, 오히려 검증시에는 성능이 안좋게 나왔습니다.
  • 그래도 각 알고리즘별로 파라미터 튜닝을 실습해보고 했던것에 만족합니다.
  • 많은 시간을 할애하여 진행했던 프로젝트지만 목표 Accuray는 0.9에 근접하게 나오긴 했지만 넘지는 못하여 아쉽습니다.
  • 추후에 같은 데이터를 딥러닝에 적용하여 해봐야 겠습니다.
This post is licensed under CC BY 4.0 by the author.