WQD7004 PROGRAMMING FOR DATA SCIENCE

TITLE:

Applying Decision Tree and Random Forest for Predicting Exercise Angina and Resting Blood Pressure

Group Member

NAME MATRIC NO.
LEONAL SIGAR ANAK JAME 24062655
ZHANG YUSHAN 23076749
LIN YUQI 24077562
ZHOU XINGYU 23099141
SYED DZAFEER BIN SYED IDRUS 22053251

Introduction

Cardiovascular diseases remain a leading global cause of death, making early prediction crucial. In this study, we use machine learning to predict two key clinical targets:

Classification: Whether a patient has Exercise-Induced Angina Regression:Their Resting Blood Pressure

We use models like Logistic Regression, Random Forest, SVR, and Polynomial Regression, and compare their performances using standard metrics.


Step 1: Data Pre-processing

In this step, we performed data cleaning to prepare the dataset for modeling. Here’s what we did:

Cleaning Steps

  1. Standardized column names: Converted all variable names to lowercase and replaced spaces with underscores for easier handling.

  2. Handled implausible values: Replaced 0 in restingbp and cholesterol with NA since a resting blood pressure or cholesterol level of zero is physiologically unrealistic.

  3. Imputed missing values: Filled NA values using the median within each gender group. This approach preserves potential gender-based differences in health indicators.

  4. Categorical encoding: Converted relevant variables to factors, such as sex, chestpaintype, restingecg, exerciseangina, st_slope, and heartdisease. This ensures correct model treatment of these attributes.

  5. Duplicate check: Verified that there were no duplicate rows in the dataset.

  6. Outlier visualization: Used boxplots to inspect outliers in all numeric columns. We observed some high values in restingbp, cholesterol, and oldpeak, but chose not to remove or cap them at this stage. These might be clinically meaningful and are retained for now.

library(tidyverse)
library(caret)
library(e1071)
library(randomForest)
library(ggplot2)
library(kernlab)
library(pROC)
library(reshape2)
library(corrplot)
library(skimr)
library(GGally)
## Data Pre-Processing / Cleaning

# Load libraries
library(tidyverse)

# Read the data
df <- read_csv("heart.csv")

# Set column names to be lower-case letter
df <- df %>% rename_with(~ tolower(.) %>% str_replace_all("\\s+", "_"))

# Check data
head(df, 5) 
## # A tibble: 5 × 12
##     age sex   chestpaintype restingbp cholesterol fastingbs restingecg maxhr exerciseangina oldpeak st_slope heartdisease
##   <dbl> <chr> <chr>             <dbl>       <dbl>     <dbl> <chr>      <dbl> <chr>            <dbl> <chr>           <dbl>
## 1    40 M     ATA                 140         289         0 Normal       172 N                  0   Up                  0
## 2    49 F     NAP                 160         180         0 Normal       156 N                  1   Flat                1
## 3    37 M     ATA                 130         283         0 ST            98 N                  0   Up                  0
## 4    48 F     ASY                 138         214         0 Normal       108 Y                  1.5 Flat                1
## 5    54 M     NAP                 150         195         0 Normal       122 N                  0   Up                  0
# Handling Missing Values
# Replace “0” values with NA
df <- df %>% 
    mutate(
        restingbp    = na_if(restingbp, 0),
        cholesterol  = na_if(cholesterol, 0)
    )

# Check missing value
df %>% summarise(across(everything(), ~ sum(is.na(.)))) %>% print()
## # A tibble: 1 × 12
##     age   sex chestpaintype restingbp cholesterol fastingbs restingecg maxhr exerciseangina oldpeak st_slope heartdisease
##   <int> <int>         <int>     <int>       <int>     <int>      <int> <int>          <int>   <int>    <int>        <int>
## 1     0     0             0         1         172         0          0     0              0       0        0            0
# Impute missing values by median (grouped by sex)
df <- df %>%
    group_by(sex) %>%
    mutate(
        restingbp   = if_else(is.na(restingbp),
                              median(restingbp, na.rm = TRUE),
                              restingbp),
        cholesterol = if_else(is.na(cholesterol),
                              median(cholesterol, na.rm = TRUE),
                              cholesterol)
    ) %>%
    ungroup()


# Convert categorical columns to factors
df <- df %>%
    mutate(
        sex              = factor(sex, levels = c("M","F")),
        chestpaintype    = factor(chestpaintype),
        restingecg       = factor(restingecg),
        exerciseangina   = factor(exerciseangina, levels = c("N","Y")),
        st_slope         = factor(st_slope),
        fastingbs        = factor(fastingbs),
        heartdisease     = factor(heartdisease, levels = c(0,1))  # 0=no, 1=yes
    )


# Count duplicate rows
df %>%
    filter(duplicated(.)) %>%
    tally()               
## # A tibble: 1 × 1
##       n
##   <int>
## 1     0
# Handling Outliers
# Select only the numeric columns
num_df <- df %>% select(where(is.numeric))

# Draw boxplots for each column
boxplot(
    num_df,
    main    = "Boxplots for All Numeric Columns",
    las     = 2,           # rotate column labels vertically
    outline = TRUE         # show outliers
)

