Heart Disease Risk Prediction

29 Aug 2026 • 15 min read

Classic ML modeling and resampling with R: A deep dive

Introduction

Within the limits of this Kaggle dataset of heart disease indicators, we’ll employ and compare two supervised learning models (logistic regression and random forest) for binary prediction. As we will discuss later, imbalance in the data will necessitate resampling, and two techniques (undersampling and ROSE) will be tested against each other as well.

We want to answer the question:

What are the biggest contributors to heart disease?


Load Libraries and Data

R
library(tidyverse)
library(knitr)
library(caret)
library(randomForest)
library(car)
library(pROC)
library(PRROC)
library(ROSE)
library(cowplot)
library(modelsummary)
library(scales)
library(gridGraphics)
library(ggdark)

theme_set(dark_theme_gray())

heart <- read_csv('https://docs.google.com/spreadsheets/d/1Q6iVZFHlQfU2R2vHfWftwuTDS7gMkqX9ftl4IoxNiTk/gviz/tq?tqx=out:csv') # Rows: 253680 Columns: 22

summary(heart)
str(heart)
CONSOLE
Rows: 253680 Columns: 22                                                                                                    
── Column specification ──────────────────────────────────────────────────────────────────────────────────────────────────────
Delimiter: ","
dbl (22): HeartDiseaseorAttack, HighBP, HighChol, CholCheck, BMI, Smoker, Stroke, Diabetes, PhysActivity, Fruits, Veggies,...

 HeartDiseaseorAttack     HighBP         HighChol        CholCheck           BMI            Smoker           Stroke       
 Min.   :0.00000      Min.   :0.000   Min.   :0.0000   Min.   :0.0000   Min.   :12.00   Min.   :0.0000   Min.   :0.00000  
 1st Qu.:0.00000      1st Qu.:0.000   1st Qu.:0.0000   1st Qu.:1.0000   1st Qu.:24.00   1st Qu.:0.0000   1st Qu.:0.00000  
 Median :0.00000      Median :0.000   Median :0.0000   Median :1.0000   Median :27.00   Median :0.0000   Median :0.00000  
 Mean   :0.09419      Mean   :0.429   Mean   :0.4241   Mean   :0.9627   Mean   :28.38   Mean   :0.4432   Mean   :0.04057  
 3rd Qu.:0.00000      3rd Qu.:1.000   3rd Qu.:1.0000   3rd Qu.:1.0000   3rd Qu.:31.00   3rd Qu.:1.0000   3rd Qu.:0.00000  
 Max.   :1.00000      Max.   :1.000   Max.   :1.0000   Max.   :1.0000   Max.   :98.00   Max.   :1.0000   Max.   :1.00000  
    Diabetes       PhysActivity        Fruits          Veggies       HvyAlcoholConsump AnyHealthcare     NoDocbcCost     
 Min.   :0.0000   Min.   :0.0000   Min.   :0.0000   Min.   :0.0000   Min.   :0.0000    Min.   :0.0000   Min.   :0.00000  
 1st Qu.:0.0000   1st Qu.:1.0000   1st Qu.:0.0000   1st Qu.:1.0000   1st Qu.:0.0000    1st Qu.:1.0000   1st Qu.:0.00000  
 Median :0.0000   Median :1.0000   Median :1.0000   Median :1.0000   Median :0.0000    Median :1.0000   Median :0.00000  
 Mean   :0.2969   Mean   :0.7565   Mean   :0.6343   Mean   :0.8114   Mean   :0.0562    Mean   :0.9511   Mean   :0.08418  
 3rd Qu.:0.0000   3rd Qu.:1.0000   3rd Qu.:1.0000   3rd Qu.:1.0000   3rd Qu.:0.0000    3rd Qu.:1.0000   3rd Qu.:0.00000  
 Max.   :2.0000   Max.   :1.0000   Max.   :1.0000   Max.   :1.0000   Max.   :1.0000    Max.   :1.0000   Max.   :1.00000  
    GenHlth         MentHlth         PhysHlth         DiffWalk           Sex              Age           Education   
 Min.   :1.000   Min.   : 0.000   Min.   : 0.000   Min.   :0.0000   Min.   :0.0000   Min.   : 1.000   Min.   :1.00  
 1st Qu.:2.000   1st Qu.: 0.000   1st Qu.: 0.000   1st Qu.:0.0000   1st Qu.:0.0000   1st Qu.: 6.000   1st Qu.:4.00  
 Median :2.000   Median : 0.000   Median : 0.000   Median :0.0000   Median :0.0000   Median : 8.000   Median :5.00  
 Mean   :2.511   Mean   : 3.185   Mean   : 4.242   Mean   :0.1682   Mean   :0.4403   Mean   : 8.032   Mean   :5.05  
 3rd Qu.:3.000   3rd Qu.: 2.000   3rd Qu.: 3.000   3rd Qu.:0.0000   3rd Qu.:1.0000   3rd Qu.:10.000   3rd Qu.:6.00  
 Max.   :5.000   Max.   :30.000   Max.   :30.000   Max.   :1.0000   Max.   :1.0000   Max.   :13.000   Max.   :6.00  
     Income     
 Min.   :1.000  
 1st Qu.:5.000  
 Median :7.000  
 Mean   :6.054  
 3rd Qu.:8.000  
 Max.   :8.000  

