Project 2

Understanding students’ final grades

Introduction

The Student Performance dataset from the UCI Machine Learning Repository provides information about students’ academic performance in two Portuguese secondary schools. The dataset includes information about students’ personal characteristics, family background, study habits, and academic performance.

The dataset contains, 395 observations and 33 variables, including students’ sex, age, parents’ education, study time, number of past class failures, absences, and grades obtained during three school periods.

For this project, I will focus mainly on the following variables, using them in my visualizations and multiple linear regression analysis:

  • G1: First-period grade.

  • G2: Second-period grade.

  • G3: Final grade.

  • studytime: Weekly study time, categorized into four levels.

  • failures: Number of past class failures.

  • sex: Student’s sex.

The dataset is available through the UCI Machine Learning Repository. I have linked the source here.

Objective

The goal of my research is to explore the relationship between students’ study habits, personal characteristics, and academic performance. I will examine how study time and previous academic performance are associated with students’ final grades. I will also compare average final grades by sex and study time through visualizations. Finally, I will use multiple linear regression to investigate how previous grades, study time, and past class failures help explain differences in students’ final academic performance.

Uploading libraries and dataset

library(readr)
library(dplyr)

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)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── 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(ggplot2)
library(highcharter)
Registered S3 method overwritten by 'quantmod':
  method            from
  as.zoo.data.frame zoo 
Highcharts (www.highcharts.com) is a Highsoft software product which is
not free for commercial and Governmental use
library(plotly)

Attaching package: 'plotly'

The following object is masked from 'package:ggplot2':

    last_plot

The following object is masked from 'package:stats':

    filter

The following object is masked from 'package:graphics':

    layout
library(RColorBrewer)
library(DataExplorer)
df<- read_delim('student-mat.csv', delim = ";")
Rows: 395 Columns: 33
── Column specification ────────────────────────────────────────────────────────
Delimiter: ";"
chr (17): school, sex, address, famsize, Pstatus, Mjob, Fjob, reason, guardi...
dbl (16): age, Medu, Fedu, traveltime, studytime, failures, famrel, freetime...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(df)
# A tibble: 6 × 33
  school sex     age address famsize Pstatus  Medu  Fedu Mjob     Fjob    reason
  <chr>  <chr> <dbl> <chr>   <chr>   <chr>   <dbl> <dbl> <chr>    <chr>   <chr> 
