Задача

Решаемая задача: Уменьшение текучести кадров.

Зачем: Для сокращения издержек на персонал, т.к. уровень текучести кадров непосредственно связан с тем, как регулярно и в каких объёмах нужно привлекать новых сотрудников (чтобы закрыть освободившиеся позиции). Привлечение новых сотрудников, в свою очередь, всегда связано с специфичными затратами – поиск, селекция, испытания, интервью, адаптация, 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» и всей остальной компанией является статистически значимой.

Теперь, когда проблемный сегмент был найден, продолжим анализ, сфокусировавшись на этом проблемном сегменте.

Модель

Предварительные приготолвения данных

Для начала определим используемые данные и переменные:

  • Используемые данные: Данные о сотрудниках отдела «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 

Всё необходимое сделано, теперь мы можем создать модель.

Создание модели

Создадим следующую модель:

  • Алгоритм, использующийся для предсказания переменной: tree-based;
  • Целевая переменная: Attrition.
  • Использование кросс-валидации Да, для уменьшения риска влияния особенностей тренирующих/тестирующих данных на качество модели;
  • Минимальное количество наблюдение в узле для дальнейшего разибения: Подбирается автоматически так, чтобы accuracy модели был наилучшим.
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

Итоги:

  • Нет оверфитинга модели: значения метрик для тестовых данных практически не отличаются от значений для данных, используемых для тренировки;
  • Высокие значения accuracy и sensitivty, но средние значения specificity.

Модель уже можно назвать рабочей, её 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% обозначенной категории сотрудников.

Симуляция:

Для начала повысим зарплаты сотрудникам:

  • Размер повышения: 1500 (у.е.);
  • Кому будут повышены зарплаты: ~15% тех, кто получает меньше 6000 (у.е.) в месяц.
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% (эффект при одновременном осуществлении обоих изменений). Это весьма позитивный факт, означающий то, что объекты изменения изолированы друг от друга, и, если какие-то проблемы возникнут с изменением одного из этих объектов, это никак не отразится на изменении другого объекта. То есть, к примеру, если вдруг окажется, что зарплаты перестали играть важную роль при принятии решения об увольнении, у нас всё равно будет (скорее всего) рабочий вариант с уменьшением переработок.

Дэшборд

Целевая аудитория: Сотрудники компании, в компетенции которых находится вопрос уменьшения текучести, а также принятие соотвествующих мер:

  • HR специалисты;
  • Менеджеры высшего и среднего звена.

Цель дэшборда: Наглядно отобразить с помощью чего и насколько можно уменьшить текучесть.

Содержание дэщборда: Графики и value box’ы, отображающие текущее состояние дел в отделе «Sales» и то, как это состояние может быть изменено при помощи разработанных мною мер.

Общие выводы

Для работы был выбран статистически наиболее проблемный отдел с точки зрения оттока сотрудников – отдел продаж. Далее, на основе tree-based алгоритма была разработана модель для предсказания того, уволится сотрудник или нет. Модель обладает высокими значениями, accuracy и sensitivity, но средними значениями specificity. Уравнивание популяций по целевой переменной (использовались up/down sampling и метод весов) никак не улучшило ситуацию. При изучении модели удалось установить, что наиболее важными при принятии решений об увольнении являются факт переработки и месячный доход. На основе этой информации, были разработаны два изменения, призванные уменьшить отток сотрудников. Изменения были протестированы и их работоспособность была доказана: 1-е изменения уменьшает отток на 15%, 2-е на 13%, а оба изменения вместе – на 28%. В конце работы был создан дэшборд наглядно демонстрирующий то, как разработанные меры способны улучшить ситуацию с оттоком сотрудников из отдела продаж.