Introduction

In Machine Learning, Experimentation refers to the systematic process of designing, executing, and analyzing different configurations to identify the optimal settings that performs best on a given task. Experimentation is learning by doing. It involves systematically changing parameters, evaluating results with metrics, and comparing different approaches to find the best solution; essentially, it’s the practice of testing and refining machine learning models through controlled experiments to improve their performance.

The key is to modify only one or a few variables at a time to isolate the impact of each change and understand its effect on model performance. In the assignment you will conduct at least 6 experiments. In real life, data scientists run anywhere from a dozen to hundreds of experiments (depending on the dataset and problem domain).

Load and Inspect the Data

library(tidyverse)
## Warning: package 'lubridate' was built under R version 4.4.2
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
library(tidyr)
library(rpart)
library(rpart.plot)
library(lubridate)
library(skimr)
## Warning: package 'skimr' was built under R version 4.4.2
library(stringr)
library(corrplot)
## Warning: package 'corrplot' was built under R version 4.4.2
## corrplot 0.95 loaded
library(ggplot2)
library(fpp3)
## Warning: package 'fpp3' was built under R version 4.4.2
## Registered S3 method overwritten by 'tsibble':
##   method               from 
##   as_tibble.grouped_df dplyr
## ── Attaching packages ──────────────────────────────────────────── fpp3 1.0.1 ──
## ✔ tsibble     1.1.6     ✔ feasts      0.4.1
## ✔ tsibbledata 0.4.1     ✔ fable       0.4.1
## Warning: package 'tsibble' was built under R version 4.4.2
## Warning: package 'tsibbledata' was built under R version 4.4.2
## Warning: package 'feasts' was built under R version 4.4.2
## Warning: package 'fabletools' was built under R version 4.4.2
## Warning: package 'fable' was built under R version 4.4.2
## ── Conflicts ───────────────────────────────────────────────── fpp3_conflicts ──
## ✖ lubridate::date()    masks base::date()
## ✖ dplyr::filter()      masks stats::filter()
## ✖ tsibble::intersect() masks base::intersect()
## ✖ tsibble::interval()  masks lubridate::interval()
## ✖ dplyr::lag()         masks stats::lag()
## ✖ tsibble::setdiff()   masks base::setdiff()
## ✖ tsibble::union()     masks base::union()
library(caret)
## Loading required package: lattice
## 
## Attaching package: 'caret'
## 
## The following objects are masked from 'package:fabletools':
## 
##     MAE, RMSE
## 
## The following object is masked from 'package:purrr':
## 
##     lift
library(highcharter)
## Warning: package 'highcharter' was built under R version 4.4.3
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo 
## Highcharts (www.highcharts.com) is a Highsoft software product which is
## not free for commercial and Governmental use
library(dplyr)
library(randomForest)
## randomForest 4.7-1.1
## Type rfNews() to see new features/changes/bug fixes.
## 
## Attaching package: 'randomForest'
## 
## The following object is masked from 'package:dplyr':
## 
##     combine
## 
## The following object is masked from 'package:ggplot2':
## 
##     margin
library(adabag)
## Warning: package 'adabag' was built under R version 4.4.3
## Loading required package: foreach
## 
## Attaching package: 'foreach'
## 
## The following objects are masked from 'package:purrr':
## 
##     accumulate, when
## 
## Loading required package: doParallel
## Warning: package 'doParallel' was built under R version 4.4.3
## Loading required package: iterators
## Loading required package: parallel
library(ROCR)           
## Warning: package 'ROCR' was built under R version 4.4.3
library(pROC)
## Type 'citation("pROC")' for a citation.
## 
## Attaching package: 'pROC'
## 
## The following objects are masked from 'package:stats':
## 
##     cov, smooth, var
library(knitr)
library(kableExtra)
## Warning: package 'kableExtra' was built under R version 4.4.2
## 
## Attaching package: 'kableExtra'
## 
## The following object is masked from 'package:dplyr':
## 
##     group_rows

Load Data