> str(heart)
spc_tbl_ [253,680 × 22] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ HeartDiseaseorAttack: num [1:253680] 0 0 0 0 0 0 0 0 1 0 ...
 $ HighBP              : num [1:253680] 1 0 1 1 1 1 1 1 1 0 ...
 $ HighChol            : num [1:253680] 1 0 1 0 1 1 0 1 1 0 ...
 $ CholCheck           : num [1:253680] 1 0 1 1 1 1 1 1 1 1 ...
 $ BMI                 : num [1:253680] 40 25 28 27 24 25 30 25 30 24 ...
 $ Smoker              : num [1:253680] 1 1 0 0 0 1 1 1 1 0 ...
 $ Stroke              : num [1:253680] 0 0 0 0 0 0 0 0 0 0 ...
 $ Diabetes            : num [1:253680] 0 0 0 0 0 0 0 0 2 0 ...
 $ PhysActivity        : num [1:253680] 0 1 0 1 1 1 0 1 0 0 ...
 $ Fruits              : num [1:253680] 0 0 1 1 1 1 0 0 1 0 ...
 $ Veggies             : num [1:253680] 1 0 0 1 1 1 0 1 1 1 ...
 $ HvyAlcoholConsump   : num [1:253680] 0 0 0 0 0 0 0 0 0 0 ...
 $ AnyHealthcare       : num [1:253680] 1 0 1 1 1 1 1 1 1 1 ...
 $ NoDocbcCost         : num [1:253680] 0 1 1 0 0 0 0 0 0 0 ...
 $ GenHlth             : num [1:253680] 5 3 5 2 2 2 3 3 5 2 ...
 $ MentHlth            : num [1:253680] 18 0 30 0 3 0 0 0 30 0 ...
 $ PhysHlth            : num [1:253680] 15 0 30 0 0 2 14 0 30 0 ...
 $ DiffWalk            : num [1:253680] 1 0 1 0 0 0 0 1 1 0 ...
 $ Sex                 : num [1:253680] 0 0 0 0 0 1 0 0 0 1 ...
 $ Age                 : num [1:253680] 9 7 9 11 11 10 9 11 9 8 ...
 $ Education           : num [1:253680] 4 6 4 3 5 6 6 4 5 4 ...
 $ Income              : num [1:253680] 3 1 8 6 4 8 7 4 1 3 ...
 - attr(*, "spec")=
  .. cols(
  ..   HeartDiseaseorAttack = col_double(),
  ..   HighBP = col_double(),
  ..   HighChol = col_double(),
  ..   CholCheck = col_double(),
  ..   BMI = col_double(),
  ..   Smoker = col_double(),
  ..   Stroke = col_double(),
  ..   Diabetes = col_double(),
  ..   PhysActivity = col_double(),
  ..   Fruits = col_double(),
  ..   Veggies = col_double(),
  ..   HvyAlcoholConsump = col_double(),
  ..   AnyHealthcare = col_double(),
  ..   NoDocbcCost = col_double(),
  ..   GenHlth = col_double(),
  ..   MentHlth = col_double(),
  ..   PhysHlth = col_double(),
  ..   DiffWalk = col_double(),
  ..   Sex = col_double(),
  ..   Age = col_double(),
  ..   Education = col_double(),
  ..   Income = col_double()
  .. )
 - attr(*, "problems")=<externalptr> 