Summary of Results

  • The dataset has 918 rows and 12 cleaned variables.
  • All missing values were addressed properly.
  • Variables were successfully transformed into correct data types.
  • Outliers are visually evident, especially in cholesterol and restingbp, but not extreme enough to be immediately excluded.
  • Based on the boxplots, there are outliers in all columns except age column.

With these preprocessing steps completed, our dataset is now clean, consistent, and ready for exploratory analysis and modeling. Importantly, by preserving subtle data nuances (like outliers and category definitions), we ensure that downstream models can capture true clinical patterns in the heart health data. These outliers might be meaningful. Decided not to cap or remove outliers.

# Overview of cleaned data
glimpse(df)
## Rows: 918
## Columns: 12
## $ age            <dbl> 40, 49, 37, 48, 54, 39, 45, 54, 37, 48, 37, 58, 39, 49, 42, 54, 38, 43, 60, 36, 43, 44, 49, 44, 40, 36,…
## $ sex            <fct> M, F, M, F, M, M, F, M, M, F, F, M, M, M, F, F, M, F, M, M, F, M, F, M, M, M, M, M, F, M, M, M, M, M, F…
## $ chestpaintype  <fct> ATA, NAP, ATA, ASY, NAP, NAP, ATA, ATA, ASY, ATA, NAP, ATA, ATA, ASY, NAP, ATA, ASY, ATA, ASY, ATA, TA,…
## $ restingbp      <dbl> 140, 160, 130, 138, 150, 120, 130, 110, 140, 120, 130, 136, 120, 140, 115, 120, 110, 120, 100, 120, 100…
## $ cholesterol    <dbl> 289, 180, 283, 214, 195, 339, 237, 208, 207, 284, 211, 164, 204, 234, 211, 273, 196, 201, 248, 267, 223…
## $ fastingbs      <fct> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ restingecg     <fct> Normal, Normal, ST, Normal, Normal, Normal, Normal, Normal, Normal, Normal, Normal, ST, Normal, Normal,…
## $ maxhr          <dbl> 172, 156, 98, 108, 122, 170, 170, 142, 130, 120, 142, 99, 145, 140, 137, 150, 166, 165, 125, 160, 142, …
## $ exerciseangina <fct> N, N, N, Y, N, N, N, N, Y, N, N, Y, N, Y, N, N, N, N, N, N, N, N, N, Y, N, N, Y, N, N, N, N, N, N, N, N…
## $ oldpeak        <dbl> 0.0, 1.0, 0.0, 1.5, 0.0, 0.0, 0.0, 0.0, 1.5, 0.0, 0.0, 2.0, 0.0, 1.0, 0.0, 1.5, 0.0, 0.0, 1.0, 3.0, 0.0…
## $ st_slope       <fct> Up, Flat, Up, Flat, Up, Up, Up, Up, Flat, Up, Up, Flat, Up, Flat, Up, Flat, Flat, Up, Flat, Flat, Up, F…
## $ heartdisease   <fct> 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0…
summary(df)
##       age        sex     chestpaintype   restingbp      cholesterol    fastingbs  restingecg      maxhr       exerciseangina
##  Min.   :28.00   M:725   ASY:496       Min.   : 80.0   Min.   : 85.0   0:704     LVH   :188   Min.   : 60.0   N:547         
##  1st Qu.:47.00   F:193   ATA:173       1st Qu.:120.0   1st Qu.:214.0   1:214     Normal:552   1st Qu.:120.0   Y:371         
##  Median :54.00           NAP:203       Median :130.0   Median :234.0             ST    :178   Median :138.0                 
##  Mean   :53.51           TA : 46       Mean   :132.5   Mean   :242.8                          Mean   :136.8                 
##  3rd Qu.:60.00                         3rd Qu.:140.0   3rd Qu.:267.0                          3rd Qu.:156.0                 
##  Max.   :77.00                         Max.   :200.0   Max.   :603.0                          Max.   :202.0                 
##     oldpeak        st_slope   heartdisease
##  Min.   :-2.6000   Down: 63   0:410       
##  1st Qu.: 0.0000   Flat:460   1:508       
##  Median : 0.6000   Up  :395               
##  Mean   : 0.8874                          
##  3rd Qu.: 1.5000                          
##  Max.   : 6.2000

Step2: EXPLORATORY DATA ANALYSIS

In this step, we explore the structure and distribution of the dataset to understand the characteristics of each variable before modeling.

We first checked the dataset dimensions and variable types, confirming a total of 918 observations and 12 attributes. Then, we used skim() to view summaries such as missing values, distributions, and data types. These steps ensure data quality before proceeding to visual EDA.

Next, we visualized distributions and class breakdowns for key variables like age, sex, chest pain type, cholesterol, resting blood pressure, and angina. These plots give us an initial feel for potential predictor–outcome relationships.