data <- read.csv("C:/Users/Admin/Desktop/Bank_full.csv")
str(data)
## 'data.frame':    45211 obs. of  17 variables:
##  $ age          : int  58 44 33 47 33 35 28 42 58 43 ...
##  $ job          : chr  "management" "technician" "entrepreneur" "blue-collar" ...
##  $ marital      : chr  "married" "single" "married" "married" ...
##  $ education    : chr  "tertiary" "secondary" "secondary" "unknown" ...
##  $ default      : chr  "no" "no" "no" "no" ...
##  $ balance      : int  2143 29 2 1506 1 231 447 2 121 593 ...
##  $ housing      : chr  "yes" "yes" "yes" "yes" ...
##  $ personal_loan: chr  "no" "no" "yes" "no" ...
##  $ contact      : chr  "unknown" "unknown" "unknown" "unknown" ...
##  $ day          : int  5 5 5 5 5 5 5 5 5 5 ...
##  $ month        : chr  "may" "may" "may" "may" ...
##  $ duration     : int  261 151 76 92 198 139 217 380 50 55 ...
##  $ campaign     : int  1 1 1 1 1 1 1 1 1 1 ...
##  $ pdays        : int  -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 ...
##  $ previous     : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ poutcome     : chr  "unknown" "unknown" "unknown" "unknown" ...
##  $ term_dep_sub : chr  "no" "no" "no" "no" ...
summary(data)
##       age            job              marital           education        
##  Min.   :18.00   Length:45211       Length:45211       Length:45211      
##  1st Qu.:33.00   Class :character   Class :character   Class :character  
##  Median :39.00   Mode  :character   Mode  :character   Mode  :character  
##  Mean   :40.94                                                           
##  3rd Qu.:48.00                                                           
##  Max.   :95.00                                                           
##    default             balance         housing          personal_loan     
##  Length:45211       Min.   : -8019   Length:45211       Length:45211      
##  Class :character   1st Qu.:    72   Class :character   Class :character  
##  Mode  :character   Median :   448   Mode  :character   Mode  :character  
##                     Mean   :  1362                                        
##                     3rd Qu.:  1428                                        
##                     Max.   :102127                                        
##    contact               day           month              duration     
##  Length:45211       Min.   : 1.00   Length:45211       Min.   :   0.0  
##  Class :character   1st Qu.: 8.00   Class :character   1st Qu.: 103.0  
##  Mode  :character   Median :16.00   Mode  :character   Median : 180.0  
##                     Mean   :15.81                      Mean   : 258.2  
##                     3rd Qu.:21.00                      3rd Qu.: 319.0  
##                     Max.   :31.00                      Max.   :4918.0  
##     campaign          pdays          previous          poutcome        
##  Min.   : 1.000   Min.   : -1.0   Min.   :  0.0000   Length:45211      
##  1st Qu.: 1.000   1st Qu.: -1.0   1st Qu.:  0.0000   Class :character  
##  Median : 2.000   Median : -1.0   Median :  0.0000   Mode  :character  
##  Mean   : 2.764   Mean   : 40.2   Mean   :  0.5803                     
##  3rd Qu.: 3.000   3rd Qu.: -1.0   3rd Qu.:  0.0000                     
##  Max.   :63.000   Max.   :871.0   Max.   :275.0000                     
##  term_dep_sub      
##  Length:45211      
##  Class :character  
##  Mode  :character  
##                    
##                    
## 
head(data)
##   age          job marital education default balance housing personal_loan
## 1  58   management married  tertiary      no    2143     yes            no
## 2  44   technician  single secondary      no      29     yes            no
## 3  33 entrepreneur married secondary      no       2     yes           yes
## 4  47  blue-collar married   unknown      no    1506     yes            no
## 5  33      unknown  single   unknown      no       1      no            no
## 6  35   management married  tertiary      no     231     yes            no
##   contact day month duration campaign pdays previous poutcome term_dep_sub
## 1 unknown   5   may      261        1    -1        0  unknown           no
## 2 unknown   5   may      151        1    -1        0  unknown           no
## 3 unknown   5   may       76        1    -1        0  unknown           no
## 4 unknown   5   may       92        1    -1        0  unknown           no
## 5 unknown   5   may      198        1    -1        0  unknown           no
## 6 unknown   5   may      139        1    -1        0  unknown           no
desc_table <- data.frame(
  Var = c("age", "job", "marital", "education", "default", "balance", 
          "housing", "loan", "contact", "day", "month", "duration", 
          "campaign", "pdays", "previous", "poutcome", "y"),
  
  Desc = c(
    "Client's age in years",
    "Client's job or occupation category",
    "Client's marital status",
    "Highest education level completed by the client",
    "Indicates if the client has a history of credit default",
    "Client's average yearly account balance (in euros)",
    "Indicates whether the client has a housing loan",
    "Indicates whether the client has a personal loan",
    "Communication method used to contact the client",
    "Day of the month the last contact was made",
    "Month when the last contact was made",
    "Duration of the last call (in seconds)",
    "Number of contacts made during the current campaign",
    "Days since the client was previously contacted (-1 means no prior contact)",
    "Number of contacts made in previous marketing campaigns",
    "Outcome of the previous marketing campaign",
    "Target variable: Did the client subscribe to a term deposit?"
  )
)

kable(desc_table, align = "ll", caption = "Description of Variables") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) %>%
  column_spec(1, width = "2in") %>%
  column_spec(2, width = "5in")
Description of Variables
Var Desc
age Client’s age in years
job Client’s job or occupation category
marital Client’s marital status
education Highest education level completed by the client
default Indicates if the client has a history of credit default
balance Client’s average yearly account balance (in euros)
housing Indicates whether the client has a housing loan
loan Indicates whether the client has a personal loan
contact Communication method used to contact the client
day Day of the month the last contact was made
month Month when the last contact was made
duration Duration of the last call (in seconds)
campaign Number of contacts made during the current campaign
pdays Days since the client was previously contacted (-1 means no prior contact)
previous Number of contacts made in previous marketing campaigns
poutcome Outcome of the previous marketing campaign
y Target variable: Did the client subscribe to a term deposit?

Data Preparation

# Convert target variable to factor
data$term_dep_sub <- as.factor(data$term_dep_sub)

# Check for missing values
sapply(data, function(x) sum(is.na(x)))
##           age           job       marital     education       default 
##             0             0             0             0             0 
##       balance       housing personal_loan       contact           day 
##             0             0             0             0             0 
##         month      duration      campaign         pdays      previous 
##             0             0             0             0             0 
##      poutcome  term_dep_sub 
##             0             0
# Train-test split
set.seed(123)
library(caret)
trainIndex <- createDataPartition(data$term_dep_sub, p = .7, list = FALSE)
train <- data[trainIndex,]
test <- data[-trainIndex,]

# Check class balance in the target
prop.table(table(train$term_dep_sub))
## 
##        no       yes 
## 0.8829979 0.1170021
prop.table(table(test$term_dep_sub))
## 
##        no       yes 
## 0.8830556 0.1169444

Experiments

Decision Tree

Experiment 1: Default Decision Tree

Decision Trees remain one of the most interpretable machine learning algorithms for classification tasks. They work by recursively splitting the dataset based on feature values that minimize classification error. The default decision tree provides a baseline for performance evaluation and serves as a foundation for further tuning and optimization.

Hypothesis: The default Decision Tree model will generate an interpretable tree structure and deliver baseline accuracy that can guide future experiments.

default_dt <- rpart(term_dep_sub ~ ., data = train, method = "class")
default_pred_prob <- predict(default_dt, test, type = "prob")[, 2]
default_pred <- predict(default_dt, test, type = "class")

# Plot the default decision tree
rpart.plot(default_dt, main = "Default Decision Tree")

In my default decision tree model, call duration is the most important factor in predicting if a client will subscribe to a term deposit. The first split happens at duration < 525, where shorter calls almost always lead to a “no” prediction—likely because quick calls signal less interest.

For longer calls, the model considers the outcome of the previous campaign (poutcome). If it was labeled as failure, other, or unknown, the model still predicts “no” in most cases. However, as call duration increases, the chances of a “yes” prediction improve.

The model also looks at the month of contact and adds more splits based on duration. Still, most branches end in a “no” prediction. Only a few leaf nodes predict “yes,” mostly when calls are longer and certain conditions are met.

Overall, the tree shows that longer conversations and a better past campaign outcome slightly increase the chances of subscription, but the model mostly predicts clients won’t subscribe.

