Project: Modeling & Predicting of Churning Customers (in R) | by Jarar Zaidi | Towards Data Science

Table of Contents
1. Introduction
2. Data Wrangling
3. Exploratory Data Analysis
4. Modeling
5. References
You have just been hired as a Data Scientist. A manager at the bank is disturbed with an alarming number of customers leaving their credit card services. You have been hired as a data scientist to predict who is gonna get leave their company so they can proactively go to the customer to provide them better services and turn customers’ decisions in the opposite direction. A data collector hands you this data to perform Data Analysis and wants you to examine trends & correlations within our data. We would like to do model & predict who will leave our companyNote: Exit Flag is the discrete target variable here for which we have to create the predictive model, but we will also use Credit Limit for another modeling example walkthrough for a continuous predictor.
Prevent the loss of potential customer churn/turnover/attrition, by catching these features early; we can prevent further damage from being done
Predict whether a customer will churn the company. This is a binary outcome. Positive (+) = 1, customer stays in companyNegative (-) = 0, customer exits the company
Experiment with Classification Models & see which yields greatest accuracy.
Examine trends & correlations within our data
Determine which features are most important for a churning customer
come up with a model that will best fit our data
CLIENTNUM — (Feature, dbl,continuous) Client NumberCustomer_Age — (Feature, int,continuous) Age of the CustomerGender — (Feature, chr, discrete) Sex of the CustomerDependent_count — (Feature, int, continuous) number of dependents a user has. That is, how many people are dependent on a credit card user for financial support. A higher count tells us that the expenditures can be high.Education_Level — (Feature, char, discrete) Education Level of the CustomerMarital_Status — (Feature, char, discrete) Martial Status of the CustomerIncome_Category — (Feature, char, discrete) Income Category of CustomerCard_Category — (Feature, char, discrete) Card Category of CustomerCredit_Limit — (Feature, dbl,continuous) Client NumberAttrition_Flag — (Predictor, discrete, binary): Exited (customer exits the company) or Current (customer stays in company)
CLIENTNUM — (Feature, dbl,continuous) Client Number
Customer_Age — (Feature, int,continuous) Age of the Customer
Gender — (Feature, chr, discrete) Sex of the Customer
Dependent_count — (Feature, int, continuous) number of dependents a user has. That is, how many people are dependent on a credit card user for financial support. A higher count tells us that the expenditures can be high.
Education_Level — (Feature, char, discrete) Education Level of the Customer
Marital_Status — (Feature, char, discrete) Martial Status of the Customer
Income_Category — (Feature, char, discrete) Income Category of Customer
Card_Category — (Feature, char, discrete) Card Category of Customer
Credit_Limit — (Feature, dbl,continuous) Client Number
Attrition_Flag — (Predictor, discrete, binary): Exited (customer exits the company) or Current (customer stays in company)
In Part 1 of 3 of Data Wrangling, we read in our data file & install all required libraries/packages for our project. We also examine if there are any problems with our dataset, & hence see that there are no issues.
```{r DataWrangling1}library(tidyverse)library(plyr)library(readr)library(dplyr)cc1 <- read_csv("BankChurners.csv") # original dataset#----------------------------#----------------------------problems(cc1) # no problems with cc1head(cc1)dim(cc1) # # returns dimensions;10127 rows   23 colcc1 %>% filter(!is.na(Income_Category))(is.na(cc1))glimpse(cc1)```
In Part 2 of 3 of Data Wrangling, we manipulate the data to get only the columns we want & to remove NA & Unknown values in our data. We also examine the dimensions & unique values for our discrete variables.
6 distinct discrete types for Income_Category :$60K — $80K, Less than $40K ,$80K — $120K ,$40K — $60K ,$120K + , Unknown  4 distinct discrete types for Marital_Status: Married, Single, Divorced, Unknown 4 distinct discrete types for Card_Category: Blue, Gold, Siler, Platinum
Note: We will also remove any rows/entries with a “Unknown”/NA value.
We see here we initally have 10,127 rows & 23 columns, but we truncate that too 8348 rows by 9 columns.
```{r DataWrangling2}# selected the columns we care aboutcc2 <- cc1 %>% select(Customer_Age,Gender,Dependent_count,Education_Level,Marital_Status,Income_Category,Card_Category,Credit_Limit, Attrition_Flag) %>% filter( !is.na(.))# see the head of ithead(cc2)dim(cc2) #dimensions 10127 rows 9 columns#(cc2 <- na.omit(cc2) ) # EXACt SAME as :  %>% filter( !is.na(.))#----------------------------cc2 %>% group_by(Income_Category,Marital_Status)#----------------------------# Lets see which distinct types there are(distinct(cc2, Income_Category))  # 6 types:$60K - $80K, Less than $40K ,$80K - $120K  ,$40K - $60K ,$120K + ,Unknown (distinct(cc2, Marital_Status))  # 4 types:  Married, Single, Divorced, Unknown (distinct(cc2, Card_Category))  # 4 types:  Blue, Gold, Siler, Platinum#----------------------------# Drop all the "unknown" rows from Marital_Status & Income_Category# 82x9, 82 rows must remove these rowscc3 <- cc2 %>% select(Customer_Age,Gender,Dependent_count,Education_Level,Marital_Status,Income_Category,Card_Category,Credit_Limit, Attrition_Flag) %>% filter(Marital_Status != "Unknown" , Income_Category != "Unknown",Education_Level !="Unknown")#----------------------------head(cc3)dim(cc3) #8348 rows by 9 cols#----------------------------```
In Part 3 of 3 Data Wrangling, we rename our predictor Column Attrition_Flag to Exited_Flag. We also rename the binary output values for this predictor from Existing Customer/Attrited Customer to Current/Exited, respectivley. We lastly, also see the cout of each discrete feature with our discrete predictor.
```{r DataWrangling3}#----------------------------#----------------------------#install.packages("dplyr")library(dplyr)# Rename Label Colum to Exited_FlagdataCC4 <- cc3 %>% rename(Exited_Flag = Attrition_Flag)#----------------------------#----------------------------dataCC4 <- cc3#Rename values dataCC4 $Attrition_Flag[dataCC4 $Attrition_Flag == "Existing Customer"] <- "Current"dataCC4 $Attrition_Flag[dataCC4 $Attrition_Flag == "Attrited Customer"] <- "Exited"#----------------------------#----------------------------(dataCC4  %>% group_by(Attrition_Flag) %>% summarize(meanAge= mean(Customer_Age), meanDepdent= mean(Dependent_count), meanCreditLim= mean(Credit_Limit)))#AKA:   summarise_mean <- function(data, vars) { data %>% summarise(n = n(), across({{ vars }}, mean))}#dataCC4  %>%   #group_by(Attrition_Flag) %>% # summarise_mean(where(is.numeric))#----------------------------#----------------------------#see the count of each(dataCC4  %>% select(Gender,Attrition_Flag) %>% group_by(Gender) %>% count(Attrition_Flag) )      (dataCC4  %>% group_by(Education_Level) %>% count(Attrition_Flag) ) (dataCC4  %>% group_by(Marital_Status) %>% count(Attrition_Flag) ) (dataCC4  %>% group_by(Income_Category) %>% count(Attrition_Flag) ) (dataCC4  %>% group_by(Card_Category) %>% count(Attrition_Flag) ) summary(dataCC4)```
Above, we can evidently see that Current Customers had higher mean credit limits than did churning customers.
In part 1 of 2 in EDA, we reorder the Factor levels of certain discrete variables, because we want them to display in order in our graphs. We have 2 discrete variables plotted against each other, with 1 of the variables being a fill. We examine the count of each visually.
```{r EDA1}# 2 discrete var, but using 1 as a fill. # Count, with Y being Income Category, Fill is our ggplot(dataCC4 , aes(y = Income_Category)) + geom_bar(aes(fill = Attrition_Flag), position = position_stack(reverse = FALSE)) +theme(legend.position = "top") + theme_classic() + xlab("Count") + ylab("Income Category") + ggtitle(" Customer Status by Income" )+  labs(fill = "Customer Status")#----------------------------# RE-ODER factor levelsdataCC4$Education_Level <- factor(dataaa$Education_Level, levels = c("Uneducated","High School",                                                                    "College",                                                                    "Graduate",                                                                    "Post-Graduate","Doctorate"))ggplot(dataCC4 , aes(y = Education_Level)) + geom_bar(aes(fill = Attrition_Flag), position = position_stack(reverse = FALSE)) + theme(legend.position = "top") + theme_classic() + xlab("Count") + ylab("Education Level") + ggtitle("Customer Status by Education Level" ) +  labs(fill = "Customer Status")#----------------------------ggplot(dataCC4 , aes(y = Marital_Status)) + geom_bar(aes(fill = Attrition_Flag), position = position_stack(reverse = FALSE)) + theme(legend.position = "top") + theme_classic() + xlab("Count") + ylab("Martial Status") + ggtitle("Customer Status by Martial Status" )+  labs(fill = "Customer Status")#----------------------------ggplot(dataCC4 , aes(y = Card_Category)) + geom_bar(aes(fill =  Attrition_Flag), position = position_stack(reverse = FALSE)) + theme(legend.position = "top") + theme_classic() + xlab("Count") + ylab("Card Category") + ggtitle("Customer Status by Card Category" )+  labs(fill = "Customer Status")#----------------------------ggplot(dataCC4 , aes(y = Gender)) + geom_bar(aes(fill =  Attrition_Flag), position = position_stack(reverse = FALSE)) + theme(legend.position = "top") + theme_classic() + xlab("Count") + ylab("Gender") + ggtitle("Customer Status by Gender" )+  labs(fill = "Customer Status")#There are more samples of females in our dataset compared to males but the percentage of difference is not that significant so we can say that genders are uniformly distributed.#----------------------------```
In part 2 of 2 in EDA, we explore 2 violin plots with quantiles, a visualization for 2 discrete variables, & area distribution plot.
```{r EDA2}#----------------------------# Discrete X, Continous Y, Violin Plotsggplot(dataCC4 , aes(Attrition_Flag,Credit_Limit,color= Credit_Limit)) + geom_violin(draw_quantiles = c(0.25,0.5,0.75),colour="red",size=1.4) + theme_classic() +xlab("Income Category") + ylab("Credit Limit") + ggtitle("Customer Status by Credit Limit" ) +   labs(fill = "Customer Status")# RE-ODER factor levelsdataCC4 $Income_Category <- factor(dataCC4 $Income_Category, levels = c("Less than $40K","$40K - $60K","$60K - $80K","$80K - $120K","$120K +"))ggplot(dataCC4 , aes(Income_Category,Credit_Limit,color= Credit_Limit)) + geom_violin(draw_quantiles = c(0.25,0.5,0.75),colour="blue",size=1) + theme_classic() +xlab("Income Category") + ylab("Credit Limit") + ggtitle("Income Category by Credit Limit" )#----------------------------# 2 discrete variables x,y#need  to specify which group the proportion is to be calculated over.ggplot(dataCC4 , aes(Income_Category,Attrition_Flag, colour= after_stat(prop), size = after_stat(prop), group = Income_Category)) + geom_count() +  scale_size_area() + theme_classic() +xlab("Income Category") + ylab("Status of Customer") + ggtitle("Customer Status by Income Proportion" )#----------------------------##library(plyr)#mu <- ddply(dataaa, "Exited_Flag", summarise, grp.mean=mean(Credit_Limit))#head(mu)#ggplot(dataCC4, aes(x=Credit_Limit, fill=Attrition_Flag)) +  geom_area(stat ="bin") + xlab("Credit Limit")+ylab("Count") +ggtitle("Customer Status by Credit Limit " ) +  labs(fill = "Customer Status")```
Quantiles can tell us a wide array of information.
The 1st horizontal line tells us the 1st quantile, or the 25th percentile- the number that separates the lowest 25% of the group from the highest 75% of the credit limit.
Next, the 2nd line is the Median, or 50th percentile — the number in the middle of the credit limit.
Lastly, the 3rd line is the 3rd quartile, Q3, or 75th percentile — the number that separates the lowest 75% of the group from the highest 25% of the group.
Quantiles are shown both in the violin plots above & below.
We have only 16.07% of customers who have churned. Hence, it’s a bit difficult to train our model to predict churning customers. We will do our best with the data given, as with any model.
Our goal in modeling is to provide a simple low-dimensional summary of a dataset.We use models to partition data into patterns and residuals. Fitted model is just the closest model from a family of models.
Classification models are models that predict a categorical label. It will be interesting to study which characteristic(s) discriminates each category and to what extent. Predicting whether a customer will a customer will exit or stay with the company. Below we will use a logistic regression algorithm.
First we must convert the non numeric variables (discrete variables) into factors. Factors are the data objects which are used to categorize the data and store it as levels.
```{r Modeling1}library(tidyverse)library(modelr)library(plyr)library(readr)library(dplyr)library(caret)# glimpse is from dplyr# output shows that the dataset has glimpse(dataCC4)# convert the non numeric into factors.names <- c(2,5,7,9) # col numbers that are charsdataCC4[,names] <- lapply(dataCC4[,names] , factor)```
```{r Modeling2}#----------------------------# DATA PARTITIONING#----------------------------#build our model on the Train dataset & evaluate its performance on #the Test dataset# aka.  holdout-validation approach to evaluating model performance.set.seed(100) # sets random seed for reproducibility of resultslibrary(caTools) # for data partioning# create the training and test datasets. The train dataset contains    # 70 percent of the data (420 observations of 10 variables) while #the #test data contains the remaining 30 percent (180 observations #of 10 variables).spl = sample.split(dataCC4$Attrition_Flag, SplitRatio = 0.7)train = subset(dataCC4, spl==TRUE)test = subset(dataCC4, spl==FALSE)print(dim(train)) # 4957 rows/obs 9 cols/variablesprint(dim(test)) # 2124 rows/obs  9 cols/variables```
We fit the logistic regression model. The first step is to instantiate the algorithm. We declare binomial because 2 possible outcomes: exit/current.
```{r Modeling3}#----------------------------#BUILD, PREDICT &  EVALUATE the Model#----------------------------# BUILD#----------------------------model_glm = glm(Attrition_Flag ~ . , family="binomial",data = train)summary(model_glm)# Baseline Accuracy   #Let's evaluate the model further,   # Since the majority class of the target (Y) variable has a #proportion of 0.84, the baseline accuracy is 84 percent.prop.table(table(train$Attrition_Flag))#Let's now evaluate the model performance on the training and test data, which should ideally be better than the baseline accuracy.# PREDICTIONS on the TRAIN setpredictTrain = predict(model_glm, data = train, type = "response")# confusion matrix w/ threshold of 0.5,  #which means that for probability predictions >= 0.5, the      #algorithm will predict the Current response for the Y #variable. table(train$Attrition_Flag, predictTrain >= 0.1)     Evaluate:#prints the accuracy of the model on the training data, using the #confusion matrix, and the accuracy comes out to be 0 percent.(50+774)/nrow(test) #Accuracy - 84% (50+774)/2124-------------------------------------------------------------#We then repeat this process on the TEST data, and the accuracy #comes out to be 84 percent.# PREDICTIONS on the TEST setpredictTest = predict(model_glm, newdata = test, type = "response")      Evaluate:# Confusion matrix on test settable(test$Attrition_Flag, predictTest >= 0.5)1790/nrow(test) #Accuracy - 84%```
The significance code ‘***’ in the above below output shows the relative importance of the feature variablesAIC estimates the relative amount of information lost by a given model:the less information a model loses, the higher the quality of that model. Lower AIC values indicate a better-fit model,
You have learned techniques of building a classification model in R using the powerful logistic regression algorithm. The baseline accuracy for the training & test data was 84 percent. Overall, the logistic regression model is beating the baseline accuracy by a big margin on both the train and test datasets, and the results are very good.
Below I will show you another way to model this, using SVM (Support Vector Machine).
```{r ModelAgain}unique(dataCC5$Attrition_Flag) #Levels: Current Exited# Encoding the target feature as factordataCC5$Attrition_Flag = factor(dataCC5$Attrition_Flag, levels = c(0, 1))# spliting datast into Train & Test Setinstall.packages('caTools')library(caTools) # for data partioningset.seed(123) #SEED#SPLITsplit = sample.split(dataCC5$Attrition_Flag, SplitRatio = 0.75)train_set = subset(dataCC5,split==TRUE) #TRAINtest_set = subset(dataCC5,split==FALSE) #TEST#FEATURE SCALING: agelibrary(caret)train_set[-9] = scale(train_set[-9])test_set[-9] = scale(test_set[-9])install.packages('e1071')library(e1071)model_glm = glm(Attrition_Flag ~ . , family="binomial",data = train)classifier = svm(formula= Attrition_Flag ~ .,                 data= train_set,                 type= 'C-classification',                 kernel = 'radial')# predicting Test Set Resultsy_pred = predict(classifier, newdata = train_set[-9])# making confusion matrixcm = table(test_set[, 3],y_pred)```
Confidence intervals are of interest in modeling because they are often used in model validation.
Next, we consider the 95% confidence interval of Credit Limit.As the Credit Limit is greater than 0, we narrow the confidence interval.There are 91.75% data locates within the confidence interval. We will keep the corresponding records and store the rest in another variable rest.data for latter analysis.
```{r CI}mean.Credit_Limit <- mean(dataCC5$Credit_Limit)std.Credit_Limit <- sqrt(var(dataCC5$Credit_Limit))df = dim(dataCC5)[1] - 9conf.Credit_Limit <- mean.Credit_Limit + c(-1, 1) * qt(0.975, df) * std.Credit_Limit# As the CreditLimit is greater than 0, we narrow the confidence intervalconf.Credit_Limit[1] <- 0conf.Credit_Limit#There are 91.75% data locates within the confidence interval. We will keep the corresponding records and store the rest in another variable rest.data for later analysis.sum(dataCC5$Credit_Limit <= conf.Credit_Limit[2]) / dim(dataCC5)[1]#----------------------------##----------------------------rest.data <- dataCC5 %>%  filter(Credit_Limit > conf.Credit_Limit[2])dataCC5 <- dataCC5 %>%  filter(Credit_Limit <= conf.Credit_Limit[2]) %>%  filter(Credit_Limit != 0)#We recall the historgrams of Credit Limitboxplot_credLim <- dataCC5 %>%  ggplot() +  geom_boxplot(aes(Credit_Limit))(boxplot_credLim)histplot <- dataCC5 %>%  ggplot() +  geom_histogram(aes(Credit_Limit))(histplot)#----------------------------##----------------------------#We consider a log-transofrmation to convert the distribution of the histogram to normal distribution. Right-skew.histplot <- dataCC5 %>%  ggplot() +  geom_histogram(aes(log(Credit_Limit)))(histplot)qqplot <- dataCC5 %>%  ggplot() +  geom_qq(aes(sample = log(Credit_Limit)))(qqplot)#It seems that normality exists. Great! There are 6 types of categorical variables.# Distrubution of Income Categoryp1 <- dataCC5 %>%  ggplot() +  geom_histogram(aes(Income_Category, fill = Income_Category), stat = "count")(p1)# Box Plots of Depdent count & Income Categoryp2 <- dataCC5 %>%  ggplot() +  geom_boxplot(aes(x = Dependent_count, y = Income_Category, color = Income_Category))(p2)```
#The following aims to build a model to predict the price. ## Evaluation We will use MSE as the criteria to measure the model performance.
We Split 20% as test dataset and 80% as training dataset.
```{r RMSE}#Train and test split#Split 20% as test dataset and 80% as training dataset.### convert character to factordataCC5$Gender <-as.factor(dataCC5$Gender)# Splitset.seed(100)train.index <- sample(nrow(dataCC5), 0.7*nrow(dataCC5), replace = FALSE)train.set <- dataCC5[train.index, ]test.set <- dataCC5[-train.index, ]# build a model to predict the price. ## Evaluation: We will use MSE as the criteria to measure the model performance.RMSE <- function(true, pred){  residuals <- true - pred  res <- sqrt(mean(residuals^2))  return(res)}----------#----------------------------#Linear regression#Linear regressoin is a kind of simple regression methods. It is easy to be conducted while it has some strict assumptions. The following code will perform the modeling process with check some assumptions. ### Multicolinearity#library(corrplot)#cor <- cor(dataCC5)#corrplot::corrplot(cor, method = 'ellipse',  type = 'lower')#cor(dataCC5$Credit_Limit, dataCC5$Dependent_count)# IF data had more numeric values use this for # correlation plot: #M <- cor(dataCC5)#corrplot(cor(dataCC5), method = "circle")```
To recal: AIC estimates the relative amount of information lost by a given model: The less information a model loses, the higher the quality of that model. Thus, the lower AIC values indicate a better-fit model,
```{r AIC}AICs <- c()models <- c()start.model <- lm(Credit_Limit ~ Customer_Age, data = train.set)# summary(start.model) models <- append(models, as.character(start.model$call)[2])AICs <- append(AICs, AIC(start.model))# Add next varaibleupdate.model <- update(start.model, . ~ . + Gender)# summary(update.model)models <- append(models, as.character(update.model$call)[2])AICs <- append(AICs, AIC(update.model))# Add next varupdate.model <- update(update.model, . ~ . + Dependent_count)# summary(update.model)models <- append(models, as.character(update.model$call)[2])AICs <- append(AICs, AIC(update.model))# Add next varupdate.model <- update(update.model, . ~ . + Education_Level)# summary(update.model)models <- append(models, as.character(update.model$call)[2])AICs <- append(AICs, AIC(update.model))# Add next variableupdate.model <- update(update.model, . ~ . + Marital_Status)# summary(update.model)models <- append(models, as.character(update.model$call)[2])AICs <- append(AICs, AIC(update.model))# Add calculated_host_listings_countupdate.model <- update(update.model, . ~ . + Income_Category)# summary(update.model)models <- append(models, as.character(update.model$call)[2])AICs <- append(AICs, AIC(update.model))# Add last varupdate.model <- update(update.model, . ~ . + Card_Category)# summary(update.model)models <- append(models, as.character(update.model$call)[2])AICs <- append(AICs, AIC(update.model))res <- data.frame(  Model = models,  AIC = AICs)knitr::kable(res)#----------------------------#----------------------------par(mfrow = c(3,1))plot(update.model, 1)plot(update.model, 2)plot(update.model, 3)```
The AIC table shows the best model is Credit_Limit ~ Customer_Age + Gender + Dependent_count + Education_Level + Marital_Status + Income_Category + Card_CategoryWith a AIC of 72870.02, we want the lowest AIC.
```{r LogLinearRegression}log.lm <- lm(log(Credit_Limit) ~ Customer_Age + Gender + Dependent_count + Education_Level + Marital_Status + Income_Category + Card_Category, data = dataCC5)summary(log.lm)```
Gender Male, Income Categories, and Silver Card Card Categories were highly significant with respect to our predictor of Credit Limit.
Here is access to the data set & code from my GitHub page:
https://github.com/jararzaidi/ModelingChurningCustomersR
https://www.kaggle.com/sakshigoyal7/credit-card-customers
Recommendations & comments are welcomed!