| Name | Matrix Number |
|---|---|
| JOANNE CHANG YII | 23085604 |
| MITRA ANUSRI A/P MURALITHARAN | 17163859 |
| URRMILAH A/P S SEKARAN | 23070853 |
| NUR ELISA SORAYA BINTI MOHD RIZAM | S2165789 |
| INTAN NUR NABILA BINTI ZULKEPLI | 23071379 |
Telco Churn Prediction
Companies providing communication services often face difficulty in maintaining their subscribers. Churn rate is one of the key factors for telecommunications. With the ability to predict churn, businesses can act before its occurrence comes up saving them from losing customers which finally leads into increased savings and satisfaction.
The dataset used for this analysis can be found on Kaggle: Telco Customer Churn Dataset
Loading Libraries
library(corrplot)
library(ggplot2)
library(dplyr)
library(gridExtra)
library(grid)
library(tidyr)
library(forcats)
library(caTools)
library(caret)
library(randomForest)
library(e1071)
library(gbm)
library(cluster)
library(factoextra)
library(kernlab)
library(knitr)
Reading the Data
telco_data <- read.csv("/Users/User/OneDrive/Documents/UM/WQD7004/Assignment/TelcoCustomerChurn.csv")
Understanding the structure of the data
glimpse(telco_data)
## Rows: 7,043
## Columns: 21
## $ customerID <chr> "7590-VHVEG", "5575-GNVDE", "3668-QPYBK", "7795-CFOCW…
## $ gender <chr> "Female", "Male", "Male", "Male", "Female", "Female",…
## $ SeniorCitizen <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ Partner <chr> "Yes", "No", "No", "No", "No", "No", "No", "No", "Yes…
## $ Dependents <chr> "No", "No", "No", "No", "No", "No", "Yes", "No", "No"…
## $ tenure <int> 1, 34, 2, 45, 2, 8, 22, 10, 28, 62, 13, 16, 58, 49, 2…
## $ PhoneService <chr> "No", "Yes", "Yes", "No", "Yes", "Yes", "Yes", "No", …
## $ MultipleLines <chr> "No phone service", "No", "No", "No phone service", "…
## $ InternetService <chr> "DSL", "DSL", "DSL", "DSL", "Fiber optic", "Fiber opt…
## $ OnlineSecurity <chr> "No", "Yes", "Yes", "Yes", "No", "No", "No", "Yes", "…
## $ OnlineBackup <chr> "Yes", "No", "Yes", "No", "No", "No", "Yes", "No", "N…
## $ DeviceProtection <chr> "No", "Yes", "No", "Yes", "No", "Yes", "No", "No", "Y…
## $ TechSupport <chr> "No", "No", "No", "Yes", "No", "No", "No", "No", "Yes…
## $ StreamingTV <chr> "No", "No", "No", "No", "No", "Yes", "Yes", "No", "Ye…
## $ StreamingMovies <chr> "No", "No", "No", "No", "No", "Yes", "No", "No", "Yes…
## $ Contract <chr> "Month-to-month", "One year", "Month-to-month", "One …
## $ PaperlessBilling <chr> "Yes", "No", "Yes", "No", "Yes", "Yes", "Yes", "No", …
## $ PaymentMethod <chr> "Electronic check", "Mailed check", "Mailed check", "…
## $ MonthlyCharges <dbl> 29.85, 56.95, 53.85, 42.30, 70.70, 99.65, 89.10, 29.7…
## $ TotalCharges <dbl> 29.85, 1889.50, 108.15, 1840.75, 151.65, 820.50, 1949…
## $ Churn <chr> "No", "No", "Yes", "No", "Yes", "Yes", "No", "No", "Y…
To find null/empty values in each column
null_values <- is.na(telco_data)
null_summary <- colSums(null_values)
print(null_summary)
## customerID gender SeniorCitizen Partner
## 0 0 0 0
## Dependents tenure PhoneService MultipleLines
## 0 0 0 0
## InternetService OnlineSecurity OnlineBackup DeviceProtection
## 0 0 0 0
## TechSupport StreamingTV StreamingMovies Contract
## 0 0 0 0
## PaperlessBilling PaymentMethod MonthlyCharges TotalCharges
## 0 0 0 11
## Churn
## 0
Out of 7043 rows, there are only 11 null values in column ‘TotalCharges’ which totals to less then 10% of the dataset. Hence, we have decided to remove all of the missing values.
print(paste("Number of rows before remove null values:" , as.character(nrow(telco_data))))
## [1] "Number of rows before remove null values: 7043"
telco_data <- telco_data[complete.cases(telco_data$TotalCharges), ]
print(paste("Number of rows after remove null values:" , as.character(nrow(telco_data))))
## [1] "Number of rows after remove null values: 7032"
Upon checking the data, we also decided to remove the ‘CustomerID’ column which could potentially affect duplicate searching operations. Additionally, the column may not provide any useful information for predicting the target variable.
telco_data <- telco_data %>% select(-customerID)
Searching for duplicated rows
duplicated_rows <- duplicated(telco_data)
print(paste("Number of duplicated rows: " , as.character(sum(duplicated_rows))))
## [1] "Number of duplicated rows: 22"
print(paste("Number of rows before remove duplicated values:" , as.character(nrow(telco_data))))
## [1] "Number of rows before remove duplicated values: 7032"
telco_data <- unique(telco_data)
print(paste("Number of rows after remove duplicated values:" , as.character(nrow(telco_data))))
## [1] "Number of rows after remove duplicated values: 7010"
Data format checking and transformation for categorical features in the dataset
print(paste("Value in Gender:" , paste(unique(telco_data$gender), collapse = ", ")))
## [1] "Value in Gender: Female, Male"
print(paste("Value in Partner:" , paste(unique(telco_data$Partner), collapse = ", ")))
## [1] "Value in Partner: Yes, No"
print(paste("Value in Dependents:" , paste(unique(telco_data$Dependents), collapse = ", ")))
## [1] "Value in Dependents: No, Yes"
print(paste("Value in Phone Service:" , paste(unique(telco_data$PhoneService), collapse = ", ")))
## [1] "Value in Phone Service: No, Yes"
print(paste("Value in Multiple Lines:" , paste(unique(telco_data$MultipleLines), collapse = ", ")))
## [1] "Value in Multiple Lines: No phone service, No, Yes"
telco_data$MultipleLines <- ifelse(telco_data$MultipleLines == "No phone service", "No", telco_data$MultipleLines)
print(paste("Value in Multiple Lines after transforming:" , paste(unique(telco_data$MultipleLines), collapse = ", ")))
## [1] "Value in Multiple Lines after transforming: No, Yes"
print(paste("Value in Internet Service:" , paste(unique(telco_data$InternetService), collapse = ", ")))
## [1] "Value in Internet Service: DSL, Fiber optic, No"
print(paste("Value in Online Security:" , paste(unique(telco_data$OnlineSecurity), collapse = ", ")))
## [1] "Value in Online Security: No, Yes, No internet service"
telco_data$OnlineSecurity <- ifelse(telco_data$OnlineSecurity == "No internet service", "No", telco_data$OnlineSecurity)
print(paste("Value in Online Security after transforming:" , paste(unique(telco_data$OnlineSecurity), collapse = ", ")))
## [1] "Value in Online Security after transforming: No, Yes"
print(paste("Value in Online Backup:" , paste(unique(telco_data$OnlineBackup), collapse = ", ")))
## [1] "Value in Online Backup: Yes, No, No internet service"
telco_data$OnlineBackup <- ifelse(telco_data$OnlineBackup== "No internet service", "No", telco_data$OnlineBackup)
print(paste("Value in Online Backup after transforming:" , paste(unique(telco_data$OnlineBackup), collapse = ", ")))
## [1] "Value in Online Backup after transforming: Yes, No"
print(paste("Value in Device Protection:" , paste(unique(telco_data$DeviceProtection), collapse = ", ")))
## [1] "Value in Device Protection: No, Yes, No internet service"
telco_data$DeviceProtection <- ifelse(telco_data$DeviceProtection== "No internet service", "No", telco_data$DeviceProtection)
print(paste("Value in Device Protection after transforming:" , paste(unique(telco_data$DeviceProtection), collapse = ", ")))
## [1] "Value in Device Protection after transforming: No, Yes"
print(paste("Value in Technical Support:" , paste(unique(telco_data$TechSupport), collapse = ", ")))
## [1] "Value in Technical Support: No, Yes, No internet service"
telco_data$TechSupport <- ifelse(telco_data$TechSupport== "No internet service", "No", telco_data$TechSupport)
print(paste("Value in Technical Support after transforming:" , paste(unique(telco_data$TechSupport), collapse = ", ")))
## [1] "Value in Technical Support after transforming: No, Yes"
print(paste("Value in Streaming TV:" , paste(unique(telco_data$StreamingTV), collapse = ", ")))
## [1] "Value in Streaming TV: No, Yes, No internet service"
telco_data$StreamingTV <- ifelse(telco_data$StreamingTV== "No internet service", "No", telco_data$StreamingTV)
print(paste("Value in Streaming TV after transforming:" , paste(unique(telco_data$StreamingTV), collapse = ", ")))
## [1] "Value in Streaming TV after transforming: No, Yes"
print(paste("Value in Streaming Movies:" , paste(unique(telco_data$StreamingMovies), collapse = ", ")))
## [1] "Value in Streaming Movies: No, Yes, No internet service"
telco_data$StreamingMovies <- ifelse(telco_data$StreamingMovies== "No internet service", "No", telco_data$StreamingMovies)
print(paste("Value in Streaming Movies after transforming:" , paste(unique(telco_data$StreamingMovies), collapse = ", ")))
## [1] "Value in Streaming Movies after transforming: No, Yes"
print(paste("Value in Contract:" , paste(unique(telco_data$Contract), collapse = ", ")))
## [1] "Value in Contract: Month-to-month, One year, Two year"
print(paste("Value in Payment Method:" , paste(unique(telco_data$PaymentMethod), collapse = ", ")))
## [1] "Value in Payment Method: Electronic check, Mailed check, Bank transfer (automatic), Credit card (automatic)"
print(paste("Value in Churn:" , paste(unique(telco_data$Churn), collapse = ", ")))
## [1] "Value in Churn: No, Yes"
Checking for outliers
detect_outliers <- function(column) {
q1 <- quantile(column, 0.25)
q3 <- quantile(column, 0.75)
iqr <- q3 - q1
lower_bound <- q1 - 1.5 * iqr
upper_bound <- q3 + 1.5 * iqr
# outliers <- column[column < lower_bound | column > upper_bound]
# return(outliers)
return(which(column < lower_bound | column > upper_bound))
}
numerical_columns <- c("tenure", "MonthlyCharges", "TotalCharges")
for (col in numerical_columns) {
outliers <- detect_outliers(telco_data[[col]])
print(paste("Outliers in column", col, ":", length(outliers)))
}
## [1] "Outliers in column tenure : 0"
## [1] "Outliers in column MonthlyCharges : 0"
## [1] "Outliers in column TotalCharges : 0"
Prepare the data for the pie chart for churn
churn_data <- telco_data %>%
count(Churn) %>%
mutate(pct = n / sum(n) * 100,
label = paste0(Churn, ": ", round(pct, 1), "%"))
# Define custom colors for the pie chart
churn_colors <- c("Yes" = "#FF6666", "No" = "#66B2FF")
# Create the pie chart
ggplot(churn_data, aes(x = "", y = n, fill = Churn)) +
geom_bar(stat = "identity", width = 1) +
coord_polar(theta = "y") +
geom_text(aes(label = label), position = position_stack(vjust = 0.5)) +
scale_fill_manual(values = churn_colors) +
labs(title = "Distribution of Churn", x = NULL, y = NULL) +
theme_void()
Plot relationship between ‘SeniorCitizen’, ‘Partner’, ‘Dependents’, and ‘Churn’
# Convert column SeniorCitizen, Partner, and Dependents to factors for better visualization
telco_data$SeniorCitizen <- as.factor(telco_data$SeniorCitizen)
telco_data$Partner <- as.factor(telco_data$Partner)
telco_data$Dependents <- as.factor(telco_data$Dependents)
# Plot the relationship between gender and Churn
plot_gender <- ggplot(telco_data, aes(x = gender, fill = Churn)) +
geom_bar(position = "dodge") +
geom_text(stat = 'count', aes(label=..count..), position = position_dodge(width=0.9), vjust=-0.5) + # Add count on top of bars
labs(title = "Churn by Gender", x = "Gender", y = "Count") +
theme_minimal()
# Plot the relationship between SeniorCitizen and Churn
plot_senior <- ggplot(telco_data, aes(x = SeniorCitizen, fill = Churn)) +
geom_bar(position = "dodge") +
geom_text(stat = 'count', aes(label=..count..), position = position_dodge(width=0.9), vjust=-0.5) + # Add count on top of bars
labs(title = "Churn by Senior Citizen Status", x = "Senior Citizen", y = "Count") +
theme_minimal()
# Plot the relationship between Partner and Churn
plot_partner <- ggplot(telco_data, aes(x = Partner, fill = Churn)) +
geom_bar(position = "dodge") +
geom_text(stat = 'count', aes(label=..count..), position = position_dodge(width=0.9), vjust=-0.5) + # Add count on top of bars
labs(title = "Churn by Partner Status", x = "Partner", y = "Count") +
theme_minimal()
# Plot the relationship between Dependents and Churn
plot_dependents <- ggplot(telco_data, aes(x = Dependents, fill = Churn)) +
geom_bar(position = "dodge") +
geom_text(stat = 'count', aes(label=..count..), position = position_dodge(width=0.9), vjust=-0.5) + # Add count on top of bars
labs(title = "Churn by Dependents Status", x = "Dependents", y = "Count") +
theme_minimal()
# Combine the plots side by side
grid.arrange(
arrangeGrob(
textGrob("Demographic", gp = gpar(fontsize = 16, fontface = "bold")),
arrangeGrob(plot_gender, plot_senior, plot_partner, plot_dependents, ncol = 2),
heights = c(0.1, 1)
)
)
## Warning: The dot-dot notation (`..count..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(count)` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
Plot the service-related variables
# Define colors
churn_colors <- c("Yes" = "#FF6666", "No" = "#66B2FF")
# Function to create side-by-side bar plots with percentage labels inside bars
create_side_by_side_plot <- function(data, variable) {
plot_data <- data %>%
count(Churn, !!sym(variable)) %>%
mutate(percent = n / sum(n) * 100, # Calculate percentage
label = ifelse(percent >= 5, paste0(round(percent, 1), "%"), "")) # Format percentage as text if >= 5%
plot <- ggplot(plot_data, aes(x = !!sym(variable), y = percent, fill = Churn)) +
geom_bar(stat = "identity", position = "dodge") +
geom_text(aes(label = label), position = position_dodge(width = 0.9), vjust = 0.5) +
labs(title = paste("Churn by", variable), x = variable, y = "Percentage") +
scale_fill_manual(values = churn_colors) + # Set fill colors manually
theme_minimal()
return(plot)
}
# List of service-related variables
service_variables <- c("PhoneService", "MultipleLines", "InternetService",
"OnlineSecurity", "OnlineBackup", "DeviceProtection",
"TechSupport", "StreamingTV", "StreamingMovies")
# Create side-by-side plots for each service-related variable using telco_data
side_by_side_plots <- lapply(service_variables, function(var) create_side_by_side_plot(telco_data, var))
# Combine plots into a grid
grid.arrange(grobs = side_by_side_plots, ncol = 3, top = "Services")
Plot the charges of Internet Service
# Calculate average MonthlyCharges and TotalCharges for each InternetService
avg_charges <- telco_data %>%
filter(!is.na(InternetService)) %>%
group_by(InternetService) %>%
summarise(avg_monthly_charges = mean(MonthlyCharges),
avg_total_charges = mean(TotalCharges))
# Convert the data from wide to long format
avg_charges_long <- pivot_longer(avg_charges, cols = c(avg_monthly_charges, avg_total_charges),
names_to = "Charges_Type", values_to = "Average_Charges")
# Create a bar plot with average numbers on top
charges_plot <- ggplot(avg_charges_long, aes(x = InternetService, y = Average_Charges, fill = Charges_Type)) +
geom_bar(stat = "identity", position = position_dodge()) +
geom_text(aes(label = sprintf("%.2f", Average_Charges)), vjust = -0.5, size = 3, position = position_dodge(width = 0.9)) +
labs(title = "Average Charges by Internet Service",
x = "Internet Service",
y = "Average Charges") +
scale_fill_manual(values = c("#66B2FF", "#FF6666"), name = "Charges Type",
labels = c("Monthly Charges", "Total Charges")) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
# Show the combined bar plot
print(charges_plot)
Plot the payment-related variables
# Function to create side-by-side bar plots for payment-related variables
create_payment_plot <- function(data, variable) {
plot_data <- data %>%
count(Churn, !!sym(variable)) %>%
mutate(percent = n / sum(n) * 100,
label = ifelse(percent >= 5, paste0(round(percent, 1), "%"), ""))
plot <- ggplot(plot_data, aes(x = !!sym(variable), y = percent, fill = Churn)) +
geom_bar(stat = "identity", position = "dodge") +
geom_text(aes(label = label), position = position_dodge(width = 0.9), vjust = -0.5, size = 3) +
labs(title = paste("Churn by", variable), x = variable, y = "Percentage") +
scale_fill_manual(values = churn_colors) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotate x-axis labels
return(plot)
}
#Analysis of Services and Monthly & Total Charges
#OnlineSecurity, DeviceProtection, TechSupport vs MonthlyCharges
# Filter the required columns
selected_cols <- telco_data %>%
select(OnlineSecurity, DeviceProtection, TechSupport, MonthlyCharges)
# Plot the relationship between service variables and MonthlyCharges
service_charge_plots <- lapply(names(selected_cols)[1:3], function(service_var) {
ggplot(telco_data, aes_string(x = service_var, y = "MonthlyCharges")) +
geom_boxplot(fill = "skyblue") +
labs(title = paste(service_var, "vs MonthlyCharges"), x = service_var, y = "MonthlyCharges") +
theme_minimal()
})
## Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
## ℹ Please use tidy evaluation idioms with `aes()`.
## ℹ See also `vignette("ggplot2-in-packages")` for more information.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# Combine plots into a grid
grid_arrange <- function(...) {
gridExtra::grid.arrange(..., nrow = 1)
}
# Show the combined plots
grid_arrange(grobs = service_charge_plots)
Plot the service-related variables vs. churn
# Function to create side-by-side bar plots for service-related variables vs. churn
# OnlineSecurity, DeviceProtection, TechSupport, vs Churn.
# Function to create bar plots for service-related variables vs. churn (only "Yes" churn)
create_service_churn_plot <- function(data, variable) {
plot_data <- data %>%
filter(Churn == "Yes") %>%
count(!!sym(variable)) %>%
mutate(percent = n / sum(n) * 100)
plot <- ggplot(plot_data, aes(x = !!sym(variable), y = percent)) +
geom_bar(stat = "identity", fill = "#FF6666") +
geom_text(aes(label = paste0(round(percent, 1), "%")),
position = position_dodge(width = 0.9), vjust = 0.5, hjust = 1, color = "black", size = 3) +
labs(title = paste(variable, "vs Churn (Yes)"), x = variable, y = "Percentage") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
return(plot)
}
# List of service-related variables
service_variables <- c("OnlineSecurity", "DeviceProtection", "TechSupport")
# Create plots for each service-related variable vs. churn ("Yes" churn only)
service_churn_plots <- lapply(service_variables, function(var) create_service_churn_plot(telco_data, var))
# Combine plots into a grid
grid.arrange(grobs = service_churn_plots, ncol = 1, top = "Service Variables vs Churn (Yes)")
Plots for each payment-related variable
# List of payment-related variables
payment_variables <- c("Contract", "PaperlessBilling", "PaymentMethod")
# Create side-by-side plots for each payment-related variable
payment_plots <- lapply(payment_variables, function(var) create_payment_plot(telco_data, var))
# Combine plots into a grid
grid.arrange(
arrangeGrob(
textGrob("Payments", gp = gpar(fontsize = 16, fontface = "bold")),
arrangeGrob(grobs = payment_plots, ncol = 2),
heights = c(0.1, 1)
)
)
#Tenure, MonthlyCharges, TotalCharges vs Churn
# Analyze patterns in numerical variables
numerical_data <- telco_data %>%
select(tenure, MonthlyCharges, TotalCharges, Churn)
# Visualize the distributions
# Tenure distribution
ggplot(numerical_data, aes(x = tenure)) +
geom_histogram(fill = "skyblue", color = "black", bins = 30) +
labs(title = "Distribution of Tenure", x = "Tenure (Months)", y = "Frequency") +
theme_minimal()
# MonthlyCharges distribution
ggplot(numerical_data, aes(x = MonthlyCharges)) +
geom_histogram(fill = "skyblue", color = "black", bins = 30) +
labs(title = "Distribution of Monthly Charges", x = "Monthly Charges", y = "Frequency") +
theme_minimal()
# Explore correlation with churn
# Box plot for tenure vs. churn
ggplot(numerical_data, aes(x = Churn, y = tenure, fill = Churn)) +
geom_boxplot() +
labs(title = "Tenure vs. Churn", x = "Churn", y = "Tenure (Months)") +
theme_minimal()
# Box plot for MonthlyCharges vs. churn
ggplot(numerical_data, aes(x = Churn, y = MonthlyCharges, fill = Churn)) +
geom_boxplot() +
labs(title = "Monthly Charges vs. Churn", x = "Churn", y = "Monthly Charges") +
theme_minimal()
# Box plot for TotalCharges vs. churn
ggplot(numerical_data, aes(x = Churn, y = TotalCharges, fill = Churn)) +
geom_boxplot() +
labs(title = "Total Charges vs. Churn", x = "Churn", y = "Total Charges") +
theme_minimal()
Feature Selection
# Perform chi-square test for categorical variables
chi_results <- lapply(telco_data[, !names(telco_data) %in% c('tenure', 'MonthlyCharges', 'TotalCharges', 'Churn')], function(x) chisq.test(x, telco_data$Churn))
p_values <- sapply(chi_results, function(result) result$p.value)
df_chi_results <- data.frame(Variable = names(p_values), P_value = p_values)
print(df_chi_results)
## Variable P_value
## gender gender 4.833762e-01
## SeniorCitizen SeniorCitizen 1.470336e-36
## Partner Partner 2.024856e-35
## Dependents Dependents 6.488025e-42
## PhoneService PhoneService 3.781550e-01
## MultipleLines MultipleLines 5.019781e-04
## InternetService InternetService 6.611624e-159
## OnlineSecurity OnlineSecurity 4.433937e-46
## OnlineBackup OnlineBackup 1.328791e-11
## DeviceProtection DeviceProtection 6.238102e-08
## TechSupport TechSupport 1.021166e-42
## StreamingTV StreamingTV 5.985662e-08
## StreamingMovies StreamingMovies 1.795866e-07
## Contract Contract 7.985062e-256
## PaperlessBilling PaperlessBilling 4.336282e-57
## PaymentMethod PaymentMethod 1.176117e-138
Dot plot
ggplot(df_chi_results, aes(x = Variable, y = P_value)) +
geom_point(size = 3, color = "blue") +
geom_hline(yintercept = 0.05, linetype = "dashed", color = "red") +
labs(title = "P-Value of Chi-square Test", x = "Categorical Variable", y = "P-value") +
theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1))
Hence, drop “gender” and “PhoneService”.
# Drop feature "gender" and "PhoneService"
telco_data$gender <- NULL
telco_data$PhoneService <- NULL
# Check correlation for numerical variables
cm <- cor(telco_data[, c("tenure", "MonthlyCharges", "TotalCharges")])
# Print the correlation matrix
print(cm)
## tenure MonthlyCharges TotalCharges
## tenure 1.0000000 0.2441939 0.8252930
## MonthlyCharges 0.2441939 1.0000000 0.6505401
## TotalCharges 0.8252930 0.6505401 1.0000000
corrplot(cm, method = "number")
Hence, drop “TotalCharges”.
# Drop feature"Totalcharges"
telco_data$TotalCharges <- NULL
Standardize Continuous Variables
num_columns <- c("tenure", "MonthlyCharges")
telco_data[num_columns] <- sapply(telco_data[num_columns], as.numeric)
telco1<- telco_data[,c("tenure", "MonthlyCharges")]
telco1<- data.frame(scale(telco1))
Binning for “tenure”
# Binning process transforming the "tenure" variable in the dataset into categorical groups or "bins". This is useful for converting continuous numerical data into discrete categories, which can make it easier to analyze patterns and trends, creating features for machine learning models.
# Create derived feature from "tenure", different bins of tenure(which is in months) such as '0-1 year', '2-3 years', '3-4 years', etc.
telco_data$tenure_bin = telco_data$tenure
telco_data$tenure_bin[telco_data$tenure_bin >= 0 & telco_data$tenure_bin <= 12] <- '0-1 year'
telco_data$tenure_bin[telco_data$tenure_bin > 12 & telco_data$tenure_bin <= 24] <- '1-2 years'
telco_data$tenure_bin[telco_data$tenure_bin > 24 & telco_data$tenure_bin <= 36] <- '2-3 years'
telco_data$tenure_bin[telco_data$tenure_bin > 36 & telco_data$tenure_bin <= 48] <- '3-4 years'
telco_data$tenure_bin[telco_data$tenure_bin > 48 & telco_data$tenure_bin <= 60] <- '4-5 years'
telco_data$tenure_bin[telco_data$tenure_bin > 60 & telco_data$tenure_bin <= 72] <- '5-6 years'
Visualize the binning result
ggplot(telco_data, aes(x = tenure_bin)) +
geom_bar(fill = c("#1f78b4", "#33a02c", "#e31a1c", "#ff7f00", "#6a3d9a", "#a6cee3"))+
labs(title = "Distribution of Customers by Tenure Bins",
x = "Tenure Bin",
y = "Count of Customers") +
theme_minimal()
Based on the plot, the distribution of data in each tenure bin suggested that greater number of customers have a tenure of 0-1 years, followed by 5-6 years.
# Convert to factor
telco_data$tenure_bin <- as.factor(telco_data$tenure_bin)
# Create dummy tenure
tenure_dummy <- telco_data$tenure
# Change numerical values in "SeniorCitizen" to categorical
telco_data <- telco_data %>% mutate(SeniorCitizen = ifelse(SeniorCitizen == "0", "No", "Yes"))
# Delete tenure col
telco_data$tenure <- NULL
# Create dummy variables
telco2 = telco_data[,-c(17)]
dummy <- data.frame(sapply(telco2,function(x) data.frame(model.matrix(~x-1,data =telco2))[,-1]))
head(dummy)
## SeniorCitizen Partner Dependents MultipleLines InternetService.xFiber.optic
## 1 0 1 0 0 0
## 2 0 0 0 0 0
## 3 0 0 0 0 0
## 4 0 0 0 0 0
## 5 0 0 0 0 1
## 6 0 0 0 1 1
## InternetService.xNo OnlineSecurity OnlineBackup DeviceProtection TechSupport
## 1 0 0 1 0 0
## 2 0 1 0 1 0
## 3 0 1 1 0 0
## 4 0 1 0 1 1
## 5 0 0 0 0 0
## 6 0 0 0 1 0
## StreamingTV StreamingMovies Contract.xOne.year Contract.xTwo.year
## 1 0 0 0 0
## 2 0 0 1 0
## 3 0 0 0 0
## 4 0 0 1 0
## 5 0 0 0 0
## 6 1 1 0 0
## PaperlessBilling PaymentMethod.xCredit.card..automatic.
## 1 1 0
## 2 0 0
## 3 1 0
## 4 0 0
## 5 1 0
## 6 1 0
## PaymentMethod.xElectronic.check PaymentMethod.xMailed.check Churn
## 1 1 0 0
## 2 0 1 0
## 3 0 1 1
## 4 0 0 0
## 5 1 0 1
## 6 1 0 1
# Create final dataset by combining numerical and dummy data frame
df_telco <- cbind(telco1, dummy)
head(df_telco)
## tenure MonthlyCharges SeniorCitizen Partner Dependents MultipleLines
## 1 -1.28547439 -1.1654394 0 1 0 0
## 2 0.06034151 -0.2640521 0 0 0 0
## 3 -1.24469209 -0.3671629 0 0 0 0
## 4 0.50894682 -0.7513334 0 0 0 0
## 5 -1.24469209 0.1932938 0 0 0 0
## 6 -0.99999829 1.1562149 0 0 0 1
## InternetService.xFiber.optic InternetService.xNo OnlineSecurity OnlineBackup
## 1 0 0 0 1
## 2 0 0 1 0
## 3 0 0 1 1
## 4 0 0 1 0
## 5 1 0 0 0
## 6 1 0 0 0
## DeviceProtection TechSupport StreamingTV StreamingMovies Contract.xOne.year
## 1 0 0 0 0 0
## 2 1 0 0 0 1
## 3 0 0 0 0 0
## 4 1 1 0 0 1
## 5 0 0 0 0 0
## 6 1 0 1 1 0
## Contract.xTwo.year PaperlessBilling PaymentMethod.xCredit.card..automatic.
## 1 0 1 0
## 2 0 0 0
## 3 0 1 0
## 4 0 0 0
## 5 0 1 0
## 6 0 1 0
## PaymentMethod.xElectronic.check PaymentMethod.xMailed.check Churn
## 1 1 0 0
## 2 0 1 0
## 3 0 1 1
## 4 0 0 0
## 5 1 0 1
## 6 1 0 1
# Ensure Churn column is a factor with two levels
df_telco$Churn <- as.factor(df_telco$Churn)
Split Data into Train Test
# Split Data into Train Test Datasets (70%,30%)
set.seed(123)
indices = sample.split(df_telco$Churn, SplitRatio = 0.7)
train = df_telco[indices,]
validation = df_telco[!(indices),]
# Check the size of the training set
train_size <- dim(train)
cat("Training set size:", train_size[1], "rows and", train_size[2], "columns\n")
## Training set size: 4907 rows and 21 columns
# Check the size of the validation set
validation_size <- dim(validation)
cat("Validation set size:", validation_size[1], "rows and", validation_size[2], "columns\n")
## Validation set size: 2103 rows and 21 columns
# Define the control for train function - configure 10-fold cross validation for the model training
train_control <- trainControl(method="cv", number=10)
Logistic Regression
log_model <- train(Churn ~ ., data=train, method="glm", family="binomial", trControl=train_control)
log_pred <- predict(log_model, validation)
confusionMatrix(log_pred, validation$Churn)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1374 259
## 1 172 298
##
## Accuracy : 0.7951
## 95% CI : (0.7772, 0.8121)
## No Information Rate : 0.7351
## P-Value [Acc > NIR] : 9.598e-11
##
## Kappa : 0.446
##
## Mcnemar's Test P-Value : 3.436e-05
##
## Sensitivity : 0.8887
## Specificity : 0.5350
## Pos Pred Value : 0.8414
## Neg Pred Value : 0.6340
## Prevalence : 0.7351
## Detection Rate : 0.6534
## Detection Prevalence : 0.7765
## Balanced Accuracy : 0.7119
##
## 'Positive' Class : 0
##
Decision Tree
dt_model <- train(Churn ~ ., data=train, method="rpart", trControl=train_control)
dt_pred <- predict(dt_model, validation)
confusionMatrix(dt_pred, validation$Churn)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1381 294
## 1 165 263
##
## Accuracy : 0.7817
## 95% CI : (0.7635, 0.7992)
## No Information Rate : 0.7351
## P-Value [Acc > NIR] : 4.472e-07
##
## Kappa : 0.3947
##
## Mcnemar's Test P-Value : 2.308e-09
##
## Sensitivity : 0.8933
## Specificity : 0.4722
## Pos Pred Value : 0.8245
## Neg Pred Value : 0.6145
## Prevalence : 0.7351
## Detection Rate : 0.6567
## Detection Prevalence : 0.7965
## Balanced Accuracy : 0.6827
##
## 'Positive' Class : 0
##
Random Forest
rf_model <- train(Churn ~ ., data=train, method="rf", trControl=train_control)
rf_pred <- predict(rf_model, validation)
confusionMatrix(rf_pred, validation$Churn)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1395 308
## 1 151 249
##
## Accuracy : 0.7817
## 95% CI : (0.7635, 0.7992)
## No Information Rate : 0.7351
## P-Value [Acc > NIR] : 4.472e-07
##
## Kappa : 0.384
##
## Mcnemar's Test P-Value : 3.302e-13
##
## Sensitivity : 0.9023
## Specificity : 0.4470
## Pos Pred Value : 0.8191
## Neg Pred Value : 0.6225
## Prevalence : 0.7351
## Detection Rate : 0.6633
## Detection Prevalence : 0.8098
## Balanced Accuracy : 0.6747
##
## 'Positive' Class : 0
##
Support Vector Machine
svm_model <- train(Churn ~ ., data=train, method="svmRadial", trControl=train_control)
svm_pred <- predict(svm_model, validation)
confusionMatrix(svm_pred, validation$Churn)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1397 306
## 1 149 251
##
## Accuracy : 0.7836
## 95% CI : (0.7654, 0.8011)
## No Information Rate : 0.7351
## P-Value [Acc > NIR] : 1.528e-07
##
## Kappa : 0.3894
##
## Mcnemar's Test P-Value : 2.605e-13
##
## Sensitivity : 0.9036
## Specificity : 0.4506
## Pos Pred Value : 0.8203
## Neg Pred Value : 0.6275
## Prevalence : 0.7351
## Detection Rate : 0.6643
## Detection Prevalence : 0.8098
## Balanced Accuracy : 0.6771
##
## 'Positive' Class : 0
##
Gradient Boosting Machine
gbm_model <- train(Churn ~ ., data=train, method="gbm", trControl=train_control, verbose=FALSE)
gbm_pred <- predict(gbm_model, validation)
confusionMatrix(gbm_pred, validation$Churn)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1371 284
## 1 175 273
##
## Accuracy : 0.7817
## 95% CI : (0.7635, 0.7992)
## No Information Rate : 0.7351
## P-Value [Acc > NIR] : 4.472e-07
##
## Kappa : 0.4021
##
## Mcnemar's Test P-Value : 4.631e-07
##
## Sensitivity : 0.8868
## Specificity : 0.4901
## Pos Pred Value : 0.8284
## Neg Pred Value : 0.6094
## Prevalence : 0.7351
## Detection Rate : 0.6519
## Detection Prevalence : 0.7870
## Balanced Accuracy : 0.6885
##
## 'Positive' Class : 0
##
Model evaluation summary
model_list <- list(Logistic_Regression = log_model,
Decision_Tree = dt_model,
Random_Forest = rf_model,
SVM = svm_model,
GBM = gbm_model)
results <- resamples(model_list)
summary(results)
##
## Call:
## summary.resamples(object = results)
##
## Models: Logistic_Regression, Decision_Tree, Random_Forest, SVM, GBM
## Number of resamples: 10
##
## Accuracy
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## Logistic_Regression 0.7759674 0.7865518 0.8034623 0.8043572 0.8189721 0.8452138
## Decision_Tree 0.7637475 0.7786826 0.7879733 0.7847949 0.7902240 0.8024440
## Random_Forest 0.7556008 0.7923469 0.8024440 0.8010948 0.8161914 0.8248473
## SVM 0.7698574 0.8009165 0.8073424 0.8025267 0.8121181 0.8187373
## GBM 0.7881874 0.8019348 0.8065173 0.8104743 0.8188776 0.8431772
## NA's
## Logistic_Regression 0
## Decision_Tree 0
## Random_Forest 0
## SVM 0
## GBM 0
##
## Kappa
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## Logistic_Regression 0.3852300 0.4234517 0.4671602 0.4697882 0.5145457 0.5661233
## Decision_Tree 0.3341906 0.3566283 0.4005281 0.3901805 0.4081829 0.4477197
## Random_Forest 0.2960237 0.4032861 0.4373632 0.4319859 0.4808388 0.4982533
## SVM 0.3654509 0.4321650 0.4507990 0.4412884 0.4665323 0.4956632
## GBM 0.4267990 0.4618120 0.4667919 0.4807179 0.4992355 0.5685265
## NA's
## Logistic_Regression 0
## Decision_Tree 0
## Random_Forest 0
## SVM 0
## GBM 0
model_names <- c("Logistic Regression", "Decision Tree", "Random Forest", "Support Vector Machine", "Gradient Boosting Machine")
accuracy <- c(0.7951, 0.7817 , 0.7817, 0.7836, 0.7817) * 100
kappa <- c(0.446, 0.3947, 0.384, 0.3894, 0.4021)
sensitivity <- c(0.8887 , 0.8933, 0.9023 , 0.9036, 0.8868) * 100
specificity <- c(0.5350 , 0.4722, 0.4470, 0.4506, 0.4901) * 100
precision <- c(0.8414 , 0.8245, 0.8191, 0.8203, 0.8284) * 100
# Create a data frame
model_comparison <- data.frame(
Model = model_names,
Accuracy = paste0(accuracy, "%"),
Kappa = paste0(kappa, " "),
Sensitivity = paste0(sensitivity, "%"),
Specificity = paste0(specificity, "%"),
Precision = paste0(precision, "%")
)
# Render the table
kable(model_comparison, format = "html", caption = "Comparison of Model Performance Metrics") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive")) %>%
row_spec(1, bold = TRUE, background = "#D3D3D3")
| Model | Accuracy | Kappa | Sensitivity | Specificity | Precision |
|---|---|---|---|---|---|
| Logistic Regression | 79.51% | 0.446 | 88.87% | 53.5% | 84.14% |
| Decision Tree | 78.17% | 0.3947 | 89.33% | 47.22% | 82.45% |
| Random Forest | 78.17% | 0.384 | 90.23% | 44.7% | 81.91% |
| Support Vector Machine | 78.36% | 0.3894 | 90.36% | 45.06% | 82.03% |
| Gradient Boosting Machine | 78.17% | 0.4021 | 88.68% | 49.01% | 82.84% |