Классификационная модель обучена 14 249 сотрудников

В HR хорошо известно, что набор новых сотрудников значительно дороже, чем сохранение существующих талантов. Уходящие сотрудники берут с собой ценный опыт и знания из вашей организации. По данным Forbes, стоимость смены должности начального уровня оценивается в 50% от заработной платы этого сотрудника. Для сотрудников среднего звена она составляет 125% от зарплаты, а для руководителей высшего звена - колоссальные 200% от зарплаты.

Мы обучим некоторые модели машинного обучения в Блокноте Jupyter, используя данные о должности, счастье сотрудника. , производительность, рабочая нагрузка и срок полномочий для прогнозирования собираются ли они остаться или уйти.

Наша целевая переменная категориальна, поэтому задача машинного обучения - классификация. (Для числовой цели задача становится регрессией.)

Мы будем использовать набор данных с elitedatascience.com, который имитирует крупную компанию с 14 249 бывшими и настоящими сотрудниками. Всего 10 столбцов.

Шаги следующие:

  1. EDA и обработка данных: исследуйте, визуализируйте и очищайте данные.
  2. Разработка функций: используйте опыт в предметной области и создавайте новые функции.
  3. Обучение модели: мы обучим и настроим некоторые проверенные алгоритмы классификации, такие как логистическая регрессия, случайные леса и деревья с градиентным усилением.
  4. Оценка эффективности. Мы рассмотрим ряд оценок, включая F1 и AUROC.
  5. Развертывание: пакетный запуск или попросите инженеров по обработке данных / инженеров машинного обучения построить автоматизированный конвейер?

В идеале компания будет использовать модель для своих нынешних постоянных сотрудников, чтобы выявлять тех, кто находится в группе риска. Это пример машинного обучения, дающего действенную информацию о бизнесе.

1. Исследование и обработка данных

Исследовательский анализ данных (EDA) помогает нам понять данные и предоставляет идеи и идеи для очистки данных и разработки функций. Очистка данных подготавливает данные для наших алгоритмов, в то время как разработка функций - это волшебный соус, который действительно поможет нашим алгоритмам извлечь основные закономерности из набора данных. Помнить:

Лучшие данные всегда лучше, чем более сложные алгоритмы!

Начнем с загрузки некоторых стандартных пакетов Python для обработки данных в JupyterLab.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sb
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier,
                             GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import confusion_matrix, accuracy_score,
                            f1_score, roc_curve, roc_auc_score
import pickle

Импортируйте набор данных:

df = pd.read_csv('employee_data.csv')

Вот снова снимок нашего фрейма данных. Форма - (14,249, 10).

Целевая переменная - статус. Эта категориальная переменная принимает значение Занят или Влево.

Есть 25 столбцов / функций:

  • отдел
  • зарплата
  • удовлетворение, поданная_ жалоба - доводы счастья
  • last_evaluation, Recent_promoted - прокси для производительности
  • avg_monthly_hrs, n_projects - прокси для рабочей нагрузки
  • срок полномочий - прокси для опыта

1.1 Числовые характеристики

Давайте построим несколько быстрых гистограмм, чтобы получить представление о распределении наших числовых характеристик.

df.hist(figsize=(10,10), xrot=-45)

Что нужно сделать с нашими числовыми функциями, чтобы данные работали правильно с нашими алгоритмами:

  • Преобразуйте NaN в filed_complaint и Recent_promoted в 0. Они были неправильно помечены.
  • Перед преобразованием NaN в ноль создайте индикаторную переменную для отсутствующих данных в функции last_evaluation.
df.filed_complaint.fillna(0, inplace=True)
df.recently_promoted.fillna(0, inplace=True)
df['last_evaluation_missing'] =         
df.last_evaluation.isnull().astype(int)
df.last_evaluation.fillna(0, inplace=True)

Вот тепловая карта корреляции для наших числовых характеристик.