# Predict classes
dt_pred_class <- predict(default_dt, test, type = "class")

# Calculate confusion matrix
dt_conf_matrix <- confusionMatrix(dt_pred_class, test$term_dep_sub)

# Extract and print accuracy
dt_accuracy <- dt_conf_matrix$overall["Accuracy"]
cat("Decision Tree Exp 1 (Default): Accuracy:", round(dt_accuracy, 4), "\n")
## Decision Tree Exp 1 (Default): Accuracy: 0.9019

The results indicate that the model correctly predicts outcomes with 90% accuracy on the test set.

# Make predictions
baseline_pred_class <- predict(default_dt, test, type = "class")

# Evaluate with confusion matrix (define positive class if binary)
baseline_conf_matrix <- confusionMatrix(baseline_pred_class, test$term_dep_sub, positive = "yes")

# Print the confusion matrix
print(baseline_conf_matrix)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    no   yes
##        no  11574   929
##        yes   402   657
##                                           
##                Accuracy : 0.9019          
##                  95% CI : (0.8967, 0.9068)
##     No Information Rate : 0.8831          
##     P-Value [Acc > NIR] : 1.619e-12       
##                                           
##                   Kappa : 0.4448          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.41425         
##             Specificity : 0.96643         
##          Pos Pred Value : 0.62040         
##          Neg Pred Value : 0.92570         
##              Prevalence : 0.11694         
##          Detection Rate : 0.04844         
##    Detection Prevalence : 0.07809         
##       Balanced Accuracy : 0.69034         
##                                           
##        'Positive' Class : yes             
## 
# Optional: Extract and print accuracy
baseline_accuracy <- baseline_conf_matrix$overall["Accuracy"]
cat("Default Decision Tree Accuracy:", round(baseline_accuracy, 4), "\n")
## Default Decision Tree Accuracy: 0.9019
confusion_default <- confusionMatrix(default_pred, test$term_dep_sub)
roc_default <- roc(test$term_dep_sub, default_pred_prob)
## Setting levels: control = no, case = yes
## Setting direction: controls < cases
try({
  roc_default <- suppressMessages(roc(test$term_dep_sub, pred_prob_default))
  auc_default <- auc(roc_default)
  
  plot_title <- paste("ROC Curve - Default Decision Tree (AUC =", round(auc_default, 3), ")")
  plot(roc_default, col = "black", main = plot_title)
})
## Error in eval(expr, envir) : object 'pred_prob_default' not found

The default decision tree model shows decent predictive performance with an AUC of 0.803. This means the model does a good job distinguishing between clients who subscribed to a term deposit and those who did not. While the overall accuracy is fair, the model still struggles a bit when it comes to identifying the actual positive cases. The ROC curve reflects this by showing moderate performance, but it’s clear there’s room for improvement, especially in capturing the clients who are most likely to subscribe. It might be helpful to refine the model further, either by tuning the tree depth or adjusting some of the features used. Doing so could improve how well the model picks up on the positive cases and boost its overall effectiveness.

Experiment 2: Max Depth Impact

✅ Absolutely! Here’s a clean and humanized write-up for your Experiment 2 based on your depth tuning approach — keeping it aligned with your classmate’s tone but original to your work:


Experiment 2: Decision Tree Model - Max Depth Impact

In this experiment, I tested how changing the decision tree’s maximum depth affects model performance. Since tree depth controls complexity, tuning it helps balance between capturing important patterns and avoiding overfitting. I tested depths of 3, 5, and 7 to see how each level impacts predictions. This helped me understand how increasing complexity changes the model’s ability to generalize and capture meaningful relationships in the data.

Hypothesis: Limiting or increasing the tree depth will affect the model’s ability to generalize. An optimal depth should balance complexity and accuracy, leading to better predictive performance without overfitting.

depths <- c(3, 5, 7)
depth_conf_list <- list()
depth_models <- list()

for (d in depths) {
  # Train Decision Tree with max depth = d
  ctrl <- rpart.control(maxdepth = d)
  model <- rpart(term_dep_sub ~ ., data = train, method = "class", control = ctrl)
  depth_models[[paste0("Depth_", d)]] <- model
  
  # Predict classes
  pred_class <- predict(model, test, type = "class")
  
  # Store Confusion Matrix
  depth_conf_list[[paste0("Depth_", d)]] <- confusionMatrix(pred_class, test$term_dep_sub)
}

# Plot each decision tree
par(mfrow = c(1, 3))  # Layout for 3 plots side by side
rpart.plot(depth_models[["Depth_3"]], main = "Decision Tree - Max Depth = 3")
rpart.plot(depth_models[["Depth_5"]], main = "Decision Tree - Max Depth = 5")
rpart.plot(depth_models[["Depth_7"]], main = "Decision Tree - Max Depth = 7")

par(mfrow = c(1, 1))  # Reset layout

The visualization shows how increasing the tree’s depth changes the model’s complexity and the way it splits the data. At depth 3, the tree is simple and focuses mostly on call duration and previous campaign outcomes to make predictions. As the depth increases to 5 and 7, the tree starts adding more variables like the month of contact and additional splits on duration.

While deeper trees capture more detail, they also risk overfitting by splitting on smaller groups. The overall structure confirms that call duration remains the most influential feature across all depths. However, as complexity increases, the model picks up more patterns, which may improve accuracy but could hurt generalization if taken too far.

This comparison highlights the trade-off between model simplicity and capturing detailed relationships within the data.

