Untitled

————————————————————————

title: “Predicting Maximum Heart Rate” subtitle: “Cross-Sectional Analysis of Heart Health Data” author: “Your Group Names” format: html ————

Introduction

Heart health can be influenced by many different factors, including age, blood pressure, cholesterol, and a person’s response to exercise. Understanding the relationships between these characteristics can help us better understand cardiovascular health.

For this project, we analyze a cross-sectional heart health dataset. Each observation represents a patient, and the dataset contains both quantitative and qualitative variables related to cardiovascular health.

Our main research question is:

What factors are associated with a patient’s maximum heart rate during exercise?

Our dependent variable, or Y variable, is thalach, which represents the maximum heart rate achieved by a patient.

We will investigate whether characteristics such as age, heart disease status, sex, chest pain type, exercise-induced angina, and ST depression can help explain differences in maximum heart rate.

Setup

First, we load the packages that will be used throughout our analysis.

# Load packages
library(tidyverse)
library(knitr)

The tidyverse package is used for data cleaning and visualization. The knitr package is used to create cleaner tables in our final report.

Importing the Data

We begin by importing the original heart.csv file.

# Import the heart dataset
heart <- read.csv("heart.csv")

We can view the first six observations to make sure the dataset imported correctly.

# Display the first six rows
head(heart)
  age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal
1  52   1  0      125  212   0       1     168     0     1.0     2  2    3
2  53   1  0      140  203   1       0     155     1     3.1     0  0    3
3  70   1  0      145  174   0       1     125     1     2.6     0  0    3
4  61   1  0      148  203   0       1     161     0     0.0     2  1    3
5  62   0  0      138  294   1       1     106     0     1.9     1  3    2
6  58   0  0      100  248   0       0     122     0     1.0     1  0    2
  target
1      0
2      0
3      0
4      0
5      0
6      1

Next, we check the dimensions of the dataset.

# Display the number of rows and columns
dim(heart)
[1] 1025   14

The first number represents the number of observations, while the second number represents the number of variables.

Understanding the Data

We can examine the structure of the original dataset using str().

# Examine the structure of the dataset
str(heart)
'data.frame':   1025 obs. of  14 variables:
 $ age     : int  52 53 70 61 62 58 58 55 46 54 ...
 $ sex     : int  1 1 1 1 0 0 1 1 1 1 ...
 $ cp      : int  0 0 0 0 0 0 0 0 0 0 ...
 $ trestbps: int  125 140 145 148 138 100 114 160 120 122 ...
 $ chol    : int  212 203 174 203 294 248 318 289 249 286 ...
 $ fbs     : int  0 1 0 0 1 0 0 0 0 0 ...
 $ restecg : int  1 0 1 1 1 0 2 0 0 0 ...
 $ thalach : int  168 155 125 161 106 122 140 145 144 116 ...
 $ exang   : int  0 1 1 0 0 0 0 1 0 1 ...
 $ oldpeak : num  1 3.1 2.6 0 1.9 1 4.4 0.8 0.8 3.2 ...
 $ slope   : int  2 0 0 2 1 1 0 1 2 1 ...
 $ ca      : int  2 0 0 1 3 0 3 1 0 2 ...
 $ thal    : int  3 3 3 3 2 2 1 3 3 2 ...
 $ target  : int  0 0 0 0 0 1 0 0 0 0 ...

The dataset contains the following variables:

  • age: Age of the patient in years
  • sex: Sex
  • cp: Chest pain type
  • trestbps: Resting blood pressure
  • chol: Serum cholesterol
  • fbs: Fasting blood sugar category
  • restecg: Resting ECG result
  • thalach: Maximum heart rate achieved
  • exang: Exercise-induced angina
  • oldpeak: ST depression induced by exercise
  • slope: Slope of the peak exercise ST segment
  • ca: Number of major vessels
  • thal: Thalassemia category
  • target: Heart disease status

Quantitative Variables

Our dataset contains several quantitative variables, including:

  1. age — age in years
  2. trestbps — resting blood pressure
  3. chol — cholesterol
  4. thalach — maximum heart rate
  5. oldpeak — ST depression

This gives us at least four quantitative variables as required by the project.

Qualitative Variables

Several variables are represented by numbers in the raw dataset but actually represent categories.

These include:

  1. sex
  2. cp
  3. fbs
  4. restecg
  5. exang
  6. slope
  7. thal
  8. target

This gives us at least four qualitative variables as required by the project.

Data Cleaning

Before analyzing the data, we check for missing observations.

