As a college student myself, I am interested in learning how our daily habits and lifestyle choices impact our academic performance.

For this project, I will analyze a Kaggle dataset containing 12 variables, including study hours, sleep, screen time, gym frequency, stress levels, and diet. My primary goal is to determine how these lifestyle factors correlate with and predict student success, which I will measure using CGPA and Attendance_Percentage. I hypothesize that while study hours are a primary driver of success, factors like sleep and stress levels will have a significant non-linear impact on CGPA.

To conduct this analysis, I will use dplyr for data cleaning and feature engineering. For my exploratory phase, I plan to use ggplot to create corplots to identify relationships between variables and Principal Component Analysis (PCA) with biplots to see how different student profiles group together. Since I want to understand both performance and student behavior, I will use Multiple Regression to predict CGPA based on continuous habits and Clustering to see if students naturally fall into high stress or balanced lifestyle groups. I also will use Classification with Logistic Regression to predict whether a student falls into a high or low attendance category based on their residence and daily screen time.

The dataset can be found here: https://www.kaggle.com/datasets/rafi003/student-lifestyle-and-academic-performance-dataset

library(dplyr)
library(corrplot)
library(factoextra)
library(tidyverse)
library(class)
Balance <- read.csv("student_lifestyle.csv")
head(Balance)
##   Age     Branch Study_Hours_per_Day Sleep_Hours Screen_Time_Hours
## 1  23        ECE                4.14        6.84              9.23
## 2  20      Civil                5.97        5.52              3.09
## 3  24 Electrical                3.19        3.39              5.02
## 4  21        CSE                4.77        6.44              9.21
## 5  23      Civil                5.42        6.54              4.76
## 6  19        ECE                3.01        7.25              5.07
##   Gym_Hours_per_Week Diet_Type Attendance_Percentage Stress_Level_1_to_10
## 1               2.67   Non-Veg                 81.24                 4.93
## 2              15.61       Veg                 90.55                 6.96
## 3               2.52       Veg                 69.40                 7.38
## 4               0.00   Non-Veg                 80.79                 5.84
## 5               9.93       Veg                 82.63                 6.67
## 6              13.89   Non-Veg                 80.43                 3.28
##     Residence Internal_Marks CGPA
## 1   Hosteller          65.86 7.52
## 2 Day Scholar          62.52 7.21
## 3   Hosteller          40.11 4.84
## 4 Day Scholar          61.25 6.74
## 5 Day Scholar          64.54 7.77
## 6   Hosteller          64.40 7.26
ncol(Balance)
## [1] 12
colnames(Balance)
##  [1] "Age"                   "Branch"                "Study_Hours_per_Day"  
##  [4] "Sleep_Hours"           "Screen_Time_Hours"     "Gym_Hours_per_Week"   
##  [7] "Diet_Type"             "Attendance_Percentage" "Stress_Level_1_to_10" 
## [10] "Residence"             "Internal_Marks"        "CGPA"
Balance <- Balance %>% rename(Study = Study_Hours_per_Day, Sleep = Sleep_Hours,Screen = Screen_Time_Hours,Gym = Gym_Hours_per_Week,
Attendance = Attendance_Percentage,Stress = Stress_Level_1_to_10)

I renamed the columns so that l could have shorter names

my_colors <- colorRampPalette(c("plum", "white", "hotpink"))(200)
numeric_data <- Balance %>% select(Age, Study, Sleep, Screen, Gym, Attendance, Stress, Internal_Marks, CGPA)
cor_matrix <- cor(numeric_data, use = "complete.obs")
corrplot(cor_matrix, method = "color", type = "upper", col = my_colors,tl.col = "purple",tl.srt = 45,addCoef.col = "black", number.cex = 0.7,diag = FALSE)              

I started my analysis with a corplot because I wanted a clear, visual way to see which lifestyle habits actually link up with academic success before building any complex models. Using this visualization it was easy to spot that Study hours “0.51” and Sleep “0.62” have the strongest positive relationship with CGPA, meaning that rest might be just as vital as studying. I found it interesting that Stress has a negative correlation of -0.17, meaning that while it does pull grades down slightly, it is not as destructive as I originally thought. Variables like Screen Time and Gym are almost pure white on the plot, which could mmean they have basically no linear effect on a student’s performance. This visualization showed me that Sleep, Study and Stress are the main three variables I should focus on for my upcoming PCA and Regression steps.

