«Большие данные могут позволить компаниям определять переменные, которые предсказывают текучесть кадров в их собственных рядах». Harvard Business Review, август 2017 г.

«Аналитика оттока сотрудников - это процесс оценки текучести кадров с целью предсказать будущее и сократить отток сотрудников». Forbes, март 2016 г.

Вступление

В этом посте представлена ​​эталонная реализация проекта анализа текучести кадров, созданного с использованием библиотеки Python Scikit-Learn. В этой статье мы познакомимся с логистической регрессией, случайным лесом и машиной опорных векторов. Мы также измеряем точность моделей, построенных с помощью машинного обучения, и оцениваем направления дальнейшего развития. И все это мы сделаем на Python. Давайте начнем!

Предварительная обработка данных

Данные были загружены с Kaggle. Это довольно просто. Каждая строка представляет сотрудника, каждый столбец содержит атрибуты сотрудника:

  • удовлетворение_уровень (0–1)
  • last_evaluation (время с момента последней оценки в годах)
  • number_projects (Количество проектов, завершенных за время работы)
  • average_monthly_hours (Среднее количество часов на рабочем месте в месяц)
  • time_spend_company (Время, проведенное в компании в годах)
  • Work_accident (Произошел ли несчастный случай на рабочем месте с сотрудником)
  • ушел (Ушел сотрудник с рабочего места или нет (1 или 0))
  • promotion_last_5years (продвигался ли сотрудник по службе за последние пять лет)
  • продажи (отдел, в котором они работают)
  • заработная плата (относительный уровень заработной платы)
import pandas as pd
hr = pd.read_csv('HR.csv')
col_names = hr.columns.tolist()
print("Column names:")
print(col_names)
print("\nSample data:")
hr.head()

Имена столбцов:
['уровень_выполнения', 'последняя_оценка', 'число_проект', 'среднее_рабочее_часы', 'время_выполнения_компании', 'работа_акцидент', 'слева', 'продвижение_последние_5 лет', 'продажи', 'зарплата']

образец данных:

Измените название столбца с «продажи» на «отдел».

hr=hr.rename(columns = {'sales':'department'})

Тип столбцов можно узнать следующим образом:

hr.dtypes

Наши данные довольно чистые, без пропущенных значений

hr.isnull().any()

Данные содержат 14999 сотрудников и 10 функций.

hr.shape

(14999, 10)

«Левый» столбец - это переменная результата, в которой записываются 1 и 0. 1 для сотрудников, которые уволились из компании, и 0 для тех, кто этого не сделал.

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

hr['department'].unique()

array (['продажи', 'бухгалтерский учет', 'часы работы', 'техническая поддержка', 'поддержка', 'менеджмент',
'ИТ', 'product_mng', 'маркетинг', ' RandD '], dtype = object)

Давайте объединим «техническую», «поддержку» и «ИТ» вместе и назовем их «техническими».

import numpy as np
hr['department']=np.where(hr['department'] =='support', 'technical', hr['department'])
hr['department']=np.where(hr['department'] =='IT', 'technical', hr['department'])

После изменения так выглядят категории отделов:

[‘продажи’ ‘бухгалтерия’ ‘hr’ ‘технический’ ‘менеджмент’ ‘product_mng’
‘Marketing’ ‘RandD’]

Исследование данных

Прежде всего, давайте выясним, сколько сотрудников ушли из компании, а кто нет:

hr['left'].value_counts()

В наших данных осталось 3571 сотрудник и 11428 сотрудников остались.

Давайте разберемся в числах этих двух классов:

hr.groupby('left').mean()

Несколько наблюдений:

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

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

hr.groupby('department').mean()

hr.groupby('salary').mean()

Визуализация данных

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

Гистограмма работы сотрудников отдела и частота текучести кадров

%matplotlib inline
import matplotlib.pyplot as plt
pd.crosstab(hr.department,hr.left).plot(kind='bar')
plt.title('Turnover Frequency for Department')
plt.xlabel('Department')
plt.ylabel('Frequency of Turnover')
plt.savefig('department_bar_chart')

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

Гистограмма уровня заработной платы сотрудников и частоты текучести кадров

table=pd.crosstab(hr.salary, hr.left)
table.div(table.sum(1).astype(float), axis=0).plot(kind='bar', stacked=True)
plt.title('Stacked Bar Chart of Salary Level vs Turnover')
plt.xlabel('Salary Level')
plt.ylabel('Proportion of Employees')
plt.savefig('salary_bar_chart')

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

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

Гистограмма числовых переменных

num_bins = 10
hr.hist(bins=num_bins, figsize=(20,15))
plt.savefig("hr_histogram_plots")
plt.show()

Создание фиктивных переменных для категориальных переменных

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