# Dimensions of dataset
dim(df)
## [1] 918  12
# List types for each attribute
sapply(df, class)
##            age            sex  chestpaintype      restingbp    cholesterol      fastingbs     restingecg          maxhr 
##      "numeric"       "factor"       "factor"      "numeric"      "numeric"       "factor"       "factor"      "numeric" 
## exerciseangina        oldpeak       st_slope   heartdisease 
##       "factor"      "numeric"       "factor"       "factor"
# Summary and overview of data using skim 
library(skimr)
skim(df)
Data summary
Name df
Number of rows 918
Number of columns 12
_______________________
Column type frequency:
factor 7
numeric 5
________________________
Group variables None

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
sex 0 1 FALSE 2 M: 725, F: 193
chestpaintype 0 1 FALSE 4 ASY: 496, NAP: 203, ATA: 173, TA: 46
fastingbs 0 1 FALSE 2 0: 704, 1: 214
restingecg 0 1 FALSE 3 Nor: 552, LVH: 188, ST: 178
exerciseangina 0 1 FALSE 2 N: 547, Y: 371
st_slope 0 1 FALSE 3 Fla: 460, Up: 395, Dow: 63
heartdisease 0 1 FALSE 2 1: 508, 0: 410

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
age 0 1 53.51 9.43 28.0 47 54.0 60.0 77.0 ▁▅▇▆▁
restingbp 0 1 132.54 17.99 80.0 120 130.0 140.0 200.0 ▁▆▇▂▁
cholesterol 0 1 242.81 53.47 85.0 214 234.0 267.0 603.0 ▁▇▁▁▁
maxhr 0 1 136.81 25.46 60.0 120 138.0 156.0 202.0 ▁▃▇▆▂
oldpeak 0 1 0.89 1.07 -2.6 0 0.6 1.5 6.2 ▁▇▆▁▁
# Distribution plots
library(corrplot)
library(ggplot2)


# Age distribution
ggplot(df, aes(age)) + geom_histogram(binwidth=5, fill="steelblue", color="white") +
    labs(title="Age Distribution")

# Gender distribution
barplot(
    table(df$sex),
    main    = "Count of Males vs. Females",
    xlab    = "Sex",
    ylab    = "Number of Patients",
    col     = c("cornflowerblue","salmon"),
    names.arg = c("Male","Female")
)

# ChestPainType distribution
ggplot(df, aes(chestpaintype)) + geom_bar(fill="steelblue") +
    labs(title="Chest Pain Type Counts") +
    theme(axis.text.x = element_text(angle=45, hjust=1))

# Restingbp distribution
ggplot(df, aes(restingbp)) + geom_histogram(binwidth=5, fill="steelblue", color="white") +
    labs(title="Resting Blood Pressure Distribution")

# Cholesterol histogram
ggplot(df, aes(cholesterol)) + geom_histogram(binwidth=50, fill="steelblue", color="white") +
    labs(title="Cholesterol Distribution")

# Fasting Blood Sugar 0 normal, 1 high
barplot(
    table(df$fastingbs),
    main      = "High Fasting Blood Sugar Distribution",
    xlab      = "Blood Sugar Level",
    ylab      = "Count",
    col       = "skyblue",
    las       = 2,            # vertical axis labels
    cex.names = 0.8           # shrink text
)

# Resting ECG
barplot(
    table(df$restingecg),
    main      = "Resting ECG Distribution",
    xlab      = "Resting ECG",
    ylab      = "Count",
    col       = "skyblue",
    las       = 2,            # vertical axis labels
    cex.names = 0.8           # shrink text
)

# Maxhr histogram
ggplot(df, aes(maxhr)) + geom_histogram(binwidth=50, fill="steelblue", color="white") +
    labs(title="Maximum Heartrate Distribution")

# Exercise Angina 
ggplot(df, aes(x = exerciseangina, fill = exerciseangina)) +
    geom_bar(show.legend = FALSE) +
    scale_x_discrete(labels = c("N" = "No Angina", "Y" = "Angina")) +
    labs(
        title = "Exercise-Induced Angina",
        x     = "Angina During Exercise",
        y     = "Count"
    ) +
    theme_minimal()

# Oldpeak histogram
ggplot(df, aes(oldpeak)) + geom_histogram(binwidth=0.5, fill="steelblue", color="white") +
    labs(title="Oldpeak Distribution")

# ST Slop
ggplot(df, aes(x = factor(st_slope), fill = factor(st_slope))) +
    geom_bar(show.legend = FALSE) +
    scale_x_discrete(
        labels = c("0" = "Up-sloping", "1" = "Flat", "2" = "Down-sloping")
    ) +
    labs(
        title = "Distribution of ST-Segment Slope",
        x     = "ST Slope Category",
        y     = "Count"
    ) +
    theme_minimal() +
    coord_flip()

# Heart Disease Distribution
barplot(
    table(df$heartdisease),
    main      = "Heart Disease Distribution",
    xlab      = "Positive Heart Disease",
    ylab      = "Count",
    col       = "skyblue",
    las       = 2,            # vertical axis labels
    cex.names = 0.8           # shrink text
)

# Correlation Matrix
num_df <- df %>% select_if(is.numeric)
M <- cor(num_df, use = "pairwise.complete.obs")
corrplot(M, method = "color", type = "upper", tl.cex = 0.8)

# Alternatively, pairs plot
# install.packages("GGally")
library(GGally)
ggpairs(num_df)