# Loop through each depth and print the full confusion matrix + stats
for (d in c(3, 5, 7)) {
  cat("\nDecision Tree - Max Depth =", d, "\n")
  print(depth_conf_list[[paste0("Depth_", d)]])
}
## 
## Decision Tree - Max Depth = 3 
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    no   yes
##        no  11663  1036
##        yes   313   550
##                                           
##                Accuracy : 0.9005          
##                  95% CI : (0.8954, 0.9055)
##     No Information Rate : 0.8831          
##     P-Value [Acc > NIR] : 5.167e-11       
##                                           
##                   Kappa : 0.3997          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.9739          
##             Specificity : 0.3468          
##          Pos Pred Value : 0.9184          
##          Neg Pred Value : 0.6373          
##              Prevalence : 0.8831          
##          Detection Rate : 0.8600          
##    Detection Prevalence : 0.9364          
##       Balanced Accuracy : 0.6603          
##                                           
##        'Positive' Class : no              
##                                           
## 
## Decision Tree - Max Depth = 5 
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    no   yes
##        no  11574   929
##        yes   402   657
##                                           
##                Accuracy : 0.9019          
##                  95% CI : (0.8967, 0.9068)
##     No Information Rate : 0.8831          
##     P-Value [Acc > NIR] : 1.619e-12       
##                                           
##                   Kappa : 0.4448          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.9664          
##             Specificity : 0.4142          
##          Pos Pred Value : 0.9257          
##          Neg Pred Value : 0.6204          
##              Prevalence : 0.8831          
##          Detection Rate : 0.8534          
##    Detection Prevalence : 0.9219          
##       Balanced Accuracy : 0.6903          
##                                           
##        'Positive' Class : no              
##                                           
## 
## Decision Tree - Max Depth = 7 
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    no   yes
##        no  11574   929
##        yes   402   657
##                                           
##                Accuracy : 0.9019          
##                  95% CI : (0.8967, 0.9068)
##     No Information Rate : 0.8831          
##     P-Value [Acc > NIR] : 1.619e-12       
##                                           
##                   Kappa : 0.4448          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.9664          
##             Specificity : 0.4142          
##          Pos Pred Value : 0.9257          
##          Neg Pred Value : 0.6204          
##              Prevalence : 0.8831          
##          Detection Rate : 0.8534          
##    Detection Prevalence : 0.9219          
##       Balanced Accuracy : 0.6903          
##                                           
##        'Positive' Class : no              
## 
# Extract and print accuracy for each depth from stored confusion matrices
for (d in c(3, 5, 7)) {
  depth_accuracy <- depth_conf_list[[paste0("Depth_", d)]]$overall["Accuracy"]
  cat("Decision Tree - Max Depth =", d, "- Accuracy:", round(depth_accuracy, 4), "\n")
}
## Decision Tree - Max Depth = 3 - Accuracy: 0.9005 
## Decision Tree - Max Depth = 5 - Accuracy: 0.9019 
## Decision Tree - Max Depth = 7 - Accuracy: 0.9019
depths <- c(3, 5, 7)  # Fixed typo: "epths" -> "depths"
roc_list <- list()
auc_list <- list()

for (d in depths) {
  # Generate predicted probabilities for the positive class ("yes")
  pred_prob <- predict(depth_models[[paste0("Depth_", d)]], test, type = "prob")[, "yes"]
  
  # Compute ROC and AUC for this depth
  roc_obj <- suppressMessages(roc(test$term_dep_sub, pred_prob))
  roc_list[[paste0("Depth_", d)]] <- roc_obj
  auc_list[[paste0("Depth_", d)]] <- auc(roc_obj)
}

# Plot ROC Curves for all depths
plot(roc_list[["Depth_3"]], col = "blue", main = "ROC Curve - Decision Tree Depths")
plot(roc_list[["Depth_5"]], col = "red", add = TRUE)
plot(roc_list[["Depth_7"]], col = "green", add = TRUE)

# Add AUC values to legend
legend("bottomright", 
       legend = c(
         paste("Depth 3 (AUC =", round(auc_list[["Depth_3"]], 3), ")"),
         paste("Depth 5 (AUC =", round(auc_list[["Depth_5"]], 3), ")"),
         paste("Depth 7 (AUC =", round(auc_list[["Depth_7"]], 3), ")")
       ),
       col = c("blue", "red", "green"),
       lwd = 2)

The model performs well, with both depth 5 and depth 7 achieving an AUC of 0.803, showing good ability to separate positive and negative cases. Compared to depth 3, which has an AUC of 0.759, increasing the tree depth improves the model’s ability to capture patterns in the data.

However, since depth 5 and depth 7 produce the same AUC, going deeper doesn’t boost performance and could lead to overfitting. Overall, the model does a good job but could still improve in detecting positive cases while keeping high specificity.

Experiment 3: Gini vs Information Gain

Building an effective decision tree also means selecting the best splitting criteria to optimize how the model learns from the data. For this experiment, I compared the Gini Index and Information Gain to see how each impacts the model’s structure and performance. The Gini Index focuses on minimizing impurity at each split, while Information Gain uses entropy to measure how well a feature separates the classes. Testing both helps determine which approach better fits the dataset and improves prediction accuracy.

Hypothesis: Using Information Gain instead of the Gini Index may produce better splits and improve model performance.

# Train Decision Tree with Gini split
dt_gini <- rpart(term_dep_sub ~ ., data = train, method = "class", parms = list(split = "gini"))

# Train Decision Tree with Information Gain split
dt_info <- rpart(term_dep_sub ~ ., data = train, method = "class", parms = list(split = "information"))

# Optional: Plot both trees side by side
par(mfrow = c(1, 2))
rpart.plot(dt_gini, main = "Decision Tree - Gini Split")
rpart.plot(dt_info, main = "Decision Tree - Info Gain Split")

par(mfrow = c(1, 1))  # Reset layout

Both decision trees follow a similar structure but show slight differences in how they prioritize splits. The Gini split uses a threshold of 525 seconds for call duration as the first split, while the Information Gain split lowers that threshold to 438 seconds, changing how early some cases are classified.

While both trees rely heavily on duration and previous campaign outcome (poutcome), the Information Gain split creates slightly deeper paths and introduces different timing for the splits, which may help capture more subtle patterns. Overall, both models perform similarly, but the Info Gain tree might be slightly better at separating positive cases due to its more aggressive early split. However, the difference is minor, showing that both criteria perform reasonably well on this dataset.

# Make class predictions
pred_gini_class <- predict(dt_gini, test, type = "class")
pred_info_class <- predict(dt_info, test, type = "class")

# Generate confusion matrices
conf_gini <- confusionMatrix(pred_gini_class, test$term_dep_sub)
conf_info <- confusionMatrix(pred_info_class, test$term_dep_sub)