The biggest issue with this dataset is the predictor variable class imbalance:
> table(heart$HeartDiseaseorAttack)
      0      1 
229787  23893

229,787 zeros (Does not have heart disease or had an attack) and 23,893 ones (Does have).


Exploratory Data Analysis

In which we examine and summarize the characteristics, patterns, and relationships in the data before building the model.

Grid Plot of Binary Data

The R Code
R
binary_cols <- sapply(heart, function(x) {
  all(x %in% c(0, 1)) && is.numeric(x)
})

heart_binary <- heart[, binary_cols]
heart_non_binary <- heart[, !binary_cols]

heart_binary_long <- heart_binary %>%
  pivot_longer(
    cols = everything(),
    names_to = 'Variable',
    values_to = "Value"
  )

heart_non_binary_long <- heart_non_binary %>%
  pivot_longer(
    cols = everything(),
    names_to = 'Variable',
    values_to = "Value"
  )

binary_col_names <- heart %>%
  select(where(~ all(. %in% c(0,1)))) %>%
  colnames()

plot_list <- lapply(binary_col_names, function(col) {
  ggplot(heart, aes(x =.data[[col]])) +
    geom_bar(fill = 'lightblue', color = '#1F78B4') +
    labs(title = paste('Distribution of', col), x = col, y = 'Count') +
    scale_x_continuous(breaks = c(0,1)) +
    scale_y_continuous(name = "Values (thousands)",
                       labels = label_number(scale = 1e-3))
})

plot_grid(plotlist = plot_list, ncol = 4) +
  theme(plot.background = element_rect(fill = 'black'))
The Plot

Binary Data Grid Plot

There are a number of features where either zero or one are predominant, indicating they may be of little predictive value. We need to keep an eye on CholCheck, Stroke, Veggies, and the other imbalanced features.


Non-Binary (Continuous) Data Box Plot

The R Code
R
ggplot(heart_non_binary_long,
       aes(x=Variable, y=Value)) +
  geom_boxplot(fill = 'lightblue', color = '#1F78B4') +
  labs(title = 'Box Plot',
        subtitle = 'Non Binary Value Columns')
The Plot

Non-Binary Data Box Plot

There are issues that need attention here: Big outliers in BMI and abnormal distributions in MentHlth and PhysHlth, something mysterious about the values in the Diabetes column, which should be either 0 or 1:

> table(heart$Diabetes)
     0      1      2 
213703   4631  35346 

After some research, we discover the exact encoding from the original dataset:

  1. No diabetes or only during pregnancy
  2. Pre- or borderline diabetes
  3. Yes diabetes

We will convert this column into a factor (a grouping for categorical variables) later. On to the remaining non-binary data.


Inspection of Continuous Variables

Plan for Preprocessing

BMI
GenHlth

BMINeed to trim away values that are outside of the US distribution of values (> 50).

GenHlthThis needs to be a factor, not a continuous value.

PhysHlth
MentHlth

PhysHlth"Number of bad health days a month". Given this distribution, we'll split down the middle and convert to a binary value.

MentHlth"Number of bad mental health days a month". Treat the same as PhysHlth.

Age
Education

AgeConvert to factor, thusly: 1 = 18-24, 2 = 25-29, 3 = 30-34; by 5's.

EducationTo factor: 1-6.

Income
BMI-after

IncomeTo Factor: 1 = Less than $10,000, 2 = Less than $15,000, 3 = Less than $20,000, 4 = Less than $25,000, 5 = Less than $35,000, 6 = Less than $50,000, 7 = Less than $75,000, 8 = $75,000 or more.

BMI(After Trim)


Execute Preprocessing Plans

R
## CAP BMI:
heart$BMI <- ifelse(heart$BMI > BMI_limit, BMI_limit, heart$BMI)

