In this assignment, We assume the role of a Data Analyst tasked with analyzing historical healthcare data. The objective of this analysis is to explore a heart disease dataset using R programming, identify patterns, and derive meaningful insights that can support better understanding of cardiovascular risk factors.
library(tidyverse)
The tidyverse library is a comprehensive collection of R packages for data science that includes tools for data manipulation (dplyr), reshaping (tidyr), visualization (ggplot2), reading data (readr), and more. By loading tidyverse, all these packages become available at once, providing everything needed to clean, transform, analyze, and visualize data efficiently in a single workflow.
heart <- read.csv("heart.csv")
# Inspecting the Dataset
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
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 str() function shows the dataset’s structure, including 1025 observations and 14 variables. Most are numeric integers
names(heart)
## [1] "age" "sex" "cp" "trestbps" "chol" "fbs"
## [7] "restecg" "thalach" "exang" "oldpeak" "slope" "ca"
## [13] "thal" "target"
The names() function lists all 14 column names in the dataset, representing the clinical attributes recorded during patients’ cardiac assessments.
head(heart, 15)
## 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
## 7 58 1 0 114 318 0 2 140 0 4.4 0 3 1
## 8 55 1 0 160 289 0 0 145 1 0.8 1 1 3
## 9 46 1 0 120 249 0 0 144 0 0.8 2 0 3
## 10 54 1 0 122 286 0 0 116 1 3.2 1 2 2
## 11 71 0 0 112 149 0 1 125 0 1.6 1 0 2
## 12 43 0 0 132 341 1 0 136 1 3.0 1 0 3
## 13 34 0 1 118 210 0 1 192 0 0.7 2 0 2
## 14 51 1 0 140 298 0 1 122 1 4.2 1 3 3
## 15 52 1 0 128 204 1 1 156 1 1.0 1 0 0
## target
## 1 0
## 2 0
## 3 0
## 4 0
## 5 0
## 6 1
## 7 0
## 8 0
## 9 0
## 10 0
## 11 1
## 12 0
## 13 1
## 14 0
## 15 0
The head() function displays the first 15 rows of the heart dataset, allowing a quick preview of the data and any recent changes made to the columns.
# Creating Age Groups with a Custom Function
age_group <- function(age)
{
if (age < 40) { return("Young") }
else if (age < 60) { return("Middle-aged") }
else { return("Senior") }
}
# Apply function
heart$Age_Group <- sapply(heart$age, age_group)
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 Age_Group
## 1 0 Middle-aged
## 2 0 Middle-aged
## 3 0 Senior
## 4 0 Senior
## 5 0 Senior
## 6 1 Middle-aged
A custom age_group() function is defined to categorize patients into age groups: “Young” (<40), “Middle-aged” (40–59), and “Senior” (60+). The function is then applied to the age column using sapply(), creating a new Age_Group column. The head() function displays the first few rows of the updated dataset to verify the new categorization.
# Filtering High-Risk Male Patients
high_risk_males <- heart %>% filter(sex == 1, chol > 240, age > 55)
cat("Number of high-risk male patients:", nrow(high_risk_males), "\n")
## Number of high-risk male patients: 164
head(high_risk_males, 10)
## age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal
## 1 58 1 0 114 318 0 2 140 0 4.4 0 3 1
## 2 56 1 2 130 256 1 0 142 1 0.6 1 1 1
## 3 70 1 2 160 269 0 1 112 1 2.9 1 1 3
## 4 59 1 0 138 271 0 0 182 0 0.0 2 0 2
## 5 64 1 0 128 263 0 1 105 1 0.2 1 1 3
## 6 67 1 0 100 299 0 0 125 1 0.9 1 2 2
## 7 59 1 3 170 288 0 0 159 0 0.2 1 0 3
## 8 59 1 0 170 326 0 0 140 1 3.4 0 0 3
## 9 56 1 0 125 249 1 0 144 1 1.2 1 1 2
## 10 65 1 0 110 248 0 0 158 0 0.6 2 2 1
## target Age_Group
## 1 0 Middle-aged
## 2 0 Middle-aged
## 3 0 Senior
## 4 1 Middle-aged
## 5 1 Senior
## 6 0 Senior
## 7 0 Middle-aged
## 8 0 Middle-aged
## 9 0 Middle-aged
## 10 0 Senior
This code identifies high-risk male patients by filtering for males (sex == 1) who have cholesterol above 240 and are older than 55. The nrow() function displays the number of patients meeting these criteria, and head() shows the first 10 rows of this subset for inspection.
# Select variables
vars <- heart %>%
select(age, thalach, oldpeak, exang, target)
# Reshape from wide to long
heart_long <- vars %>%
pivot_longer(
cols = c(thalach, oldpeak, exang),
names_to = "variable",
values_to = "value"
)
# Preview first 15 rows
head(heart_long, 15)
## # A tibble: 15 × 4
## age target variable value
## <int> <int> <chr> <dbl>
## 1 52 0 thalach 168
## 2 52 0 oldpeak 1
## 3 52 0 exang 0
## 4 53 0 thalach 155
## 5 53 0 oldpeak 3.1
## 6 53 0 exang 1
## 7 70 0 thalach 125
## 8 70 0 oldpeak 2.6
## 9 70 0 exang 1
## 10 61 0 thalach 161
## 11 61 0 oldpeak 0
## 12 61 0 exang 0
## 13 62 0 thalach 106
## 14 62 0 oldpeak 1.9
## 15 62 0 exang 0
# Handling Missing Values
cat("The missing valuse:\n")
## The missing valuse:
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 Age_Group
## 0 0 0 0 0 0 0
heart_clean <- na.omit(heart)
cat("Row-Numbers before:" , nrow(heart),"| Row-Numbers after:", nrow(heart_clean))
## Row-Numbers before: 1025 | Row-Numbers after: 1025
The colSums(is.na()) function counts missing values in each column. The na.omit() function removes any rows containing missing data. In this dataset there are no missing values, so the row count remains unchanged. This is still an important validation step in any data analysis workflow.
# Identifying Duplicates
cat("Number of duplicated rows:", sum(duplicated(heart_clean)),"\n")
## Number of duplicated rows: 723
# Removing Duplicates
heart_clean <- heart_clean[!duplicated(heart_clean), ]
cat("Number after removing duplicates:", nrow(heart_clean))
## Number after removing duplicates: 302
The duplicated() function detects rows that are exact copies of previous entries. The Kaggle version of the UCI Heart Disease dataset contains duplicate records, and removing them ensures that each patient is counted only once, preventing skewed statistics and biased predictive models.
# Sorting the Dataset
heart_sorted <- heart_clean %>%
arrange(desc(chol), desc(age), desc(trestbps))
head(heart_sorted, 10)
## age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal
## 1 67 0 2 115 564 0 0 160 0 1.6 1 0 3
## 2 65 0 2 140 417 1 0 157 0 0.8 2 1 2
## 3 56 0 0 134 409 0 0 150 1 1.9 1 2 3
## 4 63 0 0 150 407 0 0 154 0 4.0 1 3 3
## 5 62 0 0 140 394 0 0 157 0 1.2 1 0 2
## 6 65 0 2 160 360 0 0 151 0 0.8 2 0 2
## 7 57 0 0 120 354 0 1 163 1 0.6 2 0 2
## 8 55 1 0 132 353 0 1 132 1 1.2 1 1 3
## 9 55 0 1 132 342 0 1 166 0 1.2 2 0 2
## 10 43 0 0 132 341 1 0 136 1 3.0 1 0 3
## target Age_Group
## 1 1 Senior
## 2 1 Senior
## 3 0 Middle-aged
## 4 0 Senior
## 5 1 Senior
## 6 1 Senior
## 7 1 Middle-aged
## 8 0 Middle-aged
## 9 1 Middle-aged
## 10 0 Middle-aged
The dataset is sorted by risk-related variables such as cholesterol, age, and blood pressure to identify patients with higher cardiovascular risk. Sorting helps prioritize extreme cases and supports better interpretation of high-risk groups.
# Renaming Columns for Clarity
heart_renamed <- heart_clean %>% rename(
Age = age,
Sex = sex,
ChestPainType = cp,
RestingBP = trestbps,
Cholesterol = chol,
FastingBS = fbs,
RestECG = restecg,
MaxHR = thalach,
ExerciseAngina = exang,
Oldpeak = oldpeak,
ST_Slope = slope,
NumVessels = ca,
Thalassemia = thal,
Target = target )
names(heart_renamed)
## [1] "Age" "Sex" "ChestPainType" "RestingBP"
## [5] "Cholesterol" "FastingBS" "RestECG" "MaxHR"
## [9] "ExerciseAngina" "Oldpeak" "ST_Slope" "NumVessels"
## [13] "Thalassemia" "Target" "Age_Group"
The rename() function replaces the dataset’s abbreviated column names with descriptive clinical labels, improving readability for anyone reviewing the analysis who may not be familiar with the original UCI dataset codes.
# Create a composite risk score
heart_clean$risk_score <- 0.3 * heart_clean$age +
0.4 * heart_clean$chol +
0.3 * heart_clean$trestbps
head(heart_clean[, c("age", "chol", "trestbps", "risk_score")], 10)
## age chol trestbps risk_score
## 1 52 212 125 137.9
## 2 53 203 140 139.1
## 3 70 174 145 134.1
## 4 61 203 148 143.9
## 5 62 294 138 177.6
## 6 58 248 100 146.6
## 7 58 318 114 178.8
## 8 55 289 160 180.1
## 9 46 249 120 149.4
## 10 54 286 122 167.2
A composite risk score is calculated by combining age, cholesterol, and resting blood pressure into a single metric using weighted contributions (0.3, 0.4, 0.3 respectively). This score provides a simplified measure of overall cardiovascular risk, with higher scores indicating higher potential risk for heart disease. Displaying the first few rows helps verify that the calculation has been applied correctly.
# Splitting Training and Testing Sets
set.seed(1234)
train_index <- sample(1:nrow(heart_clean), size = 0.70 * nrow(heart_clean))
TrainingSet <- heart_clean[train_index, ]
TestingSet <- heart_clean[-train_index, ]
cat("Training set rows : ", nrow(TrainingSet), "\n")
## Training set rows : 211
cat("Testing set rows : ", nrow(TestingSet), "\n")
## Testing set rows : 91
This code splits the heart dataset into a 70% training set and a 30% testing set. sample() randomly selects row indices for the training set, while the remaining rows form the testing set. Setting set.seed(1234) ensures the split is reproducible. The cat() functions display the number of rows in each subset to confirm the split.
# Summary Statistics
summary(heart_clean)
## age sex cp trestbps
## Min. :29.00 Min. :0.0000 Min. :0.0000 Min. : 94.0
## 1st Qu.:48.00 1st Qu.:0.0000 1st Qu.:0.0000 1st Qu.:120.0
## Median :55.50 Median :1.0000 Median :1.0000 Median :130.0
## Mean :54.42 Mean :0.6821 Mean :0.9636 Mean :131.6
## 3rd Qu.:61.00 3rd Qu.:1.0000 3rd Qu.:2.0000 3rd Qu.:140.0
## Max. :77.00 Max. :1.0000 Max. :3.0000 Max. :200.0
## chol fbs restecg thalach
## Min. :126.0 Min. :0.000 Min. :0.0000 Min. : 71.0
## 1st Qu.:211.0 1st Qu.:0.000 1st Qu.:0.0000 1st Qu.:133.2
## Median :240.5 Median :0.000 Median :1.0000 Median :152.5
## Mean :246.5 Mean :0.149 Mean :0.5265 Mean :149.6
## 3rd Qu.:274.8 3rd Qu.:0.000 3rd Qu.:1.0000 3rd Qu.:166.0
## Max. :564.0 Max. :1.000 Max. :2.0000 Max. :202.0
## exang oldpeak slope ca
## Min. :0.0000 Min. :0.000 Min. :0.000 Min. :0.0000
## 1st Qu.:0.0000 1st Qu.:0.000 1st Qu.:1.000 1st Qu.:0.0000
## Median :0.0000 Median :0.800 Median :1.000 Median :0.0000
## Mean :0.3278 Mean :1.043 Mean :1.397 Mean :0.7185
## 3rd Qu.:1.0000 3rd Qu.:1.600 3rd Qu.:2.000 3rd Qu.:1.0000
## Max. :1.0000 Max. :6.200 Max. :2.000 Max. :4.0000
## thal target Age_Group risk_score
## Min. :0.000 Min. :0.000 Length:302 Min. :102.0
## 1st Qu.:2.000 1st Qu.:0.000 Class :character 1st Qu.:139.0
## Median :2.000 Median :1.000 Mode :character Median :152.1
## Mean :2.315 Mean :0.543 Mean :154.4
## 3rd Qu.:3.000 3rd Qu.:1.000 3rd Qu.:167.4
## Max. :3.000 Max. :1.000 Max. :280.2
The summary() function provides the five-number summary (minimum, 1st quartile, median, 3rd quartile, maximum) plus the mean for each numeric variable. Key observations: the median age is 54, median cholesterol is 223 mg/dL, and median maximum heart rate is 153 bpm. The target variable has a mean that indicates the proportion of patients diagnosed with heart disease in this cleaned dataset.
# Creating Get_mode Fun
get_mode <- function(x) {
ux <- unique(x)
ux[which.max(tabulate(match(x, ux)))]
}
# Applying Statistical Functions: Mean, Median, Mode, Range
cholmean = mean(heart_clean$chol)
cat("Chol-Mean : ", cholmean, "\n")
## Chol-Mean : 246.5
cholmedian = median(heart_clean$chol)
cat("Chol-Median : ", cholmedian, "\n")
## Chol-Median : 240.5
cholrange = range(heart_clean$chol)
cat("Chol-Range : ", cholrange, "\n")
## Chol-Range : 126 564
cholmode = get_mode(heart_clean$chol)
cat("Chol-Mode : ", cholmode, "\n")
## Chol-Mode : 204
A custom get_mode() function is defined because R does not have a built-in mode function for numeric data. Using this function, along with standard functions, we compute summary statistics for cholesterol in the dataset. The mean, median, mode, and range are calculated to describe the distribution. The mean is higher than the median, suggesting a right-skewed distribution, indicating that some patients have unusually high cholesterol values. The mode identifies the most frequently occurring cholesterol value in the dataset.
ggplot(heart_clean, aes(x = age, y = chol, color = as.factor(target))) +
geom_point(alpha = 0.6, size = 2) +
labs(title = "Scatter Plot: Age vs Cholesterol",
x = "Age",
y = "Cholesterol Level",
color = "Heart Disease") +
scale_color_manual(values = c("0" = "#0000FF", "1" = "#00FF00"),
labels = c("No Disease", "Disease")) +
theme_minimal()
This scatter plot shows the relationship between Age and Cholesterol levels, with points colored by heart disease status (No Disease vs Disease). The x-axis represents age, while the y-axis shows cholesterol levels. This visualization helps identify patterns or trends, such as whether higher age or cholesterol is associated with heart disease, and highlights differences between the two groups.
ggplot(heart_clean, aes(x = as.factor(sex), fill = as.factor(target))) +
geom_bar(position = "dodge") +
labs(title = "Heart Disease by Sex",
x = "Sex",
y = "Count",
fill = "Heart Disease") +
scale_fill_manual(values = c("0" = "#1ABC9C", "1" = "#FF00FF"),
labels = c("No Disease", "Disease")) +
scale_x_discrete(labels = c("0" = "Female", "1" = "Male")) +
theme_minimal()
This bar plot visualizes the distribution of heart disease across sexes. The x-axis represents Sex (Female vs Male), and the y-axis shows the count of patients. Bars are filled by heart disease status (No Disease vs Disease), with contrasting colors for clarity. The plot highlights differences in heart disease prevalence between males and females, making it easier to compare groups visually.
cor_value <- cor(heart_clean$chol, heart_clean$trestbps, method = "pearson")
cat("Pearson Correlation (Cholesterol vs Resting BP):", cor_value, "\n")
## Pearson Correlation (Cholesterol vs Resting BP): 0.1252563
cor.test(heart_clean$chol, heart_clean$trestbps, method = "pearson")
##
## Pearson's product-moment correlation
##
## data: heart_clean$chol and heart_clean$trestbps
## t = 2.1867, df = 300, p-value = 0.02953
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.01256927 0.23480160
## sample estimates:
## cor
## 0.1252563
The Pearson correlation measure the relationship between cholesterol and resting blood pressure is 0.125, indicating a weak positive relationship. This means that as cholesterol increases, resting blood pressure tends to increase slightly, but the relationship is not strong.
The cor.test() results show a p-value of 0.029, which is less than 0.05, indicating that the relationship is statistically significant and unlikely to be due to random chance. The 95% confidence interval (0.013 to 0.235) further supports a positive but weak association between the two variables.