1 GP     F        18 U       GT3     A           4     4 at_home  teacher course
2 GP     F        17 U       GT3     T           1     1 at_home  other   course
3 GP     F        15 U       LE3     T           1     1 at_home  other   other 
4 GP     F        15 U       GT3     T           4     2 health   servic… home  
5 GP     F        16 U       GT3     T           3     3 other    other   home  
6 GP     M        16 U       LE3     T           4     3 services other   reput…
# ℹ 22 more variables: guardian <chr>, traveltime <dbl>, studytime <dbl>,
#   failures <dbl>, schoolsup <chr>, famsup <chr>, paid <chr>,
#   activities <chr>, nursery <chr>, higher <chr>, internet <chr>,
#   romantic <chr>, famrel <dbl>, freetime <dbl>, goout <dbl>, Dalc <dbl>,
#   Walc <dbl>, health <dbl>, absences <dbl>, G1 <dbl>, G2 <dbl>, G3 <dbl>
str(df)
spc_tbl_ [395 × 33] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ school    : chr [1:395] "GP" "GP" "GP" "GP" ...
 $ sex       : chr [1:395] "F" "F" "F" "F" ...
 $ age       : num [1:395] 18 17 15 15 16 16 16 17 15 15 ...
 $ address   : chr [1:395] "U" "U" "U" "U" ...
 $ famsize   : chr [1:395] "GT3" "GT3" "LE3" "GT3" ...
 $ Pstatus   : chr [1:395] "A" "T" "T" "T" ...
 $ Medu      : num [1:395] 4 1 1 4 3 4 2 4 3 3 ...
 $ Fedu      : num [1:395] 4 1 1 2 3 3 2 4 2 4 ...
 $ Mjob      : chr [1:395] "at_home" "at_home" "at_home" "health" ...
 $ Fjob      : chr [1:395] "teacher" "other" "other" "services" ...
 $ reason    : chr [1:395] "course" "course" "other" "home" ...
 $ guardian  : chr [1:395] "mother" "father" "mother" "mother" ...
 $ traveltime: num [1:395] 2 1 1 1 1 1 1 2 1 1 ...
 $ studytime : num [1:395] 2 2 2 3 2 2 2 2 2 2 ...
 $ failures  : num [1:395] 0 0 3 0 0 0 0 0 0 0 ...
 $ schoolsup : chr [1:395] "yes" "no" "yes" "no" ...
 $ famsup    : chr [1:395] "no" "yes" "no" "yes" ...
 $ paid      : chr [1:395] "no" "no" "yes" "yes" ...
 $ activities: chr [1:395] "no" "no" "no" "yes" ...
 $ nursery   : chr [1:395] "yes" "no" "yes" "yes" ...
 $ higher    : chr [1:395] "yes" "yes" "yes" "yes" ...
 $ internet  : chr [1:395] "no" "yes" "yes" "yes" ...
 $ romantic  : chr [1:395] "no" "no" "no" "yes" ...
 $ famrel    : num [1:395] 4 5 4 3 4 5 4 4 4 5 ...
 $ freetime  : num [1:395] 3 3 3 2 3 4 4 1 2 5 ...
 $ goout     : num [1:395] 4 3 2 2 2 2 4 4 2 1 ...
 $ Dalc      : num [1:395] 1 1 2 1 1 1 1 1 1 1 ...
 $ Walc      : num [1:395] 1 1 3 1 2 2 1 1 1 1 ...
 $ health    : num [1:395] 3 3 3 5 5 5 3 1 1 5 ...
 $ absences  : num [1:395] 6 4 10 2 4 10 0 6 0 0 ...
 $ G1        : num [1:395] 5 5 7 15 6 15 12 6 16 14 ...
 $ G2        : num [1:395] 6 5 8 14 10 15 12 5 18 15 ...
 $ G3        : num [1:395] 6 6 10 15 10 15 11 6 19 15 ...
 - attr(*, "spec")=
  .. cols(
  ..   school = col_character(),
  ..   sex = col_character(),
  ..   age = col_double(),
  ..   address = col_character(),
  ..   famsize = col_character(),
  ..   Pstatus = col_character(),
  ..   Medu = col_double(),
  ..   Fedu = col_double(),
  ..   Mjob = col_character(),
  ..   Fjob = col_character(),
  ..   reason = col_character(),
  ..   guardian = col_character(),
  ..   traveltime = col_double(),
  ..   studytime = col_double(),
  ..   failures = col_double(),
  ..   schoolsup = col_character(),
  ..   famsup = col_character(),
  ..   paid = col_character(),
  ..   activities = col_character(),
  ..   nursery = col_character(),
  ..   higher = col_character(),
  ..   internet = col_character(),
  ..   romantic = col_character(),
  ..   famrel = col_double(),
  ..   freetime = col_double(),
  ..   goout = col_double(),
  ..   Dalc = col_double(),
  ..   Walc = col_double(),
  ..   health = col_double(),
  ..   absences = col_double(),
  ..   G1 = col_double(),
  ..   G2 = col_double(),
  ..   G3 = col_double()
  .. )
 - attr(*, "problems")=<pointer: 0x000001f6e54ff160> 

Cleaning the dataset

colSums(is.na(df))
    school        sex        age    address    famsize    Pstatus       Medu 
         0          0          0          0          0          0          0 
      Fedu       Mjob       Fjob     reason   guardian traveltime  studytime 
         0          0          0          0          0          0          0 
  failures  schoolsup     famsup       paid activities    nursery     higher 
         0          0          0          0          0          0          0 
  internet   romantic     famrel   freetime      goout       Dalc       Walc 
         0          0          0          0          0          0          0 
    health   absences         G1         G2         G3 
         0          0          0          0          0 

Plot 1: Average final grade based on studytime category ~ Barplot

Preparing the data

df1<- df %>% mutate(studytime_new = recode(studytime, "1" = "< 2 hours", "2" = "2-5 hours", "3" = "5-10 hours", "4" = "> 10 hours")) %>% group_by(studytime_new) %>% summarise(average_G3 = mean(G3))
df1
# A tibble: 4 × 2
  studytime_new average_G3
  <chr>              <dbl>