sb.heatmap(df.corr(),
 annot=True,
 cmap=’RdBu_r’,
 vmin=-1,
 vmax=1)

1.2 Категориальные признаки

Давайте построим несколько графиков быстрой панели для наших категориальных функций. Сиборн для этого отлично подходит.

for feature in df.dtypes[df.dtypes=='object'].index:
    sb.countplot(data=df, y='{}'.format(features))

Самый большой отдел - это продажи. Лишь небольшая часть сотрудников имеет высокую зарплату. И наш набор данных несбалансирован в том смысле, что только меньшая часть сотрудников покинула компанию, то есть лишь небольшая часть наших сотрудников имеет status = Left . Это имеет разветвления для показателей, которые мы выбираем для оценки производительности наших алгоритмов. Подробнее об этом мы поговорим в разделе «Результаты».

С точки зрения очистки данных классы IT и information_technology для функции отдела должны быть объединены вместе:

df.department.replace('information_technology', 'IT', inplace=True)

Более того, HR заботится только о постоянных сотрудниках, поэтому мы должны отфильтровать временный отдел:

df = df[df.department != 'temp']

Таким образом, наша функция отдела должна выглядеть примерно так:

Что нужно сделать с нашими категориальными функциями, чтобы гарантировать, что данные будут хорошо работать с нашими алгоритмами:

  • Отсутствующие данные для функции отдела следует объединить в отдельный класс Отсутствует.
  • Категориальные характеристики отдел и зарплата также должны быть быстро закодированы.
  • Целевая переменная status должна быть преобразована в двоичную.
df['department'].fillna('Missing', inplace=True)
df = pd.get_dummies(df, columns=['department', 'salary'])
df['status'] = pd.get_dummies(df.status).Left

1.3 Сегментация

Мы можем получить дальнейшее понимание, сегментируя числовые характеристики по сравнению с категориальными. Начнем с некоторых одномерных сегментов.

В частности, мы собираемся сегментировать числовые характеристики, представляющие счастье, производительность, рабочую нагрузку и опыт, по нашей категориальной целевой переменной статус.

Сегментировать удовлетворенность по статусу:

sb.violinplot(y='status', x='satisfaction', data=df)

Понимание состоит в том, что ряд уволенных сотрудников остались очень довольны своей работой.

Статус сегмента last_evaluation:

sb.violinplot(y='status', x='last_evaluation', data=df)

Понимание состоит в том, что большое количество уволенных сотрудников были высокоэффективными. Возможно, они больше не чувствовали возможности роста, оставаясь?

Сегментируйте avg_monthly_hrs и n_projects по статусу:

sb.violinplot(y='status', x='avg_monthly_hrs', data=df)
sb.violinplot(y='status', x='n_projects', data=df)

Похоже, что те, кто ушел, как правило, имели либо довольно большую рабочую нагрузку, либо довольно низкую рабочую нагрузку. Представляют ли они сгоревших и уволенных бывших сотрудников?

Сегментирование владения по статусу:

sb.violinplot(y='status', x='tenure', data=df)

Отметим, что внезапный отток сотрудников в течение 3-го года. Те, кто все еще живет после 6 лет, как правило, остаются.

2. Разработка функций

Обратите внимание на следующие двумерные сегменты, которые послужат стимулом для разработки наших функций в дальнейшем.

Для каждого графика мы разделим две числовые характеристики (представляющие счастье, производительность, рабочую нагрузку или опыт) по статусу. Это может дать нам несколько кластеров, основанных на стереотипах сотрудников.

Производительность и счастье:

К сожалению, занятые сотрудники затрудняют чтение этой диаграммы. Давайте просто отобразим только работников Left, поскольку именно их мы действительно пытаемся понять.

sb.lmplot(x='satisfaction',
          y='last_evaluation',
          data=df[df.status=='Left'],
          fit_reg=False
         )

У нас есть три группы уволенных сотрудников:

  • Отстающие: last_evaluation ‹0,6
  • Недовольны: уровень удовлетворенности ‹0,2
  • Успевающие: last_evaluation ›0,8 и удовлетворенность› 0,7