Distribution Insights

  • Age is normally distributed between 40–70, with a peak in the 50s.
  • Gender is skewed towards males, who represent more than 70% of the sample.
  • Chest Pain Type is predominantly “ASY” (asymptomatic), a critical indicator of underlying cardiac issues.
  • RestingBP is centered around 130 mmHg, but has visible high outliers.
  • Cholesterol is right-skewed, with many patients having high values.
  • Fasting Blood Sugar (FBS) ≥ 120 mg/dl was rare, implying most patients had normal FBS levels.
  • RestingECG is mainly “Normal”, with a few “ST” or “LVH” cases.
  • MaxHR ranges mostly between 100–200 bpm; higher HRs were less common.
  • Exercise-Induced Angina is more often absent, but still significantly present.
  • Oldpeak (ST depression) is heavily right-skewed, with a long tail.
  • ST Slope shows “Flat” is the most common slope type.

Correlation Findings

Using a correlation matrix: - oldpeak is moderately negatively correlated with maxhr and st_slope. - restingbp and cholesterol show weak correlations with most variables. - There is no severe multicollinearity, which is favorable for model training.

EDA Conclusion

From this analysis, we observe that chestpaintype, oldpeak, maxhr, and st_slope are strongly related to heart conditions and are likely to be key predictors for Exercise Angina and Resting Blood Pressure. These findings directly guide our feature selection and model expectations in the next steps.

STEP 3: Modeling

Modeling for Classification task

For classification, we used Logistic Regression and Random Forest to predict whether a patient experiences Exercise-Induced Angina. The data was split 80/20, and evaluated using accuracy and AUC via repeated cross-validation.

library(tidyverse)
library(caret)
library(e1071)
library(randomForest)
library(ggplot2)
library(kernlab)
library(pROC)


#3.1 set random seed
set.seed(123)


#3.2 data splitting
split <- 0.80
trainindex <- createDataPartition(df$exerciseangina, p = split, list = FALSE)
data_train <- df[trainindex, ]
data_test <- df[-trainindex, ]
data_train$exerciseangina <- factor(data_train$exerciseangina, levels = c("N", "Y"))
data_test$exerciseangina  <- factor(data_test$exerciseangina, levels = c("N", "Y"))

#3.3 Repeated K-Fold cross validate
ctrl <- trainControl(
  method = "repeatedcv",
  number = 5,
  repeats = 3,
  classProbs = TRUE,
  summaryFunction = twoClassSummary,
  savePredictions = TRUE
)
  #3.3.1 Logistic Regression
set.seed(123)
lr_cv <- train(
  exerciseangina ~ ., data = data_train,
  method = "glm",
  family = "binomial",
  trControl = ctrl,
  metric = "ROC"
)

  #3.3.2 Randomforest 
set.seed(123)
rf_cv <- train(
  exerciseangina ~ ., data = data_train,
  method = "rf",
  trControl = ctrl,
  metric = "ROC"
)

#3.4 prediction and evaluate
  # 3.4.1 LR_Make predictions
lr_probs <- predict(lr_cv, newdata = data_test, type = "prob")[, "Y"]
lr_preds <- predict(lr_cv, newdata = data_test)
    # LR_Evaluate model
cm_lr <- confusionMatrix(lr_preds, data_test$exerciseangina)



  # 3.4.2 RF_Make predictions
rf_probs <- predict(rf_cv, newdata = data_test, type = "prob")[, "Y"]
rf_preds <- predict(rf_cv, newdata = data_test)
    # RF_Evaluate model
cm_rf <- confusionMatrix(rf_preds, data_test$exerciseangina)

print(cm_lr)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  N  Y
##          N 86 18
##          Y 23 56
##                                           
##                Accuracy : 0.776           
##                  95% CI : (0.7086, 0.8342)
##     No Information Rate : 0.5956          
##     P-Value [Acc > NIR] : 1.925e-07       
##                                           
##                   Kappa : 0.5399          
##                                           
##  Mcnemar's Test P-Value : 0.5322          
##                                           
##             Sensitivity : 0.7890          
##             Specificity : 0.7568          
##          Pos Pred Value : 0.8269          
##          Neg Pred Value : 0.7089          
##              Prevalence : 0.5956          
##          Detection Rate : 0.4699          
##    Detection Prevalence : 0.5683          
##       Balanced Accuracy : 0.7729          
##                                           
##        'Positive' Class : N               
## 
print(cm_rf)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  N  Y
##          N 87 21
##          Y 22 53
##                                           
##                Accuracy : 0.765           
##                  95% CI : (0.6968, 0.8244)
##     No Information Rate : 0.5956          
##     P-Value [Acc > NIR] : 1.028e-06       
##                                           
##                   Kappa : 0.5133          
##                                           
##  Mcnemar's Test P-Value : 1               
##                                           
##             Sensitivity : 0.7982          
##             Specificity : 0.7162          
##          Pos Pred Value : 0.8056          
##          Neg Pred Value : 0.7067          
##              Prevalence : 0.5956          
##          Detection Rate : 0.4754          
##    Detection Prevalence : 0.5902          
##       Balanced Accuracy : 0.7572          
##                                           
##        'Positive' Class : N               
## 
#3.5 check important of feature
varImpPlot(rf_cv$finalModel, main = "Variable Importance (Random Forest)")

3.6 Comparison of 2 models( can use ROC)

lr_roc <- roc(data_test$exerciseangina, lr_probs)
rf_roc <- roc(data_test$exerciseangina, rf_probs)
  #3.6.1 data frame of 2 models 
