Warning: package 'tidymodels' was built under R version 4.3.3
Warning: package 'dials' was built under R version 4.3.3
Warning: package 'ggplot2' was built under R version 4.3.3
Warning: package 'infer' was built under R version 4.3.3
Warning: package 'modeldata' was built under R version 4.3.3
Warning: package 'parsnip' was built under R version 4.3.3
Warning: package 'recipes' was built under R version 4.3.3
Warning: package 'rsample' was built under R version 4.3.3
Warning: package 'tune' was built under R version 4.3.3
Warning: package 'workflows' was built under R version 4.3.3
Warning: package 'workflowsets' was built under R version 4.3.3
Warning: package 'yardstick' was built under R version 4.3.3
library (modeldata)
library (themis)
Warning: package 'themis' was built under R version 4.3.3
The attrition Dataset
These data are from the IBM Watson Analytics Lab . It is a fictional data set created by IBM data scientists. There are 1470 rows
attrition <- tibble (attrition)
attrition <- janitor:: clean_names (attrition)
str (attrition)
tibble [1,470 × 31] (S3: tbl_df/tbl/data.frame)
$ age : int [1:1470] 41 49 37 33 27 32 59 30 38 36 ...
$ attrition : Factor w/ 2 levels "No","Yes": 2 1 2 1 1 1 1 1 1 1 ...
$ business_travel : Factor w/ 3 levels "Non-Travel","Travel_Frequently",..: 3 2 3 2 3 2 3 3 2 3 ...
$ daily_rate : int [1:1470] 1102 279 1373 1392 591 1005 1324 1358 216 1299 ...
$ department : Factor w/ 3 levels "Human_Resources",..: 3 2 2 2 2 2 2 2 2 2 ...
$ distance_from_home : int [1:1470] 1 8 2 3 2 2 3 24 23 27 ...
$ education : Ord.factor w/ 5 levels "Below_College"<..: 2 1 2 4 1 2 3 1 3 3 ...
$ education_field : Factor w/ 6 levels "Human_Resources",..: 2 2 5 2 4 2 4 2 2 4 ...
$ environment_satisfaction : Ord.factor w/ 4 levels "Low"<"Medium"<..: 2 3 4 4 1 4 3 4 4 3 ...
$ gender : Factor w/ 2 levels "Female","Male": 1 2 2 1 2 2 1 2 2 2 ...
$ hourly_rate : int [1:1470] 94 61 92 56 40 79 81 67 44 94 ...
$ job_involvement : Ord.factor w/ 4 levels "Low"<"Medium"<..: 3 2 2 3 3 3 4 3 2 3 ...
$ job_level : int [1:1470] 2 2 1 1 1 1 1 1 3 2 ...
$ job_role : Factor w/ 9 levels "Healthcare_Representative",..: 8 7 3 7 3 3 3 3 5 1 ...
$ job_satisfaction : Ord.factor w/ 4 levels "Low"<"Medium"<..: 4 2 3 3 2 4 1 3 3 3 ...
$ marital_status : Factor w/ 3 levels "Divorced","Married",..: 3 2 3 2 2 3 2 1 3 2 ...
$ monthly_income : int [1:1470] 5993 5130 2090 2909 3468 3068 2670 2693 9526 5237 ...
$ monthly_rate : int [1:1470] 19479 24907 2396 23159 16632 11864 9964 13335 8787 16577 ...
$ num_companies_worked : int [1:1470] 8 1 6 1 9 0 4 1 0 6 ...
$ over_time : Factor w/ 2 levels "No","Yes": 2 1 2 2 1 1 2 1 1 1 ...
$ percent_salary_hike : int [1:1470] 11 23 15 11 12 13 20 22 21 13 ...
$ performance_rating : Ord.factor w/ 4 levels "Low"<"Good"<"Excellent"<..: 3 4 3 3 3 3 4 4 4 3 ...
$ relationship_satisfaction : Ord.factor w/ 4 levels "Low"<"Medium"<..: 1 4 2 3 4 3 1 2 2 2 ...
$ stock_option_level : int [1:1470] 0 1 0 0 1 0 3 1 0 2 ...
$ total_working_years : int [1:1470] 8 10 7 8 6 8 12 1 10 17 ...
$ training_times_last_year : int [1:1470] 0 3 3 3 3 2 3 2 2 3 ...
$ work_life_balance : Ord.factor w/ 4 levels "Bad"<"Good"<"Better"<..: 1 3 3 3 3 2 2 3 3 2 ...
$ years_at_company : int [1:1470] 6 10 0 8 2 7 1 1 9 7 ...
$ years_in_current_role : int [1:1470] 4 7 0 7 2 7 0 0 7 7 ...
$ years_since_last_promotion: int [1:1470] 0 1 0 3 2 3 0 0 1 7 ...
$ years_with_curr_manager : int [1:1470] 5 7 0 0 2 6 0 0 8 7 ...
Our job will be predicting the binary attrition target variable. As the positive case corresponds with the Yes level, we must modify the target variable so that it is the first level.
attrition <- attrition |>
mutate (attrition = factor (attrition, levels = c ("Yes" , "No" )))
Examining the target variable, we observe it is unbalanced (there are more negative than positive cases).
ggplot (attrition, aes (attrition)) +
geom_bar () +
theme_minimal ()
Predicting Personnel Attrition
Let’s define a predicting workflow for the attrition dataset, completing the steps described in this section.
Train and Test Split
Performing an adequate train and test split, keeping 20% of the dataset in the train set.
set.seed (123123 )
attrition_split <- initial_split (attrition, prop = 0.8 , strata = attrition)
at_train <- training (attrition_split)
at_test <- testing (attrition_split)
Data Preprocessing
Preparing an adequate preprocessing of the dataset in a recipe. Here are some hints to do the job:
Transform factors into numeric variables. Take into account that there are ordered and unordered factors.
Remove variables with low variance.
Remove variables highly correlated with other features.
Also considering oversampling or undersampling to account for class imbalance.
at_rec <- recipe (attrition ~ ., data = at_train) %>%
step_dummy (all_nominal_predictors ()) %>%
step_zv (all_predictors ()) %>%
step_corr (all_numeric_predictors ()) %>%
step_smote (attrition) #to consider oversampling
at_rec
── Recipe ──────────────────────────────────────────────────────────────────────
Number of variables by role
• Dummy variables from: all_nominal_predictors()
• Zero variance filter on: all_predictors()
• Correlation filter on: all_numeric_predictors()
• SMOTE based on: attrition
We can see the preprocessed dataset doing:
at_rec %>%
prep () %>%
bake (new_data = NULL )
# A tibble: 1,972 × 56
age daily_rate distance_from_home hourly_rate monthly_income monthly_rate
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 49 279 8 61 5130 24907
2 33 1392 3 56 2909 23159
3 27 591 2 40 3468 16632
4 32 1005 2 79 3068 11864
5 59 1324 3 81 2670 9964
6 30 1358 24 67 2693 13335
7 38 216 23 44 9526 8787
8 36 1299 27 94 5237 16577
9 35 809 16 84 2426 16479
10 29 153 15 49 4193 12682
# ℹ 1,962 more rows
# ℹ 50 more variables: num_companies_worked <dbl>, percent_salary_hike <dbl>,
# stock_option_level <dbl>, total_working_years <dbl>,
# training_times_last_year <dbl>, years_at_company <dbl>,
# years_in_current_role <dbl>, years_since_last_promotion <dbl>,
# years_with_curr_manager <dbl>, business_travel_Travel_Frequently <dbl>,
# business_travel_Travel_Rarely <dbl>, department_Sales <dbl>, …
Models
Choosing two models that can be adequate for this dataset, and define workflows to integrate them with the recipe or recipes.
# Logistic Regression Model
logistic_model <- logistic_reg () %>%
set_engine ("glm" )
logistic_workflow <- workflow () %>%
add_model (logistic_model) %>%
add_recipe (at_rec)
# Random Forest Model
forest_model <- rand_forest (trees = 50 ) %>%
set_engine ("ranger" ) %>%
set_mode ("classification" )
forest_workflow <- workflow () %>%
add_model (forest_model) %>%
add_recipe (at_rec)
Cross Validation
Defining a cross validation scheme with five folds, and evaluate each of the models with the following metrics:
Accuracy.
Sensitivity.
Specificity.
set.seed (123123 )
attrition_folds <- vfold_cv (at_train, v = 5 , strata = attrition)
metrics <- metric_set (accuracy, sens, spec)
logistic_res <- fit_resamples (
logistic_workflow,
resamples = attrition_folds,
metrics = metrics
)
forest_res <- fit_resamples (
forest_workflow,
resamples = attrition_folds,
metrics = metrics
)
Warning: package 'ranger' was built under R version 4.3.3
logistic_res %>%
collect_metrics ()
# A tibble: 3 × 6
.metric .estimator mean n std_err .config
<chr> <chr> <dbl> <int> <dbl> <chr>
1 accuracy binary 0.794 5 0.0181 Preprocessor1_Model1
2 sens binary 0.740 5 0.0244 Preprocessor1_Model1
3 spec binary 0.804 5 0.0195 Preprocessor1_Model1
forest_res %>%
collect_metrics ()
# A tibble: 3 × 6
.metric .estimator mean n std_err .config
<chr> <chr> <dbl> <int> <dbl> <chr>
1 accuracy binary 0.858 5 0.00405 Preprocessor1_Model1
2 sens binary 0.212 5 0.0220 Preprocessor1_Model1
3 spec binary 0.982 5 0.00413 Preprocessor1_Model1
Assessing Best Model
Selecting the best model from the ones tested, train it with the whole train set and obtain the prediction metrics with the test set.
-> We have an imbalanced dataset, with more negative than positive cases. When comparing the two models, the random forest model has better accuracy and specificity, making it highly effective at identifying negative cases. However, the logistic regression model (model 1) has significantly better sensitivity, which is crucial for identifying positive cases. Therefore, I chose the logistic regression model to better address the class imbalance and improve the detection of positive cases.
best_model <- logistic_workflow %>% fit (at_train)
final_fit <- best_model %>%
predict (at_test) %>%
bind_cols (at_test) %>%
conf_mat (truth= attrition, estimate = .pred_class)
metrics <- metric_set (sens, yardstick:: spec, accuracy)
best_model %>%
predict (at_test) %>%
bind_cols (at_test) %>%
metrics (truth = attrition, estimate = .pred_class)
# A tibble: 3 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 sens binary 0.688
2 spec binary 0.806
3 accuracy binary 0.786
best_model %>%
predict (at_test, type = "prob" )
# A tibble: 295 × 2
.pred_Yes .pred_No
<dbl> <dbl>
1 0.0107 0.989
2 0.412 0.588
3 0.315 0.685
4 0.00725 0.993
5 0.232 0.768
6 0.802 0.198
7 0.107 0.893
8 0.541 0.459
9 0.0122 0.988
10 0.464 0.536
# ℹ 285 more rows