library(tidyverse)
library(knitr)
library(rattle)
library(rpart.plot)
library(RColorBrewer)
library(kableExtra)
library(reactable)
library(caret)
library(car)
library(vtable)
library(outliers)
#library(rstatix)
library(GGally)
library(fixr)
library(rpart)
library(randomForest)
#library(MASS)
library(rsample)
library(tidymodels)
library(visdat)
library(rattle)
library(rpart.plot)
library(RColorBrewer)
library(corrplot)
library(partykit)
library(reshape)
library(janitor)
library(yardstick)
library(grid)
library(gridExtra)
library(likert)
I obtained this dataset from Kaggle.
df <- read.csv("/Users/mohamedhassan/Downloads/heart_attack_prediction_dataset.csv", check.names = FALSE)
reactable(df)
st(df)
| Variable | N | Mean | Std. Dev. | Min | Pctl. 25 | Pctl. 75 | Max |
|---|---|---|---|---|---|---|---|
| Age | 8763 | 54 | 21 | 18 | 35 | 72 | 90 |
| Sex | 8763 | ||||||
| … Female | 2652 | 30% | |||||
| … Male | 6111 | 70% | |||||
| Cholesterol | 8763 | 260 | 81 | 120 | 192 | 330 | 400 |
| Heart Rate | 8763 | 75 | 21 | 40 | 57 | 93 | 110 |
| Diabetes | 8763 | 0.65 | 0.48 | 0 | 0 | 1 | 1 |
| Family History | 8763 | 0.49 | 0.5 | 0 | 0 | 1 | 1 |
| Smoking | 8763 | 0.9 | 0.3 | 0 | 1 | 1 | 1 |
| Obesity | 8763 | 0.5 | 0.5 | 0 | 0 | 1 | 1 |
| Alcohol Consumption | 8763 | 0.6 | 0.49 | 0 | 0 | 1 | 1 |
| Exercise Hours Per Week | 8763 | 10 | 5.8 | 0.0024 | 5 | 15 | 20 |
| Diet | 8763 | ||||||
| … Average | 2912 | 33% | |||||
| … Healthy | 2960 | 34% | |||||
| … Unhealthy | 2891 | 33% | |||||
| Previous Heart Problems | 8763 | 0.5 | 0.5 | 0 | 0 | 1 | 1 |
| Medication Use | 8763 | 0.5 | 0.5 | 0 | 0 | 1 | 1 |
| Stress Level | 8763 | 5.5 | 2.9 | 1 | 3 | 8 | 10 |
| Sedentary Hours Per Day | 8763 | 6 | 3.5 | 0.0013 | 3 | 9 | 12 |
| Income | 8763 | 158263 | 80575 | 20062 | 88310 | 227749 | 299954 |
| BMI | 8763 | 29 | 6.3 | 18 | 23 | 34 | 40 |
| Triglycerides | 8763 | 418 | 224 | 30 | 226 | 612 | 800 |
| Physical Activity Days Per Week | 8763 | 3.5 | 2.3 | 0 | 2 | 5 | 7 |
| Sleep Hours Per Day | 8763 | 7 | 2 | 4 | 5 | 9 | 10 |
| Continent | 8763 | ||||||
| … Africa | 873 | 10% | |||||
| … Asia | 2543 | 29% | |||||
| … Australia | 884 | 10% | |||||
| … Europe | 2241 | 26% | |||||
| … North America | 860 | 10% | |||||
| … South America | 1362 | 16% | |||||
| Hemisphere | 8763 | ||||||
| … Northern Hemisphere | 5660 | 65% | |||||
| … Southern Hemisphere | 3103 | 35% | |||||
| Heart Attack Risk | 8763 | 0.36 | 0.48 | 0 | 0 | 1 | 1 |
glimpse(df)
## Rows: 8,763
## Columns: 26
## $ `Patient ID` <chr> "BMW7812", "CZE1114", "BNI9906", "JL…
## $ Age <int> 67, 21, 21, 84, 66, 54, 90, 84, 20, …
## $ Sex <chr> "Male", "Male", "Female", "Male", "M…
## $ Cholesterol <int> 208, 389, 324, 383, 318, 297, 358, 2…
## $ `Blood Pressure` <chr> "158/88", "165/93", "174/99", "163/1…
## $ `Heart Rate` <int> 72, 98, 72, 73, 93, 48, 84, 107, 68,…
## $ Diabetes <int> 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, …
## $ `Family History` <int> 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, …
## $ Smoking <int> 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ Obesity <int> 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, …
## $ `Alcohol Consumption` <int> 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, …
## $ `Exercise Hours Per Week` <dbl> 4.1681888, 1.8132416, 2.0783530, 9.8…
## $ Diet <chr> "Average", "Unhealthy", "Healthy", "…
## $ `Previous Heart Problems` <int> 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, …
## $ `Medication Use` <int> 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, …
## $ `Stress Level` <int> 9, 1, 9, 9, 6, 2, 7, 4, 5, 4, 8, 4, …
## $ `Sedentary Hours Per Day` <dbl> 6.615001, 4.963459, 9.463426, 7.6489…
## $ Income <int> 261404, 285768, 235282, 125640, 1605…
## $ BMI <dbl> 31.25123, 27.19497, 28.17657, 36.464…
## $ Triglycerides <int> 286, 235, 587, 378, 231, 795, 284, 3…
## $ `Physical Activity Days Per Week` <int> 0, 1, 4, 3, 1, 5, 4, 6, 7, 7, 0, 4, …
## $ `Sleep Hours Per Day` <int> 6, 7, 4, 4, 5, 10, 10, 7, 4, 7, 4, 8…
## $ Country <chr> "Argentina", "Canada", "France", "Ca…
## $ Continent <chr> "South America", "North America", "E…
## $ Hemisphere <chr> "Southern Hemisphere", "Northern Hem…
## $ `Heart Attack Risk` <int> 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, …
sum(is.na(df))
## [1] 0
I removed Patient ID since it isn’t a health metric:
df2 <- df %>%
select(-`Patient ID`)
I transformed the Blood Pressure column into two separate columns: Systolic and Diastolic:
df2 <- df2 %>%
mutate(s0 = str_split(`Blood Pressure`,"/")) %>%
rowwise() %>%
mutate(Systolic = as.numeric(s0[1]),
Diastolic = as.numeric(s0[2])) %>%
select(-s0)
#select(first_bp, id, amount, systole, diastole)
Next, I created a column that categorized each patient’s blood pressure level based on their systolic and diastolic values:
df2 <- df2 %>%
mutate(BPLevel = (case_when(Systolic <=120 | Diastolic <=80 ~ "Normal",
between(Systolic, 120, 139) | between(Diastolic, 80, 89)~ "Prehypertension",
Systolic>=140 | Diastolic >= 90 ~ "Hypertension",
TRUE ~ "Missing"
)))
After creating the new columns, I used the nearZeroVar
function to check if there are any predictors that have little or no
variance, which can affect the forthcoming models:
nearZeroVar(df2)
## integer(0)
df2 %>%
group_by(BPLevel) %>%
count() %>%
ggplot(aes(x=reorder(BPLevel, -n), y=n)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Blood Pressure Level",
y="Number of Patients",
title="Count of Patients in Each Blood Pressure Category") +
theme(plot.title = element_text(hjust = 0.5))
A majority of the patients have normal blood pressure.
Exploring Each Continent’s Health and Lifestyle
I wanted to analyze the relationship between features in the dataset and the Continents of each patient:
df2 %>%
group_by(Continent) %>%
mutate(`Sleep Hours Per Day` = mean(`Sleep Hours Per Day`)) %>%
ggplot(aes(x=reorder(Continent,-`Sleep Hours Per Day`), y=`Sleep Hours Per Day`, fill=Continent)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Continent",
y="Average Sleep Per Day",
title="Average Sleep Per Day in Each Continent") +
theme(plot.title = element_text(hjust = 0.5))
df2 %>%
group_by(Continent) %>%
mutate(`Sedentary Hours Per Day` = mean(`Sedentary Hours Per Day`)) %>%
ggplot(aes(x=reorder(Continent,-`Sedentary Hours Per Day`), y=`Sedentary Hours Per Day`, fill=Continent)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Continent",
y="Average Sedentary Hours Per Day",
title="Average Sedentary Hours Per Day in Each Continent") +
theme(plot.title = element_text(hjust = 0.5))
df2 %>%
group_by(Continent) %>%
mutate(`Physical Activity Days Per Week` = mean(`Physical Activity Days Per Week`)) %>%
ggplot(aes(x=reorder(Continent,-`Physical Activity Days Per Week`), y=`Physical Activity Days Per Week`, fill=Continent)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Continent",
y="Average Physical Activitiy Days",
title="Average Physical Activitiy Days Per Week in Each Continent") +
theme(plot.title = element_text(hjust = 0.5))
df2 %>%
group_by(Continent) %>%
mutate(`Exercise Hours Per Week` = mean(`Exercise Hours Per Week`)) %>%
ggplot(aes(x=reorder(Continent,-`Exercise Hours Per Week`), y=`Exercise Hours Per Week`, fill=Continent)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Continent",
y="Average Exercise Hours",
title="Average Exercise Hours Per Week in Each Continent") +
theme(plot.title = element_text(hjust = 0.5))
df2 %>%
group_by(Continent) %>%
mutate(`Stress Level` = mean(`Stress Level`)) %>%
ggplot(aes(x=reorder(Continent,-`Stress Level`), y=`Stress Level`, fill=Continent)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Continent",
y="Average Stress Level",
title="Average Stress Level in Each Continent") +
theme(plot.title = element_text(hjust = 0.5))
df2 %>%
group_by(Continent) %>%
mutate(Cholesterol = mean(Cholesterol)) %>%
ggplot(aes(x=reorder(Continent,-Cholesterol), y=Cholesterol, fill=Continent)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Continent",
y="Average Cholesterol",
title="Average Cholesterol in Each Continent") +
theme(plot.title = element_text(hjust = 0.5))
df2 %>%
group_by(Continent) %>%
mutate(BMI = mean(BMI)) %>%
ggplot(aes(x=reorder(Continent,-BMI), y=BMI, fill=Continent)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Continent",
y="Average BMI",
title="Average BMI in Each Continent") +
theme(plot.title = element_text(hjust = 0.5))
df2 %>%
group_by(Continent) %>%
mutate(Triglycerides = mean(Triglycerides)) %>%
ggplot(aes(x=reorder(Continent,-Triglycerides), y=Triglycerides, fill=Continent)) +
geom_bar(stat="identity", position="dodge") +
labs(x="Continent",
y="Average Triglycerides",
title="Average Triglycerides in Each Continent") +
theme(plot.title = element_text(hjust = 0.5))
There are some interesting observations that are noteworthy. Patients from Australia had the highest average of sleep hours per day and sedentary hours per day, while also having the lowest average stress levels and lowest average exercise hours per week. North American patients had the highest BMI and Cholesterol levels, but averaged the second-highest exercise hours per week. Patients from Africa averaged the highest exercise hours per week and the second-highest BMI, sleep hours per day, and exercise hours per week, respectively.
df3 <- df2 %>%
select(BPLevel, `Sleep Hours Per Day`, `Sedentary Hours Per Day`,
`Physical Activity Days Per Week`, BMI, `Stress Level`, `Exercise Hours Per Week`,
Cholesterol, Triglycerides)
p <- ggpairs(df3, lower = list(continuous = wrap("smooth", se=FALSE, alpha = 0.7, size=0.5)))
p[5,3] <- p[5,3] + theme(panel.border = element_rect(color = 'blue', fill = NA, size = 2))
p[3,5] <- p[3,5] + theme(panel.border = element_rect(color = 'blue', fill = NA, size = 2))
p
df4 <- df3 %>%
select(-BPLevel)
df_cor <- Hmisc::rcorr(as.matrix(df4))
data.frame(df_cor$r) %>% head() %>% kable() %>% kable_styling()
| Sleep.Hours.Per.Day | Sedentary.Hours.Per.Day | Physical.Activity.Days.Per.Week | BMI | Stress.Level | Exercise.Hours.Per.Week | Cholesterol | Triglycerides | |
|---|---|---|---|---|---|---|---|---|
| Sleep Hours Per Day | 1.0000000 | 0.0047920 | 0.0140334 | -0.0100304 | -0.0142054 | -0.0012453 | 0.0044562 | -0.0292160 |
| Sedentary Hours Per Day | 0.0047920 | 1.0000000 | -0.0061780 | -0.0000236 | -0.0053972 | 0.0087556 | 0.0189145 | -0.0057846 |
| Physical Activity Days Per Week | 0.0140334 | -0.0061780 | 1.0000000 | 0.0081104 | 0.0074046 | 0.0077252 | 0.0160559 | -0.0075564 |
| BMI | -0.0100304 | -0.0000236 | 0.0081104 | 1.0000000 | -0.0032504 | 0.0037769 | 0.0172919 | -0.0059636 |
| Stress Level | -0.0142054 | -0.0053972 | 0.0074046 | -0.0032504 | 1.0000000 | -0.0091024 | -0.0244871 | -0.0039213 |
| Exercise Hours Per Week | -0.0012453 | 0.0087556 | 0.0077252 | 0.0037769 | -0.0091024 | 1.0000000 | 0.0215171 | 0.0017169 |
Among the independent variables chosen, there doesn’t appear to be any multicollinearity, which helps eliminate a possible factor in determining model performance.
The first Decision Tree model will focus on using the newly created
BPLevel column as the dependent variable. I will include
the independent variables explored when comparing each continent:
Sleep Hours Per Day, Sedentary Hours Per Day,
Physical Activity Days Per Week,
Exercise Hours Per Week, Stress Level,
Cholesterol, BMI, and
Triglycerides.
df3 <- fix_col_spaces(df3)
set.seed(1234)
df_tree <- createDataPartition(df3$BPLevel, p=0.75, list=FALSE)
# select 20% of the data for validation
df_train <- df3[df_tree,]
# use the remaining 80% of data to training and testing the models
df_test <- df3[-df_tree,]
round(prop.table(table(select(df3, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.23 60.55 20.22
round(prop.table(table(select(df_train, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.23 60.55 20.22
round(prop.table(table(select(df_test, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.22 60.55 20.23
set.seed(123)
tree <- rpart(BPLevel ~ ., data = df_train, cp=0.00001, method="class")
rpart.plot(tree, fallen.leaves = FALSE)
tree %>%
varImp() %>%
arrange(desc(Overall))
## Overall
## Exercise_Hours_Per_Week 516.4832
## Triglycerides 496.6889
## Sedentary_Hours_Per_Day 492.9993
## Cholesterol 456.8389
## BMI 427.4433
## Stress_Level 250.2686
## Sleep_Hours_Per_Day 240.5037
## Physical_Activity_Days_Per_Week 163.5319
set.seed(123)
predict_tree <- predict(tree,df_test,type = 'class')
set.seed(123)
table(df_test$BPLevel, predict_tree)
## predict_tree
## Hypertension Normal Prehypertension
## Hypertension 47 300 74
## Normal 190 934 202
## Prehypertension 61 315 67
set.seed(123)
confusionMatrix(predict_tree,as.factor(df_test$BPLevel))
## Confusion Matrix and Statistics
##
## Reference
## Prediction Hypertension Normal Prehypertension
## Hypertension 47 190 61
## Normal 300 934 315
## Prehypertension 74 202 67
##
## Overall Statistics
##
## Accuracy : 0.4785
## 95% CI : (0.4574, 0.4997)
## No Information Rate : 0.6055
## P-Value [Acc > NIR] : 1
##
## Kappa : -0.0147
##
## Mcnemar's Test P-Value : 5.826e-11
##
## Statistics by Class:
##
## Class: Hypertension Class: Normal Class: Prehypertension
## Sensitivity 0.11164 0.7044 0.15124
## Specificity 0.85811 0.2882 0.84201
## Pos Pred Value 0.15772 0.6030 0.19534
## Neg Pred Value 0.80233 0.3885 0.79643
## Prevalence 0.19224 0.6055 0.20228
## Detection Rate 0.02146 0.4265 0.03059
## Detection Prevalence 0.13607 0.7073 0.15662
## Balanced Accuracy 0.48488 0.4963 0.49663
cm <- confusionMatrix(predict_tree,as.factor(df_test$BPLevel))
cm$byClass
## Sensitivity Specificity Pos Pred Value Neg Pred Value
## Class: Hypertension 0.1116390 0.8581119 0.1577181 0.8023256
## Class: Normal 0.7043741 0.2881944 0.6029697 0.3884555
## Class: Prehypertension 0.1512415 0.8420149 0.1953353 0.7964266
## Precision Recall F1 Prevalence Detection Rate
## Class: Hypertension 0.1577181 0.1116390 0.1307371 0.1922374 0.02146119
## Class: Normal 0.6029697 0.7043741 0.6497391 0.6054795 0.42648402
## Class: Prehypertension 0.1953353 0.1512415 0.1704835 0.2022831 0.03059361
## Detection Prevalence Balanced Accuracy
## Class: Hypertension 0.1360731 0.4848754
## Class: Normal 0.7073059 0.4962843
## Class: Prehypertension 0.1566210 0.4966282
First Decision Tree Variable Importance
tree %>%
varImp() %>%
arrange(desc(Overall))
## Overall
## Exercise_Hours_Per_Week 516.4832
## Triglycerides 496.6889
## Sedentary_Hours_Per_Day 492.9993
## Cholesterol 456.8389
## BMI 427.4433
## Stress_Level 250.2686
## Sleep_Hours_Per_Day 240.5037
## Physical_Activity_Days_Per_Week 163.5319
imp_df <- data.frame(imp = tree$variable.importance)
imp_df2 <- imp_df %>%
tibble::rownames_to_column() %>%
dplyr::rename("variable" = rowname) %>%
dplyr::arrange(imp) %>%
dplyr::mutate(variable = forcats::fct_inorder(variable))
ggplot2::ggplot(imp_df2) +
geom_col(aes(x = variable, y = imp),
col = "black", show.legend = F) +
coord_flip() +
scale_fill_grey() +
theme_bw()
I used the feature importances from the first model to determine the variables to use for the second model. This time, I will use less variables and assess what the results are:
df4 <- df2 %>%
#select(`Heart Attack Risk`, BMI)
select(BPLevel, `Sedentary Hours Per Day`, Cholesterol, Triglycerides, `Exercise Hours Per Week`)
df4 <- fix_col_spaces(df4)
set.seed(1234)
df_tree2 <- createDataPartition(df4$BPLevel, p=0.75, list=FALSE)
#df_tree2 <- createDataPartition(df4$BPLevel, p=0.8, list=FALSE)
df_train2 <- df4[df_tree2,]
df_test2 <- df4[-df_tree2,]
round(prop.table(table(select(df4, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.23 60.55 20.22
round(prop.table(table(select(df_train2, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.23 60.55 20.22
round(prop.table(table(select(df_test2, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.22 60.55 20.23
set.seed(1234)
tree2 <- rpart(BPLevel ~ ., data = df_train2, cp=0.0001, method="class")
rpart.plot(tree2, fallen.leaves=FALSE, box.palette = "BuGn")
set.seed(123)
predict_tree2 <- predict(tree2, df_test2, type = 'class')
table(df_test2$BPLevel, predict_tree2)
## predict_tree2
## Hypertension Normal Prehypertension
## Hypertension 59 292 70
## Normal 195 939 192
## Prehypertension 67 311 65
set.seed(123)
confusionMatrix(predict_tree2,as.factor(df_test2$BPLevel))
## Confusion Matrix and Statistics
##
## Reference
## Prediction Hypertension Normal Prehypertension
## Hypertension 59 195 67
## Normal 292 939 311
## Prehypertension 70 192 65
##
## Overall Statistics
##
## Accuracy : 0.4854
## 95% CI : (0.4643, 0.5066)
## No Information Rate : 0.6055
## P-Value [Acc > NIR] : 1
##
## Kappa : 0.0013
##
## Mcnemar's Test P-Value : 2.669e-10
##
## Statistics by Class:
##
## Class: Hypertension Class: Normal Class: Prehypertension
## Sensitivity 0.14014 0.7081 0.14673
## Specificity 0.85189 0.3021 0.85003
## Pos Pred Value 0.18380 0.6089 0.19878
## Neg Pred Value 0.80631 0.4028 0.79710
## Prevalence 0.19224 0.6055 0.20228
## Detection Rate 0.02694 0.4288 0.02968
## Detection Prevalence 0.14658 0.7041 0.14932
## Balanced Accuracy 0.49602 0.5051 0.49838
cm2 <- confusionMatrix(predict_tree2,as.factor(df_test2$BPLevel))
cm2$byClass
## Sensitivity Specificity Pos Pred Value Neg Pred Value
## Class: Hypertension 0.1401425 0.8518937 0.1838006 0.8063135
## Class: Normal 0.7081448 0.3020833 0.6089494 0.4027778
## Class: Prehypertension 0.1467269 0.8500286 0.1987768 0.7971014
## Precision Recall F1 Prevalence Detection Rate
## Class: Hypertension 0.1838006 0.1401425 0.1590296 0.1922374 0.02694064
## Class: Normal 0.6089494 0.7081448 0.6548117 0.6054795 0.42876712
## Class: Prehypertension 0.1987768 0.1467269 0.1688312 0.2022831 0.02968037
## Detection Prevalence Balanced Accuracy
## Class: Hypertension 0.1465753 0.4960181
## Class: Normal 0.7041096 0.5051141
## Class: Prehypertension 0.1493151 0.4983777
Second Decision Tree Variable Importance
tree2 %>%
varImp() %>%
arrange(desc(Overall))
## Overall
## Sedentary_Hours_Per_Day 586.8255
## Triglycerides 564.2332
## Exercise_Hours_Per_Week 532.8059
## Cholesterol 522.9029
imp_df <- data.frame(imp = tree2$variable.importance)
imp_df2 <- imp_df %>%
tibble::rownames_to_column() %>%
dplyr::rename("variable" = rowname) %>%
dplyr::arrange(imp) %>%
dplyr::mutate(variable = forcats::fct_inorder(variable))
ggplot2::ggplot(imp_df2) +
geom_col(aes(x = variable, y = imp),
col = "black", show.legend = F) +
coord_flip() +
scale_fill_grey() +
theme_bw()
df5 <- fix_col_spaces(df4)
df5$BPLevel <- factor(df5$BPLevel)
set.seed(123)
df_rf <- createDataPartition(df5$BPLevel, p = 0.8, list = FALSE)
df_train_rf <- df5[df_rf, ]
df_test_rf <- df5[-df_rf, ]
round(prop.table(table(select(df5, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.23 60.55 20.22
round(prop.table(table(select(df_train_rf, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.23 60.55 20.23
round(prop.table(table(select(df_test_rf, BPLevel), exclude = NULL)), 4) * 100
## BPLevel
## Hypertension Normal Prehypertension
## 19.24 60.56 20.21
set.seed(123)
rf_model <- randomForest(BPLevel ~ ., data = df_train_rf, importance=TRUE, ntree=500)
rf_model
##
## Call:
## randomForest(formula = BPLevel ~ ., data = df_train_rf, importance = TRUE, ntree = 500)
## Type of random forest: classification
## Number of trees: 500
## No. of variables tried at each split: 2
##
## OOB estimate of error rate: 41.98%
## Confusion matrix:
## Hypertension Normal Prehypertension class.error
## Hypertension 43 1263 42 0.96810089
## Normal 139 3961 145 0.06690224
## Prehypertension 36 1318 64 0.95486601
set.seed(123)
rf_pred <- predict(rf_model,newdata = df_test_rf)
set.seed(123)
confusionMatrix(rf_pred, df_test_rf$BPLevel)
## Confusion Matrix and Statistics
##
## Reference
## Prediction Hypertension Normal Prehypertension
## Hypertension 3 30 8
## Normal 322 993 336
## Prehypertension 12 38 10
##
## Overall Statistics
##
## Accuracy : 0.5742
## 95% CI : (0.5507, 0.5975)
## No Information Rate : 0.6056
## P-Value [Acc > NIR] : 0.9966
##
## Kappa : -0.0189
##
## Mcnemar's Test P-Value : <2e-16
##
## Statistics by Class:
##
## Class: Hypertension Class: Normal Class: Prehypertension
## Sensitivity 0.008902 0.93591 0.028249
## Specificity 0.973145 0.04776 0.964235
## Pos Pred Value 0.073171 0.60145 0.166667
## Neg Pred Value 0.804793 0.32673 0.796690
## Prevalence 0.192352 0.60559 0.202055
## Detection Rate 0.001712 0.56678 0.005708
## Detection Prevalence 0.023402 0.94235 0.034247
## Balanced Accuracy 0.491023 0.49183 0.496242
cm3 <- confusionMatrix(rf_pred, df_test_rf$BPLevel)
Random Forest Variable Importance
set.seed(123)
rf_model %>%
varImp()
## Hypertension Normal Prehypertension
## Sedentary_Hours_Per_Day 4.117624 1.0014847 2.880540
## Cholesterol 6.415973 -1.7999160 2.604052
## Triglycerides 0.615599 -1.3743438 3.640563
## Exercise_Hours_Per_Week 5.031793 0.1870341 0.946421
set.seed(123)
cm_d <- as.data.frame(cm$table)
# confusion matrix statistics as data.frame
cm_st <-data.frame(cm$overall)
# round the values
cm_st$cm.overall <- round(cm_st$cm.overall,2)
# here we also have the rounded percentage values
cm_p <- as.data.frame(prop.table(cm$table))
cm_d$Perc <- round(cm_p$Freq*100,2)
set.seed(123)
# plotting the matrix
cm_d_p <- ggplot(data = cm_d, aes(x = Prediction , y = Reference, fill = Freq))+
geom_tile() +
geom_text(aes(label = paste("",Freq,",",Perc,"%")), color = 'white', size = 2.6) +
theme_light() +
guides(fill=FALSE)
# plotting the stats
cm_st_p <- tableGrob(cm_st)
# all together
grid.arrange(cm_d_p, cm_st_p,nrow = 1, ncol = 2,
top=textGrob("Decision Tree 1 Confusion Matrix",gp=gpar(fontsize=25,font=1)))
cm2$byClass
## Sensitivity Specificity Pos Pred Value Neg Pred Value
## Class: Hypertension 0.1401425 0.8518937 0.1838006 0.8063135
## Class: Normal 0.7081448 0.3020833 0.6089494 0.4027778
## Class: Prehypertension 0.1467269 0.8500286 0.1987768 0.7971014
## Precision Recall F1 Prevalence Detection Rate
## Class: Hypertension 0.1838006 0.1401425 0.1590296 0.1922374 0.02694064
## Class: Normal 0.6089494 0.7081448 0.6548117 0.6054795 0.42876712
## Class: Prehypertension 0.1987768 0.1467269 0.1688312 0.2022831 0.02968037
## Detection Prevalence Balanced Accuracy
## Class: Hypertension 0.1465753 0.4960181
## Class: Normal 0.7041096 0.5051141
## Class: Prehypertension 0.1493151 0.4983777
set.seed(123)
cm_d <- as.data.frame(cm2$table)
# confusion matrix statistics as data.frame
cm_st <-data.frame(cm2$overall)
# round the values
cm_st$cm2.overall <- round(cm_st$cm2.overall,2)
# here we also have the rounded percentage values
cm_p <- as.data.frame(prop.table(cm2$table))
cm_d$Perc <- round(cm_p$Freq*100,2)
set.seed(123)
# plotting the matrix
cm_d_p <- ggplot(data = cm_d, aes(x = Prediction , y = Reference, fill = Freq))+
geom_tile() +
geom_text(aes(label = paste("",Freq,",",Perc,"%")), color = 'white', size = 2.6) +
theme_light() +
guides(fill=FALSE)
# plotting the stats
cm_st_p <- tableGrob(cm_st)
# all together
grid.arrange(cm_d_p, cm_st_p,nrow = 1, ncol = 2,
top=textGrob("Decision Tree 2 Confusion Matrix",gp=gpar(fontsize=25,font=1)))
cm3$byClass
## Sensitivity Specificity Pos Pred Value Neg Pred Value
## Class: Hypertension 0.008902077 0.97314488 0.07317073 0.8047925
## Class: Normal 0.935909519 0.04775687 0.60145366 0.3267327
## Class: Prehypertension 0.028248588 0.96423462 0.16666667 0.7966903
## Precision Recall F1 Prevalence
## Class: Hypertension 0.07317073 0.008902077 0.01587302 0.1923516
## Class: Normal 0.60145366 0.935909519 0.73230088 0.6055936
## Class: Prehypertension 0.16666667 0.028248588 0.04830918 0.2020548
## Detection Rate Detection Prevalence Balanced Accuracy
## Class: Hypertension 0.001712329 0.02340183 0.4910235
## Class: Normal 0.566780822 0.94235160 0.4918332
## Class: Prehypertension 0.005707763 0.03424658 0.4962416
set.seed(123)
cm_d <- as.data.frame(cm3$table)
# confusion matrix statistics as data.frame
cm_st <-data.frame(cm3$overall)
# round the values
cm_st$cm3.overall <- round(cm_st$cm3.overall,2)
# here we also have the rounded percentage values
cm_p <- as.data.frame(prop.table(cm3$table))
cm_d$Perc <- round(cm_p$Freq*100,2)
set.seed(123)
# plotting the matrix
cm_d_p <- ggplot(data = cm_d, aes(x = Prediction , y = Reference, fill = Freq))+
geom_tile() +
geom_text(aes(label = paste("",Freq,",",Perc,"%")), color = 'white', size = 2.6) +
theme_light() +
guides(fill=FALSE)
# plotting the stats
cm_st_p <- tableGrob(cm_st)
# all together
grid.arrange(cm_d_p, cm_st_p,nrow = 1, ncol = 2,
top=textGrob("Random Forest Model Confusion Matrix",gp=gpar(fontsize=25,font=1)))
Precision
set.seed(123)
precision_results <- data.frame("Decision Tree 1: Precision" = cm[["byClass"]][ , "Precision"],
"Decision Tree 2: Precision" = cm2[["byClass"]][ , "Precision"],
"Random Forest: Precision" = cm3[["byClass"]][ , "Precision"], check.names = FALSE)
precision_results %>%
kbl(align = 'c') %>%
kable_styling()
| Decision Tree 1: Precision | Decision Tree 2: Precision | Random Forest: Precision | |
|---|---|---|---|
| Class: Hypertension | 0.1577181 | 0.1838006 | 0.0731707 |
| Class: Normal | 0.6029697 | 0.6089494 | 0.6014537 |
| Class: Prehypertension | 0.1953353 | 0.1987768 | 0.1666667 |
Recall
set.seed(123)
recall_results <- data.frame("Decision Tree 1: Recall" = cm[["byClass"]][ , "Recall"],
"Decision Tree 2: Recall" = cm2[["byClass"]][ , "Recall"],
"Random Forest: Recall" = cm3[["byClass"]][ , "Recall"], check.names = FALSE)
recall_results %>%
kbl(align = 'c') %>%
kable_styling()
| Decision Tree 1: Recall | Decision Tree 2: Recall | Random Forest: Recall | |
|---|---|---|---|
| Class: Hypertension | 0.1116390 | 0.1401425 | 0.0089021 |
| Class: Normal | 0.7043741 | 0.7081448 | 0.9359095 |
| Class: Prehypertension | 0.1512415 | 0.1467269 | 0.0282486 |
F1 Score
set.seed(123)
f1_results <- data.frame("Decision Tree 1: F1 Score" = cm[["byClass"]][ , "F1"],
"Decision Tree 2: F1 Score" = cm2[["byClass"]][ , "F1"],
"Random Forest: F1 Score" = cm3[["byClass"]][ , "F1"], check.names = FALSE)
f1_results %>%
kbl(align = 'c') %>%
kable_styling()
| Decision Tree 1: F1 Score | Decision Tree 2: F1 Score | Random Forest: F1 Score | |
|---|---|---|---|
| Class: Hypertension | 0.1307371 | 0.1590296 | 0.0158730 |
| Class: Normal | 0.6497391 | 0.6548117 | 0.7323009 |
| Class: Prehypertension | 0.1704835 | 0.1688312 | 0.0483092 |
Accuracy
accuracy_results <- data.frame("Decision Tree 1: Accuracy" = cm[["byClass"]][ , "Balanced Accuracy"],
"Decision Tree 2: Accuracy" = cm2[["byClass"]][ , "Balanced Accuracy"],
"Random Forest: Accuracy" = cm3[["byClass"]][ , "Balanced Accuracy"], check.names = FALSE)
accuracy_results %>%
kbl(align = 'c') %>%
kable_styling()
| Decision Tree 1: Accuracy | Decision Tree 2: Accuracy | Random Forest: Accuracy | |
|---|---|---|---|
| Class: Hypertension | 0.4848754 | 0.4960181 | 0.4910235 |
| Class: Normal | 0.4962843 | 0.5051141 | 0.4918332 |
| Class: Prehypertension | 0.4966282 | 0.4983777 | 0.4962416 |
The dataset I used explored Heart Attack Risk. I wanted to create a
multiclass classification model focused on Blood Pressure. I used the
values provided in the Blood Pressure column to categorize
patients into 3 classes: Normal, Prehypertension, and Hypertension,
creating aBPLevel column. The first decision tree model had
8 features used to predict BPLevel. The independent
variables were all continuous variables, which was purposely done to
limit the complications of using a categorical variable with the
dependent categorical variable. The visualization of both Decision Tree
models did not render clearly, which is a danger of decision trees in
terms of producing a complex diagram, making it hard to interpret. When
assessing the accuracy of each class for each model, they were
relatively unchanged, ranging from 0.487 to 0.504. However, the overall
accuracy of each model varied, particularly between the two Decision
Tree models and the Random Forest model. The first Decision Tree model
produced an overall accuracy of 0.4789, improving slightly with less
features in the second Decision Tree model to 0.4897. Using the same
number of features as the second Decision Tree model, the Random Forest
model performed better, with an overall accuracy of 0.5799.
In a scenario where we want to predict when a patient has pre-hypertension or hypertension, recall would be the metric we would want to use to identify the patients that have hypertension or prehypertension, since this metric is measured by true positives divided by true positives plus false negatives. If a patient actually has high or near-high blood pressure, we want the models to accurately predict those cases rather than predict that the patient has normal blood pressure when their blood pressure is actually not normal. Overall, each of the models don’t perform well when predicting having high or near-high blood pressure. Among the models, Random Forest performed the worst, having a recall value for hypertension of 0.0089021, or 0.89%, and a recall value for prehypertension of 0.0254237, or around 2.54%. The second Decision Tree performed the best when recalling patients with hypertension at around 14% and the first Decision Tree recalling hypertension patients at 11%. Conversely, the first Decision performed performed best when recalling prehypertension patients with a value of around 15.1%, which was slightly better than the second Decision Tree which had a recall value of 14.7%. The reduced number of variables in the second Decision Tree may have impacted the recall performance of patients with hypertension. While Random Forest performed the best when recalling patients with normal blood pressure, recalling patients in this category at a 93.6% rate, its poor performance in recalling patients with prehypertension and hypertension may indicate that the multiclass nature of the dependent variable may be too complex for the model to accurately predict patients in the prehypertension and hypertension categories, despite the low number of independent variables in the model.
The concerns expressed in the blog regarding Decision Trees are
valid. Using a multiclass classification model made the visualizations
from the Decision Tree models difficult to understand. Removing features
from the first model did not reduce the complexity of the second model.
I thought the low correlation among the independent variables and zero
features that were detected using the nearZeroVar function
would produce a more interpretable model. However, I think the
multiclass nature of the dependent variable made it difficult to assess
how each independent variable impacted each class of the dependent
variable.