# Count the number of missing values in each variable
colSums(is.na(heart))
     age      sex       cp trestbps     chol      fbs  restecg  thalach 
       0        0        0        0        0        0        0        0 
   exang  oldpeak    slope       ca     thal   target 
       0        0        0        0        0        0 

If every variable returns zero, there are no missing values.

Checking for Duplicates

We also check whether there are any exact duplicate rows.

# Count duplicate observations
sum(duplicated(heart))
[1] 723

Duplicate rows can cause certain observations to have more influence on our results.

We therefore remove exact duplicates.

# Remove duplicate rows
heart <- heart |>
  distinct()

Now we check the dimensions again.

# Check dataset size after removing duplicates
dim(heart)
[1] 302  14

Converting Qualitative Variables

Some variables are stored as numbers even though they actually represent categories.

We convert these variables into factors so R treats them as qualitative variables.

# Convert categorical variables into factors
heart <- heart |>
  mutate(
    sex = factor(sex),
    cp = factor(cp),
    fbs = factor(fbs),
    restecg = factor(restecg),
    exang = factor(exang),
    slope = factor(slope),
    thal = factor(thal),
    target = factor(target)
  )

We can check the structure again to make sure the conversion worked.

# Check variable types after cleaning
str(heart)
'data.frame':   302 obs. of  14 variables:
 $ age     : int  52 53 70 61 62 58 58 55 46 54 ...
 $ sex     : Factor w/ 2 levels "0","1": 2 2 2 2 1 1 2 2 2 2 ...
 $ cp      : Factor w/ 4 levels "0","1","2","3": 1 1 1 1 1 1 1 1 1 1 ...
 $ trestbps: int  125 140 145 148 138 100 114 160 120 122 ...
 $ chol    : int  212 203 174 203 294 248 318 289 249 286 ...
 $ fbs     : Factor w/ 2 levels "0","1": 1 2 1 1 2 1 1 1 1 1 ...
 $ restecg : Factor w/ 3 levels "0","1","2": 2 1 2 2 2 1 3 1 1 1 ...
 $ thalach : int  168 155 125 161 106 122 140 145 144 116 ...
 $ exang   : Factor w/ 2 levels "0","1": 1 2 2 1 1 1 1 2 1 2 ...
 $ oldpeak : num  1 3.1 2.6 0 1.9 1 4.4 0.8 0.8 3.2 ...
 $ slope   : Factor w/ 3 levels "0","1","2": 3 1 1 3 2 2 1 2 3 2 ...
 $ ca      : int  2 0 0 1 3 0 3 1 0 2 ...
 $ thal    : Factor w/ 4 levels "0","1","2","3": 4 4 4 4 3 3 2 4 4 3 ...
 $ target  : Factor w/ 2 levels "0","1": 1 1 1 1 1 2 1 1 1 1 ...

Summary Statistics

Before creating our regression model, we examine summary statistics for the quantitative variables.

# Create summary statistics table
summary_stats <- data.frame(
  Variable = c(
    "Age",
    "Resting Blood Pressure",
    "Cholesterol",
    "Maximum Heart Rate",
    "Oldpeak"
  ),

  Mean = c(
    mean(heart$age),
    mean(heart$trestbps),
    mean(heart$chol),
    mean(heart$thalach),
    mean(heart$oldpeak)
  ),

  Median = c(
    median(heart$age),
    median(heart$trestbps),
    median(heart$chol),
    median(heart$thalach),
    median(heart$oldpeak)
  ),

  SD = c(
    sd(heart$age),
    sd(heart$trestbps),
    sd(heart$chol),
    sd(heart$thalach),
    sd(heart$oldpeak)
  ),

  Minimum = c(
    min(heart$age),
    min(heart$trestbps),
    min(heart$chol),
    min(heart$thalach),
    min(heart$oldpeak)
  ),

  Maximum = c(
    max(heart$age),
    max(heart$trestbps),
    max(heart$chol),
    max(heart$thalach),
    max(heart$oldpeak)
  )
)

# Display the table
kable(
  summary_stats,
  digits = 2,
  caption = "Summary Statistics for Quantitative Variables"
)
Summary Statistics for Quantitative Variables
Variable Mean Median SD Minimum Maximum
Age 54.42 55.5 9.05 29 77.0
Resting Blood Pressure 131.60 130.0 17.56 94 200.0
Cholesterol 246.50 240.5 51.75 126 564.0
Maximum Heart Rate 149.57 152.5 22.90 71 202.0
Oldpeak 1.04 0.8 1.16 0 6.2