## TURN GenHlth AROUND (1 POOR -> 5 EXC.):
heart$GenHlth <- (5 + 1) - heart$GenHlth

## TO BINARY -> SPLIT AT MID-MONTH
# PhysHlth and MentHlth
heart$PhysHlth <- ifelse(heart$PhysHlth >= 14, 1, 0)
heart$MentHlth <- ifelse(heart$MentHlth >= 14, 1, 0)

## CONVERT TO FACTOR:
# Diabetes
# GenHlth
# Age
# Education
# Income
cols_2_factor <- c('Diabetes', 'GenHlth', 'Age',
                   'Education', 'Income')
heart[cols_2_factor] <- lapply(heart[cols_2_factor], as.factor)

Final Feature Engineering

First, we fit a logistic regression model with the full dataset and remove any clearly unnecessary features based on the results.

Fit Initial Logistic Regression Model

R
glm_fulldata <- glm(HeartDiseaseorAttack ~ ., data = heart,
                    family = 'binomial')
summary(glm_fulldata)
CONSOLE
Coefficients:
                    Estimate Std. Error z value Pr(>|z|)    
(Intercept)       -5.2701018  0.3094751 -17.029  < 2e-16 ***
HighBP             0.5183896  0.0178200  29.090  < 2e-16 ***
HighChol           0.6092463  0.0165162  36.888  < 2e-16 ***
CholCheck          0.5278795  0.0662617   7.967 1.63e-15 ***
BMI                0.0007163  0.0013361   0.536 0.591866    
Smoker             0.3531223  0.0158474  22.283  < 2e-16 ***
Stroke             0.9784955  0.0244193  40.071  < 2e-16 ***
Diabetes1          0.0406503  0.0468097   0.868 0.385166    
Diabetes2          0.2881299  0.0180498  15.963  < 2e-16 ***
PhysActivity       0.0404904  0.0171952   2.355 0.018535 *  
Fruits             0.0102149  0.0163542   0.625 0.532229    
Veggies            0.0432974  0.0189320   2.287 0.022196 *  
HvyAlcoholConsump -0.2922830  0.0392995  -7.437 1.03e-13 ***
AnyHealthcare     -0.0125718  0.0413392  -0.304 0.761042    
NoDocbcCost        0.2555854  0.0268816   9.508  < 2e-16 ***
GenHlth2          -0.4156209  0.0278989 -14.897  < 2e-16 ***
GenHlth3          -0.9199520  0.0305948 -30.069  < 2e-16 ***
GenHlth4          -1.4651259  0.0342406 -42.789  < 2e-16 ***
GenHlth5          -1.8845704  0.0450285 -41.853  < 2e-16 ***
MentHlth           0.0722619  0.0243803   2.964 0.003037 ** 
PhysHlth           0.0263175  0.0222865   1.181 0.237653    
DiffWalk           0.2971911  0.0193226  15.380  < 2e-16 ***
Sex                0.7664149  0.0160640  47.710  < 2e-16 ***
Age2               0.2519897  0.2317730   1.087 0.276937    
Age3               0.5974870  0.2075401   2.879 0.003991 ** 
Age4               0.6560660  0.2008413   3.267 0.001089 ** 
Age5               0.9738349  0.1949251   4.996 5.85e-07 ***
Age6               1.3151899  0.1911185   6.882 5.92e-12 ***
Age7               1.5781639  0.1891938   8.342  < 2e-16 ***
Age8               1.7852644  0.1885041   9.471  < 2e-16 ***
Age9               2.0845657  0.1881145  11.081  < 2e-16 ***
Age10              2.3591391  0.1880531  12.545  < 2e-16 ***
Age11              2.6270995  0.1882092  13.958  < 2e-16 ***
Age12              2.7908133  0.1885247  14.803  < 2e-16 ***
Age13              3.0749285  0.1883228  16.328  < 2e-16 ***
Education2        -0.1309905  0.2356959  -0.556 0.578375    
Education3        -0.0181619  0.2334769  -0.078 0.937996    
Education4        -0.0244023  0.2319826  -0.105 0.916225    
Education5         0.0604128  0.2320772   0.260 0.794622    
Education6        -0.0298843  0.2322287  -0.129 0.897607    
Income2           -0.0165386  0.0409344  -0.404 0.686194    
Income3           -0.0541897  0.0398174  -1.361 0.173527    
Income4           -0.0925133  0.0392515  -2.357 0.018426 *  
Income5           -0.1502599  0.0389082  -3.862 0.000113 ***
Income6           -0.1827138  0.0384826  -4.748 2.05e-06 ***
Income7           -0.2090350  0.0392091  -5.331 9.75e-08 ***
Income8           -0.2904193  0.0389427  -7.458 8.81e-14 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Select Columns to Drop

