library(readr)
library(dplyr)
library(tidyr)
library(ggplot2)
library(base)
#library(tigerstats)
library(mosaic)
library(pROC) # for ROC/AUC
library(caret)Predicting Incidence of Heart Disease as a Result of Lifestyle Habits
Lifestyle Habits
A mock data set similar to The Behavioral Risk Factor Surveillance System is being used in this project to create a model that will reliably predict the incidence of heart disease based off a number of ‘lifestyle habits’. Lifestyle habits are defined for the purpose of the study as : smoking, drinking, and the different food consumption.
The Behavioral Risk Factor Surveillance System (BRFSS) is a state-based national system of telephone surveys in the United States that collects data on health-related risk behaviors, chronic health conditions, and the use of preventive services for adults aged 18 or older.
Data Import
#data import and manipulation
cvd <- read_csv("D:/python stuff/CVD_cleaned.csv")
cvd <-as.data.frame(cvd)
cvd <- cvd %>%
rename(
height = `Height_(cm)`,
weight = `Weight_(kg)`,
age = Age_Category,
Fruit = Fruit_Consumption,
Fries = FriedPotato_Consumption,
Alcohol = Alcohol_Consumption,
Smoking = Smoking_History,
Greens = Green_Vegetables_Consumption) %>% mutate(
Heart_Disease = factor(ifelse(Heart_Disease == "Yes", "1", "0"), levels = c("0","1")),
Smoking = factor(ifelse(Smoking == "Yes", 1, 0)),
Exercise = factor(Exercise))
#subset dataframe
lifestyle_df <- cvd[,c(3,4,15:19)]The dataset has 19 variables with a sample size of 308854. There is strong class imbalance between the Heart Disease variable with ‘0’ accounting for ~92% of the dataset.
## Data Quality Check + +`{r message=FALSE, warning=FALSE} +# Missingness by variable +
sum(is.na(cvd))[1] 0
lifestyle_df |> count(Heart_Disease) |> mutate(Proportion = round(n / sum(n), 3)) Heart_Disease n Proportion
1 0 283883 0.919
2 1 24971 0.081
ggplot(lifestyle_df, aes(x = Heart_Disease, fill = Heart_Disease)) + geom_bar() +
geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5) +
labs(title = "Heart Disease Class Distribution", x = "Heart Disease", y = "Count") +
scale_y_continuous(labels = scales::label_comma())+
theme_bw() +
theme(legend.position = "none") Data Exploration and Visualization
#data exploration
head(cvd) General_Health Checkup Exercise Heart_Disease Skin_Cancer
1 Poor Within the past 2 years No 0 No
2 Very Good Within the past year No 1 No
3 Very Good Within the past year Yes 0 No
4 Poor Within the past year Yes 1 No
5 Good Within the past year No 0 No
6 Good Within the past year No 0 No
Other_Cancer Depression Diabetes Arthritis Sex age height weight BMI
1 No No No Yes Female 70-74 150 32.66 14.54
2 No No Yes No Female 70-74 165 77.11 28.29
3 No No Yes No Female 60-64 163 88.45 33.47
4 No No Yes No Male 75-79 180 93.44 28.73
5 No No No No Male 80+ 191 88.45 24.37
6 No Yes No Yes Male 60-64 183 154.22 46.11
Smoking Alcohol Fruit Greens Fries
1 1 0 30 16 12
2 0 0 30 0 4
3 0 4 12 3 16
4 0 0 30 30 8
5 1 0 8 4 0
6 0 0 12 12 12
str(cvd)'data.frame': 308854 obs. of 19 variables:
$ General_Health: chr "Poor" "Very Good" "Very Good" "Poor" ...
$ Checkup : chr "Within the past 2 years" "Within the past year" "Within the past year" "Within the past year" ...
$ Exercise : Factor w/ 2 levels "No","Yes": 1 1 2 2 1 1 2 2 1 1 ...
$ Heart_Disease : Factor w/ 2 levels "0","1": 1 2 1 2 1 1 2 1 1 1 ...
$ Skin_Cancer : chr "No" "No" "No" "No" ...
$ Other_Cancer : chr "No" "No" "No" "No" ...
$ Depression : chr "No" "No" "No" "No" ...
$ Diabetes : chr "No" "Yes" "Yes" "Yes" ...
$ Arthritis : chr "Yes" "No" "No" "No" ...
$ Sex : chr "Female" "Female" "Female" "Male" ...
$ age : chr "70-74" "70-74" "60-64" "75-79" ...
$ height : num 150 165 163 180 191 183 175 165 163 163 ...
$ weight : num 32.7 77.1 88.5 93.4 88.5 ...
$ BMI : num 14.5 28.3 33.5 28.7 24.4 ...
$ Smoking : Factor w/ 2 levels "0","1": 2 1 1 1 2 1 2 2 2 1 ...
$ Alcohol : num 0 0 4 0 0 0 0 3 0 0 ...
$ Fruit : num 30 30 12 30 8 12 16 30 12 12 ...
$ Greens : num 16 0 3 30 4 12 8 8 12 12 ...
$ Fries : num 12 4 16 8 0 12 0 8 4 1 ...
life_long <- lifestyle_df %>%
pivot_longer(
cols = c(Greens, Fruit,Fries,Alcohol),
names_to = "Food",
values_to = "Value")
ggplot(life_long,aes(Heart_Disease,y=Value, fill = Food=='Alcohol'))+
geom_col(position = "dodge")+
theme_minimal()+
scale_fill_manual(
values = c(`0` = "#440154",
`1` = "#FDE725"))Warning: No shared levels found between `names(values)` of the manual scale and the
data's fill values.
No shared levels found between `names(values)` of the manual scale and the
data's fill values.
life_long %>% select(Heart_Disease,Smoking,Food,Value) %>% group_by(Heart_Disease,Food) %>% summarise(n = sum(Value)) %>%
ggplot(aes(Food,n, fill=Heart_Disease))+
geom_col( position = 'dodge')+
scale_y_continuous(labels = scales::label_comma())+
theme_bw()`summarise()` has grouped output by 'Heart_Disease'. You can override using the
`.groups` argument.
ggplot(life_long) +
aes(x = Food, y = Value, fill = Heart_Disease) +
geom_col() +
scale_fill_hue(direction = 1) +
theme_minimal() +
facet_wrap(vars(Heart_Disease), scales = "free_y")#life_long %>% select(Heart_Disease,Smoking) %>% filter(Heart_Disease=='1') %>%
# ggplot(life_long,aes(Heart_Disease,fill=Smoking))+
# geom_bar(position = 'dodge')+facet_grid(~Heart_Disease)# Smoking x Heart Disease
ggplot(life_long) +
aes(x = Heart_Disease, fill = Smoking) +
geom_bar(aes(y = after_stat(count / sum(count))), position = "dodge2") +
scale_fill_manual(values = c(`0` = "#440154", `1` = "#FDE725")) +
labs(title = "Smoking x Heart Disease", y = "Proportion") +
theme_bw() +
facet_wrap(vars(Heart_Disease), scales = "free")# Exercise x Heart Disease
ggplot(life_long) +
aes(x = Heart_Disease, fill = Exercise) +
geom_bar(aes(y = after_stat(count / sum(count))), position = "dodge2") +
scale_fill_manual(values = c(No = "#440154", Yes = "#FDE725")) +
labs(title = "Exercise x Heart Disease", y = "Proportion") +
theme_bw() +
facet_wrap(vars(Heart_Disease), scales = "free")From the graphs above, we can see that smoking is much higher represented in the heart disease group. Inversely, given no heart disease ‘no smoking’ represents much higher proportion of the population.
A different trend is observed for exercise such that while exercise is in higher proportion in both Heart Disease populations, it is much more likely for those who do not have heart disease to indicate that they do exercise.
A logistic regression to determine what variables are significant predictors of heart disease diagnosis
# compute class weights to correct for 92/8 imbalance
class_counts <- table(lifestyle_df$Heart_Disease)
class_weights <- 1 / class_counts[lifestyle_df$Heart_Disease]
#class_weights
#main effects
lifestyle_df$Heart_Disease<-relevel(lifestyle_df$Heart_Disease, ref = "0")
life_lm <- glm(Heart_Disease ~ ., data = lifestyle_df,
family = binomial());summary(life_lm)
Call:
glm(formula = Heart_Disease ~ ., family = binomial(), data = lifestyle_df)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -2.2029434 0.0173526 -126.952 < 2e-16 ***
ExerciseYes -0.6219205 0.0144355 -43.083 < 2e-16 ***
Smoking1 0.7679578 0.0136314 56.337 < 2e-16 ***
Alcohol -0.0187079 0.0009083 -20.596 < 2e-16 ***
Fruit 0.0002526 0.0002857 0.884 0.377
Greens -0.0022513 0.0004931 -4.566 4.98e-06 ***
Fries -0.0070181 0.0008540 -8.218 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 173478 on 308853 degrees of freedom
Residual deviance: 167313 on 308847 degrees of freedom
AIC: 167327
Number of Fisher Scoring iterations: 5
life_lm_reduced <- glm(Heart_Disease ~ . - Fruit, data = lifestyle_df,
family = binomial())
anova(life_lm_reduced, life_lm, test = "Chisq") # no leading +Analysis of Deviance Table
Model 1: Heart_Disease ~ (Exercise + Smoking + Alcohol + Fruit + Greens +
Fries) - Fruit
Model 2: Heart_Disease ~ Exercise + Smoking + Alcohol + Fruit + Greens +
Fries
Resid. Df Resid. Dev Df Deviance Pr(>Chi)
1 308848 167313
2 308847 167313 1 0.78005 0.3771
cbind(minus_fruit = AIC(life_lm_reduced), with_fruit = AIC(life_lm)) minus_fruit with_fruit
[1,] 167325.4 167326.6
round(exp(cbind(OR = coef(life_lm_reduced),
confint.default(life_lm_reduced))), 3) OR 2.5 % 97.5 %
(Intercept) 0.111 0.108 0.115
ExerciseYes 0.538 0.523 0.553
Smoking1 2.153 2.097 2.212
Alcohol 0.981 0.980 0.983
Greens 0.998 0.997 0.999
Fries 0.993 0.991 0.995
The main effects model indicates that only fruit consumption is not a statistical significant co-variate, and the model could likely be improved by removing it. Smoking has a high odds of heart disease with (~2.15 OR) . Surprisingly, Fruit, Greens and Fries Consumption shows effectively little to no difference in odds of a positive heart disease (1.0, .99, .99,1.00) and the confidence intervals are very close to 1. Alcohol consumption indicates a 2.2% decrease in odds for every 1 extra alcoholic drink taken. Only exercise seems to have a protective effect with a 0.53 (0.980, 0.983) odds of a heart attack compared to those that don’t exercise. The seeming protective effects of fries consumption on heart-disease outcome should be investigated as there may be confounding variable or it may indicate that given a heart disease diagnoses may reduce alcohol and fries intake.
Model Creation and Comparisons
library(broom)
or_df <- tidy(life_lm_reduced, exponentiate = TRUE, conf.int = TRUE) %>%
filter(term != "(Intercept)")
ggplot(or_df, aes(x = estimate, y = reorder(term, estimate))) +
geom_point(size = 3, colour = "#440154") +
geom_errorbar(aes(xmin = conf.low, xmax = conf.high), height = 0.2) +
geom_vline(xintercept = 1, linetype = "dashed", colour = "grey50") +
labs(title = "Odds ratios (95% CI)", x = "Odds ratio", y = NULL) +
theme_bw()`height` was translated to `width`.
library(broom)
aug_df <- augment(life_lm_reduced, type.predict = "response", type.residuals = "deviance")
ggplot(aug_df, aes(x = .fitted, y = .resid)) +
geom_point(alpha = 0.1) +
geom_smooth(se = FALSE, colour = "#440154") +
geom_hline(yintercept = 0, linetype = "dashed", colour = "grey50") +
labs(title = "Deviance residuals vs fitted values",
x = "Fitted probability", y = "Deviance residual") +
theme_bw()`geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
life_lm_reduced <- glm(Heart_Disease ~ . - Fruit, data = lifestyle_df, family = binomial())
+ anova(life_lm_reduced, life_lm, test = 'Chisq') Resid. Df Resid. Dev Df Deviance Pr(>Chi)
1 308848 167313.4 NA NA NA
2 308847 167312.6 1 0.780054 0.3771246
#comparing AIC
cbind(minus_fruit=AIC(life_lm_reduced),with_fruit=AIC(life_lm)) minus_fruit with_fruit
[1,] 167325.4 167326.6
Removing fruit from the main model improved the model’s AIC score and comparing the models with a chi-square test indicates that they are not statistically different.
life_lm_interact <- glm(Heart_Disease ~ .^2, data = lifestyle_df,
family = binomial())
summary(life_lm_interact)
Call:
glm(formula = Heart_Disease ~ .^2, family = binomial(), data = lifestyle_df)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -2.216e+00 2.971e-02 -74.588 < 2e-16 ***
ExerciseYes -6.155e-01 3.215e-02 -19.146 < 2e-16 ***
Smoking1 7.343e-01 3.100e-02 23.689 < 2e-16 ***
Alcohol -1.821e-02 2.647e-03 -6.881 5.96e-12 ***
Fruit 1.847e-03 6.766e-04 2.730 0.006326 **
Greens 2.094e-03 1.248e-03 1.679 0.093216 .
Fries -1.388e-02 2.085e-03 -6.661 2.72e-11 ***
ExerciseYes:Smoking1 1.225e-01 2.950e-02 4.153 3.28e-05 ***
ExerciseYes:Alcohol -2.903e-04 2.031e-03 -0.143 0.886324
ExerciseYes:Fruit -1.660e-03 6.225e-04 -2.667 0.007653 **
ExerciseYes:Greens -2.718e-03 1.073e-03 -2.534 0.011277 *
ExerciseYes:Fries 2.169e-05 1.733e-03 0.013 0.990012
Smoking1:Alcohol -4.690e-04 1.979e-03 -0.237 0.812627
Smoking1:Fruit -3.857e-04 5.771e-04 -0.668 0.503975
Smoking1:Greens -4.616e-03 9.946e-04 -4.641 3.47e-06 ***
Smoking1:Fries 5.149e-03 1.769e-03 2.910 0.003614 **
Alcohol:Fruit -1.142e-05 4.043e-05 -0.283 0.777489
Alcohol:Greens 4.452e-05 6.715e-05 0.663 0.507308
Alcohol:Fries -3.320e-05 1.126e-04 -0.295 0.768028
Fruit:Greens -2.883e-05 1.639e-05 -1.759 0.078629 .
Fruit:Fries 4.453e-05 3.087e-05 1.443 0.149151
Greens:Fries 1.191e-04 3.229e-05 3.687 0.000227 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 173478 on 308853 degrees of freedom
Residual deviance: 167229 on 308832 degrees of freedom
AIC: 167273
Number of Fisher Scoring iterations: 5
anova(life_lm_reduced, life_lm_interact, test = "Chisq")Analysis of Deviance Table
Model 1: Heart_Disease ~ (Exercise + Smoking + Alcohol + Fruit + Greens +
Fries) - Fruit
Model 2: Heart_Disease ~ (Exercise + Smoking + Alcohol + Fruit + Greens +
Fries)^2
Resid. Df Resid. Dev Df Deviance Pr(>Chi)
1 308848 167313
2 308832 167229 16 84.1 3.01e-11 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
cbind(AIC(life_lm_reduced), AIC(life_lm_interact)) [,1] [,2]
[1,] 167325.4 167273.3
exp(cbind(OR = coef(life_lm), confint.default(life_lm))) OR 2.5 % 97.5 %
(Intercept) 0.1104775 0.1067833 0.1142995
ExerciseYes 0.5369123 0.5219343 0.5523202
Smoking1 2.1553601 2.0985377 2.2137211
Alcohol 0.9814660 0.9797203 0.9832148
Fruit 1.0002526 0.9996927 1.0008128
Greens 0.9977512 0.9967874 0.9987159
Fries 0.9930065 0.9913457 0.9946700
ggplot(lifestyle_df, aes(sample = Fruit)) +
stat_qq() +
stat_qq_line(colour = "steelblue") +
labs(title = "Q-Q plot: Fruit consumption") +
theme_bw()var.test(lifestyle_df$Fruit ~ lifestyle_df$Heart_Disease)
F test to compare two variances
data: lifestyle_df$Fruit by lifestyle_df$Heart_Disease
F = 1.0387, num df = 283882, denom df = 24970, p-value = 5.209e-05
alternative hypothesis: true ratio of variances is not equal to 1
95 percent confidence interval:
1.019821 1.057831
sample estimates:
ratio of variances
1.038726
t.test(lifestyle_df$Fruit ~ lifestyle_df$Heart_Disease,
mu = 0, var.equal = FALSE, alternative = "greater")
Welch Two Sample t-test
data: lifestyle_df$Fruit by lifestyle_df$Heart_Disease
t = 11.326, df = 29720, p-value < 2.2e-16
alternative hypothesis: true difference in means between group 0 and group 1 is greater than 0
95 percent confidence interval:
1.564253 Inf
sample estimates:
mean in group 0 mean in group 1
29.98316 28.15314
var.test(lifestyle_df$Fries ~ lifestyle_df$Heart_Disease)
F test to compare two variances
data: lifestyle_df$Fries by lifestyle_df$Heart_Disease
F = 1.0195, num df = 283882, denom df = 24970, p-value = 0.0397
alternative hypothesis: true ratio of variances is not equal to 1
95 percent confidence interval:
1.000910 1.038215
sample estimates:
ratio of variances
1.019464
t.test(lifestyle_df$Fries ~ lifestyle_df$Heart_Disease,
mu = 0, var.equal = FALSE, alternative = "two.sided")
Welch Two Sample t-test
data: lifestyle_df$Fries by lifestyle_df$Heart_Disease
t = 5.1697, df = 29628, p-value = 2.361e-07
alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
95 percent confidence interval:
0.1803644 0.4006544
sample estimates:
mean in group 0 mean in group 1
6.320104 6.029594
Model Testing
#model test
# train/test split
train_idx <- createDataPartition(lifestyle_df$Heart_Disease, p = 0.8, list = FALSE)
train_df <- lifestyle_df[ train_idx, ]
test_df <- lifestyle_df[-train_idx, ]
train_weights <- class_weights[train_idx]
final_model <- glm(Heart_Disease ~ . - Fruit, data = train_df,
family = binomial())
# predictions on test set
pred_prob <- predict(final_model, newdata = test_df, type = "response")
pred_class <- factor(ifelse(pred_prob > 0.5, "1", "0"), levels = c("0", "1"))
# confusion matrix
confusionMatrix(pred_class, test_df$Heart_Disease, positive = "1")Confusion Matrix and Statistics
Reference
Prediction 0 1
0 56776 4994
1 0 0
Accuracy : 0.9192
95% CI : (0.917, 0.9213)
No Information Rate : 0.9192
P-Value [Acc > NIR] : 0.5038
Kappa : 0
Mcnemar's Test P-Value : <2e-16
Sensitivity : 0.00000
Specificity : 1.00000
Pos Pred Value : NaN
Neg Pred Value : 0.91915
Prevalence : 0.08085
Detection Rate : 0.00000
Detection Prevalence : 0.00000
Balanced Accuracy : 0.50000
'Positive' Class : 1
# ROC / AUC
roc_obj <- roc(test_df$Heart_Disease, pred_prob)Setting levels: control = 0, case = 1
Setting direction: controls < cases
auc(roc_obj)Area under the curve: 0.6558
ggroc(roc_obj, colour = "#440154") +
geom_abline(slope = 1, intercept = 1, linetype = "dashed", colour = "grey60") +
labs(title = paste0("ROC curve (AUC = ", round(auc(roc_obj), 3), ")"),
x = "Specificity", y = "Sensitivity") +
theme_bw()The model selected produced an AUC value of 0.653 indicating that the model does not do a great job of correctly classifying those with heart disease. The model itself could be much improved through the use of investigating additional , possible confounding, variables.The result is not surpr ising considering that only Exercise and Smoking were the only two covariates with significant odds that were much different from 1.0 in the logistic regression.