1 2-5 hours           10.2
2 5-10 hours          11.4
3 < 2 hours           10.0
4 > 10 hours          11.3
df1$studytime_new <- factor(
  df1$studytime_new,
  levels = c(
    "< 2 hours",
    "2-5 hours",
    "5-10 hours",
    "> 10 hours")) #Reordering the categories with factor() since they are not numeric
plot1<- df1 %>% ggplot(aes(studytime_new, average_G3, fill=studytime_new)) +
  geom_col() +
  scale_fill_brewer(palette = "PuRd")+
  geom_text(aes(label = round(average_G3, 2), hjust = 0.50, size = 2))+
  labs(x = "Studytime categories",
       y = "Average final grade (Out of 20)",
       title = "Average final grade per studytime category",
       caption = "Source: Machine learning repository")+
    theme_minimal()
plot1

Plot 2: Average Final Grage by Sex ~ Barplot

Preparing the data

df2<- df %>% group_by(sex) %>% summarise(Average_finalG = mean(G3))
df2
# A tibble: 2 × 2
  sex   Average_finalG
  <chr>          <dbl>
1 F               9.97
2 M              10.9 
cols <- brewer.pal(2, "Set1")
Warning in brewer.pal(2, "Set1"): minimal value for n is 3, returning requested palette with 3 different levels
plot2<- highchart () %>%
  hc_add_series(data = df2,
                type = "bar",
                hcaes(x = sex,
                      y = Average_finalG, 
                      group = sex)) %>%
  hc_colors(cols) %>%
  hc_xAxis(title = list(text="Sex")) %>%
  hc_yAxis(title = list(text="Average Final Grade")) %>%
  hc_title(text = "Average Final Grage by Sex") %>% 
  hc_caption(text = "Source: Machine learning repository", align = "right",    verticalAlign = "bottom") %>%
  hc_legend(align = "center", verticalAlign = "top",
            layout = "horizontal") 
plot2

Plot 3: ~ Distribution of students’ grades across the three different periods ~ Boxplot

Preparing the data

df3 <- df %>% select(G1, G2, G3) %>% pivot_longer(cols = c(G1, G2, G3), names_to = "Period",values_to = "Grade") # Turning the columns into rows representing each sutdents' grades for each period
df3
# A tibble: 1,185 × 2
   Period Grade
   <chr>  <dbl>
 1 G1         5
 2 G2         6
 3 G3         6
 4 G1         5
 5 G2         5
 6 G3         6
 7 G1         7
 8 G2         8
 9 G3        10
10 G1        15
# ℹ 1,175 more rows
plot3 <- ggplot(df3, aes(x = Period, y = Grade, fill = Period)) +
  geom_boxplot() +
  scale_fill_brewer(palette = "RdPu") +
  labs(
    title = "Distribution of Students' Grades Across Three Periods",
    x = "School period",
    y = "Grade (Out of 20)",
    caption = "Source: UCI Machine Learning Repository"
  ) +
  theme_minimal()

ggplotly(plot3)

Multiple linear regression model

Examining which variable has the strongest correlation with the final Grade one:

df4<- df %>% select(G3,G2,G1,studytime,failures,sex,absences,health,Medu,Fedu)

plot_correlation(df4)

The first period grades, the second period grades, the studytime and the failure variables have the strongest correlation with the final grades one.

Model analysing the relationship between Final Grade and other variables such as Studytime, Failures, First period and Second period grades.

model1 <- lm(df4$G3~ G2 + G1 + studytime + failures , data = df4)
summary(model1)

Call:
lm(formula = df4$G3 ~ G2 + G1 + studytime + failures, data = df4)

Residuals:
    Min      1Q  Median      3Q     Max 
-9.4132 -0.3780  0.2720  0.9855  3.5168 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) -1.19907    0.42944  -2.792  0.00549 ** 
G2           0.97653    0.04965  19.666  < 2e-16 ***
G1           0.15034    0.05639   2.666  0.00799 ** 
studytime   -0.19667    0.11824  -1.663  0.09706 .  
failures    -0.26300    0.14172  -1.856  0.06424 .  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 1.928 on 390 degrees of freedom
Multiple R-squared:  0.8246,    Adjusted R-squared:  0.8228 
F-statistic: 458.5 on 4 and 390 DF,  p-value: < 2.2e-16