# Print Confusion Matrices and Statistics
cat("\nConfusion Matrix - Gini:\n")
## 
## Confusion Matrix - Gini:
print(conf_gini)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    no   yes
##        no  11574   929
##        yes   402   657
##                                           
##                Accuracy : 0.9019          
##                  95% CI : (0.8967, 0.9068)
##     No Information Rate : 0.8831          
##     P-Value [Acc > NIR] : 1.619e-12       
##                                           
##                   Kappa : 0.4448          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.9664          
##             Specificity : 0.4142          
##          Pos Pred Value : 0.9257          
##          Neg Pred Value : 0.6204          
##              Prevalence : 0.8831          
##          Detection Rate : 0.8534          
##    Detection Prevalence : 0.9219          
##       Balanced Accuracy : 0.6903          
##                                           
##        'Positive' Class : no              
## 
cat("\nConfusion Matrix - Information Gain:\n")
## 
## Confusion Matrix - Information Gain:
print(conf_info)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    no   yes
##        no  11660   999
##        yes   316   587
##                                          
##                Accuracy : 0.903          
##                  95% CI : (0.8979, 0.908)
##     No Information Rate : 0.8831         
##     P-Value [Acc > NIR] : 5.968e-14      
##                                          
##                   Kappa : 0.4227         
##                                          
##  Mcnemar's Test P-Value : < 2.2e-16      
##                                          
##             Sensitivity : 0.9736         
##             Specificity : 0.3701         
##          Pos Pred Value : 0.9211         
##          Neg Pred Value : 0.6501         
##              Prevalence : 0.8831         
##          Detection Rate : 0.8598         
##    Detection Prevalence : 0.9334         
##       Balanced Accuracy : 0.6719         
##                                          
##        'Positive' Class : no             
## 
# Extract and print accuracy for both
accuracy_gini <- conf_gini$overall["Accuracy"]
accuracy_info <- conf_info$overall["Accuracy"]

cat("Decision Tree - Gini Accuracy:", round(accuracy_gini, 4), "\n")
## Decision Tree - Gini Accuracy: 0.9019
cat("Decision Tree - Information Gain Accuracy:", round(accuracy_info, 4), "\n")
## Decision Tree - Information Gain Accuracy: 0.903
# Ensure levels are properly set and print them
train$term_dep_sub <- factor(train$term_dep_sub, levels = c("no", "yes"))
test$term_dep_sub <- factor(test$term_dep_sub, levels = c("no", "yes"))

cat("Train Levels:", levels(train$term_dep_sub), "\n")
## Train Levels: no yes
cat("Test Levels:", levels(test$term_dep_sub), "\n")
## Test Levels: no yes
# Print ROC direction intention
cat("ROC will be computed with levels = c('no', 'yes') and direction = '<' (yes is the positive class)\n")
## ROC will be computed with levels = c('no', 'yes') and direction = '<' (yes is the positive class)
# Generate predicted probabilities
pred_gini_prob <- predict(dt_gini, test, type = "prob")[, "yes"]
pred_info_prob <- predict(dt_info, test, type = "prob")[, "yes"]

# Compute ROC curves and AUCs
roc_gini <- roc(test$term_dep_sub, pred_gini_prob, levels = c("no", "yes"), direction = "<")
roc_info <- roc(test$term_dep_sub, pred_info_prob, levels = c("no", "yes"), direction = "<")

auc_gini <- auc(roc_gini)
auc_info <- auc(roc_info)

# Plot ROC Comparison with AUC
plot(roc_gini, col = "purple", main = "ROC Curve - Gini vs Information Gain")
plot(roc_info, col = "orange", add = TRUE)

# Add AUC to legend
legend("bottomright", 
       legend = c(
         paste("Gini (AUC =", round(auc_gini, 3), ")"),
         paste("Information Gain (AUC =", round(auc_info, 3), ")")
       ),
       col = c("purple", "orange"), 
       lwd = 2)

The comparison between Gini and Information Gain shows that the Gini split performs slightly better in this case. The Gini model achieved an AUC of 0.803, while the Information Gain model reached an AUC of 0.781. Both models demonstrate decent performance in distinguishing between positive and negative cases, but Gini has a slight advantage in overall classification power.

This difference suggests that Gini may be better suited for this dataset, likely because it creates more balanced splits that improve predictive accuracy. However, the gap isn’t large, meaning both splitting criteria are reasonable choices. Still, if maximizing performance is the priority, Gini is the better option here.

*Decision Tree Results Summary Table**