We display the table using kable().

# Display summary statistics
kable(
  summary_stats,
  digits = 2,
  caption = "Summary Statistics for Quantitative Variables"
)
Summary Statistics for Quantitative Variables
Variable Mean Median SD Minimum Maximum
Age 54.42 55.5 9.05 29 77.0
Resting Blood Pressure 131.60 130.0 17.56 94 200.0
Cholesterol 246.50 240.5 51.75 126 564.0
Maximum Heart Rate 149.57 152.5 22.90 71 202.0
Oldpeak 1.04 0.8 1.16 0 6.2

We can also examine a basic summary of every variable.

# Summary of all variables
summary(heart)
      age        sex     cp         trestbps          chol       fbs    
 Min.   :29.00   0: 96   0:143   Min.   : 94.0   Min.   :126.0   0:257  
 1st Qu.:48.00   1:206   1: 50   1st Qu.:120.0   1st Qu.:211.0   1: 45  
 Median :55.50           2: 86   Median :130.0   Median :240.5          
 Mean   :54.42           3: 23   Mean   :131.6   Mean   :246.5          
 3rd Qu.:61.00                   3rd Qu.:140.0   3rd Qu.:274.8          
 Max.   :77.00                   Max.   :200.0   Max.   :564.0          
 restecg    thalach      exang      oldpeak      slope         ca        
 0:147   Min.   : 71.0   0:203   Min.   :0.000   0: 21   Min.   :0.0000  
 1:151   1st Qu.:133.2   1: 99   1st Qu.:0.000   1:140   1st Qu.:0.0000  
 2:  4   Median :152.5           Median :0.800   2:141   Median :0.0000  
         Mean   :149.6           Mean   :1.043           Mean   :0.7185  
         3rd Qu.:166.0           3rd Qu.:1.600           3rd Qu.:1.0000  
         Max.   :202.0           Max.   :6.200           Max.   :4.0000  
 thal    target 
 0:  2   0:138  
 1: 18   1:164  
 2:165          
 3:117          
                
                

Visualizing Quantitative Variables

We now visualize each of our main quantitative variables.

Age

ggplot(heart, aes(x = age)) +
  geom_histogram(
    binwidth = 5,
    color = "black"
  ) +
  labs(
    title = "Distribution of Patient Age",
    x = "Age",
    y = "Number of Patients"
  ) +
  theme_minimal()

This graph shows the age distribution of patients in our sample.

Resting Blood Pressure

ggplot(heart, aes(x = trestbps)) +
  geom_histogram(
    binwidth = 10,
    color = "black"
  ) +
  labs(
    title = "Distribution of Resting Blood Pressure",
    x = "Resting Blood Pressure",
    y = "Number of Patients"
  ) +
  theme_minimal()

Cholesterol

ggplot(heart, aes(x = chol)) +
  geom_histogram(
    binwidth = 20,
    color = "black"
  ) +
  labs(
    title = "Distribution of Cholesterol",
    x = "Cholesterol",
    y = "Number of Patients"
  ) +
  theme_minimal()

Maximum Heart Rate

ggplot(heart, aes(x = thalach)) +
  geom_histogram(
    binwidth = 10,
    color = "black"
  ) +
  labs(
    title = "Distribution of Maximum Heart Rate",
    x = "Maximum Heart Rate",
    y = "Number of Patients"
  ) +
  theme_minimal()

Maximum heart rate is particularly important because it is the Y variable that we will attempt to predict with our regression model.

Oldpeak

ggplot(heart, aes(x = oldpeak)) +
  geom_histogram(
    binwidth = 0.5,
    color = "black"
  ) +
  labs(
    title = "Distribution of Oldpeak",
    x = "Oldpeak",
    y = "Number of Patients"
  ) +
  theme_minimal()

Visualizing Qualitative Variables

For qualitative variables, bar charts allow us to compare the number of observations in each category.

Sex

ggplot(heart, aes(x = sex)) +
  geom_bar() +
  labs(
    title = "Patients by Sex",
    x = "Sex",
    y = "Number of Patients"
  ) +
  theme_minimal()

Chest Pain Type

ggplot(heart, aes(x = cp)) +
  geom_bar() +
  labs(
    title = "Distribution of Chest Pain Types",
    x = "Chest Pain Type",
    y = "Number of Patients"
  ) +
  theme_minimal()

Fasting Blood Sugar