Linear regression equation:

(Note. Image generated using ChatGPT (OpenAI, 2026))

Linear regression diagnostic plots for the model

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

Conclusion

Cleaning explanation

The data-cleaning process was relatively straightforward. I started by identifying the missing values (NA) in my dataset using colSums(is.na(df)). This allowed me to check whether any variables contained missing observations. Since the dataset did not contain missing values, I did not need to remove any observations.

I then selected the variables needed for my analysis, including the three grade variables (G1, G2, and G3), studytime, failures, sex, absences, health, Medu, and Fedu. I also created a new categorical variable called studytime_new by using recode() to make the study-time categories easier to understand and interpret.

Visualizations explanation and interpretation

The first plot is a bar chart titled: Average final grade per studytime category. I created this plot by grouping students according to their study-time categories and calculating the average final grade (G3) for each group. I used geom_col() to represent the average grades, with the x-axis representing the study-time categories and the y-axis representing the average final grade. I also used scale_fill_brewer() with the Set3 palette to assign different colors to the bars. The plot suggests that the more longer the studytime, the greater the average grade is.

The second plot is a bar chart titled: Average Final Grade by Sex. I created this plot by grouping students according to sex and calculating the average final grade for each group. I used highcharter to create an interactive bar chart, with the x-axis representing sex and the y-axis representing the average final grade. I also used the Set1 color palette to distinguish the two groups. The chart suggests that male students tend to have higher grades than female students.

The third plot is a boxplot titled: Distribution of Students’ Grades Across Three Periods. I created this plot using the three grade variables (G1, G2, and G3). Since these variables were originally stored in separate columns, I used pivot_longer() to reorganize the dataset into a longer format. I then used geom_boxplot() to compare the distribution of grades across the three school periods. The visualization shows that the distribution of students’ grades across the three periods is generally constant.

Linear regression model interpretation

After examining the correlation between the variables using plot_correlation(), I decided to use multiple linear regression to investigate the relationship between the final grade (G3) and four explanatory variables: G2, G1, studytime, and failures.

The regression model shows that G2 and G1 have positive relationships with the final grade, while studytime and failures have negative relationships with G3.

The coefficient for G2 is approximately 0.97653, indicating that, when the other variables remain constant, a one unit increase in the second-period grade is associated with an increase of approximately 0.98 points in the final grade. The coefficient for G1 is 0.15034, indicating that a one unit increase in the first-period grade is associated with an increase of approximately 0.15 points in the final grade, holding the other variables constant. Plus, the p-values for G2 and G1 are respectively less than 0.001 and 0.01, indicating statistically significant relationships with the final grade.

On the other hand, the coefficient for studytime is -0.19667, while the coefficient for failures is -0.26300, meaning for every one unit increase in studytime there is a decrease of approximately 0.20, and a one unit increase in failure is associated with a decrease of approximately 0.26 in the final grade . However, their p-values are respectively 0.09706 and 0.06424, which are greater than 0.05. Therefore, these two variables are not statistically significant in this model.

The R-squared value of 0.8246 means that the model explains approximately 82.46% of the variation in students’ final grades. The p-value smaller than 2.2e-16. This indicates that the regression model is statistically significant overall.

Finally, the diagnostic plots show that the residuals are not normally distributed and that some observations have unusually large errors. The spread of the residuals also look somewhat uneven, suggesting that the model does not fully meet all regression assumptions.

In sum, the results suggest that previous academic performance, especially the second-period grade, is strongly associated with students' final grades, while studytime and failures do not show statistically significant relationships with G3. However, adding them to my model still allowed me to slightly increase its R-squared value, proving that they slightly impact students' final grades.

Summary of the model:

(Note. Image generated using ChatGPT (OpenAI, 2026))

Obstacles encountered and Solutions

During my analysis, I encountered some difficulties. The first was comparing the three grade variables (G1, G2, and G3) in a single visualization. Since these variables were stored in separate columns, I needed to reorganize the dataset before creating the graph. To address this issue, I used pivot_longer() to transform the data into a longer format, allowing me to represent the three grades in the same boxplot.

Resources

UCI Machine Learning Repository. (n.d.). https://archive.ics.uci.edu/dataset/320/student+performance

OpenAI. (2026). ChatGPT [AI image generation tool]. https://chatgpt.com/