dt_results <- tribble(
  ~Model, ~Accuracy, ~Sensitivity, ~Specificity, ~F1_Score, ~AUC,
  
  # Default Decision Tree
  "Decision Tree - Default", 
  as.numeric(confusion_default$overall["Accuracy"]),
  as.numeric(confusion_default$byClass["Sensitivity"]),
  as.numeric(confusion_default$byClass["Specificity"]),
  2 * ((as.numeric(confusion_default$byClass["Precision"]) * as.numeric(confusion_default$byClass["Recall"])) /
         (as.numeric(confusion_default$byClass["Precision"]) + as.numeric(confusion_default$byClass["Recall"]))),
  as.numeric(auc(roc_default)),
  
  # Decision Tree - Depth 3
  "Decision Tree - Depth 3", 
  as.numeric(depth_conf_list[["Depth_3"]]$overall["Accuracy"]),
  as.numeric(depth_conf_list[["Depth_3"]]$byClass["Sensitivity"]),
  as.numeric(depth_conf_list[["Depth_3"]]$byClass["Specificity"]),
  2 * ((as.numeric(depth_conf_list[["Depth_3"]]$byClass["Precision"]) * as.numeric(depth_conf_list[["Depth_3"]]$byClass["Recall"])) /
         (as.numeric(depth_conf_list[["Depth_3"]]$byClass["Precision"]) + as.numeric(depth_conf_list[["Depth_3"]]$byClass["Recall"]))),
  as.numeric(auc(roc_list[["Depth_3"]])),
  
  # Decision Tree - Depth 5
  "Decision Tree - Depth 5", 
  as.numeric(depth_conf_list[["Depth_5"]]$overall["Accuracy"]),
  as.numeric(depth_conf_list[["Depth_5"]]$byClass["Sensitivity"]),
  as.numeric(depth_conf_list[["Depth_5"]]$byClass["Specificity"]),
  2 * ((as.numeric(depth_conf_list[["Depth_5"]]$byClass["Precision"]) * as.numeric(depth_conf_list[["Depth_5"]]$byClass["Recall"])) /
         (as.numeric(depth_conf_list[["Depth_5"]]$byClass["Precision"]) + as.numeric(depth_conf_list[["Depth_5"]]$byClass["Recall"]))),
  as.numeric(auc(roc_list[["Depth_5"]])),
  
  # Decision Tree - Depth 7
  "Decision Tree - Depth 7", 
  as.numeric(depth_conf_list[["Depth_7"]]$overall["Accuracy"]),
  as.numeric(depth_conf_list[["Depth_7"]]$byClass["Sensitivity"]),
  as.numeric(depth_conf_list[["Depth_7"]]$byClass["Specificity"]),
  2 * ((as.numeric(depth_conf_list[["Depth_7"]]$byClass["Precision"]) * as.numeric(depth_conf_list[["Depth_7"]]$byClass["Recall"])) /
         (as.numeric(depth_conf_list[["Depth_7"]]$byClass["Precision"]) + as.numeric(depth_conf_list[["Depth_7"]]$byClass["Recall"]))),
  as.numeric(auc(roc_list[["Depth_7"]])),
  
  # Decision Tree - Gini
  "Decision Tree - Gini Split", 
  as.numeric(conf_gini$overall["Accuracy"]),
  as.numeric(conf_gini$byClass["Sensitivity"]),
  as.numeric(conf_gini$byClass["Specificity"]),
  2 * ((as.numeric(conf_gini$byClass["Precision"]) * as.numeric(conf_gini$byClass["Recall"])) /
         (as.numeric(conf_gini$byClass["Precision"]) + as.numeric(conf_gini$byClass["Recall"]))),
  as.numeric(auc(roc_gini)),
  
  # Decision Tree - Info Gain
  "Decision Tree - Information Gain", 
  as.numeric(conf_info$overall["Accuracy"]),
  as.numeric(conf_info$byClass["Sensitivity"]),
  as.numeric(conf_info$byClass["Specificity"]),
  2 * ((as.numeric(conf_info$byClass["Precision"]) * as.numeric(conf_info$byClass["Recall"])) /
         (as.numeric(conf_info$byClass["Precision"]) + as.numeric(conf_info$byClass["Recall"]))),
  as.numeric(auc(roc_info))
)

# Print nicely
kable(dt_results, caption = "Decision Tree Experiment Results") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
Decision Tree Experiment Results
Model Accuracy Sensitivity Specificity F1_Score AUC
Decision Tree - Default 0.9018581 0.9664329 0.4142497 0.9456269 0.8027379
Decision Tree - Depth 3 0.9005309 0.9738644 0.3467844 0.9453293 0.7593561
Decision Tree - Depth 5 0.9018581 0.9664329 0.4142497 0.9456269 0.8027379
Decision Tree - Depth 7 0.9018581 0.9664329 0.4142497 0.9456269 0.8027379
Decision Tree - Gini Split 0.9018581 0.9664329 0.4142497 0.9456269 0.8027379
Decision Tree - Information Gain 0.9030379 0.9736139 0.3701135 0.9466207 0.7811965

Visualize All Metrics (Bar Plot)

# Reshape dt_results to long format for plotting
dt_results_long <- dt_results %>%
  pivot_longer(cols = c(Accuracy, Sensitivity, Specificity, F1_Score, AUC),
               names_to = "Metric",
               values_to = "Value")

# Create lollipop chart comparing all Decision Tree experiments
ggplot(dt_results_long, aes(x = Value, y = reorder(Model, Value), color = Metric)) +
  geom_segment(aes(x = 0, xend = Value, yend = reorder(Model, Value)), size = 1) +
  geom_point(size = 4) +
  facet_wrap(~Metric, scales = "free_x") +
  theme_minimal() +
  labs(title = "Decision Tree Model Comparison - Lollipop Chart",
       x = "Score", y = "Model") +
  theme(axis.text.y = element_text(size = 10),
        strip.text = element_text(size = 12, face = "bold"),
        plot.title = element_text(size = 14, face = "bold"),
        legend.position = "none")
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

# Plot ROC Curves for all Decision Tree models
plot(roc_default, col = "blue", main = "Decision Tree Models - ROC Comparison")

# Add ROC for Depth 3, 5, 7
plot(roc_list[["Depth_3"]], col = "orange", add = TRUE)
plot(roc_list[["Depth_5"]], col = "red", add = TRUE)
plot(roc_list[["Depth_7"]], col = "darkgreen", add = TRUE)

# Add ROC for Gini and Information Gain
plot(roc_gini, col = "purple", add = TRUE)
plot(roc_info, col = "brown", add = TRUE)

# Add legend
legend("bottomright", 
       legend = c("Default Tree", "Depth 3", "Depth 5", "Depth 7", "Gini", "Info Gain"), 
       col = c("blue", "orange", "red", "darkgreen", "purple", "brown"), 
       lwd = 2)

The decision tree experiments showed that the default model, depth 5, depth 7, and Gini split all performed similarly, with around 90.18% accuracy and an AUC of 0.802. The Information Gain model had the highest accuracy at 90.30% but a slightly lower AUC (0.781). The depth 3 model performed the weakest with an AUC of 0.759.

ROC results confirmed that increasing depth beyond 5 didn’t improve performance, and the Gini split was just as strong as the tuned models. All models had high sensitivity but struggled with specificity, meaning they predicted positives well but had more false positives.

Overall, depth tuning and split criteria had minimal impact. The default and depth 5 models offered the best balance of simplicity and performance.

Random Forest

The Random Forest algorithm is a powerful and reliable machine learning tool known for its balance of accuracy and flexibility. Its ability to handle complex datasets while reducing overfitting makes it a strong choice for both research and real-world applications. As data complexity grows, Random Forests will continue to play a key role in driving insights and supporting decision-making across industries.

Experiment 1: Random Forest - Default (Baseline)

Random Forest serves as a solid starting point by using default hyperparameters to build a baseline model. With enough trees, the model reduces the risk of overfitting while still capturing important patterns in the data. This experiment helps assess how well the model performs without any tuning or adjustments.

Hypothesis: The baseline Random Forest model with default settings will achieve good predictive performance on the dataset.

# Default Random Forest (ntree = 500, mtry = sqrt(p))
rf_default <- randomForest(term_dep_sub ~ ., data = train)