cat_vars=['department','salary']
for var in cat_vars:
    cat_list='var'+'_'+var
    cat_list = pd.get_dummies(hr[var], prefix=var)
    hr1=hr.join(cat_list)
    hr=hr1

Фактическая категориальная переменная должна быть удалена после создания фиктивных переменных.

Имена столбцов после создания фиктивных переменных для категориальных переменных:

hr.drop(hr.columns[[8, 9]], axis=1, inplace=True)
hr.columns.values

array (['уровень_выполнения', 'последняя_оценка', 'число_проект',
'среднее_число_часов', 'time_spend_company', 'Work_accident',
'left', 'promotion_last_5years', ' Department_RandD ',
' Department_accounting ',' Department_hr ',' Department_management ',
' Department_marketing ',' Department_product_mng ',
' Department_sales ',' Department_technical ',' salary_high ',
'salary_low', 'salary_medium'], dtype = object)

Переменная результата - «левая», а все остальные переменные являются предикторами.

hr_vars=hr.columns.values.tolist()
y=['left']
X=[i for i in hr_vars if i not in y]

Выбор функции

Рекурсивное исключение признаков (RFE) работает путем рекурсивного удаления переменных и построения модели на тех переменных, которые остались. Он использует точность модели, чтобы определить, какие переменные (и комбинация переменных) больше всего способствуют прогнозированию целевого атрибута.

Давайте воспользуемся отбором функций, чтобы помочь нам решить, какие переменные являются важными, которые могут с большой точностью предсказать текучесть кадров. Всего в X 18 столбцов, как насчет выбора 10?

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
rfe = RFE(model, 10)
rfe = rfe.fit(hr[X], hr[y])
print(rfe.support_)
print(rfe.ranking_)

[Истина Истина Ложь Ложь Истина Истина Истина Истина Ложь Истина Истина Ложь
Ложь Ложь Ложь Истина Истина Ложь]
[1 1 3 9 1 1 1 1 1 5 1 1 6 8 7 4 1 1 2]

Вы можете видеть, что RFE выбрало для нас 10 переменных, которые помечены как True в массиве support_ и отмечены выбором «1» в массиве rank_. Они есть:

['уровень_выполнения', 'последняя_оценка', 'время_выполнения_компании', 'работа_акцидент', 'продвижение_последний_5лет', 'отдел_RandD', 'отдел_чр', 'управление_отделом', 'salary_high', 'salary_low']

cols=['satisfaction_level', 'last_evaluation', 'time_spend_company', 'Work_accident', 'promotion_last_5years', 
      'department_RandD', 'department_hr', 'department_management', 'salary_high', 'salary_low'] 
X=hr[cols]
y=hr['left']

Модель логистической регрессии

from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
from sklearn.linear_model import LogisticRegression
from sklearn import metrics
logreg = LogisticRegression()
logreg.fit(X_train, y_train)

LogisticRegression (C = 1.0, class_weight = None, dual = False, fit_intercept = True, intercept_scaling = 1, max_iter = 100, multi_class = 'ovr', n_jobs = 1, штраф = 'l2', random_state = Нет, solver = 'liblinear', tol = 0,0001, verbose = 0, warm_start = False)

from sklearn.metrics import accuracy_score
print('Logistic regression accuracy: {:.3f}'.format(accuracy_score(y_test, logreg.predict(X_test))))

Точность логистической регрессии: 0,771

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

from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier()
rf.fit(X_train, y_train)

RandomForestClassifier (bootstrap = True, class_weight = None, criterion = 'gini', max_depth = None, max_features = 'auto', max_leaf_nodes = None, min_impurity_split = 1e-07, min_samples_leaf = 1, min_samples_samples, min_samples_leaf = 1, min_samples_ min_weight_fraction_leaf = 0.0, n_estimators = 10, n_jobs = 1, oob_score = False, random_state = None, verbose = 0, warm_start = False)

print('Random Forest Accuracy: {:.3f}'.format(accuracy_score(y_test, rf.predict(X_test))))

Точность случайного леса: 0,978

Машина опорных векторов

from sklearn.svm import SVC
svc = SVC()
svc.fit(X_train, y_train)

SVC (C = 1.0, cache_size = 200, class_weight = None, coef0 = 0.0,
solution_function_shape = None, степень = 3, gamma = 'auto', kernel = 'rbf',
max_iter = -1, вероятность = False, random_state = None, сжатие = True,
tol = 0,001, verbose = False)

print('Support vector machine accuracy: {:.3f}'.format(accuracy_score(y_test, svc.predict(X_test))))

Машинная точность опорных векторов: 0,909

Победитель ... Случайный лес, верно?

Перекрестная проверка

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

