Решение задачи предсказания ухода сотрудников компании. Цель - выявить, какие факторы влияют на уход сотрудников из компании, а также предложить меры по увеличению лояльности сотрудников.
library(DBI)
library(RMariaDB)
library(dplyr)
library(ggplot2)
library(plotly)
library(DT)
library(tidymodels)
con <- dbConnect(RMariaDB::MariaDB(),
user='studentminor',
password='DataMinorHSE!2020',
dbname='employee',
host='34.88.193.134',
port = 3306)
#медианное значение возраста
age = dbGetQuery(con, "SELECT Age FROM profile")
median_age = median(age$Age)
#Подсчет доли сотрудников, покинувших компанию
young = dbGetQuery(con, "SELECT Attrition, count(*) as n FROM portfolio join profile on portfolio.EmployeeNumber = profile.EmployeeNumber where Age<36 group by Attrition")
all = dbGetQuery(con, "SELECT Attrition, count(*) as n FROM portfolio join profile on portfolio.EmployeeNumber = profile.EmployeeNumber where Age>=36 group by Attrition")
y_attr = round(young[2,2]/(young[1,2]+young[2,2]),3)
a_attr = round(all[1,2]/(all[2,2]+all[1,2]),3)
young = young %>% mutate(cat = "y")
all =all %>% mutate(cat = "a")
ALL = rbind(young,all)
ALL$Attrition=ALL$Attrition %>% as.factor()
ALL$n = as.numeric(ALL$n)
count = ALL %>% group_by(cat) %>% summarize(n = sum(n))
young$n = young$n %>% as.numeric()
all$n = all$n %>% as.numeric()
Выбрана подгруппа молодых сотрудников, возраст которых меньше медианного уровня (36 лет). Сохранение молодых кадров - важная задача для фирмы, ведь они могут принести пользу в долгосрочной перспективе. Молодые сотрудники более мобильны, а следовательно с большей вероятностью могут покинуть компанию. Это подтверждается и данными: доля молодых сотрудников, покинувших компанию - 0.228, а для сотрудников, которым больше 36 лет, эта доля составляет 0.112.
plot_ly(
labels = c("Всего", "Старше 35 лет", "Молодые", "Остались", "Ушли",
"Ушли ", "Остались "),
parents = c("", "Всего", "Всего", "Молодые", "Молодые", "Старше 35 лет", "Старше 35 лет"),
values = c(sum(ALL$n), count$n, young$n, all$n),
type = 'sunburst',
branchvalues = 'total'
) %>% layout(title="Доля покинувших компанию")
Одним из важных факторов лоялности сотрудника видится зарплата.
income = dbGetQuery(con, "SELECT MonthlyIncome, Attrition FROM portfolio join profile on portfolio.EmployeeNumber = profile.EmployeeNumber where Age<36")
ggplot(income)+geom_boxplot(aes(x = as.factor(Attrition),y =MonthlyIncome),fill = "#BC8F8F")+ggtitle("Распределение ежемесячного дохода \nв зависимости от статуса")+ylab("Доход")+ xlab("Статус")+scale_x_discrete(labels = c("Остался", "Ушел"))+theme_bw()
График показывает, что молодые сотрудники, покинувшие компанию, в среднем получали меньше, чем те, кто остался. После анализа моделей можно будет сделать окончательный вывод о важности этой переменной.
Для дальнейшей работы необходимо загрузить датасет со всеми необходимыми переменными, предварительно отфильтровав наблюдения по возрасту сотрудников.
data = dbGetQuery(con, "SELECT Attrition, Education, MonthlyIncome, EduFieldId, Gender, MaritalStatus, NumCompaniesWorked, TotalWorkingYears, WorkLifeBalance, DistanceFromHome, BusinessTravel, Department, EnvironmentSatisfaction, JobInvolvement, JobRole, JobSatisfaction, OverTime, PercentSalaryHike, PerformanceRating, RelationshipSatisfaction, TrainingTimesLastYear, YearsAtCompany, YearsInCurrentRole, YearsSinceLastPromotion, YearsWithCurrManager FROM portfolio join profile on portfolio.EmployeeNumber = profile.EmployeeNumber where Age<36")
data = data %>% mutate_if(is.character, as.factor)
data$Attrition=data$Attrition %>% as.factor()
#отключаемся от базы
dbDisconnect(con)
set.seed(101)
split <- initial_split(data)
train <- training(split)
test <- testing(split)
balance = round(table(train$Attrition)[1]/table(train$Attrition)[2],1)
Выборка несбалансированная - сотрудников, покинувших компанию, в 3.4 раза больше, чем оставшихся.
a = table(train$Attrition) %>% as.data.frame() %>% rename(status = Var1,number = Freq)
a$status = ifelse(a$status=="0","Остался", "Ушел")
knitr::kable(a)
| status | number |
|---|---|
| Остался | 302 |
| Ушел | 90 |
train_A1 <- train %>% filter(Attrition == "1")
train_w <- train
for (i in 1:2){
train_w <- rbind(train_w, train_A1)
}
balance_new=round(table(train_w$Attrition)[1]/table(train_w$Attrition)[2],1)
Чтобы решить эту проблему, будут переопределены веса.Соотношение оставшихся к ушедшим стало 1.1.
log.wf = logistic_reg() %>%
fit(Attrition~., data = train_w)
#logreg <- logistic_reg()
#log.wf <- workflow() %>%
# add_formula(Attrition ~.) %>%
# add_model(logreg) %>%
# fit(train_w)
predtest.logreg = log.wf %>%
predict(test)
log.metrics_test = predtest.logreg %>%
cbind(test) %>%
conf_mat(Attrition, .pred_class) %>% summary() %>% filter(.metric == "accuracy"|.metric == "sens"|.metric == "spec") %>% select(-.estimator)
predtrain.logreg = log.wf %>%
predict(train_w)
log.metrics_train = predtrain.logreg %>%
cbind(train_w) %>%
conf_mat(Attrition, .pred_class) %>% summary() %>% filter(.metric == "accuracy"|.metric == "sens"|.metric == "spec") %>% select(-.estimator)
log.metrics_train=log.metrics_train %>% rename(train = .estimate)
log.metrics_test=log.metrics_test %>% rename(test =.estimate)
knitr::kable(log.metrics_train %>% inner_join(log.metrics_test))
| .metric | train | test |
|---|---|---|
| accuracy | 0.8601399 | 0.8015267 |
| sens | 0.8642384 | 0.8333333 |
| spec | 0.8555556 | 0.6896552 |
Модель с хорошими показателями accuracy, specificity и sensetivity. Она не является переобученной (результаты на тестовой выборке не так сильно отличаются от результатов на обучающей выборке).
tree <- decision_tree(
mode = "classification") %>%
set_engine("rpart")
tree.wf <- workflow() %>%
add_formula(Attrition ~.) %>%
add_model(tree) %>%
fit(train_w)
predtest.tree = tree.wf %>%
predict(test)
tree.metrics_test = test %>%
cbind(predtest.tree) %>%
conf_mat(Attrition, .pred_class)%>% summary() %>% filter(.metric == "accuracy"|.metric == "sens"|.metric == "spec") %>% select(-.estimator)
predtrain.tree = tree.wf %>%
predict(train_w)
tree.metrics_train = train_w %>%
cbind(predtrain.tree) %>%
conf_mat(Attrition, .pred_class)%>% summary() %>% filter(.metric == "accuracy"|.metric == "sens"|.metric == "spec") %>% select(-.estimator)
tree.metrics_test=tree.metrics_test %>% rename(test = .estimate)
tree.metrics_train=tree.metrics_train %>% rename(train=.estimate)
knitr::kable(tree.metrics_test %>% inner_join(tree.metrics_train))
| .metric | test | train |
|---|---|---|
| accuracy | 0.7328244 | 0.8531469 |
| sens | 0.7549020 | 0.8211921 |
| spec | 0.6551724 | 0.8888889 |
Показатели этой модели хуже, чем логистической регресии.
rf = rand_forest(mode = "classification", mtry = 7) %>%
set_engine('randomForest')
wf_rf = workflow() %>%
add_model(rf) %>%
add_formula(Attrition~.) %>%
fit(train_w)
predtrain.rf = predict(wf_rf, train_w)
predtest.rf = predict(wf_rf, test)
accuracyTrain.rf =
accuracy_vec(train_w$Attrition, predtrain.rf$.pred_class)
accuracyTest.rf =
accuracy_vec(test$Attrition, predtest.rf$.pred_class)
accuracy.rf = tree.metrics_test %>% inner_join(tree.metrics_train) %>% filter(.metric=="accuracy") %>% mutate(test = accuracyTest.rf, train =accuracyTrain.rf )
knitr::kable(accuracy.rf)
| .metric | test | train |
|---|---|---|
| accuracy | 0.8167939 | 1 |
set.seed(101)
xgb = boost_tree(mode = "classification", mtry = 3) %>%
set_engine('xgboost')
wf_xgb = workflow() %>%
add_model(xgb) %>%
add_formula(Attrition~.) %>%
fit(train_w)
predtrain.xgb = predict(wf_xgb, train_w)
predtest.xgb = predict(wf_xgb, test)
accuracyTrain.xgb =
accuracy_vec(train_w$Attrition, predtrain.xgb$.pred_class)
accuracyTest.xgb =
accuracy_vec(test$Attrition, predtest.xgb$.pred_class)
accuracy_xgb = accuracy.rf %>% mutate(test = accuracyTest.xgb, train =accuracyTrain.xgb)
knitr::kable(accuracy_xgb)
| .metric | test | train |
|---|---|---|
| accuracy | 0.7862595 | 0.9597902 |
Модели 3 и 4 оказались сильно переобучены. Будет использоваться модель логистической регрессии.
library(vip)
vip:::vip.model_fit(log.wf)
Самым важным показателем в модели оказалось наличие переработок.
ggplot(data)+geom_bar(aes(x = as.factor(OverTime), fill = Attrition), position = 'fill') +xlab("Переработки")+ scale_x_discrete(labels = c("Нет", "Есть")) +scale_fill_brewer(labels = c("Остался", "Ушел"), name = "Статус", palette = "Dark2")+theme_bw()
Действительно, доля покинувших компанию среди тех, у кого есть переработки значительно выше (почти 50%), чем среди тех, кто не перерабатывает (около 13%).
Предположим, что компания может реорганизовать рабочий процесс и это позволит снизить число переработок в 3 раза.
test2 = test
set.seed(101)
test2$OverTime[test2$OverTime == "Yes"] =
sample(c("Yes", "No"),
size = length(test2$OverTime[test2$OverTime == "Yes"]),
replace = T, prob = c(0.33, 0.67))
predTest_new = predict(log.wf, test2)
ggplot(test) + geom_bar(aes(x = Attrition), alpha = 0.5) +
geom_bar(data = data.frame(predTest_new), aes(x = .pred_class), alpha = 0.5, fill = "red")+xlab("Статус")+scale_x_discrete(labels = c("Остался", "Ушел"))+ylab(" ")+theme_bw()+scale_fill_discrete("Этап")
После изменения ситуация улучшилась, но не очень сильно. Другим важным фактором является число тренингов. Имеет смысл попробовать провести для каждого сотрудника на 2 тренинга в год больше. Это относительно несложно реализовать. Также можно постарасться повысить мобльность сотрудников внутри компании, воздействуя на показатель YearsInCurrentRole. Это не обязательно должно быть повышение (вертикальная мобильность), это может быть горизонтальная мобильность, в определенной степени смена должностных обязанностей, получение новой роли в процессах компании. Предположим, что удасться уменьшить пребывание сотрудника в одной роли в среднем на полгода.
test2$TrainingTimesLastYear = test2$TrainingTimesLastYear+2
test2$YearsInCurrentRole = test2$YearsInCurrentRole-0.5
predTest_new2 = predict(log.wf, test2)
ggplot(test) + geom_bar(aes(x = Attrition), alpha = 0.5) +
geom_bar(data = data.frame(predTest_new2), aes(x = .pred_class), alpha = 0.5, fill = "red")+xlab("Статус")+scale_x_discrete(labels = c("Остался", "Ушел"))+ylab(" ")+theme_bw()
Модель предсказывает, что после предлагаемых изменений отток сотрудников уменьшится.
Молодые сотрудники являются наиболее мобильной частью персонала, в связи с чем уровень их оттока достаточно высокий, однако приоритетом компании может быть сохранение ценных кадров и долгосрочное сотрудничество. Важнейшими аспектами, влияющими на желание сотрудника покинуть фирму являются наличие переработок, продолжительность работы в одной роли и число тренингов. Положительные изменения в этих показателях могут привести к снижению вероятности ухода сотрудника из компании.