Introduction: At Regork Telecom, we are currently tackling the crucial business challenge of predicting why customers leave and implementing strategies for retention. This imperative endeavor not only aims to bolster revenue streams but also underscores our commitment to prioritizing customer satisfaction and loyalty. By discerning which customers are likely to leave in the future, we can proactively engage with them, tailor solutions to address their needs, and ultimately fortify our relationship with them. To do this, we will do exploratory data analysis to find trends in the data, perform machine learning through logistic regression, MARS, decision trees, and random forest, in order to find the best model at predicting if someone will leave. Through this, we will be better able to make adjustments to the business in order to retain customers. This initiative exemplifies Regork Telecom’s dedication to delivering exceptional service and ensuring that our valued customers remain at the forefront of our business priorities.
Libraries Required
library(tidymodels)
library(tidyverse)
library(baguette)
library(vip)
library(pdp)
library(here)
library(kernlab)
library(ggplot2)
library(earth)
Importing the data
Import and clean the data:
retention <- read.csv('Labs/data/customer_retention.csv')
sum(is.na(retention))
## [1] 11
Cleaning up dataset:
retention <- retention %>%
mutate(Status = factor(Status)) %>%
na.omit
sum(is.na(retention))
## [1] 0
Exploratory Analysis:
os_counts <- retention %>%
count(OnlineSecurity) %>%
arrange(desc(n))
ggplot(os_counts, aes(x = fct_reorder(OnlineSecurity, n), y = n)) +
geom_col(fill = "skyblue", width = 0.7) +
theme_minimal() +
labs(title = "Presence of Online Security Among Users Who Left",
x = "Online Security",
y = "Number of Users") +
coord_flip() +
theme(axis.text.y = element_text(size = 10))
contract_counts <- retention %>%
count(Contract) %>%
arrange(desc(n))
# Plotting
ggplot(contract_counts, aes(x = fct_reorder(Contract, n), y = n)) +
geom_col(fill = "skyblue", width = 0.7) +
theme_minimal() +
labs(title = "Contract Types of Users Who Left",
x = "Contract Type",
y = "Number of Users") +
coord_flip() + # Flipping axes for better readability
theme(axis.text.y = element_text(size = 10))
Here we see there is a trend in customers leaving when they have month to month contracts.
tenure_counts <- retention %>%
count(Tenure) %>%
arrange(desc(n))
ggplot(tenure_counts, aes(x = Tenure, y = n)) +
geom_line(color = "skyblue", linewidth = 1) +
geom_point(color = "skyblue", linewidth = 3) +
theme_minimal() +
labs(title = "Distribution of Tenure Among Departed Users",
x = "Months",
y = "Number of Users")
As tenure increases, the customer leaving decreases until 60 months, then it increases again.
ggplot(retention, aes(Tenure)) +
geom_bar(fill = "pink") +
ggtitle("Partner and Tenure") +
facet_wrap(~Partner)
This graph shows that people who are married are less likely to leave.
Next, lets move onto machine learning.
Machine Learning:
For our first model, we will look at logistic regression.
set.seed(123)
log_split <- initial_split(retention, prop = .7, strata = Status)
log_train <- training(log_split)
log_test <- testing(log_split)
set.seed(123)
log_kfolds <- vfold_cv(log_train, v = 5, strata = Status)
results <- logistic_reg() %>%
fit_resamples(Status ~ ., log_kfolds)
collect_metrics(results)
## # A tibble: 2 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 accuracy binary 0.799 5 0.00401 Preprocessor1_Model1
## 2 roc_auc binary 0.845 5 0.00521 Preprocessor1_Model1
We found an roc_auc value of .8448, which is good, but should be able to be improved.
final_fit <- logistic_reg() %>%
fit(Status ~ ., data = log_train)
tidy(final_fit)
## # A tibble: 31 × 5
## term estimate std.error statistic p.value
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 1.02 0.969 1.05 2.95e- 1
## 2 GenderMale -0.0461 0.0779 -0.592 5.54e- 1
## 3 SeniorCitizen 0.258 0.101 2.55 1.08e- 2
## 4 PartnerYes -0.141 0.0925 -1.52 1.29e- 1
## 5 DependentsYes -0.0475 0.108 -0.441 6.59e- 1
## 6 Tenure -0.0646 0.00762 -8.47 2.35e-17
## 7 PhoneServiceYes -0.0417 0.774 -0.0539 9.57e- 1
## 8 MultipleLinesNo phone service NA NA NA NA
## 9 MultipleLinesYes 0.442 0.211 2.10 3.56e- 2
## 10 InternetServiceFiber optic 1.48 0.950 1.56 1.18e- 1
## # ℹ 21 more rows
final_fit %>%
predict(log_test) %>%
bind_cols(log_test %>% select(Status)) %>%
conf_mat(truth = Status, estimate = .pred_class)
## Truth
## Prediction Current Left
## Current 1362 225
## Left 178 332
We find that our model is usually predicting correctly, but we are experiencing some false positives.
Next up, we will assess a Mars Model
set.seed(123) # for reproducibility
mars_split <- initial_split(retention, prop = .7, strata = Status)
mars_train <- training(mars_split)
mars_test <- testing(mars_split)
mars_recipe <- recipe(Status ~ ., data = mars_train) %>%
step_YeoJohnson(all_numeric_predictors()) %>%
step_normalize(all_numeric_predictors())
set.seed(123)
kfolds <- vfold_cv(mars_train, v = 5, strata = Status)
mars_mod <- mars(num_terms = tune(), prod_degree = tune()) %>%
set_mode('classification')
mars_grid <- grid_regular(num_terms(range = c(1,30)), prod_degree(), levels = 40)
mars_wf <- workflow() %>%
add_recipe(mars_recipe) %>%
add_model(mars_mod)
tuning_results <- mars_wf %>%
tune_grid(resamples = kfolds, grid = mars_grid)
tuning_results %>%
collect_metrics() %>%
filter(.metric == "roc_auc") %>%
arrange(desc(mean))
## # A tibble: 60 × 8
## num_terms prod_degree .metric .estimator mean n std_err .config
## <int> <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 15 1 roc_auc binary 0.850 5 0.00509 Preprocessor1_M…
## 2 16 1 roc_auc binary 0.849 5 0.00502 Preprocessor1_M…
## 3 18 1 roc_auc binary 0.849 5 0.00503 Preprocessor1_M…
## 4 19 1 roc_auc binary 0.849 5 0.00503 Preprocessor1_M…
## 5 20 1 roc_auc binary 0.849 5 0.00503 Preprocessor1_M…
## 6 21 1 roc_auc binary 0.849 5 0.00503 Preprocessor1_M…
## 7 22 1 roc_auc binary 0.849 5 0.00503 Preprocessor1_M…
## 8 23 1 roc_auc binary 0.849 5 0.00503 Preprocessor1_M…
## 9 24 1 roc_auc binary 0.849 5 0.00503 Preprocessor1_M…
## 10 25 1 roc_auc binary 0.849 5 0.00503 Preprocessor1_M…
## # ℹ 50 more rows
Here we find a max roc_auc of .8499, but we hope that we can still do better. We achieve this value with 15 terms and a degree of 1, making it a lasso model
autoplot(tuning_results)
Next we will finalize our workflow object with the optimal hyperparameter values,
best_hyperparameters <- select_best(tuning_results, metric = "roc_auc")
final_wf <- workflow() %>%
add_recipe(mars_recipe) %>%
add_model(mars_mod) %>%
finalize_workflow(best_hyperparameters)
Then we fit our final workflow object across the full training set data
final_fit <- final_wf %>%
fit(data = mars_train)
final_fit %>%
extract_fit_parsnip() %>%
vip()
This model shows our most important features to be tenure, total charges, and payment method.
final_fit %>%
predict(mars_test) %>%
bind_cols(mars_test %>% select(Status)) %>%
conf_mat(truth = Status, estimate = .pred_class)
## Truth
## Prediction Current Left
## Current 1382 245
## Left 158 312
Decision Trees
dt_mod <- decision_tree(mode = 'classification') %>%
set_engine("rpart")
model_recipe <- recipe(Status ~ ., data = log_train)
dt_fit <- workflow() %>%
add_recipe(model_recipe) %>%
add_model(dt_mod) %>%
fit(data = log_train)
rpart.plot::rpart.plot(dt_fit$fit$fit$fit)
The root node of the decision tree is contract = 1 year or 2 years
set.seed(123)
kfold <- vfold_cv(log_train, v = 5)
dt_results <- fit_resamples(dt_mod, model_recipe, kfold)
collect_metrics(dt_results)
## # A tibble: 2 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 accuracy binary 0.785 5 0.00552 Preprocessor1_Model1
## 2 roc_auc binary 0.803 5 0.00777 Preprocessor1_Model1
The decision tree gave us a roc_auc of .8033, which is not an optimal value. With further tuning, we can hope to create an even better model.
dt_mod <- decision_tree(
mode = "classification",
cost_complexity = tune(),
tree_depth = tune(),
min_n = tune()
) %>%
set_engine("rpart")
dt_hyper_grid <- grid_regular(
cost_complexity(),
tree_depth(),
min_n(),
levels = 5
)
set.seed(123)
dt_results <- tune_grid(dt_mod, model_recipe, resamples = kfold, grid = dt_hyper_grid)
show_best(dt_results, metric = "roc_auc", n = 5)
## # A tibble: 5 × 9
## cost_complexity tree_depth min_n .metric .estimator mean n std_err
## <dbl> <int> <int> <chr> <chr> <dbl> <int> <dbl>
## 1 0.0000000001 15 40 roc_auc binary 0.814 5 0.00780
## 2 0.0000000178 15 40 roc_auc binary 0.814 5 0.00780
## 3 0.00000316 15 40 roc_auc binary 0.814 5 0.00780
## 4 0.0000000001 11 40 roc_auc binary 0.814 5 0.00827
## 5 0.0000000178 11 40 roc_auc binary 0.814 5 0.00827
## # ℹ 1 more variable: .config <chr>
We now got the roc_auc up to .8142, which is better, but still not the optimal model.
dt_best_model <- select_best(dt_results, metric = 'roc_auc')
dt_final_wf <- workflow() %>%
add_recipe(model_recipe) %>%
add_model(dt_mod) %>%
finalize_workflow(dt_best_model)
dt_final_fit <- dt_final_wf %>%
fit(data = log_train)
dt_final_fit %>%
extract_fit_parsnip() %>%
vip(20)
We find that with decision trees, the most important features are tenure, contract, and total charges.
dt_final_fit %>%
predict(log_test) %>%
bind_cols(log_test %>% select(Status)) %>%
conf_mat(truth = Status, estimate = .pred_class)
## Truth
## Prediction Current Left
## Current 1346 275
## Left 194 282
Finally, we will look at a Random Forest
set.seed(123)
rf_split <- initial_split(retention, prop = 0.7, strata = Status)
rf_train <- training(rf_split)
rf_test <- testing(rf_split)
rf_recipe <- recipe(Status ~ ., data = rf_train)
rf_mod <- rand_forest(mode = "classification") %>%
set_engine("ranger")
set.seed(123)
rf_kfold <- vfold_cv(rf_train, v = 5)
rf_results <- fit_resamples(rf_mod, rf_recipe, rf_kfold)
collect_metrics(rf_results)
## # A tibble: 2 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 accuracy binary 0.798 5 0.00509 Preprocessor1_Model1
## 2 roc_auc binary 0.837 5 0.0103 Preprocessor1_Model1
This model gives us a roc_auc of .8368, but with hyperparameter tuning we hope to do even better.
rf_mod <- rand_forest(
mode = "classification",
trees = tune(),
mtry = tune(),
min_n = tune()) %>%
set_engine("ranger", importance = "impurity")
rf_hyper_grid <- grid_regular(
trees(range = c(50, 800)),
mtry(range = c(2, 50)),
min_n(range = c(1, 20)),
levels = 5)
set.seed(123)
rf_results <- tune_grid(rf_mod, rf_recipe, resamples = rf_kfold, grid = rf_hyper_grid)
show_best(rf_results, metric = "roc_auc")
## # A tibble: 5 × 9
## mtry trees min_n .metric .estimator mean n std_err .config
## <int> <int> <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 2 612 20 roc_auc binary 0.844 5 0.0103 Preprocessor1_Model1…
## 2 2 800 20 roc_auc binary 0.843 5 0.0103 Preprocessor1_Model1…
## 3 2 425 15 roc_auc binary 0.843 5 0.00984 Preprocessor1_Model0…
## 4 2 425 20 roc_auc binary 0.843 5 0.0103 Preprocessor1_Model1…
## 5 2 237 20 roc_auc binary 0.843 5 0.0107 Preprocessor1_Model1…
With hyperparameter tuning, we get the roc_auc to .8435
rf_best_hyperparameters <- select_best(rf_results, metric = "roc_auc")
final_rf_wf <- workflow() %>%
add_recipe(rf_recipe) %>%
add_model(rf_mod) %>%
finalize_workflow(rf_best_hyperparameters)
# fit final workflow object
rf_final_fit <- final_rf_wf %>%
fit(data = rf_train)
# plot feature importance
rf_final_fit %>%
extract_fit_parsnip() %>%
vip(num_features = 20)
With our random forest model, we find the most important features to be tenure, total charges, and contract
rf_final_fit %>%
predict(rf_test) %>%
bind_cols(rf_test %>% select(Status)) %>%
conf_mat(truth = Status, estimate = .pred_class)
## Truth
## Prediction Current Left
## Current 1393 276
## Left 147 281
Conclusion:
After testing four models, we have found the most accurate model to be the MARS model. It had the highest AUC of .850 when it had 15 terms and a degree of 1. With the MARS model, we found the most important features to be tenure, total charges, and payment method electronic check. Tenure and total charges were also 2 of the top features in our random forest, decision tree, and logistic regression model. The MARS model was also performed well with its confusion matrix. It performed with an accuracy of about 81%.
In order to better retain customers, Regork should focus on on the most influential factors in whether a customer stayed or left, which were tenure, total charges, payment method, and contract. To do this, they could create an incentive program where customers are rewarded for signing longer phone contracts, and to make sure that the contracts do not have many slight up-charges or random monthly charges. A long term contract where the fees are clearly listed and built into the monthly cost should improve customer satisfaction and lead to them staying longer. This contract should accept all payment methods, which again will increase customer satisfaction. Because the customers are signing longer contracts to begin with, their tenure with Regork will increase, and brand loyalty may be produced, and as tenure is the most important factor, getting customers to sign long contracts should be the most focused on factor.
To reward customers for their loyalty, Regork should offer customers incentives such as a discount on their phone plan if they sign a contract over 1 to 2 years in length, offering major discounts or even a free device if they reach a certain amount of time with Regork, or offering discounts when they sign up for multiple phones at a time.
One limitation to our analysis was having a dataset of only ~ 7000 variables. Having a larger dataset to sample from would have allowed for a more accurate model. Our data also had no information on time or the date it is from, so we cannot access trends in the data over time. Another limitation is in our machine learning, in that we did not test every possible model, and we could have tested more of a range in values. Unfortunately with machine learning, there are so many models and so many value ranges to test, so it is very difficult to find the definitive number 1 model. Despite this, we are still very happy with our model and believe that it is very accurate.