DATASET OVERVIEW AND QUALITY ASSESSMENT
# Read the CSV file; using sep=";" based on the dataset documentation
bank <- read.csv("bank-additional-full.csv", sep = ";", stringsAsFactors = TRUE)
# Examine the structure and variables to understand data types
str(bank)
## 'data.frame': 41188 obs. of 21 variables:
## $ age : int 56 57 37 40 56 45 59 41 24 25 ...
## $ job : Factor w/ 12 levels "admin.","blue-collar",..: 4 8 8 1 8 8 1 2 10 8 ...
## $ marital : Factor w/ 4 levels "divorced","married",..: 2 2 2 2 2 2 2 2 3 3 ...
## $ education : Factor w/ 8 levels "basic.4y","basic.6y",..: 1 4 4 2 4 3 6 8 6 4 ...
## $ default : Factor w/ 3 levels "no","unknown",..: 1 2 1 1 1 2 1 2 1 1 ...
## $ housing : Factor w/ 3 levels "no","unknown",..: 1 1 3 1 1 1 1 1 3 3 ...
## $ loan : Factor w/ 3 levels "no","unknown",..: 1 1 1 1 3 1 1 1 1 1 ...
## $ contact : Factor w/ 2 levels "cellular","telephone": 2 2 2 2 2 2 2 2 2 2 ...
## $ month : Factor w/ 10 levels "apr","aug","dec",..: 7 7 7 7 7 7 7 7 7 7 ...
## $ day_of_week : Factor w/ 5 levels "fri","mon","thu",..: 2 2 2 2 2 2 2 2 2 2 ...
## $ duration : int 261 149 226 151 307 198 139 217 380 50 ...
## $ campaign : int 1 1 1 1 1 1 1 1 1 1 ...
## $ pdays : int 999 999 999 999 999 999 999 999 999 999 ...
## $ previous : int 0 0 0 0 0 0 0 0 0 0 ...
## $ poutcome : Factor w/ 3 levels "failure","nonexistent",..: 2 2 2 2 2 2 2 2 2 2 ...
## $ emp.var.rate : num 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 ...
## $ cons.price.idx: num 94 94 94 94 94 ...
## $ cons.conf.idx : num -36.4 -36.4 -36.4 -36.4 -36.4 -36.4 -36.4 -36.4 -36.4 -36.4 ...
## $ euribor3m : num 4.86 4.86 4.86 4.86 4.86 ...
## $ nr.employed : num 5191 5191 5191 5191 5191 ...
## $ y : Factor w/ 2 levels "no","yes": 1 1 1 1 1 1 1 1 1 1 ...
# Display summary statistics to evaluate central tendency, spread, and missing values
summary(bank)
## age job marital
## Min. :17.00 admin. :10422 divorced: 4612
## 1st Qu.:32.00 blue-collar: 9254 married :24928
## Median :38.00 technician : 6743 single :11568
## Mean :40.02 services : 3969 unknown : 80
## 3rd Qu.:47.00 management : 2924
## Max. :98.00 retired : 1720
## (Other) : 6156
## education default housing loan
## university.degree :12168 no :32588 no :18622 no :33950
## high.school : 9515 unknown: 8597 unknown: 990 unknown: 990
## basic.9y : 6045 yes : 3 yes :21576 yes : 6248
## professional.course: 5243
## basic.4y : 4176
## basic.6y : 2292
## (Other) : 1749
## contact month day_of_week duration
## cellular :26144 may :13769 fri:7827 Min. : 0.0
## telephone:15044 jul : 7174 mon:8514 1st Qu.: 102.0
## aug : 6178 thu:8623 Median : 180.0
## jun : 5318 tue:8090 Mean : 258.3
## nov : 4101 wed:8134 3rd Qu.: 319.0
## apr : 2632 Max. :4918.0
## (Other): 2016
## campaign pdays previous poutcome
## Min. : 1.000 Min. : 0.0 Min. :0.000 failure : 4252
## 1st Qu.: 1.000 1st Qu.:999.0 1st Qu.:0.000 nonexistent:35563
## Median : 2.000 Median :999.0 Median :0.000 success : 1373
## Mean : 2.568 Mean :962.5 Mean :0.173
## 3rd Qu.: 3.000 3rd Qu.:999.0 3rd Qu.:0.000
## Max. :56.000 Max. :999.0 Max. :7.000
##
## emp.var.rate cons.price.idx cons.conf.idx euribor3m
## Min. :-3.40000 Min. :92.20 Min. :-50.8 Min. :0.634
## 1st Qu.:-1.80000 1st Qu.:93.08 1st Qu.:-42.7 1st Qu.:1.344
## Median : 1.10000 Median :93.75 Median :-41.8 Median :4.857
## Mean : 0.08189 Mean :93.58 Mean :-40.5 Mean :3.621
## 3rd Qu.: 1.40000 3rd Qu.:93.99 3rd Qu.:-36.4 3rd Qu.:4.961
## Max. : 1.40000 Max. :94.77 Max. :-26.9 Max. :5.045
##
## nr.employed y
## Min. :4964 no :36548
## 1st Qu.:5099 yes: 4640
## Median :5191
## Mean :5167
## 3rd Qu.:5228
## Max. :5228
##
# Check the distribution of the target variable 'y' to confirm class imbalance
prop.table(table(bank$y))
##
## no yes
## 0.8873458 0.1126542
EXPLORATORY DATA ANALYSIS (EDA)
# Identify missing/unknown values in categorical features
table(bank$job, useNA = "ifany")
##
## admin. blue-collar entrepreneur housemaid management
## 10422 9254 1456 1060 2924
## retired self-employed services student technician
## 1720 1421 3969 875 6743
## unemployed unknown
## 1014 330
table(bank$default, useNA = "ifany")
##
## no unknown yes
## 32588 8597 3
# Visualize the distribution of a categorical variable
barplot(table(bank$education),
main = "Distribution of Education Levels",
col = "coral",
las = 2,
cex.names = 0.7)

