Instructor : Dr Pauline Yeoh Shan Qing
Group Number: 7
Group Members:
| Name | Matrix Number |
|---|---|
| KASTURI A/P RAJASAERAN | 24212929 |
| POONARESI A/P SUBRAMANIAM | S2023729 |
| PEAN LI FANG | 24222202 |
| SAIDAH MUHTAR LUBIS | 24221312 |
| NUR HAZIRAH BINTI KAMAL BAHRIN | 24214519 |
| NUR SYAHIRRA BINTI MOHD ALI | 23122846 |
Diabetes mellitus is a major public health concern due to long-term complications, rising healthcare costs, and reduced quality of life. Its increasing global prevalence is strongly associated with lifestyle factors such as physical inactivity, unhealthy diet, smoking, and overweight/obesity. Because many individuals remain unaware of their risk until symptoms develop, there is a need for data-driven methods that enable earlier identification and targeted prevention. This project uses a large, publicly available health survey dataset containing lifestyle behaviours and basic health indicators (e.g., BMI, smoking status, physical/mental health, hypertension, and cholesterol) to predict diabetes risk. By applying exploratory data analysis and interpretable predictive modelling, the study aims to uncover key risk factors, support scalable risk assessment, and provide actionable insights for healthcare stakeholders, including ministries of health, clinics, insurers, and public health organisations.
Which supervised classification model performs best for diabetes risk prediction?
Can HbA1c be predicted using demographic and lifestyle variables?
Which predictive models demonstrate the highest sensitivity and discriminative ability for effectively screening high-risk diabetes patients?
To develop, validate, and compare multiple predictive classification models for diabetes risk assessment using appropriate performance metrics.
To predict HbA1c using demographic and lifestyle variables.
To interpret model performance and feature importance to support early diabetes screening and preventive healthcare.
Title: Diabetes Health Indicators Dataset
Dimension: 101,000 rows, 31 columns
Description: Dataset includes patient profiles with features based on demographics, lifestyle habits, family history, and clinical measurements that are well-established indicators of diabetes risk.
| Columns Name | Description |
|---|---|
| patient_id | Unique patient identifier |
| age | Age of patient in years |
| gender | Patient gender |
| ethnicity | Ethnic background |
| education_level | Highest completed education |
| income_level | Income category |
| employment_status | Employment type |
| smoking_status | Smoking behavior |
| alcohol_consumption_per_week | Drinks consumed per week |
| physical_activity_minutes_per_week | Physical activity |
| diet_score | Diet quality (higher = healthier) |
| sleep_hours_per_day | Average daily sleep hours |
| screen_time_hours_per_day | Average daily screen time hours |
| family_history_diabetes | Family history of diabetes |
| hypertension_history | Hypertension history |
| cardiovascular_history | Cardiovascular history |
| bmi | Body Mass Index (kg/m²) |
| waist_to_hip_ratio | Waist-to-hip ratio |
| systolic_bp | Systolic blood pressure (mmHg) |
| diastolic_bp | Diastolic blood pressure (mmHg) |
| heart_rate | Resting heart rate (bpm) |
| cholesterol_total | Total cholesterol (mg/dL) |
| hdl_cholesterol | HDL cholesterol (mg/dL) |
| ldl_cholesterol | LDL cholesterol (mg/dL) |
| triglycerides | Triglycerides (mg/dL) |
| glucose_fasting | Fasting glucose (mg/dL) |
| glucose_postprandial | Post-meal glucose (mg/dL) |
| insulin_level | Blood insulin level (µU/mL) |
| hba1c | HbA1c (%) |
| diabetes_risk_score | Risk score (calculated, 0–100) |
| diabetes_stage | Stage of diabetes |
| diagnosed_diabetes | Target: Diabetes diagnosis |
library(stringr)
## Warning: package 'stringr' was built under R version 4.5.2
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.5.2
library(patchwork)
## Warning: package 'patchwork' was built under R version 4.5.2
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.5.2
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.2
## Warning: package 'tidyr' was built under R version 4.5.2
## Warning: package 'readr' was built under R version 4.5.2
## Warning: package 'purrr' was built under R version 4.5.2
## Warning: package 'forcats' was built under R version 4.5.2
## Warning: package 'lubridate' was built under R version 4.5.2
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats 1.0.1 ✔ readr 2.1.5
## ✔ lubridate 1.9.4 ✔ tibble 3.3.0
## ✔ purrr 1.2.0 ✔ tidyr 1.3.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(corrplot)
## Warning: package 'corrplot' was built under R version 4.5.2
## corrplot 0.95 loaded
library(gridExtra)
## Warning: package 'gridExtra' was built under R version 4.5.2
##
## Attaching package: 'gridExtra'
##
## The following object is masked from 'package:dplyr':
##
## combine
library(broom)
## Warning: package 'broom' was built under R version 4.5.2
library(caret)
## Warning: package 'caret' was built under R version 4.5.2
## Loading required package: lattice
##
## Attaching package: 'caret'
##
## The following object is masked from 'package:purrr':
##
## lift
library(rpart.plot)
## Warning: package 'rpart.plot' was built under R version 4.5.2
## Loading required package: rpart
library(kernlab)
## Warning: package 'kernlab' was built under R version 4.5.2
##
## Attaching package: 'kernlab'
##
## The following object is masked from 'package:purrr':
##
## cross
##
## The following object is masked from 'package:ggplot2':
##
## alpha
library(randomForest)
## Warning: package 'randomForest' was built under R version 4.5.2
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
##
## Attaching package: 'randomForest'
##
## The following object is masked from 'package:gridExtra':
##
## combine
##
## The following object is masked from 'package:dplyr':
##
## combine
##
## The following object is masked from 'package:ggplot2':
##
## margin
library(e1071)
## Warning: package 'e1071' was built under R version 4.5.2
##
## Attaching package: 'e1071'
##
## The following object is masked from 'package:ggplot2':
##
## element
df <- read.csv("diabetes_dataset.csv", stringsAsFactors = FALSE)
# Overview dataset information
glimpse(df)
## Rows: 101,000
## Columns: 31
## $ age <int> 58, 48, 60, 74, 46, 46, 75, 62, 42,…
## $ gender <chr> "Male", "Female", "Male", "Female",…
## $ ethnicity <chr> "Asian", "White", "Hispanic", "Blac…
## $ education_level <chr> "Highschool", "Highschool", "Highsc…
## $ income_level <chr> "Lower-Middle", "Middle", "Middle",…
## $ employment_status <chr> "Employed", "Employed", "Unemployed…
## $ smoking_status <chr> "Never", "Former", "Never", "Never"…
## $ alcohol_consumption_per_week <int> 0, 1, 1, 0, 1, 2, 0, 1, 1, 3, 1, 1,…
## $ physical_activity_minutes_per_week <int> 215, 143, 57, 49, 109, 124, 53, 75,…
## $ diet_score <dbl> 5.7, 6.7, 6.4, 3.4, 7.2, 9.0, 9.2, …
## $ sleep_hours_per_day <dbl> 7.9, 6.5, 10.0, 6.6, 7.4, 6.2, 7.8,…
## $ screen_time_hours_per_day <dbl> 7.9, 8.7, 8.1, 5.2, 5.0, 5.4, 8.0, …
## $ family_history_diabetes <int> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ hypertension_history <int> 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1,…
## $ cardiovascular_history <int> 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,…
## $ bmi <chr> "30.5", "23.1", "22.2", "26.8", "21…
## $ waist_to_hip_ratio <dbl> 0.89, 0.80, 0.81, 0.88, 0.78, 0.85,…
## $ systolic_bp <int> 134, 129, 115, 120, 92, 0, 129, 128…
## $ diastolic_bp <int> 78, 76, 73, 93, 67, 81, 77, 83, 0, …
## $ heart_rate <int> 68, 67, 74, 68, 67, 57, 81, 76, 72,…
## $ cholesterol_total <int> 239, 116, 213, 171, 210, 218, 238, …
## $ hdl_cholesterol <int> 41, 55, 66, 50, 52, 61, 46, 49, 33,…
## $ ldl_cholesterol <int> 160, 50, 99, 79, 125, 119, 161, 159…
## $ triglycerides <int> 145, 30, 36, 140, 160, 179, 155, 12…
## $ glucose_fasting <int> 136, 93, 118, 139, 137, 100, 101, 1…
## $ glucose_postprandial <int> 0, 150, 195, 253, 184, 133, 100, 18…
## $ insulin_level <dbl> 6.36, 2.00, 5.07, 5.28, 12.74, 8.77…
## $ hba1c <dbl> 8.18, 5.63, 7.51, 9.03, 7.20, 6.03,…
## $ diabetes_risk_score <dbl> 29.6, 23.0, 44.7, 38.2, 23.5, 23.5,…
## $ diabetes_stage <chr> "Type 2", "No Diabetes", "Type 2", …
## $ diagnosed_diabetes <int> 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1,…
summary(df)
## age gender ethnicity education_level
## Min. :-91.00 Length:101000 Length:101000 Length:101000
## 1st Qu.: 39.00 Class :character Class :character Class :character
## Median : 50.00 Mode :character Mode :character Mode :character
## Mean : 49.92
## 3rd Qu.: 61.00
## Max. : 90.00
## income_level employment_status smoking_status
## Length:101000 Length:101000 Length:101000
## Class :character Class :character Class :character
## Mode :character Mode :character Mode :character
##
##
##
## alcohol_consumption_per_week physical_activity_minutes_per_week
## Min. : 0.000 Min. : 0.0
## 1st Qu.: 1.000 1st Qu.: 57.0
## Median : 2.000 Median :100.0
## Mean : 2.004 Mean :118.9
## 3rd Qu.: 3.000 3rd Qu.:160.0
## Max. :10.000 Max. :833.0
## diet_score sleep_hours_per_day screen_time_hours_per_day
## Min. : 0.000 Min. : 3.000 Min. : 0.500
## 1st Qu.: 4.800 1st Qu.: 6.300 1st Qu.: 4.300
## Median : 6.000 Median : 7.000 Median : 6.000
## Mean : 5.995 Mean : 6.998 Mean : 5.998
## 3rd Qu.: 7.200 3rd Qu.: 7.700 3rd Qu.: 7.700
## Max. :10.000 Max. :10.000 Max. :16.800
## family_history_diabetes hypertension_history cardiovascular_history
## Min. :0.0000 Min. :0.0000 Min. :0.00000
## 1st Qu.:0.0000 1st Qu.:0.0000 1st Qu.:0.00000
## Median :0.0000 Median :0.0000 Median :0.00000
## Mean :0.2195 Mean :0.2507 Mean :0.07921
## 3rd Qu.:0.0000 3rd Qu.:1.0000 3rd Qu.:0.00000
## Max. :1.0000 Max. :1.0000 Max. :1.00000
## bmi waist_to_hip_ratio systolic_bp diastolic_bp
## Length:101000 Min. :0.6700 Min. : 0.0 Min. : 0.00
## Class :character 1st Qu.:0.8200 1st Qu.:104.0 1st Qu.: 69.00
## Mode :character Median :0.8600 Median :115.0 Median : 75.00
## Mean :0.8561 Mean :111.1 Mean : 72.21
## 3rd Qu.:0.8900 3rd Qu.:125.0 3rd Qu.: 80.00
## Max. :1.0600 Max. :179.0 Max. :110.00
## heart_rate cholesterol_total hdl_cholesterol ldl_cholesterol
## Min. : 40.00 Min. : 0.0 Min. :20.00 Min. : 50
## 1st Qu.: 64.00 1st Qu.:161.0 1st Qu.:47.00 1st Qu.: 78
## Median : 70.00 Median :184.0 Median :54.00 Median :102
## Mean : 69.63 Mean :178.5 Mean :54.04 Mean :103
## 3rd Qu.: 75.00 3rd Qu.:207.0 3rd Qu.:61.00 3rd Qu.:126
## Max. :105.00 Max. :318.0 Max. :98.00 Max. :263
## triglycerides glucose_fasting glucose_postprandial insulin_level
## Min. : 30.0 Min. : 0.0 Min. : 0.0 Min. : 0.000
## 1st Qu.: 92.0 1st Qu.:100.0 1st Qu.:136.0 1st Qu.: 4.580
## Median : 122.0 Median :110.0 Median :158.0 Median : 8.540
## Mean : 126.9 Mean :106.7 Mean :153.6 Mean : 9.091
## 3rd Qu.: 152.0 3rd Qu.:120.0 3rd Qu.:180.0 3rd Qu.: 12.340
## Max. :2280.0 Max. :172.0 Max. :287.0 Max. :245.600
## hba1c diabetes_risk_score diabetes_stage diagnosed_diabetes
## Min. :4.000 Min. : 2.70 Length:101000 Min. :0.0000
## 1st Qu.:5.970 1st Qu.:23.80 Class :character 1st Qu.:0.0000
## Median :6.520 Median :29.00 Mode :character Median :1.0000
## Mean :6.521 Mean :30.22 Mean :0.5999
## 3rd Qu.:7.070 3rd Qu.:35.60 3rd Qu.:1.0000
## Max. :9.800 Max. :67.20 Max. :1.0000
# Basic text cleanup
df <- df %>%
mutate(across(where(is.character), str_trim)) %>%
mutate(across(where(is.character), ~na_if(., "")))
# Standardise gender
if ("gender" %in% names(df)) {
df$gender <- tolower(df$gender)
df$gender[df$gender %in% c("m","male","mle")] <- "Male"
df$gender[df$gender %in% c("f","female","femla")] <- "Female"
df$gender[!df$gender %in% c("Male","Female")] <- "Unknown"
df$gender <- factor(df$gender)
}
# Fix BMI formatting
if ("bmi" %in% names(df)) {
df$bmi <- as.numeric(gsub(",", ".", df$bmi))
}
# Replace biologically invalid zeros
invalid_zero <- intersect(
c("bmi","systolic_bp","diastolic_bp","cholesterol_total",
"hdl_cholesterol","ldl_cholesterol","triglycerides",
"glucose_fasting","glucose_postprandial","insulin_level"),
names(df)
)
df[invalid_zero] <- lapply(df[invalid_zero], function(x) ifelse(x == 0, NA, x))
# Median imputation (numeric only)
num_cols <- names(df)[sapply(df, is.numeric)]
df[num_cols] <- lapply(df[num_cols], function(x) {
x[is.na(x)] <- median(x, na.rm = TRUE)
x
})
# Fix invalid ages
if ("age" %in% names(df)) {
df$age[df$age <= 0] <- median(df$age[df$age > 0], na.rm = TRUE)
}
# Remove duplicates
df <- distinct(df)
# Replace NA with "Unknown" to preserve data integrity
df_clean <- df %>%
mutate(across(where(is.character), ~replace_na(., "Unknown"))) %>%
mutate(across(where(is.character), as.factor))
# Recheck dataset
summary(df_clean)
## age gender ethnicity education_level
## Min. :18.00 Female :48673 Asian :11615 Graduate :34350
## 1st Qu.:39.00 Male :47788 Black :17633 Highschool :44034
## Median :50.00 Unknown: 3565 Hispanic:19695 No formal : 4993
## Mean :50.12 Other : 4955 Postgraduate:14649
## 3rd Qu.:61.00 Unknown : 2001 Unknown : 2000
## Max. :90.00 White :44127
##
## income_level employment_status smoking_status
## High : 4903 Employed :60190 Current:19784
## Low :14539 Retired :21766 Former :19607
## Lower-Middle:24645 Student : 6149 Never :58635
## Middle :34500 Unemployed:11921 Unknown: 2000
## Unknown : 2000
## Upper-Middle:19439
##
## alcohol_consumption_per_week physical_activity_minutes_per_week
## Min. : 0.000 Min. : 0.0
## 1st Qu.: 1.000 1st Qu.: 57.0
## Median : 2.000 Median :100.0
## Mean : 2.004 Mean :118.9
## 3rd Qu.: 3.000 3rd Qu.:160.0
## Max. :10.000 Max. :833.0
##
## diet_score sleep_hours_per_day screen_time_hours_per_day
## Min. : 0.000 Min. : 3.000 Min. : 0.500
## 1st Qu.: 4.800 1st Qu.: 6.300 1st Qu.: 4.300
## Median : 6.000 Median : 7.000 Median : 6.000
## Mean : 5.995 Mean : 6.998 Mean : 5.996
## 3rd Qu.: 7.200 3rd Qu.: 7.700 3rd Qu.: 7.700
## Max. :10.000 Max. :10.000 Max. :16.800
##
## family_history_diabetes hypertension_history cardiovascular_history
## Min. :0.0000 Min. :0.0000 Min. :0.0000
## 1st Qu.:0.0000 1st Qu.:0.0000 1st Qu.:0.0000
## Median :0.0000 Median :0.0000 Median :0.0000
## Mean :0.2194 Mean :0.2508 Mean :0.0792
## 3rd Qu.:0.0000 3rd Qu.:1.0000 3rd Qu.:0.0000
## Max. :1.0000 Max. :1.0000 Max. :1.0000
##
## bmi waist_to_hip_ratio systolic_bp diastolic_bp
## Min. :15.00 Min. :0.6700 Min. : 90.0 Min. : 50.00
## 1st Qu.:23.30 1st Qu.:0.8200 1st Qu.:106.0 1st Qu.: 70.00
## Median :25.60 Median :0.8600 Median :116.0 Median : 75.00
## Mean :25.61 Mean :0.8561 Mean :115.8 Mean : 75.22
## 3rd Qu.:27.90 3rd Qu.:0.8900 3rd Qu.:125.0 3rd Qu.: 80.00
## Max. :39.20 Max. :1.0600 Max. :179.0 Max. :110.00
##
## heart_rate cholesterol_total hdl_cholesterol ldl_cholesterol
## Min. : 40.00 Min. :100 Min. :20.00 Min. : 50
## 1st Qu.: 64.00 1st Qu.:165 1st Qu.:47.00 1st Qu.: 78
## Median : 70.00 Median :186 Median :54.00 Median :102
## Mean : 69.63 Mean :186 Mean :54.04 Mean :103
## 3rd Qu.: 75.00 3rd Qu.:207 3rd Qu.:61.00 3rd Qu.:126
## Max. :105.00 Max. :318 Max. :98.00 Max. :263
##
## triglycerides glucose_fasting glucose_postprandial insulin_level
## Min. : 30.0 Min. : 60.0 Min. : 70 Min. : 2.000
## 1st Qu.: 92.0 1st Qu.:102.0 1st Qu.:140 1st Qu.: 5.290
## Median : 122.0 Median :111.0 Median :160 Median : 8.830
## Mean : 126.9 Mean :111.1 Mean :160 Mean : 9.447
## 3rd Qu.: 152.0 3rd Qu.:120.0 3rd Qu.:180 3rd Qu.: 12.340
## Max. :2280.0 Max. :172.0 Max. :287 Max. :245.600
##
## hba1c diabetes_risk_score diabetes_stage diagnosed_diabetes
## Min. :4.000 Min. : 2.70 Type 2 :57997 Min. :0.0
## 1st Qu.:5.970 1st Qu.:23.80 Pre-Diabetes:30886 1st Qu.:0.0
## Median :6.520 Median :29.00 No Diabetes : 7759 Median :1.0
## Mean :6.521 Mean :30.22 TYPE 2 : 1791 Mean :0.6
## 3rd Qu.:7.070 3rd Qu.:35.60 PRE-DIABETES: 970 3rd Qu.:1.0
## Max. :9.800 Max. :67.20 Gestational : 266 Max. :1.0
## (Other) : 357
# Select continuous numeric columns
continuous_vars <- df %>%
select(age, bmi,
glucose_fasting, glucose_postprandial, hba1c, insulin_level,
cholesterol_total, ldl_cholesterol, hdl_cholesterol, triglycerides,
systolic_bp, diastolic_bp, heart_rate,
physical_activity_minutes_per_week, sleep_hours_per_day,
diet_score, alcohol_consumption_per_week)
# Function to Calculate Outliers
get_outlier_stats <- function(column_data) {
Q1 <- quantile(column_data, 0.25, na.rm = TRUE)
Q3 <- quantile(column_data, 0.75, na.rm = TRUE)
IQR <- Q3 - Q1
lower <- Q1 - 1.5 * IQR
upper <- Q3 + 1.5 * IQR
# Return count of values outside range
return(sum(column_data < lower | column_data > upper, na.rm = TRUE))
}
# Apply to All Columns and Print Table
outlier_report <- continuous_vars %>%
summarise(across(everything(), get_outlier_stats)) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Outlier_Count") %>%
arrange(desc(Outlier_Count)) %>%
filter(Outlier_Count > 0) # Only show the ones with outliers
print(outlier_report)
## # A tibble: 16 × 2
## Variable Outlier_Count
## <chr> <int>
## 1 physical_activity_minutes_per_week 3200
## 2 diastolic_bp 1183
## 3 bmi 1030
## 4 sleep_hours_per_day 900
## 5 glucose_postprandial 881
## 6 insulin_level 862
## 7 heart_rate 855
## 8 cholesterol_total 814
## 9 triglycerides 784
## 10 glucose_fasting 709
## 11 hba1c 618
## 12 hdl_cholesterol 566
## 13 systolic_bp 514
## 14 alcohol_consumption_per_week 458
## 15 ldl_cholesterol 349
## 16 diet_score 337
Outlier analysis was performed by using the Interquartile Range (IQR) method. All the outliers values fall within biologically plausible ranges for high-risk diabetes populations will retained to prevent the loss of critical diagnostic information.
# 1. Age
mean_age <- mean(df$age, na.rm = TRUE)
p1 <- ggplot(df_clean, aes(x = age)) +
geom_histogram(bins = 30, fill = "orange", color = "black") +
geom_vline(aes(xintercept = mean_age), color = "red", linetype = "dashed", linewidth = 1)+
annotate("text", x = mean_age, y = 8000,
label = paste("Mean Age:", round(mean_age, 1)),
color = "red", fontface = "bold") +
labs(title = "Distribution of Age", x = "age") +
theme_minimal()
p1
Mean age approximately 50 years and standard deviation around 15.71. This indicates adequate representation of middle-aged and older adults, who are more susceptible to diabetes.
# 2. BMI
mean_bmi <- mean(df$bmi, na.rm = TRUE)
p2 <- ggplot(df_clean, aes(x = bmi)) +
geom_histogram(bins = 30, fill = "skyblue", color = "black") +
geom_vline(aes(xintercept = mean_bmi), color = "red", linetype = "dashed", linewidth = 1)+
annotate("text", x = mean_bmi, y = 8000,
label = paste("Mean BMI:", round(mean_bmi, 1)),
color = "red", fontface = "bold") +
labs(title = "Distribution of BMI", x = "BMI") +
theme_minimal()
p2
The average BMI of 25.6 indicates the population at the borderline of the “overweight” category according to WHO classification.
# 3. Diagnosed_diabetes
p3 <- ggplot(df_clean, aes(x = as.factor(diagnosed_diabetes), fill = as.factor(diagnosed_diabetes))) +
geom_bar() +
scale_fill_manual(values = c("0" = "forestgreen", "1" = "firebrick"))+
labs(title = "Diagnosed Diabetes Count", x = "0 = No, 1 = Yes") +
theme_minimal() +
theme(legend.position = "none")
p3
There is slight imbalance diagnosed_diabetes in this dataset where diabetes is around 60% while non-diabetes is 40%.The imbalance is within acceptable limits for machine learning applications, should prioritize sensitivity and F1-score over accuracy in model evaluation.
# 4. HbA1c
p4 <- ggplot(df_clean, aes(x = as.factor(diagnosed_diabetes), y = hba1c, fill = as.factor(diagnosed_diabetes))) +
geom_boxplot() +
# Set the colors AND the labels for the legend
scale_fill_manual(
values = c("1" = "firebrick", "0" = "forestgreen"),
labels = c("1" = "Diabetes", "0" = "Non-diabetes")
) +
# Set the labels for the X-axis
scale_x_discrete(
labels = c("1" = "Diabetes", "0" = "Non-diabetes")
) +
labs(
title = "Clinical Factor: HbA1c",
x = "Diagnosis Status",
y = "HbA1c Level",
fill = "Diagnosis"
) +
theme_minimal()
p4
The data showed significant separation where diabetes patients exhibited higher median HbA1c level of 6.97% compared to average of 5.84% in non-diabetic.
# 5. Family History
plot_data <- df %>%
mutate(family_history = ifelse(family_history_diabetes == 1, "Yes", "No")) %>%
mutate(status = ifelse(diagnosed_diabetes == 1, "Diabetic", "Non-diabetic"))
p5 <- ggplot(plot_data, aes(x = family_history, fill = status)) +
geom_bar(position = "dodge", alpha = 0.9) +
geom_text(stat = "count",
aes(label = after_stat(count)),
position = position_dodge(width = 0.9),
vjust = -0.5,
fontface = "bold") +
scale_fill_manual(values = c("Diabetic" = "#E41A1C", "Non-diabetic" = "#377EB8")) +
labs(title = "Impact of Family History on Diabetes Risk",
x = "Family History",
y = "Number of Patients",
fill = "Diagnosis") +
theme_minimal() +
theme(legend.position = "top")
p5
The number of diabetes patients 17182 is higher with a positive family history compared to no history group 4766.This indicated that part of patients may be developing diabetes primarily due to genetic insulin resistance rather than purely lifestyle habits.
# 6. Physical Activity
p6 <- ggplot(df_clean, aes(x = physical_activity_minutes_per_week, fill = as.factor(diagnosed_diabetes))) +
geom_density(alpha = 0.5) +
scale_fill_manual(values = c("0" = "forestgreen", "1" = "firebrick"),
labels = c("0" = "Non-diabetes", "1" = "Diabetes")) +
labs(title = "Distribution of Physical Activity Levels",
x = "physical activity",
y = "Density",
fill = "Diagnosis") +
theme_minimal() +
theme(legend.position = "top")
p6
The density plot show substantial overlap in physical activity levels
between the diabetic and non-diabetic group. It show that higher
physical activity is generally associated with non-diabetic status.
#Select numeric columns only
numeric_vars <- df_clean %>%
select(where(is.numeric))
#Plot the correlation matrix
cor_matrix <- cor(numeric_vars, use = "complete.obs")
corrplot(cor_matrix,
method= "color",
tl.col = "black",
tl.cex = 0.6,
title = "Correlation Matrix",
mar = c(0,0,1,0))
The strongest positive correlations identified are hba1c 0.68, glucose postprandial 0.62 and glucose fasting 0.50. It suggesting clinical markers are more predictive than demographic and lifestyle factors in this dataset. Family history 0.20, Age 0.14 and BMI 0.09 are moderately low correlation with diabetes.Physical activity showed negative correlation -0.10 with diabetes indicating that increased physical exercise can reduce risk of diabetes.The weak linear correlation between lifestyle and diabetes indicate potential non-linear relationship occured.
# Decision Tree Model for explainable rules
# Support Vector Machine (linear) Model for robust generalization
# Target Variable: diagnosed_diabetes (binary outcome)
# Data preprocessing
df_model <- df_clean %>%
mutate(across(where(is.character), as.factor)) %>%
mutate(diagnosed_diabetes = as.factor(diagnosed_diabetes)) %>%
select(-diabetes_risk_score, -diabetes_stage)
df_model$diagnosed_diabetes <- factor(
df_model$diagnosed_diabetes,
levels = c(0, 1),
labels = c("No", "Yes")
)
# Train-Test Split (Classification)
set.seed(123)
trainIndex_cls <- createDataPartition(
df_model$diagnosed_diabetes,
p = 0.8,
list = FALSE
)
train_cls <- df_model[trainIndex_cls, ]
test_cls <- df_model[-trainIndex_cls, ]
# Objective: Classify individuals as diabetic or non-diabetic based on relevant health indicators.
# Model Training
# Model 1: Decision Tree
dt_diag <- rpart(
diagnosed_diabetes ~ age + bmi + hba1c +
glucose_fasting + glucose_postprandial +
family_history_diabetes + hypertension_history,
data = train_cls,
method = "class"
)
# Decision Tree Evaluation
# Visualise the decision tree
rpart.plot(dt_diag)
# Predict on test set
dt_pred <- predict(dt_diag, test_cls, type = "class")
# Confusion matrix and accuracy
confusionMatrix(dt_pred, test_cls$diagnosed_diabetes)
## Confusion Matrix and Statistics
##
## Reference
## Prediction No Yes
## No 8002 1587
## Yes 0 10415
##
## Accuracy : 0.9207
## 95% CI : (0.9168, 0.9244)
## No Information Rate : 0.6
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.84
##
## Mcnemar's Test P-Value : < 2.2e-16
##
## Sensitivity : 1.0000
## Specificity : 0.8678
## Pos Pred Value : 0.8345
## Neg Pred Value : 1.0000
## Prevalence : 0.4000
## Detection Rate : 0.4000
## Detection Prevalence : 0.4794
## Balanced Accuracy : 0.9339
##
## 'Positive' Class : No
##
#The decision tree classifier is used as an interpretable baseline model for diabetes classification. Its rule-based structure provides explicit decision paths based on predictor thresholds, supporting transparency and ease of explanation in a clinical screening context.
# Model 2: Support Vector Machine (SVM)
set.seed(123)
# Training on stratified subset for computational efficiency
# Create a stratified sample (10,000 rows)
svm_index <- createDataPartition(
train_cls$diagnosed_diabetes,
p = 10000 / nrow(train_cls),
list = FALSE
)
train_cls_svm <- train_cls[svm_index, ]
svm_diag <- svm(
diagnosed_diabetes ~ age + bmi + hba1c +
glucose_fasting + glucose_postprandial +
family_history_diabetes + hypertension_history,
data = train_cls_svm,
kernel = "linear",
scale = TRUE
)
# SVM Evaluation
svm_pred <- predict(svm_diag, test_cls)
confusionMatrix(svm_pred, test_cls$diagnosed_diabetes)
## Confusion Matrix and Statistics
##
## Reference
## Prediction No Yes
## No 6898 1374
## Yes 1104 10628
##
## Accuracy : 0.8761
## 95% CI : (0.8715, 0.8807)
## No Information Rate : 0.6
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.7434
##
## Mcnemar's Test P-Value : 6.523e-08
##
## Sensitivity : 0.8620
## Specificity : 0.8855
## Pos Pred Value : 0.8339
## Neg Pred Value : 0.9059
## Prevalence : 0.4000
## Detection Rate : 0.3448
## Detection Prevalence : 0.4135
## Balanced Accuracy : 0.8738
##
## 'Positive' Class : No
##
# The Support Vector Machine classifier is included to provide a robust alternative to the decision tree by maximising class separation in a high-dimensional feature space. This model prioritises generalisation capability and enables comparison between interpretable and margin-based classification approaches.
# Linear Regression (Baseline)
# Random Forest to capture non-linear predictors
# Target Variable: HbA1c
set.seed(123)
trainIndex_reg <- createDataPartition(
df_clean$hba1c,
p = 0.8,
list = FALSE
)
train_reg <- df_clean[trainIndex_reg, ]
# Reduce training size for balancing processing efficiency with sufficient statistical power
train_reg_small <- train_reg[sample(nrow(train_reg), 10000), ]
test_reg <- df_clean[-trainIndex_reg, ]
#Objective: Predict HbA1c as a continuous measure of long-term glycaemic control using demographic and lifestyle predictors.
# Model Training
# Model 1: Linear Regression
lm_model <- lm(
hba1c ~ age + gender + bmi + physical_activity_minutes_per_week +
diet_score + smoking_status + sleep_hours_per_day + systolic_bp + diastolic_bp,
data = train_reg_small
)
# Linear Regression Evaluation
summary(lm_model)
##
## Call:
## lm(formula = hba1c ~ age + gender + bmi + physical_activity_minutes_per_week +
## diet_score + smoking_status + sleep_hours_per_day + systolic_bp +
## diastolic_bp, data = train_reg_small)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.54771 -0.53933 0.00159 0.54151 2.90781
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.823e+00 1.239e-01 46.982 < 2e-16 ***
## age 7.329e-03 6.218e-04 11.786 < 2e-16 ***
## genderMale -1.347e-02 1.643e-02 -0.820 0.412331
## genderUnknown -4.461e-02 4.351e-02 -1.025 0.305280
## bmi 2.068e-02 2.439e-03 8.477 < 2e-16 ***
## physical_activity_minutes_per_week -1.143e-03 9.452e-05 -12.087 < 2e-16 ***
## diet_score -1.684e-02 4.615e-03 -3.649 0.000265 ***
## smoking_statusFormer -1.748e-02 2.556e-02 -0.684 0.494004
## smoking_statusNever -7.319e-03 2.086e-02 -0.351 0.725642
## smoking_statusUnknown -1.901e-02 6.148e-02 -0.309 0.757180
## sleep_hours_per_day -9.747e-03 7.357e-03 -1.325 0.185257
## systolic_bp 9.891e-04 6.984e-04 1.416 0.156780
## diastolic_bp 1.017e-04 1.031e-03 0.099 0.921428
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.8056 on 9987 degrees of freedom
## Multiple R-squared: 0.05065, Adjusted R-squared: 0.04951
## F-statistic: 44.41 on 12 and 9987 DF, p-value: < 2.2e-16
lm_pred <- predict(lm_model, test_reg)
RMSE(lm_pred, test_reg$hba1c)
## [1] 0.7969858
MAE(lm_pred, test_reg$hba1c)
## [1] 0.6379452
# Linear regression is used as a baseline regression model to establish a transparent and interpretable relationship between HbA1c and the selected predictors. Its simplicity allows direct examination of linear effects and serves as a reference point for assessing the added value of more complex models.
# Model 2: Random Forest Regression
set.seed(123)
rf_reg <- randomForest(
hba1c ~ age + gender + bmi + physical_activity_minutes_per_week +
diet_score + smoking_status + sleep_hours_per_day + systolic_bp + diastolic_bp,
data = train_reg_small,
ntree = 100
)
# Random Forest Evaluation
rf_pred <- predict(rf_reg, test_reg)
RMSE(rf_pred, test_reg$hba1c)
## [1] 0.8124269
MAE(rf_pred, test_reg$hba1c)
## [1] 0.6499545
importance(rf_reg)
## IncNodePurity
## age 827.0418
## gender 179.3296
## bmi 886.7035
## physical_activity_minutes_per_week 996.7515
## diet_score 817.7876
## smoking_status 264.0152
## sleep_hours_per_day 785.3858
## systolic_bp 770.8593
## diastolic_bp 713.3565
# Random Forest regression is employed to model non-linear relationships and interaction effects that may not be captured by linear regression. As an ensemble method, it enhances modelling flexibility and robustness, making it suitable for complex health-related data where predictor interactions are expected.
Classification Model
The decision tree classifier achieved a high classification accuracy of approximately 92%, with a Cohen’s Kappa value of 0.84, indicating strong agreement beyond chance. The model demonstrated excellent sensitivity, ensuring effective identification of diabetic cases, which is particularly important in clinical screening contexts.
The SVM classifier exhibited strong generalisation performance on the test dataset, producing balanced sensitivity and specificity. While less interpretable than the decision tree, the SVM provided a robust classification benchmark, confirming the reliability of the predictive features.
Regression Model
Overall, linear regression model achieved a lower RMSE (0.7969) and MAE(0.6379) than the random forest RMSE(0.8124) and MAE(0.6499). However, linear regression models recorded low R² values, suggesting that only a limited proportion of HbA1c variability is explained by the selected non-glucose predictors. As such, the regression models are more suitable for exploratory analysis rather than clinical-grade prediction.
The significant features found in analysis are age, BMI, physical activity and diet scores with p-value <0.05.This indicate increasing in age and body mass are the primary drivers of elevated HbA1c in this dataset. In contrast, higher exercise levels and better nutrition were directly contribute to lower (healthier) HbA1c results.
This project used a large health survey dataset to predict diabetes risk using demographic, lifestyle, and clinical indicators. After cleaning and preprocessing the data, exploratory analysis showed that clinical biomarkers were the stronger predictors, while lifestyle and demographic variables were weaker but still informative.
For classification model, decision tree classifier achieved a high accuracy, Cohen’s Kappa value and excellent sensitivity indicating strong predictive performance over SVM. Classification models evaluation prirotize high sensitivity to minimise missed diabetes cases in screening.
For HbA1c prediction, linear regression slightly outperformed but low R² indicating limited ability to predict HbA1c without glucose-related inputs. Overall, the work supports scalable diabetes screening with interpretable models, with future improvements through threshold tuning, additional models, and external validation