| NAME | MATRIC NO. |
|---|---|
| LEONAL SIGAR ANAK JAME | 24062655 |
| ZHANG YUSHAN | 23076749 |
| LIN YUQI | 24077562 |
| ZHOU XINGYU | 23099141 |
| SYED DZAFEER BIN SYED IDRUS | 22053251 |
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.
In this step, we performed data cleaning to prepare the dataset for modeling. Here’s what we did:
Standardized column names: Converted all variable names to lowercase and replaced spaces with underscores for easier handling.
Handled implausible values: Replaced
0 in restingbp and cholesterol
with NA since a resting blood pressure or cholesterol level
of zero is physiologically unrealistic.
Imputed missing values: Filled NA
values using the median within each gender group. This approach
preserves potential gender-based differences in health
indicators.
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.
Duplicate check: Verified that there were no duplicate rows in the dataset.
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
)
cholesterol and restingbp, but not extreme
enough to be immediately excluded.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
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)
| 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)
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.
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.
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)")
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
| 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.
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
This ROC plot compares the classification performance of two models:
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.
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.
Data Splitting
The dataset is split into 80% training and 20% testing to evaluate
out-of-sample performance.
Cross-Validation Setup
A repeated 5-fold cross-validation (3 repeats) is used to reduce
variability and improve performance stability.
Linear Regression
A simple baseline model using all features. Preprocessing steps
(centering and scaling) are applied to normalize input values.
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.
Evaluation & Visualization
We compare models using RMSE, MAE, and
R². 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 | R² | MAE |
|---|---|---|---|
| Linear Regression | 15.9994 | 0.0871 | 12.8092 |
| SVR | 16.184 | 0.0726 | 12.8862 |
# 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 | RMSE | R² | MAE |
|---|---|---|---|
| Linear Regression | 15.8576 | 0.1047 | 12.7303 |
| SVR | 16.0332 | 0.0879 | 12.7400 |
| Polynomial Regression | 15.9087 | 0.0982 | 12.7486 |
restingbp has some non-linear
patterns, the linear relationship between selected variables is strong
enough to provide robust predictions.# 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
| Model | RMSE | R² | 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.
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.