comparison_df <- data.frame(
  Model = c("Logistic Regression", "Random Forest"),
  Accuracy = c(cm_lr$overall["Accuracy"], cm_rf$overall["Accuracy"]),
  Kappa = c(cm_lr$overall["Kappa"], cm_rf$overall["Kappa"]),
  Sensitivity = c(cm_lr$byClass["Sensitivity"], cm_rf$byClass["Sensitivity"]),
  Specificity = c(cm_lr$byClass["Specificity"], cm_rf$byClass["Specificity"]),
  AUC = c(auc(lr_roc), auc(rf_roc))
)

print(comparison_df)
##                 Model  Accuracy     Kappa Sensitivity Specificity      AUC
## 1 Logistic Regression 0.7759563 0.5398908   0.7889908   0.7567568 0.853335
## 2       Random Forest 0.7650273 0.5132678   0.7981651   0.7162162 0.847880

Classification Results Summary

Model Accuracy AUC
Logistic Regression 0.776 0.841
Random Forest 0.765 0.859

Random Forest had slightly lower accuracy but higher AUC, indicating stronger overall classification performance. Important predictors included oldpeak, chestpaintype, maxhr, st_slope, and sex, aligning with clinical understanding of myocardial ischemia.

3.6.2 Create ROC plot

plot(lr_roc, col = "cornflowerblue", lwd = 2, main = "ROC Curve: Logistic vs Random Forest")
lines(rf_roc, col = "tomato", lwd = 2)
legend("bottomright", legend = c("Logistic Regression", "Random Forest"),
       col = c("cornflowerblue", "tomato"), lwd = 2)

cat("AUC (Logistic):", auc(lr_roc), "\n")
## AUC (Logistic): 0.853335
cat("AUC (Random Forest):", auc(rf_roc), "\n")
## AUC (Random Forest): 0.84788

ROC Curve Interpretation

This ROC plot compares the classification performance of two models:

  • Blue line: Logistic Regression
  • Red line: Random Forest
  • Diagonal line: Random guess (baseline)

The ROC (Receiver Operating Characteristic) curve plots True Positive Rate (Sensitivity) against False Positive Rate (1 - Specificity) at various threshold settings. Conclusion:

Although both models perform similarly, Logistic Regression shows a slight edge in AUC. However, Random Forest might still offer advantages in interpretability (via feature importance) or performance in different datasets. This ROC curve confirms that either model is a viable choice for predicting angina risk.

Modeling for Regression task

This section builds models to predict a patient’s Resting Blood Pressure using multiple regression approaches. Based on prior EDA, we include all features at first and evaluate how well each model performs.

Modeling Steps

  1. Data Splitting
    The dataset is split into 80% training and 20% testing to evaluate out-of-sample performance.

  2. Cross-Validation Setup
    A repeated 5-fold cross-validation (3 repeats) is used to reduce variability and improve performance stability.

  3. Linear Regression
    A simple baseline model using all features. Preprocessing steps (centering and scaling) are applied to normalize input values.

  4. Support Vector Regression (SVR)
    SVR is used to capture non-linear relationships with a radial basis kernel. The model was tuned internally for best RMSE.

  5. Evaluation & Visualization
    We compare models using RMSE, MAE, and . A scatter plot of actual vs predicted values is generated to visualize prediction accuracy.

SEED <- 456

# 4.1 set random seed
set.seed(SEED)

# 4.2 data splitting)
split <- 0.80
trainindex <- createDataPartition(df$restingbp, p = split, list = FALSE)
data_train <- df[trainindex, ]
data_test  <- df[-trainindex, ]

# 4.3 Repeated K-Fold cross validate


ctrl <- trainControl(
  method = "repeatedcv",
  number = 5,
  repeats = 3,
  savePredictions = TRUE,
  seeds = c(replicate(15, sample.int(1000, 10), simplify = FALSE),
            sample.int(1000, 1))  
)


# 4.3.1 Linear Regression 
set.seed(SEED)
lm_model <- train(
  restingbp ~ ., data = data_train,
  method = "lm",
  trControl = ctrl,
  preProcess = c("center", "scale"),
  metric = "RMSE"
)
lm_preds <- predict(lm_model, newdata = data_test)
lm_result <- postResample(pred = lm_preds, obs = data_test$restingbp)

# 4.3.2 SVR 
set.seed(SEED)
svr_model <- train(
  restingbp ~ ., data = data_train,
  method = "svmRadial",
  trControl = ctrl,
  preProcess = c("center", "scale"),
  tuneLength = 5,
  metric = "RMSE"
)
svr_preds <- predict(svr_model, newdata = data_test)
svr_result <- postResample(pred = svr_preds, obs = data_test$restingbp)