Рабочая нагрузка и производительность:

sb.lmplot(x='last_evaluation',
          y='avg_monthly_hrs',
          data=df[df.status=='Left'],
          fit_reg=False
         )

У нас есть две группы уволенных сотрудников:

  • Звезды: avg_monthly_hrs ›215 и last_evaluation› 0,75
  • Slackers: avg_monthly_hrs ‹165 и last_evaluation‹ 0,65

Работа и счастье:

sb.lmplot(x='satisfaction',
          y='avg_monthly_hrs',
          data=df[df.status=='Left'],
          fit_reg=False,
         )

У нас есть три группы уволенных сотрудников:

  • Трудоголики: avg_monthly_hrs ›210 и удовлетворение› 0,7
  • Просто работа: avg_monthly_hrs ‹170
  • Перегружен: в среднем_месяц_часов ›225 и удовлетворенность‹ 0,2

Давайте разработаем новые функции для этих 8 «стереотипных» групп сотрудников:

df['underperformer'] = ((df.last_evaluation < 0.6) & (df.last_evaluation_missing==0)).astype(int)
df['unhappy'] = (df.satisfaction < 0.2).astype(int)
df['overachiever'] = ((df.last_evaluation > 0.8) & (df.satisfaction > 0.7)).astype(int)
df['stars'] = ((df.avg_monthly_hrs > 215) & (df.last_evaluation > 0.75)).astype(int)
df['slackers'] = ((df.avg_monthly_hrs < 165) & (df.last_evaluation < 0.65) & (df.last_evaluation_missing==0)).astype(int)
df['workaholic'] = ((df.avg_monthly_hrs > 210) & (df.satisfaction > 0.7)).astype(int)
df['justajob'] = (df.avg_monthly_hrs < 170).astype(int)
df['overworked'] = ((df.avg_monthly_hrs > 225) & (df.satisfaction < 0.2)).astype(int)

Мы можем взглянуть на долю сотрудников в каждой из этих 8 групп.

df[['underperformer', 'unhappy', 'overachiever', 'stars', 
    'slackers', 'workaholic', 'justajob', 'overworked']].mean()
underperformer    0.285257
unhappy           0.092195
overachiever      0.177069
stars             0.241825
slackers          0.167686
workaholic        0.226685
justajob          0.339281
overworked        0.071581

34% сотрудников являются сотрудниками только на работу, не вдохновляясь и только здесь для еженедельной проверки заработной платы, в то время как только 7% полностью перегружены работой.

Аналитическая базовая таблица. Набор данных после применения всех этих шагов по очистке данных и разработке функций является нашей аналитической базовой таблицей. Это данные, на которых мы обучаем наши модели.

В нашем ABT 14 068 сотрудников и 31 столбец - см. Фрагмент ниже. Вспомните, в нашем исходном наборе данных было 14 249 сотрудников и всего 10 столбцов!

3. Моделирование

Мы собираемся обучить четыре проверенные модели классификации:

  • логистические регрессии (регуляризованные L1 и L2)
  • случайные леса
  • деревья с градиентным усилением

Во-первых, давайте разделим нашу аналитическую базовую таблицу.

y = df.status
X = df.drop('status', axis=1)

Затем мы разделимся на обучающие и тестовые наборы. Наш набор данных слегка несбалансирован, поэтому мы воспользуемся стратифицированной выборкой для компенсации.

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1234, stratify=df.status)

Мы создадим объект конвейера для обучения. Это упростит процесс обучения нашей модели.

pipelines = {
       'l1': make_pipeline(StandardScaler(), 
             LogisticRegression(penalty='l1', random_state=123)),
       'l2': make_pipeline(StandardScaler(), 
             LogisticRegression(penalty='l2', random_state=123)),
       'rf': make_pipeline(
             RandomForestClassifier(random_state=123)),
       'gb': make_pipeline(
             GradientBoostingClassifier(random_state=123))
            }