pca_data <- Balance %>% select(Age, Study, Sleep, Screen, Gym, Attendance, Stress, Internal_Marks, CGPA) %>%scale() 
res.pca <- prcomp(pca_data)
fviz_pca_biplot(res.pca, repel = TRUE,col.var = "purple",col.ind = "lightblue",title = "Student Lifestyle PCA - Biplot",geom = "point") + theme_minimal()

I decided to perform a PCA and create this biplot because analyzing 12 variables at once can be overwhelming and I wanted to see how they group together to form different student profiles. In the plot, the light blue dots represent individual students, while the purple arrows show how each variable influences the data. I can see that CGPA, Internal_Marks and Sleep are all pointing toward the top left, whilst Study and Attendance are clustered toward the bottom left, showing that these two groups of variables are the main drivers of student success. Stress points in the complete opposite direction of Sleep, which confirms that as a student’s stress levels rise, their sleep quality tends to drop significantly. Using PCA this way is a huge help because it simplifies the whole dataset into one map that shows me exactly which lifestyle habits are pulling students toward higher or lower performance.

MR_model <- lm(CGPA ~ Study + Sleep + Screen + Stress + Attendance, data = Balance)
summary(MR_model)
## 
## Call:
## lm(formula = CGPA ~ Study + Sleep + Screen + Stress + Attendance, 
##     data = Balance)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.66858 -0.37645  0.01498  0.33903  1.61259 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  4.9052254  0.2516513  19.492   <2e-16 ***
## Study        0.6046459  0.0248025  24.378   <2e-16 ***
## Sleep        0.1953554  0.0211229   9.249   <2e-16 ***
## Screen       0.0054629  0.0081025   0.674     0.50    
## Stress      -0.2936136  0.0166279 -17.658   <2e-16 ***
## Attendance   0.0002044  0.0032782   0.062     0.95    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.5051 on 994 degrees of freedom
## Multiple R-squared:  0.721,  Adjusted R-squared:  0.7196 
## F-statistic: 513.7 on 5 and 994 DF,  p-value: < 2.2e-16

I used Multiple Regression to see which lifestyle factors are the most influencial when predicting a student’s CGPA. The results were super clear because Study, Sleep and Stress all came back with three stars , meaning they are highly significant predictors. For every extra hour of study, CGPA tends to go up by about 0.60, and for every extra hour of sleep, it rises by 0.19. Stress has a negative estimate of -0.29, confirming that high stress levels really do pull grades down. What’s more interesting is that Screen Time and Attendance had p-values much higher than 0.05, which basically means they don’t have a reliable impact on CGPA in this specific model. The model has an Adjusted R-squared of 0.72, which is amazing and means these lifestyle factors explain about 72% of the variation in student grades!

par(mfrow = c(2,2))
plot(MR_model)

Everything looks great, the Residuals vs Fitted and Scale-Location plots both have nice, flat red lines, which means my model is being consistent. The Normal Q-Q plot is good too, because almost all the dots sit right on the diagonal line,and then looking at Residuals vs Leverage, there are not any crazy outliers pulling the results in a weird direction. The model seems reliable.

Balance$Predicted_CGPA <- predict(MR_model)
Balance$Residuals <- residuals(MR_model)
Balance %>% select(CGPA, Predicted_CGPA, Residuals) %>%head(5)
##   CGPA Predicted_CGPA  Residuals
## 1 7.52       7.364202  0.1557978
## 2 7.21       7.585160 -0.3751600
## 3 4.84       5.371040 -0.5310404
## 4 6.74       7.399597 -0.6595974
## 5 7.77       7.544520  0.2254805

To see how the Multiple Regression performs in practice, I generated a list of Predicted_CGPA values and compared them to the actual scores. By calculating the Residuals, I can see exactly where the model is strongest. For most students in the sample, the predicted values are remarkably close to the actual results, often within a range of 0.2 to 0.5 grade points.

ggplot(Balance, aes(x = Predicted_CGPA, y = CGPA)) +geom_point(color = "lightblue", alpha = 0.6) + geom_abline(intercept = 0, slope = 1, color = "hotpink", linewidth = 1) +labs(title = "Actual vs. Predicted CGPA", x = "Model's Predicted CGPA",y = "Actual CGPA") +
theme_minimal() 

I created this scatter plot just to visualize the multiple regression model. Each light blue dot represents a student, and the hot pink line represents a perfect prediction where the actual CGPA matches the predicted one exactly. Because the vast majority of the dots are clustered tightly around this line, it visually confirms that my model is highly reliable.