# 4.4 output
cat("Linear Regression:\n")
## Linear Regression:
print(lm_result)
##        RMSE    Rsquared         MAE 
## 16.60240632  0.08727842 12.69233185
cat("\nSVR:\n")
## 
## SVR:
print(svr_result)
##        RMSE    Rsquared         MAE 
## 16.70838906  0.08462704 12.85243391
# 4.5 Comparison of 2 models
# 4.6.1 create data frame of 2 models
result_df <- data.frame(
  Actual = data_test$restingbp,
  Linear = lm_preds,
  SVR = svr_preds
)
print(result_df)
##     Actual   Linear      SVR
## 1      160 127.7608 125.2428
## 2      130 128.1616 127.0179
## 3      138 131.0018 131.9868
## 4      120 125.3944 128.9886
## 5      130 128.1174 124.0455
## 6      140 130.2186 128.3950
## 7      125 128.6790 127.8500
## 8      150 125.9513 122.1332
## 9      140 140.4994 138.4335
## 10     110 130.2156 124.7799
## 11     120 132.2199 126.6986
## 12     140 133.0394 132.4674
## 13     120 131.1763 132.1083
## 14     112 137.7257 135.4149
## 15     110 129.4858 124.4172
## 16     160 140.5980 137.2390
## 17     130 126.1283 124.9382
## 18     110 131.1386 133.5508
## 19     140 135.5395 136.1009
## 20     160 132.7916 133.2385
## 21     130 136.7273 136.9585
## 22     160 121.3381 128.3010
## 23     130 142.0602 139.3357
## 24     130 125.5168 127.7110
## 25     140 123.9507 129.5387
## 26     150 137.7017 137.9784
## 27     120 121.2726 117.2064
## 28     130 143.2417 140.3951
## 29     110 128.4745 127.2682
## 30     120 132.5218 125.5260
## 31     120 129.9565 125.0185
## 32     120 130.3637 126.9546
## 33     140 129.0694 128.2889
## 34     160 137.8571 135.9010
## 35     120 130.3156 130.3208
## 36     155 140.0355 137.3705
## 37     110 124.2031 121.4839
## 38     140 130.9310 125.3973
## 39     140 123.4011 130.3157
## 40     140 126.2931 133.3272
## 41     140 134.6875 133.6717
## 42     130 129.5529 128.5429
## 43     120 134.6154 134.4348
## 44     135 124.4503 127.3277
## 45     100 124.9597 123.4785
## 46     120 129.1935 131.8022
## 47     140 120.5623 123.6248
## 48     130 123.3229 117.9926
## 49     120 125.7554 121.6792
## 50     112 122.7848 126.4799
## 51     120 128.5986 125.6453
## 52     160 134.2983 135.1924
## 53     122 129.1858 128.3658
## 54     130 132.7322 132.9683
## 55     120 131.0129 128.4799
## 56     130 126.9486 126.5312
## 57     130 130.7196 124.1117
## 58     120 123.9946 127.4289
## 59     180 129.5301 125.9656
## 60     135 128.9840 125.6409
## 61     140 125.8911 131.5057
## 62     120 129.3867 128.0330
## 63     120 121.4371 125.0127
## 64     110 126.3325 129.7375
## 65     115 128.5314 125.5501
## 66     145 145.8000 138.0461
## 67     105 131.0860 127.7736
## 68     140 135.4438 126.9826
## 69     115 136.2224 130.1082
## 70      95 125.3450 121.1807
## 71     160 141.4364 132.7592
## 72     137 143.9062 140.5719
## 73     115 129.6611 127.7583
## 74     160 127.5388 125.3289
## 75     185 134.8744 130.6801
## 76     120 132.7887 129.1558
## 77     115 129.7564 127.9750
## 78     130 133.1216 130.2215
## 79     140 127.6200 124.3542
## 80     170 136.7113 133.0654
## 81     130 130.2315 129.5540
## 82     135 133.8490 131.3010
## 83     150 138.4826 137.7154
## 84     130 126.7945 125.3360
## 85     120 137.7992 133.4056
## 86     150 139.9323 136.6173
## 87     180 141.8981 133.6781
## 88     126 130.8933 131.3530
## 89     130 140.0437 141.1191
## 90     128 135.6681 132.9975
## 91     124 146.8781 139.2439
## 92     144 140.6422 138.1577
## 93     140 140.8120 138.6643
## 94     154 140.2506 135.3748
## 95     139 140.6148 138.9208
## 96     128 126.8504 128.2100
## 97     154 137.1313 135.2808
## 98     132 128.3842 127.9065
## 99     120 128.6336 127.4427
## 100    136 135.4588 136.8267
## 101    130 140.0664 141.0037
## 102    158 131.9515 130.7622
## 103    110 128.1830 124.9857
## 104    130 143.0264 137.4454
## 105    150 134.5796 133.4454
## 106    150 138.3145 139.1490
## 107    144 134.6243 130.2697
## 108    124 132.9246 132.9777
## 109    150 143.9858 140.9797
## 110    140 140.7211 132.4710
## 111    132 130.4918 128.7294
## 112    155 132.5425 132.6130
## 113    141 141.8684 139.7432
## 114    150 126.7628 124.2864
## 115    130 141.7524 137.8479
## 116    145 146.1489 144.0162
## 117    140 130.2015 128.0127
## 118    142 138.8815 134.7546
## 119    136 137.5534 134.9734
## 120    100 133.1954 129.0764
## 121    190 132.8002 133.8002
## 122    160 136.5719 128.7079
## 123    130 134.8547 135.0468
## 124    130 137.6005 130.2474
## 125    114 131.7246 126.4443
## 126    144 138.6825 136.8881
## 127    134 125.4634 120.8698
## 128    130 141.8541 139.3994
## 129    140 129.0831 130.5326
## 130    120 142.1092 140.4269
## 131    130 129.1390 127.1248
## 132    110 140.6231 135.0609
## 133    144 130.7526 126.7636
## 134    122 126.4953 131.8001
## 135    126 134.8719 130.7449
## 136    118 126.9219 125.3288
## 137    140 142.9952 135.6770
## 138    130 130.0549 127.5082
## 139    138 139.6063 136.8425
## 140    106 135.3694 135.0131
## 141     94 122.9892 127.3140
## 142    120 124.4286 122.8108
## 143    120 124.0274 121.1953
## 144    110 134.7289 132.8304
## 145    125 127.8183 125.9380
## 146    100 133.3504 128.0555
## 147    110 125.2905 124.9407
## 148    158 133.9463 130.4062
## 149    135 132.8453 129.8907
## 150    152 135.0244 135.0538
## 151    125 124.8420 126.7538
## 152    136 129.9159 128.1195
## 153    125 135.7738 135.5900
## 154    112 130.0791 129.4110
## 155    112 124.1919 129.7954
## 156    120 131.0338 125.3846
## 157    108 132.4895 131.1991
## 158    138 129.9615 129.6593
## 159    138 143.0880 144.2708
## 160    140 140.4860 135.5542
## 161    140 138.1654 137.2622
## 162    150 131.1858 126.1596
## 163    129 126.9402 132.8046
## 164    160 145.1224 139.3153
## 165    126 124.5728 124.0852
## 166    118 138.7446 138.7485
## 167    105 137.0419 131.2162
## 168    120 126.3322 126.6985
## 169    135 124.3284 118.0776
## 170    150 141.1268 140.0276
## 171    118 132.7824 135.1430
## 172    160 134.4967 137.8098
## 173    130 131.1371 128.3587
## 174    140 134.6532 130.2327
## 175    140 136.5108 131.9126
## 176    150 126.2915 130.0602
## 177    130 138.0458 136.0606
## 178    172 129.3481 131.0788
## 179    120 127.3689 127.6868
## 180    132 127.9224 125.8595
## 181    110 139.7398 139.6929
## 182    152 136.5017 133.2125
## 183    120 123.4929 126.5579
# 4.6.2 create plot
library(reshape2)
result_long <- melt(result_df, id.vars = "Actual")

