Project Title: Predicting Diabetes Risk Using Demographic, Lifestyle & Health Indicators

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

1.0 Project Background

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.

1.1 Question:

  1. Which supervised classification model performs best for diabetes risk prediction?

  2. Can HbA1c be predicted using demographic and lifestyle variables?

  3. Which predictive models demonstrate the highest sensitivity and discriminative ability for effectively screening high-risk diabetes patients?

1.2 Objective:

  1. To develop, validate, and compare multiple predictive classification models for diabetes risk assessment using appropriate performance metrics.

  2. To predict HbA1c using demographic and lifestyle variables.

  3. To interpret model performance and feature importance to support early diabetes screening and preventive healthcare.

1.3 Dataset Description

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

Dataset Link

2.0 Data Cleaning

2.1 Load Libraries

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

2.2 Data Exploration and Preprocessing

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

2.3 Outlier Check

# 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.

3.0 Exploratory Data Analysis (EDA)

3.1 Univariate Analysis

# 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.

3.2 Correlation Analysis

# 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.

3.4 Multivariate Analysis

#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.

4.0 Data Modeling

Objective 1: To develop, validate, and compare multiple predictive classification models for diabetes risk assessment using appropriate performance metrics.

4.1 Classification Model (Decision Tree Model & Support Vector Machine)

# 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.

Objective 2: To predict HbA1c using demographic and lifestyle features.

4.2 Regression Model (Linear Regression & Random Forest)

# 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.

5.0 Model Evaluation & Data Interpretation

Objective 3: To interpret model performance and feature importance to support early diabetes screening and preventive healthcare.

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.

6.0 Conclusion

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