# Predictions and Probabilities
pred_rf_default_prob <- predict(rf_default, test, type = "prob")[, 2]
pred_rf_default_class <- predict(rf_default, test, type = "class")

# Confusion matrix only (No ROC)
conf_rf_default <- confusionMatrix(pred_rf_default_class, test$term_dep_sub)

# ✅ OOB Error Rate Plot (Optional but kept since you mentioned it)
plot(rf_default, main = "Random Forest - OOB Error Rate")

# Extract and print accuracy from the stored confusion matrix
rf_default_accuracy <- conf_rf_default$overall["Accuracy"]
cat("Random Forest (Default) Accuracy:", round(rf_default_accuracy, 4), "\n")
## Random Forest (Default) Accuracy: 0.9073
varImpPlot(rf_default, main = "Random Forest - Variable Importance (Default Model)")

The first graph shows how the Out-of-Bag (OOB) error rate changes as more trees are added to the random forest model. Initially, the error drops as trees increase, which means the model is learning and improving. After a certain point, the error levels off, showing that adding more trees doesn’t significantly boost performance. This plateau suggests the model has reached stability, balancing bias and variance.

The second graph highlights the most important variables in the default random forest model. According to the Mean Decrease in Gini, ‘duration’ has the most influence on predictions, followed by ‘balance’, ‘age’, and ‘month’. These variables contribute the most to reducing impurity and improving accuracy. This variable importance plot gives a clear view of which features the model relies on most when making predictions.

Experiment 2: Random Forest - Number of Trees Tuning (100 vs 300)

In this experiment, I tested how changing the number of trees impacts the model’s performance. Adding more trees generally helps the model capture more patterns and reduces variance, but it also increases computational time. The goal was to find out if increasing from 100 to 300 trees would noticeably improve accuracy without overcomplicating the model.

Hypothesis: Increasing the number of trees will improve model performance by reducing variance and enhancing prediction stability.

# Random Forest with 100 trees
rf_100 <- randomForest(term_dep_sub ~ ., data = train, ntree = 100)
pred_rf100_class <- predict(rf_100, test, type = "class")
conf_rf100 <- confusionMatrix(pred_rf100_class, test$term_dep_sub)

# Random Forest with 300 trees
rf_300 <- randomForest(term_dep_sub ~ ., data = train, ntree = 300)
pred_rf300_class <- predict(rf_300, test, type = "class")
conf_rf300 <- confusionMatrix(pred_rf300_class, test$term_dep_sub)

# ✅ Extract and print accuracies
rf100_accuracy <- conf_rf100$overall["Accuracy"]
rf300_accuracy <- conf_rf300$overall["Accuracy"]

cat("Random Forest (100 Trees) Accuracy:", round(rf100_accuracy, 4), "\n")
## Random Forest (100 Trees) Accuracy: 0.9069
cat("Random Forest (300 Trees) Accuracy:", round(rf300_accuracy, 4), "\n")
## Random Forest (300 Trees) Accuracy: 0.9072
# ✅ OOB Error Rate Plot for 100 Trees
plot(rf_100, main = "Random Forest - OOB Error (100 Trees)")

# ✅ OOB Error Rate Plot for 300 Trees
plot(rf_300, main = "Random Forest - OOB Error (300 Trees)")

# ✅ Feature Importance for 100 Trees
varImpPlot(rf_100, main = "Random Forest - Feature Importance (100 Trees)")

# ✅ Feature Importance for 300 Trees
varImpPlot(rf_300, main = "Random Forest - Feature Importance (300 Trees)")

The OOB error plots show that increasing the number of trees from 100 to 300 leads to a slight improvement in model stability. While the error rate decreases quickly early on, it eventually levels off, showing that adding more trees beyond a certain point offers limited performance gains. Both models perform well, but the 300-tree model maintains a slightly more consistent error rate.

The feature importance plots for both 100 and 300 trees remain consistent. Variables like duration, balance, age, and month continue to play the most significant roles in predicting the outcome. This confirms that increasing the number of trees does not drastically change which features the model considers important.

Overall, tuning the number of trees helps improve stability but has minimal impact on changing the model’s key drivers.

**Experiment 3: Random Forest - mtry Tuning

In this experiment, I explored how adjusting the mtry parameter, which controls the number of features randomly selected at each tree split, impacts the Random Forest model’s performance. Tuning mtry helps balance bias and variance by controlling model complexity. A lower mtry might underfit the data, while a higher mtry could risk overfitting.

Hypothesis: Tuning the mtry parameter will improve model performance by finding the optimal balance between model complexity and predictive power.

# Random Forest with mtry tuning
rf_mtry <- randomForest(term_dep_sub ~ ., data = train, ntree = 300, mtry = 5)
pred_rf_mtry_class <- predict(rf_mtry, test, type = "class")
conf_rf_mtry <- confusionMatrix(pred_rf_mtry_class, test$term_dep_sub)

# ✅ Extract and print accuracy
rf_mtry_accuracy <- conf_rf_mtry$overall["Accuracy"]
cat("Random Forest (mtry = 5) Accuracy:", round(rf_mtry_accuracy, 4), "\n")
## Random Forest (mtry = 5) Accuracy: 0.9075
# ✅ OOB Error Rate Plot for mtry tuned model
plot(rf_mtry, main = "Random Forest - OOB Error (mtry = 5)")

varImpPlot(rf_mtry, main = "Random Forest - Feature Importance (mtry = 5)")

library(ggplot2)

# Replace with your actual accuracy values from your confusions
rf_accuracy_df <- data.frame(
  Model = c("Default RF", "RF 100 Trees", "RF 300 Trees", "RF mtry=5"),
  Accuracy = c(rf_default_accuracy, rf100_accuracy, rf300_accuracy, rf_mtry_accuracy)
)

ggplot(rf_accuracy_df, aes(x = Model, y = Accuracy, fill = Model)) +
  geom_bar(stat = "identity") +
  geom_text(aes(label = round(Accuracy, 4)), vjust = -0.5, size = 4) +
  scale_fill_manual(values = c("red", "blue", "green", "purple")) +
  theme_minimal() +
  labs(title = "Random Forest Accuracy Comparison", y = "Accuracy")