# Visualize the distribution of client age
hist(bank$age, main = "Histogram of Client Age", xlab = "Age", col = "lightblue")

# Visualize outliers in campaign contacts using a boxplot
boxplot(bank$campaign, main = "Boxplot of Campaign Contacts", ylab = "Number of Contacts", col = "lightgreen")

# Scatterplot to investigate the relationship between age and campaign contacts
plot(bank$age, bank$campaign,
main = "Scatterplot of Age vs. Campaign Contacts",
xlab = "Client Age",
ylab = "Number of Contacts During Campaign",
col = ifelse(bank$y == "yes", "blue", "darkgray"),
pch = 16,
cex = 0.8)
legend("topright", legend = c("Subscribed (Yes)", "Did Not Subscribe (No)"),
col = c("blue", "darkgray"), pch = 16)

# Visual Correlation Matrix to identify macroeconomic multicollinearity
library(psych)
# Take a random sample to drastically reduce rendering time
set.seed(123)
bank_sample <- bank[sample(nrow(bank), 1000), ]
# Generate the plot directly in the RStudio pane using the sampled data
pairs.panels(bank_sample[, c("emp.var.rate", "cons.price.idx", "cons.conf.idx", "euribor3m", "nr.employed")],
main = "Visual Correlation Matrix of Macroeconomic Indicators",
method = "pearson",
hist.col = "lightblue",
density = TRUE,
ellipses = TRUE)