from sklearn import model_selection
from sklearn.model_selection import cross_val_score
kfold = model_selection.KFold(n_splits=10, random_state=7)
modelCV = RandomForestClassifier()
scoring = 'accuracy'
results = model_selection.cross_val_score(modelCV, X_train, y_train, cv=kfold, scoring=scoring)
print("10-fold cross validation average accuracy: %.3f" % (results.mean()))

10-кратная средняя точность перекрестной проверки: 0,977

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

Точность и отзыв

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

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

from sklearn.metrics import classification_report
print(classification_report(y_test, rf.predict(X_test)))

y_pred = rf.predict(X_test)
from sklearn.metrics import confusion_matrix
import seaborn as sns
forest_cm = metrics.confusion_matrix(y_pred, y_test, [1,0])
sns.heatmap(forest_cm, annot=True, fmt='.2f',xticklabels = ["Left", "Stayed"] , yticklabels = ["Left", "Stayed"] )
plt.ylabel('True class')
plt.xlabel('Predicted class')
plt.title('Random Forest')
plt.savefig('random_forest')

Логистическая регрессия

print(classification_report(y_test, logreg.predict(X_test)))

logreg_y_pred = logreg.predict(X_test)
logreg_cm = metrics.confusion_matrix(logreg_y_pred, y_test, [1,0])
sns.heatmap(logreg_cm, annot=True, fmt='.2f',xticklabels = ["Left", "Stayed"] , yticklabels = ["Left", "Stayed"] )
plt.ylabel('True class')
plt.xlabel('Predicted class')
plt.title('Logistic Regression')
plt.savefig('logistic_regression')

Машина опорных векторов

print(classification_report(y_test, svc.predict(X_test)))

svc_y_pred = svc.predict(X_test)
svc_cm = metrics.confusion_matrix(svc_y_pred, y_test, [1,0])
sns.heatmap(svc_cm, annot=True, fmt='.2f',xticklabels = ["Left", "Stayed"] , yticklabels = ["Left", "Stayed"] )
plt.ylabel('True class')
plt.xlabel('Predicted class')
plt.title('Support Vector Machine')
plt.savefig('support_vector_machine')

Когда сотрудник увольняется, как часто мой классификатор предсказывает это правильно? Это измерение называется «отзывом», и беглый взгляд на эти диаграммы может продемонстрировать, что случайный лес явно лучше всего подходит для этого критерия. Из всех случаев оборота случайный лес правильно извлек 991 из 1038. Это соответствует «отзыву» оборота около 95% (991/1038), что намного лучше, чем логистическая регрессия (26%) или вспомогательные векторные машины (85%). ).

Когда классификатор предсказывает, что сотрудник уйдет, как часто этот сотрудник на самом деле уходит? Это измерение называется «точностью». Случайный лес снова превосходит два других с точностью около 95% (991 из 1045) с логистической регрессией около 51% (273 из 540) и векторной машиной поддержки около 77% (890 из 1150).

Кривая ROC

from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
logit_roc_auc = roc_auc_score(y_test, logreg.predict(X_test))
fpr, tpr, thresholds = roc_curve(y_test, logreg.predict_proba(X_test)[:,1])
rf_roc_auc = roc_auc_score(y_test, rf.predict(X_test))
rf_fpr, rf_tpr, rf_thresholds = roc_curve(y_test, rf.predict_proba(X_test)[:,1])
plt.figure()
plt.plot(fpr, tpr, label='Logistic Regression (area = %0.2f)' % logit_roc_auc)
plt.plot(rf_fpr, rf_tpr, label='Random Forest (area = %0.2f)' % rf_roc_auc)
plt.plot([0, 1], [0, 1],'r--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic')
plt.legend(loc="lower right")
plt.savefig('ROC')
plt.show()

Кривая Рабочей характеристики приемника (ROC) - еще один распространенный инструмент, используемый с двоичными классификаторами. Пунктирная линия представляет собой кривую ROC чисто случайного классификатора; хороший классификатор находится как можно дальше от этой линии (в сторону верхнего левого угла).

Важность функции для модели случайного леса

feature_labels = np.array(['satisfaction_level', 'last_evaluation', 'time_spend_company', 'Work_accident', 'promotion_last_5years', 
      'department_RandD', 'department_hr', 'department_management', 'salary_high', 'salary_low'])
importance = rf.feature_importances_
feature_indexes_by_importance = importance.argsort()
for index in feature_indexes_by_importance:
    print('{}-{:.2f}%'.format(feature_labels[index], (importance[index] *100.0)))

Promotion_last_5years-0.20%
Department_management-0.22%
Department_hr-0.29%
Department_RandD-0.34%
salary_high-0.55%
salary_low-1.35%
Work_accident-1,46%
last_evaluation-19,19%
time_spend_company-25,73%
удовлетворенности-50,65%

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

Резюме

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

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