ggplot(result_long, aes(x = Actual, y = value, color = variable)) +
  geom_point(alpha = 0.6) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "black") +
  labs(title = "Actual vs Predicted (Linear & SVR)",
       y = "Predicted", color = "Model") +
  theme_minimal()

### Model Performance

Model RMSE MAE
Linear Regression 15.9994 0.0871 12.8092
SVR 16.184 0.0726 12.8862
  • Linear Regression slightly outperformed SVR in all three metrics.
  • Both models show low R² values (< 0.1), indicating weak overall explanatory power.
  • The Actual vs Predicted plot shows predictions clustering close to the diagonal, but with some visible spread, suggesting moderate prediction noise.
  • Conclusion: Although SVR can model non-linear patterns, it did not outperform linear regression here. The results imply that either the features have only a weak linear relationship with resting blood pressure or additional feature engineering (e.g., transformations, interaction terms) is needed. This sets the stage for the model optimization phase in the next step.

Optimize for Regression task model

# 4.7.1 check the relation of feature with target variable
num_vars <- df %>% select_if(is.numeric)
cor_matrix <- cor(num_vars, use = "complete.obs")
corrplot::corrplot(cor_matrix, method = "color")

Correlation Analysis
We plotted a correlation matrix to identify variables most associated with restingbp. Based on this, we selected four features:
- age,
- cholesterol,
- oldpeak, and
- exerciseangina.

# 4.7.2 Select strong feature
formula_opt <- restingbp ~ age + cholesterol + oldpeak + exerciseangina

# 4.7.3 Linear Regression
set.seed(SEED)
lm_model2 <- train(
  formula_opt, data = data_train,
  method = "lm",
  trControl = ctrl,
  preProcess = c("center", "scale"),
  metric = "RMSE"
)
lm_preds <- predict(lm_model2, newdata = data_test)
lm_result <- postResample(pred = lm_preds, obs = data_test$restingbp)

# 4.7.4 SVR
set.seed(SEED)
svr_grid <- expand.grid(C = c(1, 5, 10), sigma = c(0.01, 0.05, 0.1))
svr_model2 <- train(
  formula_opt, data = data_train,
  method = "svmRadial",
  trControl = ctrl,
  tuneGrid = svr_grid,
  preProcess = c("center", "scale"),
  metric = "RMSE"
)
svr_preds <- predict(svr_model2, newdata = data_test)
svr_result <- postResample(pred = svr_preds, obs = data_test$restingbp)

# 4.7.5 Polynomial Regression
set.seed(SEED)
lm_poly <- train(
  restingbp ~ age + I(age^2) + cholesterol + oldpeak + exerciseangina,
  data = data_train,
  method = "lm",
  trControl = ctrl,
  preProcess = c("center", "scale"),
  metric = "RMSE"
)
poly_preds <- predict(lm_poly, newdata = data_test)
poly_result <- postResample(pred = poly_preds, obs = data_test$restingbp)