The Random Forest experiments show strong model performance across all configurations. Increasing the number of trees from 100 to 300 slightly reduced the out-of-bag (OOB) error, but the overall accuracy remained very similar. Tuning the mtry parameter to 5 resulted in the highest accuracy at 0.9075, though the improvement was marginal compared to the default model.

The OOB error plots confirm that more trees stabilize the error rate, but gains plateau. The feature importance plots consistently rank ‘duration’, ‘balance’, and ‘age’ as the top predictors across all experiments, reinforcing their significance in the dataset.

Overall, Random Forest performed well without overfitting, and tuning mtry showed slight accuracy gains. However, the default settings already provided strong, reliable results.

Adaptive Boositng

In this section, I evaluated the model’s performance and the impact of increasing the number of boosting iterations using the AdaBoost technique. AdaBoost, or Adaptive Boosting, is an ensemble method that combines several weak learners to create a stronger predictive model. By adjusting to errors from previous iterations, AdaBoost improves accuracy with each round. Increasing the number of iterations helped strengthen the model and capture more complex patterns in the data.

Experiment 1: AdaBoost Default

In this experiment, I tested the baseline AdaBoost model using default settings. AdaBoost works by combining multiple weak learners to create a stronger predictive model. Each learner focuses more on the samples misclassified by the previous one, which helps improve accuracy. The goal was to see how the model performs without any tuning. By default, AdaBoost adjusts the weights of each learner to minimize errors, making it less likely to overfit.

Hypothesis: The default AdaBoost model will deliver solid baseline performance by effectively combining weak learners.

library(adabag)
library(caret)
library(dplyr)

# Ensure the target variable is a factor
data$term_dep_sub <- as.factor(data$term_dep_sub)

# Split data into train and test
set.seed(123)
splitIndex <- createDataPartition(data$term_dep_sub, p = 0.7, list = FALSE)
train <- data[splitIndex, ]
test <- data[-splitIndex, ]

# Align factor levels between train and test for categorical variables
for (col in names(train)) {
  if (is.factor(train[[col]])) {
    test[[col]] <- factor(test[[col]], levels = levels(train[[col]]))
  }
}

# Train AdaBoost default model with 50 iterations
ada_default <- boosting(term_dep_sub ~ ., data = train, mfinal = 50, boos = TRUE)

# Predict on test set
pred_ada_default <- predict(ada_default, newdata = test)

# Evaluate model performance
conf_ada_default <- confusionMatrix(as.factor(pred_ada_default$class), test$term_dep_sub)
ada_default_accuracy <- conf_ada_default$overall["Accuracy"]

# Print Accuracy
cat("AdaBoost Default Accuracy:", round(ada_default_accuracy, 4), "\n")
## AdaBoost Default Accuracy: 0.9076
# Calculate error evolution
evol_ada_default <- errorevol(ada_default, newdata = test)

# Plot error evolution
plot(evol_ada_default$error, type = "l",
     ylim = c(0, max(evol_ada_default$error) + 0.05),
     main = "AdaBoost (mfinal = 50) Error Evolution",
     xlab = "Iterations",
     ylab = "Error",
     col = "red",
     lwd = 2)

In this experiment, AdaBoost was run with 50 iterations (mfinal = 50) using the default settings. The model achieved a strong accuracy of 0.9076, indicating good predictive performance on the test set. The error evolution graph shows a low and stable error rate, fluctuating slightly but consistently hovering around 0.10 throughout the boosting iterations. This steady trend suggests that the model quickly stabilizes and additional iterations beyond this point offer minimal changes to the error rate, reinforcing the model’s reliability on this dataset.

Experiment 2: AdaBoost Tuned (mfinal = 100)

In this experiment, I increased the number of boosting iterations to 100 to evaluate if a higher number of weak learners improves model performance. By increasing mfinal, the model has more opportunities to focus on harder-to-classify observations and refine predictions. The goal was to test whether tuning the number of iterations could enhance accuracy without leading to overfitting.

Hypothesis: Increasing the number of boosting iterations to 100 will improve the model’s performance by reducing errors and capturing more complex patterns in the data.

# Train AdaBoost with 100 iterations
ada_tuned <- boosting(term_dep_sub ~ ., data = train, mfinal = 100)

# Predict
pred_ada_tuned <- predict(ada_tuned, newdata = test)
pred_class_ada_tuned <- pred_ada_tuned$class

# Confusion matrix
conf_ada_tuned <- confusionMatrix(as.factor(pred_class_ada_tuned), test$term_dep_sub)

# Accuracy Only
ada_tuned_accuracy <- conf_ada_tuned$overall["Accuracy"]
cat("AdaBoost Tuned (100 iterations) Accuracy:", round(ada_tuned_accuracy, 4), "\n")
## AdaBoost Tuned (100 iterations) Accuracy: 0.9086

Error Evaluation (Confusion Matrix)

# ✅ Print the full confusion matrix separately
print(conf_ada_tuned)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    no   yes
##        no  11605   868
##        yes   371   718
##                                           
##                Accuracy : 0.9086          
##                  95% CI : (0.9037, 0.9134)
##     No Information Rate : 0.8831          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.4881          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.9690          
##             Specificity : 0.4527          
##          Pos Pred Value : 0.9304          
##          Neg Pred Value : 0.6593          
##              Prevalence : 0.8831          
##          Detection Rate : 0.8557          
##    Detection Prevalence : 0.9197          
##       Balanced Accuracy : 0.7109          
##                                           
##        'Positive' Class : no              
## 
# Calculate error evolution
evol_ada_tuned <- errorevol(ada_tuned, newdata = test)

# Plot error evolution
plot(evol_ada_tuned$error, type = "l", ylim = c(0, max(evol_ada_tuned$error) + 0.05),
     main = "AdaBoost (mfinal = 100) Error Evolution",
     xlab = "Iterations", ylab = "Error", col = "red")

Both error evolution graphs follow a similar trend, where the error rate stabilizes quickly after the initial iterations. The model with mfinal = 100 shows a slightly smoother and lower error curve compared to mfinal = 50, but the difference is minimal. Overall, increasing the number of iterations slightly improves performance. The final accuracy for the tuned model with mfinal = 100 is 0.9076, indicating strong model performance with minimal additional error reduction from increasing the iterations.