# Load packages
library(tidyverse)
library(knitr)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.
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 yearssex: Sexcp: Chest pain typetrestbps: Resting blood pressurechol: Serum cholesterolfbs: Fasting blood sugar categoryrestecg: Resting ECG resultthalach: Maximum heart rate achievedexang: Exercise-induced anginaoldpeak: ST depression induced by exerciseslope: Slope of the peak exercise ST segmentca: Number of major vesselsthal: Thalassemia categorytarget: Heart disease status
Quantitative Variables
Our dataset contains several quantitative variables, including:
age— age in yearstrestbps— resting blood pressurechol— cholesterolthalach— maximum heart rateoldpeak— 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:
sexcpfbsrestecgexangslopethaltarget
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"
)| 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"
)| 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:
agetargetsexcpexangoldpeak
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:
- Which explanatory variables have p-values below 0.05.
- Whether statistically significant relationships are positive or negative.
- The magnitude of important regression coefficients.
- The R-squared value.
- 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.