Balance <- Balance %>%mutate(High_Attendance = ifelse(Attendance >= 80, 1, 0))
log_model <- glm(High_Attendance ~ Study + Sleep + Screen + Stress, data = Balance, family = "binomial")
summary(log_model)
## 
## Call:
## glm(formula = High_Attendance ~ Study + Sleep + Screen + Stress, 
##     family = "binomial", data = Balance)
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -7.965531   1.020766  -7.803 6.02e-15 ***
## Study        1.845788   0.145277  12.705  < 2e-16 ***
## Sleep        0.045514   0.122766   0.371    0.711    
## Screen      -0.009781   0.045671  -0.214    0.830    
## Stress       0.067138   0.093997   0.714    0.475    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 1386.04  on 999  degrees of freedom
## Residual deviance:  758.56  on 995  degrees of freedom
## AIC: 768.56
## 
## Number of Fisher Scoring iterations: 6

I used Logistic Regression to see if I could predict whether a student would be a High Attender (80% and/or above) based on their habits. This was a really cool shift in the project because instead of predicting a grade, I was predicting the probability of a specific behavior. Looking at the results, Study is the only significant predictor with a massive positive estimate of 1.85. This tells me that students who are dedicated to studying are also significantly more likely to show up to class. Sleep, Screen, and Stress all have high p-values, which means they don’t actually help predict attendance in this model. This step proves that while sleep and stress are huge for your grades (CGPA), your actual attendance is almost entirely driven by your study habits.

Balance$Prob_Attendance <- predict(log_model, type = "response")
Balance$Predicted_Attendance <- ifelse(Balance$Prob_Attendance > 0.5, 1, 0)
Balance %>% select(Attendance, High_Attendance, Prob_Attendance, Predicted_Attendance) %>% 
head(10)
##    Attendance High_Attendance Prob_Attendance Predicted_Attendance
## 1       81.24               1      0.55676966                    1
## 2       90.55               1      0.97684500                    1
## 3       69.40               0      0.18591174                    0
## 4       80.79               1      0.80752279                    1
## 5       82.63               1      0.93920665                    1
## 6       80.43               1      0.12908121                    0
## 7       76.35               0      0.08600884                    0
## 8       76.02               0      0.37327030                    0
## 9       79.34               0      0.21830860                    0
## 10      87.37               1      0.96404973                    1

I added a prediction column to my dataset to see how well my logistic model actually performs in the real world. Using the predict() function, I generated a probability for each student and then I used a 0.5 cutoff to decide if the model thinks they have High Attendance or not.

comparison_table <- table(Actual = Balance$High_Attendance, Predicted = Balance$Predicted_Attendance)
(comparison_table)
##       Predicted
## Actual   0   1
##      0 404  88
##      1  74 434
accuracy <- sum(diag(comparison_table)) / sum(comparison_table) * 100
cat("The model accuracy is:", round(accuracy, 2), "%")
## The model accuracy is: 83.8 %

To wrap up my logistic regression, I created a confusion matrix to see exactly how many predictions the model got right. The results show that the model correctly identified 404 students with low attendance and 434 students with high attendance. While there were a few misses like the 88 times it predicted high attendance when it was actually low the overall accuracy rate of 83.8% is very high. This proves that my model is highly effective at using lifestyle habits to categorize students.

set.seed(123) 
cluster_data <- Balance %>% select(Study, Sleep, Screen, Stress, CGPA) %>% scale()
km_res <- kmeans(cluster_data, centers = 3, nstart = 25)
Balance$cluster <- as.factor(km_res$cluster)
Balance %>%group_by(cluster) %>%summarise(Avg_Study = mean(Study),Avg_Sleep = mean(Sleep),Avg_Stress = mean(Stress),Avg_CGPA = mean(CGPA),Avg_Screen = mean(Screen),Student_Count = n())
## # A tibble: 3 × 7
##   cluster Avg_Study Avg_Sleep Avg_Stress Avg_CGPA Avg_Screen Student_Count
##   <fct>       <dbl>     <dbl>      <dbl>    <dbl>      <dbl>         <int>
## 1 1            2.51      6.53       3.04     6.78       4.71           316
## 2 2            4.77      7.56       4.04     8.28       5.05           335
## 3 3            4.71      5.53       6.50     6.89       5.08           349