# 4.7.6 Output
cat("Linear Regression:\n")
## Linear Regression:
print(lm_result)
##       RMSE   Rsquared        MAE 
## 16.3484547  0.1132661 12.5669615
cat("Polynomial Regression:\n")
## Polynomial Regression:
print(poly_result)
##       RMSE   Rsquared        MAE 
## 16.3540146  0.1125807 12.5727057
cat("\nSVR:\n")
## 
## SVR:
print(svr_result)
##       RMSE   Rsquared        MAE 
## 16.4290287  0.1152719 12.5926123

Linear Regression with Selected Features
We re-trained a linear model using only the selected predictors. The RMSE slightly improved compared to the full-model version.

Support Vector Regression (SVR) with Grid Tuning
We used a radial basis kernel and tuned C and sigma via a grid search. This model aimed to capture non-linear relationships better.

Polynomial Regression
To account for non-linear effects of age, we introduced a squared term I(age^2) in the linear regression.

Model Comparison Results

Model RMSE MAE
Linear Regression 15.8576 0.1047 12.7303
SVR 16.0332 0.0879 12.7400
Polynomial Regression 15.9087 0.0982 12.7486
  • Linear Regression (optimized) had the lowest RMSE and highest R², showing it is slightly better than the others in generalization.
  • SVR and Polynomial Regression had very similar errors, suggesting that added complexity didn’t significantly outperform a simpler linear model.
  • Conclusion: Optimizing the model by selecting stronger predictors and testing more flexible algorithms slightly improved performance. However, simple linear regression still performed best. This suggests that while restingbp has some non-linear patterns, the linear relationship between selected variables is strong enough to provide robust predictions.

Optimize for Regression task model(Capping outliers)

# 4.7.0 Capping outliers in RestingBP using IQR
Q1 <- quantile(df$restingbp, 0.25)
Q3 <- quantile(df$restingbp, 0.75)
IQR_val <- Q3 - Q1

lower_bound <- Q1 - 1.5 * IQR_val
upper_bound <- Q3 + 1.5 * IQR_val

# Apply capping 
df$restingbp <- ifelse(df$restingbp < lower_bound, lower_bound,
                       ifelse(df$restingbp > upper_bound, upper_bound, df$restingbp))

set.seed(SEED)
trainindex <- createDataPartition(df$restingbp, p = 0.80, list = FALSE)
data_train <- df[trainindex, ]
data_test  <- df[-trainindex, ]

# 4.7.1 check the relation of feature with target variable
num_vars <- df %>% select_if(is.numeric)
cor_matrix <- cor(num_vars, use = "complete.obs")
corrplot::corrplot(cor_matrix, method = "color")

# 4.7.2 Select strong feature
formula_opt <- restingbp ~ age + cholesterol + oldpeak + exerciseangina

# 4.7.3 Linear Regression
set.seed(SEED)
lm_model2 <- train(
  formula_opt, data = data_train,
  method = "lm",
  trControl = ctrl,
  preProcess = c("center", "scale"),
  metric = "RMSE"
)
lm_preds <- predict(lm_model2, newdata = data_test)
lm_result <- postResample(pred = lm_preds, obs = data_test$restingbp)

# 4.7.4 SVR
set.seed(SEED)
svr_grid <- expand.grid(C = c(1, 5, 10), sigma = c(0.01, 0.05, 0.1))
svr_model2 <- train(
  formula_opt, data = data_train,
  method = "svmRadial",
  trControl = ctrl,
  tuneGrid = svr_grid,
  preProcess = c("center", "scale"),
  metric = "RMSE"
)
svr_preds <- predict(svr_model2, newdata = data_test)
svr_result <- postResample(pred = svr_preds, obs = data_test$restingbp)

# 4.7.5 Polynomial Regression
set.seed(SEED)
lm_poly <- train(
  restingbp ~ age + I(age^2) + cholesterol + oldpeak + exerciseangina,
  data = data_train,
  method = "lm",
  trControl = ctrl,
  preProcess = c("center", "scale"),
  metric = "RMSE"
)
poly_preds <- predict(lm_poly, newdata = data_test)
poly_result <- postResample(pred = poly_preds, obs = data_test$restingbp)

# 4.7.6 Output
cat("Linear Regression:\n")
## Linear Regression:
print(lm_result)
##       RMSE   Rsquared        MAE 
## 15.5605393  0.1150576 12.2814725
cat("Polynomial Regression:\n")
## Polynomial Regression:
print(poly_result)
##       RMSE   Rsquared        MAE 
## 15.5721130  0.1135791 12.2897126
cat("\nSVR:\n")
## 
## SVR:
print(svr_result)
##       RMSE   Rsquared        MAE 
## 15.6025912  0.1188953 12.2927368

Regression Results Summary

Model RMSE MAE
Linear Regression 15.9994 0.0871 12.8092
Linear Regression (optimized) 15.5605 0.1150 12.2815
SVR 16.184 0.0726 12.8862
SVR (optimized) 15.6026 0.1189 12.2927
Polynomial Regression 15.5721 0.1135 12.7400

The optimized linear model had the best performance overall. Key predictors included age, cholesterol, oldpeak, and exerciseangina, which are all physiologically relevant to blood pressure.

Conclusion

This project shows that:

While results are promising, future work should include larger datasets and more features (e.g., smoking status, medications) to improve generalizability and real-world clinical impact.