BMI
One thing that jumps out is BMI. Why, against expectations, is it not significant? After investigation, it turns out that in the presence of features that are connected to BMI (HighBP, HighChol, Diabetes), BMI itself is not predictive - drop it. R code: cols_to_drop <- c('BMI)
GenHlth vs. PhysHlth
R code: cor(as.numeric(heart$GenHlth), heart$PhysHlth) = -0.48. This is a big enough correlation and the latter is not significant, so let’s drop it. PhysActivity, Fruits, and Veggies aren’t doing anything that isn’t covered by GenHlth, so let’s drop them too for the sake of explainability. R code: cols_to_drop <- append(cols_to_drop, c('PhysHlth', 'PhysActivity', 'Fruits', 'Veggies'))
HvyAlcoholConsump
HvyAlcoholConsump is significant and as a negative coefficient, indicates a reduction in risk. Why would drinking too much decrease the likelihood of heart disease? It turns out there is a known “Sick Quitter” effect, where people who get sick stop drinking. This is a bogus feature. R code: cols_to_drop <- append(cols_to_drop, 'HvyAlcoholConsump')
AnyHealthcare and Education
With very low statistical significance and near zero coefficent, both AnyHealthcare and Education are useless in the model. R code: cols_to_drop <- append(cols_to_drop, c('AnyHealthcare', 'Education'))

Drop selected columns and check the fit again

CONSOLE
> heart <- heart[ , !names(heart) %in% cols_to_drop]

> glm_fulldata_trimmed <- glm(HeartDiseaseorAttack ~ ., data = heart,
+                     family = 'binomial')

> summary(glm_fulldata_trimmed)

Call:
glm(formula = HeartDiseaseorAttack ~ ., family = "binomial", 
    data = heart)

Coefficients:
             Estimate Std. Error z value Pr(>|z|)    
(Intercept) -5.210400   0.201399 -25.871  < 2e-16 ***
HighBP       0.515246   0.017617  29.247  < 2e-16 ***
HighChol     0.608525   0.016504  36.872  < 2e-16 ***
CholCheck    0.536737   0.066046   8.127 4.41e-16 ***
Smoker       0.345434   0.015665  22.052  < 2e-16 ***
Stroke       0.979966   0.024374  40.205  < 2e-16 ***
Diabetes1    0.041882   0.046732   0.896 0.370135    
Diabetes2    0.296642   0.017655  16.802  < 2e-16 ***
NoDocbcCost  0.257304   0.026437   9.733  < 2e-16 ***
GenHlth2    -0.420088   0.026816 -15.665  < 2e-16 ***
GenHlth3    -0.924738   0.027785 -33.282  < 2e-16 ***
GenHlth4    -1.468296   0.031354 -46.830  < 2e-16 ***
GenHlth5    -1.890275   0.042717 -44.251  < 2e-16 ***
MentHlth     0.072438   0.023983   3.020 0.002525 ** 
DiffWalk     0.300149   0.018513  16.213  < 2e-16 ***
Sex          0.758143   0.015908  47.658  < 2e-16 ***
Age2         0.245896   0.231698   1.061 0.288563    
Age3         0.591380   0.207467   2.850 0.004365 ** 
Age4         0.650509   0.200764   3.240 0.001195 ** 
Age5         0.967658   0.194846   4.966 6.83e-07 ***
Age6         1.306288   0.191043   6.838 8.05e-12 ***
Age7         1.569761   0.189129   8.300  < 2e-16 ***
Age8         1.779491   0.188438   9.443  < 2e-16 ***
Age9         2.078418   0.188042  11.053  < 2e-16 ***
Age10        2.351037   0.187939  12.510  < 2e-16 ***
Age11        2.616675   0.188078  13.913  < 2e-16 ***
Age12        2.779729   0.188375  14.756  < 2e-16 ***
Age13        3.063582   0.188105  16.287  < 2e-16 ***
Income2     -0.005725   0.040822  -0.140 0.888472    
Income3     -0.041892   0.039613  -1.058 0.290276    
Income4     -0.075351   0.038866  -1.939 0.052532 .  
Income5     -0.128935   0.038320  -3.365 0.000766 ***
Income6     -0.160354   0.037627  -4.262 2.03e-05 ***
Income7     -0.187229   0.038057  -4.920 8.67e-07 ***
Income8     -0.276194   0.037149  -7.435 1.05e-13 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Every feature that remains is significant - we’re finally ready for modeling.


Modeling

Since the class imbalance is so severe (roughly 9 No’s to 1 Yes), there are some concerns that need to be addressed:

  1. Training on the whole dataset will teach the model that always guessing “NO heart disease” will be correct 90% of the time.
  2. Either “over” or “under” sampling (or both) needs to be done for the training data. For comparison’s sake, we’ll make two training sets: a. Undersampled - trim away the majority class until balanced, and b. “ROSE” (Random Over-Sampling Examples) which generates artificial data to balance the classes.
  3. BUT, testing on the balanced sets will make the models look like they perform better than they are, especially in terms of precision (true positives/all positive predictions).

Therefore, the plan is this - two models, two resampling techniques:

  1. Split the imbalanced data 70/30, keeping the imbalance.
  2. Create an undersampled training set (train_under) and an oversampled one (train_rose).
  3. Fit a Logistic Regression model and a Random Forest on both train_under and train_rose.
  4. Evaluate on the imbalanced 30% test_set from 1.

Split

R
set.seed(99)
train_index <- createDataPartition(heart$HeartDiseaseorAttack, p=0.7, list=F)
train_data <- heart[train_index, ]
test_data <- heart[-train_index, ]

Compare Proportions

CONSOLE
> prop.table(table(train_data$HeartDiseaseorAttack))

         0          1 
0.90642317 0.09357683 

> prop.table(table(test_data$HeartDiseaseorAttack))

         0          1 
0.90439399 0.09560601

The test/train splits are of nearly equal proportion and are therefore acceptable.

Create Training Sets and Run Predictions

R
train_under <- ovun.sample(HeartDiseaseorAttack ~ ., data = train_data,
                           method = 'under')$data
train_under$HeartDiseaseorAttack <- as.factor(train_under$HeartDiseaseorAttack)

train_rose <- ROSE(HeartDiseaseorAttack ~ ., data = train_data,
                   seed = 99)$data
train_rose$HeartDiseaseorAttack <- as.factor(train_rose$HeartDiseaseorAttack)

# Fit and predict
glm_rose <- glm(HeartDiseaseorAttack ~ ., data = train_rose,
                     family = 'binomial')
glm_under <- glm(HeartDiseaseorAttack ~ ., data = train_under,
                     family = 'binomial')
rnd_forest_rose <- randomForest(HeartDiseaseorAttack ~ .,
                                 data = train_rose, ntree=500) # ⚠ little long! ⚠
rnd_forest_under <- randomForest(HeartDiseaseorAttack ~ .,
                                 data = train_under, ntree=500)

pred_glm_rose <- predict(glm_rose, test_data, type = 'response')
pred_glm_under <- predict(glm_under, test_data, type = 'response')
pred_rf_rose_yes <- predict(rnd_forest_rose, test_data, type = 'prob')[ ,2]
pred_rf_under_yes <- predict(rnd_forest_under, test_data, type = 'prob')[ ,2]

Results

For this dataset the Area Under the Precision Recall Curve (AUC-PR) is the best way to rate/compare classification model performance as the data is highly imbalanced. It is particularly useful in our case, where the positive class is rare and when the cost of false positives and false negatives is high.

AUC-PR is Precision (the proportion of true positives among all positive predictions) against Recall (correctly identified true positives) at various threshold settings (the decision boundary of no/yes)


AUC-PR Implementation and Plot

R
# AUC-PR
test_data_labels <- ifelse(test_data$HeartDiseaseorAttack == 1, 1, 0)
pr_glm_rose <- pr.curve(scores.class0 = pred_glm_rose, weights.class0 = test_data_labels, curve = T)
pr_glm_under <- pr.curve(scores.class0 = pred_glm_under, weights.class0 = test_data_labels, curve = T)
pr_rf_rose <- pr.curve(scores.class0 = pred_rf_rose_yes, weights.class0 = test_data_labels, curve = T)
pr_rf_under <- pr.curve(scores.class0 = pred_rf_under_yes, weights.class0 = test_data_labels, curve = T)

# Build Comparative Plot
par(bg='black', fg='white', col.main='white', col.lab='white', col.axis='white')
plot(pr_glm_rose, main='PR Curve Model Comparison', col='purple', lwd=4, lty=5, auc.main=F, legend=F)
lines(pr_rf_rose$curve[ ,1], pr_rf_rose$curve[ ,2], col='red', lwd=4, lty=4)
lines(pr_rf_under$curve[ ,1], pr_rf_under$curve[ ,2], col='orange', lwd=4, lty=6)
lines(pr_glm_under$curve[ ,1], pr_glm_under$curve[ ,2], col='green', lwd=4, lty=3)
legend('bottomleft',
       legend=c(paste('Logistic Regression (ROSE), AUC:', round(pr_glm_rose$auc.integral, 3)),
                paste('Logistic Regression (Und/samp), AUC:', round(pr_glm_under$auc.integral, 3)),
                paste('Random Forest (ROSE), AUC:', round(pr_rf_rose$auc.integral, 3)),
                paste('Random Forest (Und/samp), AUC:', round(pr_rf_under$auc.integral, 3))),
       col=c('purple', 'green', 'red', 'orange'), lwd=3)

AUC-PR Comparison Plot

PR Curve Comparison Plot


AUC-PR Comparison Table

Ranked Model Performance

RankModelSample MethodAUC.PRNotes
1Logistic RegressionUndersampled0.376Simplest and best
2Logistic RegressionROSE0.374Same performance as 1, more memory req’d
3Random ForestROSE0.358Slighly worse performance, computationally heavier
4Random ForestUndersampled0.312Poor, starved for data

Calculated Odds by Predictor

R
odds_ratios <- as.data.frame(exp(coef(glm_under)))
or_desc <- odds_ratios %>%
  select('Odds.Ratio' = 'exp(coef(glm_under))') %>%
  arrange(desc(Odds.Ratio))

Odds Ratios (Greater than 1 = increased odds)

PredictorOdds Ratio
Age: 80+23.6132546
Had a Stroke3.1873698
Sex: Male2.3467927
Cholesterol Check2.0617602
Has High Cholesterol1.8479634
Has High Blood Pressure1.7788505
Has Difficulty Walking1.4610933
Is a Smoker1.4252963
Has Diabetes1.3986615
No Doctor Because of Cost1.3310711
More Bad Mental Health Days Than Not1.0718933
Pre- or Borderline Diabetes1.0110897
Income: Less than $15,0001.0092877
GenHlth1 (Poor)0.6791978
For each predictor, only the category with the highest odds is retained in the table above.

Conclusion

Age is the strongest predictor of heart disease in our dataset, with a person of 80+ years having 23.61 times higher odds of having heart disease than an 18-24 year old. After age:

Stroke
Having had a stroke increases the odds by 3.19x
Sex
Males have 2.35x the odds
Cholesterol Check (usually a reactive check)
2.06x the odds
High Cholesterol
1.85x
High Blood Pressure
1.78x

Surprisingly, smoking and having diabetes do not on their own (at 1.43x and 1.4x respectively) indicate as much increased risk as one might expect.

Those who report excellent health have reduced odds of 85 percent, while having an income of $75,000 or more reduces the odds of having heart disease by 32 percent.

According to our model, the highest-risk individual would be a 80+ year old low-income male who has had a stroke, has high cholesterol, and reports poor general health.

Start searching

Enter keywords to search articles.