Мы также хотим настроить гиперпараметры для каждого алгоритма. Для логистической регрессии наиболее важным гиперпараметром является сила регуляризации, C.

l1_hyperparameters = {'logisticregression__C' : [0.001, 0.005, 0.01, 
                       0.05, 0.1, 0.5, 1, 5, 10, 50, 100, 500, 1000]
                     }
l2_hyperparameters = {'logisticregression__C' : 
                       [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 
                        1, 5, 10, 50, 100, 500, 1000]
                     }

Для нашего случайного леса мы настроим количество оценок (n_estimators), максимальное количество функций, которые следует учитывать во время разделения (max_features), и минимальное количество образцов, которое должно быть листом (min_samples_leaf).

rf_hyperparameters = {
    'randomforestclassifier__n_estimators' : [100, 200],
    'randomforestclassifier__max_features' : ['auto', 'sqrt', 0.33],
    'randomforestclassifier__min_samples_leaf' : [1, 3, 5, 10]
    }

Для нашего дерева с градиентным усилением мы настроим количество оценок (n_estimators), скорость обучения и максимальную глубину каждого дерева (max_depth ).

gb_hyperparameters = {
    'gradientboostingclassifier__n_estimators' : [100, 200],
    'gradientboostingclassifier__learning_rate' : [0.05, 0.1, 0.2],
    'gradientboostingclassifier__max_depth' : [1, 3, 5]
    }

Мы сохраним эти гиперпараметры в словаре.

hyperparameters = {
    'l1' : l1_hyperparameters,
    'l2' : l2_hyperparameters,
    'rf' : rf_hyperparameters,
    'gb' : gb_hyperparameters
    }

Наконец, мы настроим и настроим наши модели. Используя GridSearchCV, мы можем обучить все эти модели с перекрестной проверкой по всем нашим объявленным гиперпараметрам с помощью всего нескольких строк кода!

fitted_models = {}
for name, pipeline in pipelines.items():
    model = GridSearchCV(pipeline, 
                         hyperparameters[name], 
                         cv=10, 
                         n_jobs=-1)
    model.fit(X_train, y_train)
    fitted_models[name] = model

4. Оценка

Я написал специальную статью о популярных показателях машинного обучения, в том числе о тех, которые используются ниже.

4.1 Оценка производительности

Начнем с печати результатов перекрестной проверки. Это средняя производительность по 10 временным границам, позволяющая получить надежную оценку производительности модели, используя только данные вашего обучения.

for name, model in fitted_models.items():
    print(name, model.best_score_)
Output:
l1 0.9088324151412831
l2 0.9088324151412831
rf 0.9793851075173272
gb 0.975475386529234

Переходя к тестовым данным, мы:

  • рассчитать точность;
  • распечатайте матрицу путаницы и вычислите точность, отзывчивость и показатель F1;
  • отобразите ROC и вычислите показатель AUROC.

Точность измеряет долю правильно помеченных прогнозов, однако этот показатель не подходит для несбалансированных наборов данных, например фильтрация спама в электронной почте (спам или не спам) и медицинское тестирование (больной или здоровый). Например, если в нашем наборе данных только 1% сотрудников удовлетворяет target = Left, то модель, которая всегда прогнозирует, что сотрудник все еще работает в компании, мгновенно получит 99%. точность. В таких ситуациях более уместны точность или отзыв. Что бы вы ни использовали, зависит от того, хотите ли вы минимизировать ошибки типа 1 (ложные срабатывания) или ошибки типа 2 (ложные отрицательные результаты). Для спамовых писем ошибки типа 1 хуже (некоторый спам допустим, если вы случайно не отфильтруете важное письмо!), В то время как ошибки типа 2 неприемлемы для медицинского тестирования (когда кому-то говорят, что у него нет рака это катастрофа!). F1-оценка дает вам лучшее из обоих миров, взяв средневзвешенное значение точности и запоминания.