FEATURE ENGINEERING AND SELECTION
# Address Data Leakage: Drop the 'duration' column
# Call duration is unknown before the call and perfectly predicts outcome if 0
bank$duration <- NULL
# Set a random seed to ensure reproducibility of the random split
set.seed(12345)
# Create a random sample vector for a 90/10 training/test split
train_sample <- sample(41188, 37069)
# Split the data frame into training and testing sets
bank_train <- bank[train_sample, ]
bank_test <- bank[-train_sample, ]
MODEL TRAINING / IMBALANCES
# Load the C50 package for decision tree classification
library(C50)
# Train a baseline C5.0 decision tree
# Exclude the target variable (column 20) from the training features
bank_model <- C5.0(bank_train[-20], bank_train$y)
# View basic tree footprint and feature importance (replacing the massive summary output)
print(bank_model)
##
## Call:
## C5.0.default(x = bank_train[-20], y = bank_train$y)
##
## Classification Tree
## Number of samples: 37069
## Number of predictors: 19
##
## Tree size: 63
##
## Non-standard options: attempt to group attributes
C5imp(bank_model)
## Overall
## poutcome 100.00
## nr.employed 100.00
## month 90.13
## contact 9.30
## pdays 7.82
## emp.var.rate 7.61
## day_of_week 3.53
## campaign 1.93
## education 1.50
## default 1.45
## housing 1.07
## job 0.92
## loan 0.24
## previous 0.21
## marital 0.19
## cons.price.idx 0.07
## euribor3m 0.07
## cons.conf.idx 0.04
## age 0.02
# Plot a focused subtree (Node 2) to visualize the highest-propensity splits clearly
plot(bank_model, subtree = 2)

# Make predictions on the unseen test dataset
bank_pred <- predict(bank_model, bank_test)
# Load the gmodels package to evaluate performance
library(gmodels)
CrossTable(bank_test$y, bank_pred,
prop.chisq = FALSE, prop.c = FALSE, prop.r = FALSE,
dnn = c('Actual Subscription', 'Predicted Subscription'))
##
##
## Cell Contents
## |-------------------------|
## | N |
## | N / Table Total |
## |-------------------------|
##
##
## Total Observations in Table: 4119
##
##
## | Predicted Subscription
## Actual Subscription | no | yes | Row Total |
## --------------------|-----------|-----------|-----------|
## no | 3602 | 65 | 3667 |
## | 0.874 | 0.016 | |
## --------------------|-----------|-----------|-----------|
## yes | 342 | 110 | 452 |
## | 0.083 | 0.027 | |
## --------------------|-----------|-----------|-----------|
## Column Total | 3944 | 175 | 4119 |
## --------------------|-----------|-----------|-----------|
##
##
OPTIMIZING FOR BUSINESS GOALS (COST SENSITIVE LEARNING)
# Address the severe class imbalance using a cost matrix
# Goal: Prioritize high-propensity contacts by penalizing false negatives
matrix_dimensions <- list(c("no", "yes"), c("no", "yes"))
names(matrix_dimensions) <- c("predicted", "actual")
# Assign a 4x penalty to false negatives (predicting 'no' when actual is 'yes')
error_cost <- matrix(c(0, 1, 4, 0), nrow = 2, dimnames = matrix_dimensions)
# Train a cost-sensitive C5.0 decision tree to maximize the 25% conversion target
bank_cost_model <- C5.0(bank_train[-20], bank_train$y, costs = error_cost)
# Evaluate the cost-sensitive model predictions
bank_cost_pred <- predict(bank_cost_model, bank_test)
CrossTable(bank_test$y, bank_cost_pred,
prop.chisq = FALSE, prop.c = FALSE, prop.r = FALSE,
dnn = c('Actual Subscription', 'Predicted Subscription'))
##
##
## Cell Contents
## |-------------------------|
## | N |
## | N / Table Total |
## |-------------------------|
##
##
## Total Observations in Table: 4119
##
##
## | Predicted Subscription
## Actual Subscription | no | yes | Row Total |
## --------------------|-----------|-----------|-----------|
## no | 3343 | 324 | 3667 |
## | 0.812 | 0.079 | |
## --------------------|-----------|-----------|-----------|
## yes | 206 | 246 | 452 |
## | 0.050 | 0.060 | |
## --------------------|-----------|-----------|-----------|
## Column Total | 3549 | 570 | 4119 |
## --------------------|-----------|-----------|-----------|
##
##