ggplot(heart, aes(x = fbs)) +
  geom_bar() +
  labs(
    title = "Fasting Blood Sugar Categories",
    x = "Fasting Blood Sugar Category",
    y = "Number of Patients"
  ) +
  theme_minimal()

Resting ECG

ggplot(heart, aes(x = restecg)) +
  geom_bar() +
  labs(
    title = "Resting ECG Results",
    x = "Resting ECG Category",
    y = "Number of Patients"
  ) +
  theme_minimal()

Exercise-Induced Angina

ggplot(heart, aes(x = exang)) +
  geom_bar() +
  labs(
    title = "Exercise-Induced Angina",
    x = "Exercise-Induced Angina",
    y = "Number of Patients"
  ) +
  theme_minimal()

ST Slope

ggplot(heart, aes(x = slope)) +
  geom_bar() +
  labs(
    title = "ST Slope Categories",
    x = "ST Slope",
    y = "Number of Patients"
  ) +
  theme_minimal()

Number of Major Vessels

Although ca is stored numerically, it only takes a small number of discrete values. A bar chart makes the distribution easy to interpret.

ggplot(heart, aes(x = factor(ca))) +
  geom_bar() +
  labs(
    title = "Number of Major Vessels",
    x = "Number of Major Vessels",
    y = "Number of Patients"
  ) +
  theme_minimal()

Thalassemia Category

ggplot(heart, aes(x = thal)) +
  geom_bar() +
  labs(
    title = "Thalassemia Categories",
    x = "Thalassemia Category",
    y = "Number of Patients"
  ) +
  theme_minimal()

Heart Disease Status

ggplot(heart, aes(x = target)) +
  geom_bar() +
  labs(
    title = "Heart Disease Status",
    x = "Heart Disease Status",
    y = "Number of Patients"
  ) +
  theme_minimal()

Exploring Maximum Heart Rate

Now that we have examined each variable individually, we look more closely at variables that may help explain our Y variable, maximum heart rate.

Age vs. Maximum Heart Rate

ggplot(
  heart,
  aes(
    x = age,
    y = thalach
  )
) +
  geom_point() +
  geom_smooth(
    method = "lm",
    se = FALSE
  ) +
  labs(
    title = "Age vs. Maximum Heart Rate",
    x = "Age",
    y = "Maximum Heart Rate"
  ) +
  theme_minimal()
`geom_smooth()` using formula = 'y ~ x'

The fitted line allows us to see the general direction of the relationship between age and maximum heart rate.

Heart Disease Status vs. Maximum Heart Rate

ggplot(
  heart,
  aes(
    x = target,
    y = thalach
  )
) +
  geom_boxplot() +
  labs(
    title = "Maximum Heart Rate by Heart Disease Status",
    x = "Heart Disease Status",
    y = "Maximum Heart Rate"
  ) +
  theme_minimal()

The boxplots allow us to compare the distribution of maximum heart rate between patients in the two heart disease categories.

Exercise-Induced Angina vs. Maximum Heart Rate

ggplot(
  heart,
  aes(
    x = exang,
    y = thalach
  )
) +
  geom_boxplot() +
  labs(
    title = "Maximum Heart Rate by Exercise-Induced Angina",
    x = "Exercise-Induced Angina",
    y = "Maximum Heart Rate"
  ) +
  theme_minimal()

Chest Pain Type vs. Maximum Heart Rate

ggplot(
  heart,
  aes(
    x = cp,
    y = thalach
  )
) +
  geom_boxplot() +
  labs(
    title = "Maximum Heart Rate by Chest Pain Type",
    x = "Chest Pain Type",
    y = "Maximum Heart Rate"
  ) +
  theme_minimal()

Multiple Linear Regression

We now use multiple linear regression to investigate which characteristics are associated with maximum heart rate.

Our dependent variable is:

Y = thalach (Maximum Heart Rate)

Our explanatory variables are:

  • age
  • target
  • sex
  • cp
  • exang
  • oldpeak

The model can be represented generally as:

Maximum Heart Rate = Age + Heart Disease Status + Sex + Chest Pain Type + Exercise-Induced Angina + Oldpeak

We estimate the model using the lm() function.

# Run the multiple linear regression
heart_model <- lm(
  thalach ~ age + target + sex + cp + exang + oldpeak,
  data = heart
)

We then use summary() to view the regression results.

# Display regression results
summary(heart_model)

Call:
lm(formula = thalach ~ age + target + sex + cp + exang + oldpeak, 
    data = heart)

Residuals:
    Min      1Q  Median      3Q     Max 