Область под ROC, известная как AUROC, является еще одним стандартным показателем для проблем классификации. Это эффективное измерение способности классификатора различать классы и отделять сигнал от шума. Этот показатель также устойчив к несбалансированным наборам данных.

Вот код для создания этих оценок и графиков:

for name, model in fitted_models.items():
    print('Results for:', name)
    
    # obtain predictions
    pred = fitted_models[name].predict(X_test)
    # confusion matrix
    cm = confusion_matrix(y_test, pred)
    print(cm)
    # accuracy score
    print('Accuracy:', accuracy_score(y_test, pred))
    
    # precision
    precision = cm[1][1]/(cm[0][1]+cm[1][1])
    print('Precision:', precision)
    
    # recall
    recall = cm[1][1]/(cm[1][0]+cm[1][1])
    print('Recall:', recall)
    
    # F1_score
    print('F1:', f1_score(y_test, pred))
    
    # obtain prediction probabilities
    pred = fitted_models[name].predict_proba(X_test)
    pred = [p[1] for p in pred]
    # plot ROC
    fpr, tpr, thresholds = roc_curve(y_test, pred) 
    plt.title('Receiver Operating Characteristic (ROC)')
    plt.plot(fpr, tpr, label=name)
    plt.legend(loc='lower right')
    plt.plot([0,1],[0,1],'k--')
    plt.xlim([-0.1,1.1])
    plt.ylim([-0.1,1.1])
    plt.ylabel('True Positive Rate (TPR) i.e. Recall')
    plt.xlabel('False Positive Rate (FPR)')
    plt.show()
    
    # AUROC score
    print('AUROC:', roc_auc_score(y_test, pred))

Логистическая регрессия (регуляризация L1):

Output:
[[2015  126]
 [ 111  562]]

Accuracy:  0.9157782515991472
Precision: 0.8168604651162791
Recall:    0.8350668647845468
F1:        0.8258633357825129
AUROC:     0.9423905869485105

Логистическая регрессия (L2-регуляризованная):

Output:
[[2014  127]
 [ 110  563]]

Accuracy:  0.9157782515991472
Precision: 0.8159420289855073
Recall:    0.836552748885587
F1:        0.8261188554658841
AUROC:     0.9423246556128734

Дерево с градиентным усилением:

Output:
[[2120   21]
 [  48  625]]

Accuracy:  0.9754797441364605
Precision: 0.9674922600619195
Recall:    0.9286775631500743
F1:        0.9476876421531464
AUROC:     0.9883547910913578

Случайный лес:

Output:
[[2129   12]
 [  45  628]]
Accuracy:  0.9797441364605544
Precision: 0.98125
Recall:    0.9331352154531947
F1:        0.9565879664889566
AUROC:     0.9916117990718256

Выигрышный алгоритм - случайный лес с AUROC 99% и F1-оценкой 96%. Этот алгоритм имеет 99% шанс отличить левого и занятого работника… очень хорошо!

Из 2814 сотрудников в тестовой выборке алгоритм:

  • правильно классифицировал 628 левых рабочих (истинно положительных результатов) при 12 неправильных (ошибках типа I), и
  • правильно классифицировал 2129 занятых работников (True Negative), получив 45 неправильных ответов (ошибки типа II).

К вашему сведению, вот гиперпараметры выигравшего случайного леса, настроенные с помощью GridSearchCV.

RandomForestClassifier(bootstrap=True, 
                       class_weight=None, 
                       criterion='gini',
                       max_depth=None, 
                       max_features=0.33, 
                       max_leaf_nodes=None,
                       min_impurity_decrease=0.0,
                       min_impurity_split=None,
                       min_samples_leaf=1, 
                       min_samples_split=2,
                       min_weight_fraction_leaf=0, 
                       n_estimators=200,
                       n_jobs=None, 
                       oob_score=False, 
                       random_state=123,
                       verbose=0, 
                       warm_start=False
                      )

