Решаемая задача: Уменьшение текучести кадров.
Зачем: Для сокращения издержек на персонал, т.к. уровень текучести кадров непосредственно связан с тем, как регулярно и в каких объёмах нужно привлекать новых сотрудников (чтобы закрыть освободившиеся позиции). Привлечение новых сотрудников, в свою очередь, всегда связано с специфичными затратами – поиск, селекция, испытания, интервью, адаптация, etc. Таким образом, уменьшив текучесть кадров мы уменьшим затраты компании на привлечение новых сотрудников. Такое уменьшение затрат положительно отразится на отчетности компании в целом.
План действий:
Подготовительный этап:
# Подключение библиотек
library(RSQLite)
library(plotly)
library(DBI)
library(RMariaDB)
library(fastDummies)
library(tidymodels)
library(dbplyr)
library(formattable)
library(partykit)
library(caret)
library(vip)
library(plotly)
# Подключение к базе
connection = dbConnect(
RMariaDB::MariaDB(),
user='studentminor',
password='DataMinorHSE!2020',
dbname='employee',
host='34.88.193.134',
port = 3306)
Переменная, на основе которой выбирается сегмент для анализа: Рабочий отдел (Department);
Основание для выбора этой переменной: Специфика труда (характер требуемых знаний/навыков, тип и объём нагрузки, etc.) может быть тем, что модерирует влияние других характеристик на целевую переменную. Таким образом, переменные, определенные как важные при предсказание оттока, могут быть важным сугубо для какого-то одного отдела сотрудников (к примеру, для самого многчисленного «Research & Development»), но не для всех остальных отделов. В контексте этого предположения кажется логичным сконцентрироваться на каком-то одном департаменте, так как в противном случае выводы анализа могут быть слишком общими, не учитывающими особенности отделов по отдельности.
Теперь посмотрим, есть ли в компании проблемные отделы с точки зрения оттока сотрудников. Для этого посчитаем, какую долю составляют уволившиеся сотрудники в каждом конкретном отделе:
departments_workers = dbGetQuery(
connection,
"SELECT Department, COUNT(*) AS number_of_workers
FROM portfolio
GROUP BY Department
ORDER BY Department ASC")
departments_atrrition = dbGetQuery(
connection,
"SELECT Department, Attrition, COUNT(*) AS number_of_workers
FROM portfolio
GROUP BY Department, Attrition
ORDER BY Department ASC")
departments_atrrition_table = departments_atrrition %>%
inner_join(departments_workers, by = "Department") %>%
rename(churners_number = number_of_workers.x) %>%
mutate(percent_of_churners = round(((churners_number/number_of_workers.y)*100), 1)) %>%
filter(Attrition == 1) %>%
select(-number_of_workers.y, -Attrition)
customRed_1 = "#ffa09e"
customRed_2 = "#ff5f5c"
formattable(
departments_atrrition_table %>% select(-churners_number) %>% rename('Percent of churners' = percent_of_churners),
align = c("l", "l"),
list(
`Department` = formatter("span", style = ~ style(color = "grey", font.weight = "bold")),
`Percent of churners` = color_bar(customRed_1)))
| Department | Percent of churners |
|---|---|
| Human Resources | 15.8 |
| Research & Development | 15.3 |
| Sales | 20.7 |
Проблемный отдел: Отдел продаж («Sales»);
Обоснование Доля уволившихся сотрудников в отделе «Sales» составляет 20.7%, в то время как в двух других – 15.8% и 15.3% (разница в ~30-35 процентных пунктов).
Подтверждение статистической значимости наблюдаемой разницы c помощью Chi-Square теста:
dep_churners_table = data.frame(
Human_Resources = as.numeric(c(departments_atrrition$n[1], departments_atrrition$n[2])),
Research_Development = c(departments_atrrition$n[3], departments_atrrition$n[4]),
Sales = c(departments_atrrition$n[5], departments_atrrition$n[6]))
rownames(dep_churners_table) = c("Not churned", "Churned")
dep_churners_table$Human_Resources = as.numeric(dep_churners_table$Human_Resources)
dep_churners_table$Research_Development = as.numeric(dep_churners_table$Research_Development)
dep_churners_table$Sales = as.numeric(dep_churners_table$Sales)
dep_churners_table = as.data.frame(t(dep_churners_table))
dep_churners_table_1 = dep_churners_table[1:2,]
# Объединение в виду крайне малого количества сотрудников в отделе "Human Resources"
dep_churners_table_1 = dep_churners_table_1 %>% summarise(`Not churned` = sum(`Not churned`), Churned = sum(Churned))
dep_churners_table_2 = dep_churners_table[3,]
table_for_test = rbind(dep_churners_table_1, dep_churners_table_2)
test = chisq.test(table_for_test)
test
Pearson's Chi-squared test with Yates' continuity correction
data: table_for_test
X-squared = 4.2441, df = 1, p-value = 0.03939
Итог: P-value < 0.05. Исходя из этого можно сказать, что разница в оттоке сотруников между отделом «Sales» и всей остальной компанией является статистически значимой.
Теперь, когда проблемный сегмент был найден, продолжим анализ, сфокусировавшись на этом проблемном сегменте.
Для начала определим используемые данные и переменные:
Получим нужные данные с помощью SQL-запроса:
request_for_sales_dep = dbGetQuery(
connection,
"SELECT Attrition,
BusinessTravel,
EnvironmentSatisfaction,
JobInvolvement,
JobRole,
JobSatisfaction,
MonthlyIncome,
OverTime,
PercentSalaryHike,
PerformanceRating,
RelationshipSatisfaction,
TrainingTimesLastYear,
YearsInCurrentRole,
YearsSinceLastPromotion,
YearsWithCurrManager,
DistanceFromHome,
WorkLifeBalance,
EduFieldId,
Education
FROM portfolio
INNER JOIN profile USING(EmployeeNumber)
WHERE Department = 'Sales'")
dbDisconnect(connection)
# Превращение текстовых переменных в факторные
request_for_sales_dep[sapply(request_for_sales_dep, is.character)] = lapply(request_for_sales_dep[sapply(request_for_sales_dep, is.character)], as.factor)
request_for_sales_dep$Attrition = as.factor(request_for_sales_dep$Attrition)
cat(paste0(
"Количество наблюдений: ",
dim(request_for_sales_dep)[1],
"\n",
"Количество переменных: ",
dim(request_for_sales_dep)[2]),
collapse = "\n")
Количество наблюдений: 319
Количество переменных: 19
Итог: Наши данные содержат 319 наблюдений по 19 переменным.
Теперь разделим данные на обучующую и тестовые выборки в стандартном соотношении 80/20:
set.seed(315)
split = initial_split(request_for_sales_dep)
train_data = training(split)
test_data = testing(split)
cat(paste0(
"Количество наблюдений в тестовой выборке: ",
nrow(test_data),
'\n',
"Количество наблюдений в тренировочной выборке: ",
nrow(train_data)),
collapse = "\n")
Количество наблюдений в тестовой выборке: 80
Количество наблюдений в тренировочной выборке: 239
Всё необходимое сделано, теперь мы можем создать модель.
Создадим следующую модель:
Attrition.set.seed(35)
сross_val = vfold_cv(train_data, v = 5)
set.seed(35)
model = decision_tree(
mode = "classification",
min_n = tune())
workflow = workflow() %>%
add_model(model) %>%
add_formula(Attrition ~.)
tuning = tune_grid(workflow, resamples = сross_val) %>%
select_best("accuracy")
final_tree = finalize_model(model, tuning)
tree_model = final_tree %>%
fit(Attrition ~., train_data)
Оценим модель на тренерующей и тестовой выборках:
# Предсказания оттока для тренерующих данных
predictions_on_train = tree_model %>%
predict(train_data)
# Оценка предскзаания
c_mat_train = train_data %>%
cbind(predictions_on_train) %>%
conf_mat(Attrition, .pred_class)
c_mat_train_summary = c_mat_train %>% summary %>% select(-.estimator) %>% rename(train_estimate = .estimate)
# Предсказания оттока для тестовых данных
predictions_on_test = tree_model %>%
predict(test_data)
# Оценка предскзаания
c_mat_test = test_data %>%
cbind(predictions_on_test) %>%
conf_mat(Attrition, .pred_class)
c_mat_test_summary = c_mat_test %>% summary %>% select(-.estimator, -.metric) %>% rename(test_estimate = .estimate)
# Отображение оценки предсказания
c_mat = c_mat_train_summary %>% cbind(c_mat_test_summary) %>% rename(metric = .metric)
c_mat = c_mat[c(1, 3:4),]
c_mat[c(2,3)] = round(c_mat[c(2,3)], 2)
row.names(c_mat) = NULL
formattable(
c_mat,
align = c("l", "l", "l"),
list(
`metric` = formatter("span", style = ~ style(color = "grey", font.weight = "bold")),
area(col = train_estimate:test_estimate) ~ color_tile("transparent", "lightblue")))
| metric | train_estimate | test_estimate |
|---|---|---|
| accuracy | 0.84 | 0.82 |
| sens | 0.93 | 0.90 |
| spec | 0.49 | 0.53 |
Итоги:
Модель уже можно назвать рабочей, её sensitivity довольно высокий – 0.90, то есть если модель предсказывет увольненение сотруднику, то очень вероятно, что он действительно уволится. В то же время значение specificity куда ниже – 0.5. Данное число можно трактовать следующим образом: каждому ~2-у сотруднику, который не уволился, мы предсказали увольнение. Для борьбы с текучестью это значит следующее: примерно в половине случаев мы бы потратили лишние ресурсы для того, чтобы предотвратить увольнение тех, кто и не планировал увольняться. Нехорошо, но не критично, так как вряд ли бороться с текучестью таким образом менее выгодно, чем просто ничего с ней не делать и тратить, вероятно, куда большие ресурсы на привлечение новых сотрудников. Но, да такой уровень specificity устраивает нас не до конца – есть куда расти. В связи с этим попробуем улучшить модель.
Низкое specificity может являться следствием неравномерного распрделения данных по целевой переменной. В нашем случае мы как раз имеем дело с таким распределением – доля уволившихся почти в 4 раза меньше доли действущих сотрудников:
cat(paste0(
"Доля уволившихся сотрудников: ",
round(table(request_for_sales_dep$Attrition)[2]/nrow(request_for_sales_dep) * 100, 2), "%",
'\n',
"Доля действущих сотрудников: ",
round(table(request_for_sales_dep$Attrition)[1]/nrow(request_for_sales_dep) * 100, 2), "%"),
collapse = "\n")
Доля уволившихся сотрудников: 20.69%
Доля действущих сотрудников: 79.31%
Что дальше: Для улучшения дел в такой ситуации необходимо уровнять количество уволившихся и действущих сотрудников. Сделаем это 3 способами: up sampling’ом, down sampling’ом и вменением данных методом весов. После создадим модели на основе соответсвующих данных, оценим получившиеся модели на тестовой выборке и сравним по ключевым метрикам с первоначальной моделью:
# Функция для оценки модели на тестовых данных
check_prediction = function(data_train, naming) {
set.seed(35)
сross_val = vfold_cv(data_train, v = 5)
set.seed(35)
model = decision_tree(mode = "classification", min_n = tune())
workflow = workflow() %>% add_model(model) %>% add_formula(Attrition ~.)
tuning = tune_grid(workflow, resamples = сross_val) %>% select_best("accuracy")
final_tree = finalize_model(model, tuning)
tree_model = final_tree %>% fit(Attrition ~., data_train)
predictions_on_test = tree_model %>% predict(test_data)
c_mat_test = test_data %>% cbind(predictions_on_test) %>% conf_mat(Attrition, .pred_class)
c_mat_test_summary = c_mat_test %>% summary %>% select(-.estimator, -.metric)
colnames(c_mat_test_summary) = naming
c_mat_test_summary = c_mat_test_summary[c(1, 3:4),]
return(c_mat_test_summary)}
# Up sampling данных
train_data_up = recipe(Attrition ~ ., data = train_data) %>%
themis::step_upsample(Attrition) %>%
prep(training = train_data, retain = TRUE) %>%
bake(new_data = NULL)
# nrow(train_data_up) = 380
c_mat_up = check_prediction(train_data_up, naming = "up_estimate")
# Down sampling данных
train_data_down = recipe(Attrition ~ ., data = train_data) %>%
themis::step_downsample(Attrition) %>%
prep(training = train_data, retain = TRUE) %>%
bake(new_data = NULL)
# nrow(train_data_down) = 98
c_mat_down = check_prediction(train_data_down, naming = "down_estimate")
# Вменение данных методом весов
# table(request_for_sales_dep$Attrition)[1]/table(request_for_sales_dep$Attrition)[2] = 3.8(3)
train_data_t = train_data %>% filter(Attrition == "1")
train_data_weighted = train_data
for (i in 1:3) {train_data_weighted = rbind(train_data_weighted, train_data_t)}
# nrow(train_data_weighted) = 386
c_mat_weighted = check_prediction(train_data_weighted, naming = "weighted_estimate")
# Вывод значений метрик модели при разных тренирующих данных
c_mat_sum = c_mat %>%
select(-train_estimate) %>%
rename(base_estimate = test_estimate) %>%
cbind(c_mat_up, c_mat_down, c_mat_weighted)
c_mat_sum[c(2:5)] = round(c_mat_sum[c(2:5)], 2)
formattable(
c_mat_sum,
align = c("l", "l", "l", "l"),
list(
`metric` = formatter("span", style = ~ style(color = "grey", font.weight = "bold")),
area(col = base_estimate:weighted_estimate) ~ color_tile("transparent", "lightblue")))
| metric | base_estimate | up_estimate | down_estimate | weighted_estimate |
|---|---|---|---|---|
| accuracy | 0.82 | 0.74 | 0.44 | 0.76 |
| sens | 0.90 | 0.79 | 0.33 | 0.83 |
| spec | 0.53 | 0.53 | 0.82 | 0.53 |
Сравнение моделей, полученных на новых тренировочных данных с изначальной моделью: Что касается моделей up и down sampling’а, то да, мы получаем увеличенный specificity, но при этом значительно жертвуя sensetivity (особенно в случае с down sampling’ом) в контексте, где верно предсказать того, кто уволится (sensetivity) кажется куда более важным, чем не ошибиться, предсказав увольнение тому, кто останется в компании (specificity). В случае с моделью, натренированной на взвешенных данных, всё ещё более очевидно – никакого прироста specificity при меньшем значении sensetivity.
Итог: Первоначальная модель является наилучшей.
Рассмотрим переменные с точки зрения их важности при принятии сотрудником решения о том, увольняться или нет:
var_importance = vi(tree_model)
formattable(
var_importance,
align = c("l", "l"),
list(
`Variable` = formatter("span", style = ~ style(color = "grey", font.weight = "bold")),
`Importance`= color_tile("transparent", "lightblue")))
| Variable | Importance |
|---|---|
| OverTime | 6.2382171 |
| MonthlyIncome | 6.1318291 |
| JobSatisfaction | 5.2653846 |
| YearsWithCurrManager | 2.6297901 |
| YearsInCurrentRole | 1.9048855 |
| YearsSinceLastPromotion | 0.8047987 |
| JobRole | 0.7190474 |
| DistanceFromHome | 0.4283867 |
| WorkLifeBalance | 0.3832116 |
| Education | 0.2726112 |
| JobInvolvement | 0.1382828 |
| TrainingTimesLastYear | 0.1343284 |
| BusinessTravel | 0.1065255 |
Итог: Наиболее важными при принятии решений об увольнении являются факт переработки (OverTime) и месячный доход (MonthlyIncome).
Заметка: На предыдущем этапе были обнаружены переменные, которые явлются ключевыми при принятии решения об увольнении. Но одного знания этих переменных недостаточно для того, чтобы предложить изменения – нужно также понимать как именно значениях этих переменных связаны с решением об увольнении. В этом блоке эта связь будет изучена для каждой из ключевых переменных.
data_overwork = request_for_sales_dep %>%
mutate(
Attrition = ifelse(Attrition == 1, "Да", "Нет"),
OverTime = ifelse(OverTime == "Yes", "Да", "Нет"),
Group = case_when(
Attrition == "Да" & OverTime == "Да" ~ 1,
Attrition == "Нет" & OverTime == "Да" ~ 2,
Attrition == "Да" & OverTime == "Нет" ~ 3,
Attrition == "Нет" & OverTime == "Нет" ~ 4))
data_overwork_counts = data_overwork %>% group_by(OverTime, Attrition) %>%
group_by(OverTime) %>%
mutate(n_over_t = n()) %>%
ungroup() %>%
group_by(OverTime, Attrition, n_over_t) %>%
summarise(Count = n()) %>%
mutate(Share = round((100*Count/n_over_t), 2)) %>%
select(-n_over_t)
tooltip_data = as.vector(data_overwork_counts[3:4])
data_overwork = data_overwork %>%
mutate(
Text = case_when(
Group == 1 ~ paste0("Кол-во сотрудников: ", tooltip_data[1,1], "\n", "Доля: ", tooltip_data[1,2], "%"),
Group == 2 ~ paste0("Кол-во сотрудников: ", tooltip_data[2,1], "\n", "Доля: ", tooltip_data[2,2], "%"),
Group == 3 ~ paste0("Кол-во сотрудников: ", tooltip_data[3,1], "\n", "Доля: ", tooltip_data[3,2], "%"),
Group == 4 ~ paste0("Кол-во сотрудников: ", tooltip_data[4,1], "\n", "Доля: ", tooltip_data[4,2], "%")))
bar_overwork = ggplot(data_overwork) +
geom_bar(
aes(
fill = Attrition,
x = OverTime,
text = Text),
position = "fill") +
scale_fill_manual(name = "Уволился", values = c("brown2", "seagreen3")) +
theme_minimal() +
ylab("Доля уволившихся") +
xlab("Наличие переработки") +
ggtitle("Доля уволившихся в зависимости от наличия переработки")
ggplotly(bar_overwork, tooltip = c("text", "x", "fill"))
Как свяазано с оттоком: Из графика видно, что наличие переработки (OverTime = 1) положительно связано с решением об увольнении: среди, тех, кто перерабатывает/перерабатывал доля уволившихся составляет 37%, в то время как среди тех, кто не перерабатывает/не перерабатывал – только 14%.
Предлагаемое изменение: Оптимизировать рабочий процесс сотрудников так, чтобы им не приходилось перерабатывать. В качестве проверки этого изменения мы можем начать с оптимизации рабочей нагрузки ~25% перерабатывающих сотрудников.
Симуляция:
Первым делом сократим кол-во перерабатывающих сотрудников на ~25%:
work_data = request_for_sales_dep
work_data$OverTimeBefore = work_data$OverTime
# Предсказние оттока до изменения ситуации с переработкой
predicted_attr_init = tree_model %>% predict(work_data)
# Улучшение ситуации с переработкой
set.seed(315)
overtime_change = sample(
c("No", "Yes"),
size = length(work_data$OverTime[work_data$OverTime == "Yes"]),
replace = T,
prob = c(0.25, 0.75))
work_data$OverTime[work_data$OverTime == "Yes"] = overtime_change
# Предсказние оттока после изменения ситуации с переработкой
predicted_attr = tree_model %>% predict(work_data)
work_data = work_data %>%
cbind(predicted_attr_init) %>%
rename(
AttritionBefore = .pred_class) %>%
cbind(predicted_attr) %>%
rename(
AttritionAfter = .pred_class,
OverTimeAfter = OverTime)
rus_levels = c("Нет", "Да")
levels(work_data$AttritionAfter) = rus_levels
levels(work_data$AttritionBefore) = rus_levels
levels(work_data$OverTimeAfter) = rus_levels
levels(work_data$OverTimeBefore) = rus_levels
cat(paste0(
"Кол-во перерабатывающих сотрудников до изменения: ",
length(work_data$OverTimeAfter[work_data$OverTimeBefore == "Да"]), "\n",
"Кол-во перерабатывающих после изменения: ",
length(work_data$OverTimeAfter[work_data$OverTimeAfter == "Да"]), "\n",
"Наскольо уменьшилось количество перерабатывающих сотрудников: ",
length(work_data$OverTimeAfter[work_data$OverTimeBefore == "Да"]) - length(work_data$OverTimeBefore[work_data$OverTimeAfter == "Да"]), "\n",
"Наскольо уменьшилось доля перерабатывающих сотрудников: ",
round(100*(1 - length(work_data$OverTimeAfter[work_data$OverTimeAfter == "Да"])/length(work_data$OverTimeBefore[work_data$OverTimeBefore == "Да"])), 2), "%"),
collapse = "\n")
Кол-во перерабатывающих сотрудников до изменения: 91
Кол-во перерабатывающих после изменения: 69
Наскольо уменьшилось количество перерабатывающих сотрудников: 22
Наскольо уменьшилось доля перерабатывающих сотрудников: 24.18%
Теперь посмотрим как осуществленное изменение отразилось на оттоке:
attrition_change = data.frame(
`Уволившиеся` = c(
length(work_data$AttritionBefore[work_data$AttritionBefore == "Да"]),
length(work_data$AttritionAfter[work_data$AttritionAfter == "Да"])))
rownames(attrition_change) = c("До измнения", "После измнения")
formattable(
attrition_change,
align = c("r"),
list(`Уволившиеся` = color_bar("lightgray")))
| Уволившиеся | |
|---|---|
| До измнения | 53 |
| После измнения | 45 |
Итог: Изменение сработало. Количество сотрудников, готовых уволиться уменьшилось на 8 человек с 53 до 45 (-15%).
data_month_income = request_for_sales_dep %>% mutate(Attrition = ifelse(Attrition == 1, "Да", "Нет"))
income_histogram = plot_ly(alpha = 0.6, hovertemplate = 'Количество: %{y}, Доход: %{x}<extra></extra> ') %>%
add_histogram(
data = data_month_income %>% filter(Attrition == "Нет"),
x = ~MonthlyIncome, color = ~Attrition,
colors = c("brown2", "seagreen3")) %>%
add_histogram(
data = data_month_income %>% filter(Attrition == "Да"),
x = ~MonthlyIncome, color = ~Attrition,
colors = c("brown2", "seagreen3")) %>%
layout(
barmode = "overlay",
title = "Месячный доход в зависимости от того уволился ли сотрудник или нет",
legend = list(title = list(text='Уволился или нет')),
xaxis = list(title = 'Месячный доход (y.e.)'))
income_histogram
Как свяазано с оттоком: Из графика видно, что распрделение месячных доходов тех, кто уволился, сильнее смещено влево, чем распрделение месячных доходов тех, кто продолжает работать – зарплаты текущих сотрудников чаще всего расположены в диапозоне от 4000 до 7000 (у.e.), в то время как зарпалты уволившихся – в диапозоне от 2000 до 6000 (у.е.). Таким образом можно сделать вывод, что меньшая зарплата положительно связана с решением об увольнении.
Предлагаемое изменение: Повысить зарплаты низкооплачиваемых сотрудников, то есть тех сотрудников, которые в текущий момент получают меньше 6000 (у.е.) в месяц. В качестве проверки этого изменения мы можем начать с повышения зарплаты ~30% обозначенной категории сотрудников.
Симуляция:
Для начала повысим зарплаты сотрудникам:
work_data_2 = request_for_sales_dep
work_data_2$MonthlyIncomeBefore = work_data_2$MonthlyIncome
# Предсказние оттока до повышения зарплат
predicted_attr_init_2 = tree_model %>% predict(work_data_2)
# Повышение зарплат
set.seed(315)
pay_change = sample(
c(1500, 0),
size = length(work_data_2$MonthlyIncome[work_data_2$MonthlyIncome < 6000]),
replace = T,
prob = c(0.15, 0.85))
work_data_2$MonthlyIncome[work_data_2$MonthlyIncome < 6000] = work_data_2$MonthlyIncome[work_data_2$MonthlyIncome < 6000] + pay_change
# Предсказние оттока после повышения зарплат
predicted_attr_2 = tree_model %>% predict(work_data_2)
work_data_2 = work_data_2 %>%
cbind(predicted_attr_init_2) %>%
rename(
AttritionBefore = .pred_class) %>%
cbind(predicted_attr_2) %>%
rename(
AttritionAfter = .pred_class,
MonthlyIncomeAfter = MonthlyIncome)
levels(work_data_2$AttritionAfter) = rus_levels
levels(work_data_2$AttritionBefore) = rus_levels
cat(paste0(
"Кол-во сотрудников, которым была повышена зарплата: ",
length(pay_change[pay_change == 1500]), "\n",
"Доля сотрудников из очерченного сегмента, которым была повышена зарплата: ",
round(100*length(pay_change[pay_change == 1500])/length(pay_change), 2), "%", "\n",
"Кол-во сотрудников, которым дополнительно можно было бы повысить зарплату: ",
length(pay_change) - length(pay_change[pay_change == 1500]), "\n",
"Насколько суммарно возросли месячные расходы на зарплаты: ",
sum(pay_change), "\n",
"Насколько возросли месячные расходы на зарплаты в процентах: ",
round((100*(sum(work_data_2$MonthlyIncomeAfter)/sum(work_data_2$MonthlyIncomeBefore)) - 100), 2), "%"))
Кол-во сотрудников, которым была повышена зарплата: 25
Доля сотрудников из очерченного сегмента, которым была повышена зарплата: 15.43%
Кол-во сотрудников, которым дополнительно можно было бы повысить зарплату: 137
Насколько суммарно возросли месячные расходы на зарплаты: 37500
Насколько возросли месячные расходы на зарплаты в процентах: 1.67%
Теперь посмотрим как осуществленное изменение отразилось на оттоке:
attrition_change = data.frame(
`Уволившиеся` = c(
length(work_data_2$AttritionBefore[work_data_2$AttritionBefore == "Да"]),
length(work_data_2$AttritionAfter[work_data_2$AttritionAfter == "Да"])))
rownames(attrition_change) = c("До измнения", "После измнения")
formattable(
attrition_change,
align = c("r"),
list(`Уволившиеся` = color_bar("lightgray")))
| Уволившиеся | |
|---|---|
| До измнения | 53 |
| После измнения | 46 |
Итог: Изменение сработало. Количество сотрудников, готовых уволиться уменьшилось на 7 человек с 53 до 46 (-13%).
Возможности: Из-за данного изменения расходы на заработную плату увеличились менее, чем на 1,5%, однако это все же привело к заметному снижению оттока (13%). В то же время зарплаты были подняты лишь ~15% низкооплачиваемых сотрудников, в то время как у оставшейся части (~75%) месячный заработок не изменился – повысив зарплату этой части, отток можно было бы сократить ещё значительнее.
Взглянем на то, как изменится отток, если оба вышеописанных изменения осуществить одновременно:
work_data_3 = request_for_sales_dep
work_data_3$OverTimeBefore = work_data_3$OverTime
work_data_3$MonthlyIncomeBefore = work_data_3$MonthlyIncome
# Предсказние оттока до изменений
predicted_attr_init_3 = tree_model %>% predict(work_data_3)
# Повышение зарплат
work_data_3$MonthlyIncome[work_data_3$MonthlyIncome < 6000] = work_data_3$MonthlyIncome[work_data_3$MonthlyIncome < 6000] + pay_change
# Улучшение ситуации с переработкой
work_data_3$OverTime[work_data_3$OverTime == "Yes"] = overtime_change
# Предсказние оттока после повышения зарплат
predicted_attr_3 = tree_model %>% predict(work_data_3)
work_data_3 = work_data_3 %>%
cbind(predicted_attr_init_3) %>%
rename(
AttritionBefore = .pred_class) %>%
cbind(predicted_attr_3) %>%
rename(
AttritionAfter = .pred_class,
MonthlyIncomeAfter = MonthlyIncome)
levels(work_data_3$AttritionAfter) = rus_levels
levels(work_data_3$AttritionBefore) = rus_levels
attrition_change = data.frame(
`Уволившиеся` = c(
length(work_data_3$AttritionBefore[work_data_3$AttritionBefore == "Да"]),
length(work_data_3$AttritionAfter[work_data_3$AttritionAfter == "Да"])))
rownames(attrition_change) = c("До измнения", "После измнения")
formattable(
attrition_change,
align = c("r"),
list(`Уволившиеся` = color_bar("lightgray")))
| Уволившиеся | |
|---|---|
| До измнения | 53 |
| После измнения | 38 |
Итог: Изменение сработало. Количество сотрудников, готовых уволиться уменьшилось на 15 человек с 53 до 38 (-28%).
Заметка: Эффект при одновременном осуществлении двух изменений фактически представляет сумму эффектов каждого из изменений по отдельности (при том сумму без каких либо потерь): 15% (изолированный эффект первого изменения) + 13% (изолированный эффект второго изменения) = 28% (эффект при одновременном осуществлении обоих изменений). Это весьма позитивный факт, означающий то, что объекты изменения изолированы друг от друга, и, если какие-то проблемы возникнут с изменением одного из этих объектов, это никак не отразится на изменении другого объекта. То есть, к примеру, если вдруг окажется, что зарплаты перестали играть важную роль при принятии решения об увольнении, у нас всё равно будет (скорее всего) рабочий вариант с уменьшением переработок.
Целевая аудитория: Сотрудники компании, в компетенции которых находится вопрос уменьшения текучести, а также принятие соотвествующих мер:
Цель дэшборда: Наглядно отобразить с помощью чего и насколько можно уменьшить текучесть.
Содержание дэщборда: Графики и value box’ы, отображающие текущее состояние дел в отделе «Sales» и то, как это состояние может быть изменено при помощи разработанных мною мер.
Для работы был выбран статистически наиболее проблемный отдел с точки зрения оттока сотрудников – отдел продаж. Далее, на основе tree-based алгоритма была разработана модель для предсказания того, уволится сотрудник или нет. Модель обладает высокими значениями, accuracy и sensitivity, но средними значениями specificity. Уравнивание популяций по целевой переменной (использовались up/down sampling и метод весов) никак не улучшило ситуацию. При изучении модели удалось установить, что наиболее важными при принятии решений об увольнении являются факт переработки и месячный доход. На основе этой информации, были разработаны два изменения, призванные уменьшить отток сотрудников. Изменения были протестированы и их работоспособность была доказана: 1-е изменения уменьшает отток на 15%, 2-е на 13%, а оба изменения вместе – на 28%. В конце работы был создан дэшборд наглядно демонстрирующий то, как разработанные меры способны улучшить ситуацию с оттоком сотрудников из отдела продаж.