To truly understand the different types of students in my dataset, I used the group_by and summarise functions to calculate the average habits for each cluster. This revealed three distinct student groups. Cluster 2 represents the “Academic Weapons,” who achieve the highest average CGPA (8.2) by balancing high study hours with the most sleep (7.5 hours). In contrast, Cluster 3 represents the “Burnouts”, even though they study just as much as the top performers, their high stress levels and lack of sleep (only 5.5 hours) result in a significantly lower CGPA. Finally, Cluster 1 represents the “Chillers,” who have the lowest stress but also the lowest study time and grades. This table is the clearly demonstrates that studying hard is only half the battle and that sleep and stress management are the actual keys to peak academic performance.

Balance$cluster <- as.factor(km_res$cluster)
levels(Balance$cluster) <- c("The Chillers", "Academic Weapons", "The Burnouts")
fviz_cluster(list(data = cluster_data, cluster = Balance$cluster),palette = c("hotpink", "purple", "lightblue"), geom = "point",
ellipse.type = "convex", ggtheme = theme_minimal(),main = "Student Lifestyle Profiles: The Three Tribes")

I finalized my clustering analysis by labeling the three distinct groups based on their average lifestyle metrics. l transformed the numerical clusters into three descriptive profiles:The Academic Weapons, The Burnouts, and The Chillers to make the data become much more actionable. This visualization shows that the most successful students (Cluster 2) are not just those who work the hardest, but those who protect their sleep. Meanwhile, the “Burnout” group (Cluster 3) serves as a cautionary tale, showing that studying without rest leads to higher stress and diminishing returns on CGPA and then the chillers group (Cluster 1) so not work as hard, they have lower study hours thus the bad CGPA.

set.seed(123)
total.wi.ss <- c()
for (i in 1:10) {
  total.wi.ss[i] <- (kmeans(cluster_data, centers = i))$tot.withinss
}
total.wi.ss
##  [1] 4995.000 3735.289 2948.870 2583.959 2342.229 2101.001 1916.857 1819.711
##  [9] 1658.860 1579.746

Before I officially grouped the students, I wanted to make sure k=3 was actually the best choice and not just a guess. I ran a for loop to check the Total Within-Cluster Sum of Squares for 1 through 10 groups. Looking at the hot pink plot, there is a super clear elbow or bend at 3. After that, adding more clusters does not really give us more information, so it confirms that three clusters is the perfect way to organize this data.

plot(x = 1:10, y = total.wi.ss, type = "b", pch = 19, col = "hotpink",)
abline(v = 3, lty = 2, col = "purple") 

knn_data <- Balance %>% select(Study, Sleep, Screen, Stress) %>%scale()
set.seed(123)
train_indices <- sample(1:nrow(knn_data), 0.8 * nrow(knn_data))
train_data <- knn_data[train_indices, ]
test_data <- knn_data[-train_indices, ]
train_labels <- Balance$High_Attendance[train_indices]
test_labels <- Balance$High_Attendance[-train_indices]
knn_pred <- knn(train = train_data, test = test_data, cl = train_labels, k = 3)
knn_table <- table(Actual = test_labels, Predicted = knn_pred)
knn_accuracy <- sum(diag(knn_table)) / sum(knn_table) * 100
cat("knn Accuracy:", round(knn_accuracy, 2), "%")
## knn Accuracy: 77.5 %

I used k-Nearest Neighbors to see if I could predict attendance differently than the logistic model. My knn model got 77.5% accuracy, which is good, but lower than my Logistic Regression which had 83.8% accuracy. This could mean that attendance follows a pretty straight, predictable path rather than a complicated one. It was cool to see two different types of models try to solve the same problem

CONCLUSION:

After looking at 1,000 students through all these different models, l noticed a few things that stood out. First, sleep is honestly a game changer. The top performing students were the ones actually getting rest. My regression model also showed that while studying is obviously huge, high stress is a total grade killer. It shows that pushing yourself too hard can actually backfire on your CGPA.

One of the most surprising things I found was that attendance was not a big predictor for grades in this specific group. It showed that it’s more about how you are spending your time outside of class, like finding that balance between reading the books and catching up on sleep.

Of course, this is just one dataset, so it does not mean every student fits these rules perfectly. If I were to take this further, I would love to test these models on more data and maybe look into things like mental health or social support to see how they fit into the bigger picture. This project really showed me that academic success is more of an ecosystem than just a simple formula.