4.2 Важность функций

Рассмотрим следующий код.

coef = winning_model.feature_importances_
ind = np.argsort(-coef)
for i in range(X_train.shape[1]):
    print("%d. %s (%f)" % (i + 1, X.columns[ind[i]], coef[ind[i]]))
x = range(X_train.shape[1])
y = coef[ind][:X_train.shape[1]]
plt.title("Feature importances")
ax = plt.subplot()
plt.barh(x, y, color='red')
ax.set_yticks(x)
ax.set_yticklabels(X.columns[ind])
plt.gca().invert_yaxis()

Будет напечатан список функций, ранжированных по важности, и соответствующая гистограмма.

Ranking of feature importance:
1. n_projects (0.201004)
2. satisfaction (0.178810)
3. tenure (0.169454)
4. avg_monthly_hrs (0.091827)
5. stars (0.074373)
6. overworked (0.068334)
7. last_evaluation (0.063630)
8. slackers (0.028261)
9. overachiever (0.027244)
10. workaholic (0.018925)
11. justajob (0.016831)
12. unhappy (0.016486)
13. underperformer (0.006015)
14. last_evaluation_missing (0.005084)
15. salary_low (0.004372)
16. filed_complaint (0.004254)
17. salary_high (0.003596)
18. department_engineering (0.003429)
19. department_sales (0.003158)
20. salary_medium (0.003122)
21. department_support (0.002655)
22. department_IT (0.001628)
23. department_finance (0.001389)
24. department_management (0.001239)
25. department_Missing (0.001168)
26. department_marketing (0.001011)
27. recently_promoted (0.000983)
28. department_product (0.000851)
29. department_admin (0.000568)
30. department_procurement (0.000296)

Есть три особенно сильных предиктора оттока сотрудников:

  • n_projects (рабочая нагрузка)
  • удовлетворение (счастье) и
  • владение (опыт).

Более того, эти две спроектированные функции также имеют высокий рейтинг по важности:

  • звезды (хорошее настроение и загруженность) и
  • перегружен (низкий уровень счастья и высокая загруженность).

Интересно, но не совсем удивительно. Звезды, возможно, ушли в поисках лучших возможностей, в то время как переутомленные ушли после сгорания.

5. Развертывание

Исполняемую версию этой модели (.pkl) можно сохранить из записной книжки Jupyter.

with open('final_model.pkl', 'wb') as f:
    pickle.dump(fitted_models['rf'].best_estimator_, f)

HR может предварительно обработать данные о новых сотрудниках, прежде чем вводить их в обученную модель. Это называется запуском партии.

В большой организации они могут захотеть развернуть модель в производственной среде, обратившись к инженерам данных и инженерам машинного обучения. Эти специалисты создают автоматизированный конвейер на основе нашей модели, обеспечивая возможность предварительной обработки свежих данных и регулярной передачи прогнозов в HR.

Заключительные комментарии

Мы начали с бизнес-проблемы: HR в крупной компании хотели получить практическую информацию об уходе сотрудников.

Мы обучили победившую модель случайного леса на большом количестве исторических данных, охватывающих более 14 000 бывших и настоящих сотрудников.

Отдел кадров может обрабатывать новые данные в нашем обученном файле .pkl вручную, или их инженерный отдел может создать автоматизированный конвейер.

Наша модель была бинарной классификационной моделью, в которой целевая переменная является категориальной. Он предсказывает дискретное количество возможных вариантов - здесь отток или без оттока.

Другой стороной медали для обучения с учителем являются регрессионные модели, целевая переменная которых является числовой. Здесь я обучил человека, который предсказывает цены на жилье.

Наконец, я написал статью здесь о том, где машинное обучение находится в области математического моделирования.

Мои социальные сети

Зарегистрируйтесь в Medium

Впервые на Medium?

Поддержите мой анализ, подписавшись здесь!

Я заработаю небольшую комиссию без каких-либо дополнительных затрат для вас.

Хорошего дня.