-65.241 -11.189   2.319  12.487  41.435 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) 188.4373     8.1034  23.254  < 2e-16 ***
age          -0.7532     0.1257  -5.992 6.08e-09 ***
target1       7.1626     2.8895   2.479   0.0137 *  
sex1          0.7259     2.4751   0.293   0.7695    
cp1           7.8258     3.5514   2.204   0.0283 *  
cp2           4.4549     2.9868   1.492   0.1369    
cp3           9.0015     4.4793   2.010   0.0454 *  
exang1       -8.9980     2.7072  -3.324   0.0010 ** 
oldpeak      -2.4568     1.0687  -2.299   0.0222 *  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 18.73 on 293 degrees of freedom
Multiple R-squared:  0.3492,    Adjusted R-squared:  0.3314 
F-statistic: 19.65 on 8 and 293 DF,  p-value: < 2.2e-16

Understanding the Regression Output

There are several important parts of the regression output.

Estimate

The estimate shows the predicted relationship between an explanatory variable and maximum heart rate while holding the other variables constant.

For example, the coefficient on age tells us how much maximum heart rate is predicted to change when age increases by one year, holding the other explanatory variables constant.

P-value

The p-value helps us determine whether there is statistical evidence that an explanatory variable is associated with maximum heart rate.

For this project, we use a significance level of:

0.05

A p-value below 0.05 will therefore be considered statistically significant.

R-Squared

R-squared tells us what proportion of the variation in maximum heart rate is explained by the explanatory variables included in our model.

A higher R-squared means that the model explains more of the variation in our Y variable.

Regression Diagnostics

Linear regression relies on several assumptions.

We can examine diagnostic plots to determine whether our model appears reasonable.

Residuals vs. Fitted Values

# Residuals vs fitted values
plot(
  heart_model,
  which = 1
)

Ideally, the residuals should appear randomly scattered around zero without a strong curved pattern.

Normal Q-Q Plot

# Normal Q-Q plot
plot(
  heart_model,
  which = 2
)

If the residuals are approximately normally distributed, most of the points should be reasonably close to the diagonal line.

Actual vs. Predicted Maximum Heart Rate

Our regression model can generate a predicted maximum heart rate for each patient.

First, we add the predictions to our dataset.

# Add predicted maximum heart rate
heart <- heart |>
  mutate(
    predicted_thalach = predict(heart_model)
  )

We can then compare the model’s predictions with the actual maximum heart rates.

ggplot(
  heart,
  aes(
    x = predicted_thalach,
    y = thalach
  )
) +
  geom_point() +
  geom_smooth(
    method = "lm",
    se = FALSE
  ) +
  labs(
    title = "Actual vs. Predicted Maximum Heart Rate",
    x = "Predicted Maximum Heart Rate",
    y = "Actual Maximum Heart Rate"
  ) +
  theme_minimal()
`geom_smooth()` using formula = 'y ~ x'

If our model predicts maximum heart rate well, we should see a positive relationship between the predicted and actual values.

Key Findings

Our analysis examined the relationship between maximum heart rate and several patient characteristics.

When presenting the regression results, we will focus on:

  1. Which explanatory variables have p-values below 0.05.
  2. Whether statistically significant relationships are positive or negative.
  3. The magnitude of important regression coefficients.
  4. The R-squared value.
  5. Whether the diagnostic plots suggest that our linear regression model is reasonable.

The regression model allows us to examine each variable’s relationship with maximum heart rate while controlling for the other variables included in the model.

Limitations

Our analysis has several limitations.

First, the dataset is cross-sectional and observational. Therefore, the regression results show associations between variables, but they do not necessarily establish cause and effect.

Second, maximum heart rate can be affected by factors that are not included in this dataset.

Third, our results depend on the patients included in this particular dataset, so they may not perfectly represent every population.

Finally, linear regression makes assumptions about the relationships between the variables. Our diagnostic plots help us evaluate whether these assumptions are reasonable.

Conclusion

The purpose of this project was to investigate the factors associated with maximum heart rate during exercise.

Our dependent variable was maximum heart rate (thalach), and we used age, heart disease status, sex, chest pain type, exercise-induced angina, and ST depression as explanatory variables.

Through data cleaning, summary statistics, visualization, and multiple linear regression, we can identify which characteristics have the strongest relationships with maximum heart rate.

The regression results provide a way to estimate these relationships while holding the other explanatory variables constant.

Overall, this project demonstrates how cross-sectional health data and multiple linear regression can be used to investigate relationships between patient characteristics and cardiovascular exercise response.