Wine Quality Analysis

DATA 607: Week 1 Assignment Examplar

Author

DW

Published

September 1, 2026

Wine Quality Analysis

Approach

In this assignment, I selected a dataset from the UCI Machine Learning Repository that contains information about the chemical attributes of wine. The dataset contains 11 features and provides a useful opportunity to practice working with numerical data, feature selection, and classification or regression analysis.

My plan for this assignment is to load the data into R and create a label for the different wine types to use in a later classification task.

I will also perform some simple exploratory data analysis (EDA) on the datasets.

Data source:
https://raw.githubusercontent.com/DW8888/Wine-Quality/refs/heads/main/winequality-white.csv https://raw.githubusercontent.com/DW8888/Wine-Quality/refs/heads/main/winequality-red.csv

Cortez, P., Cerdeira, A., Almeida, F., Matos, T., & Reis, J. (2009). Wine Quality [Dataset]. UCI Machine Learning Repository. https://doi.org/10.24432/C56S3T.

Research Question

Can the chemical attributes of a wine be used to classify it as red or white?

Codebase

First, we need to import the library and datasets:

# Getting the tidyverse package
library("tidyverse")
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.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
# Importing the data: the dataset uses semicolons as delimiters, so I specify that in the read function.

red_wines<- read.csv("winequality-red.csv", sep=";")
white_wines<- read.csv("winequality-white.csv" , sep=";")

Let’s inspect the structure of the datasets.

str(red_wines)
'data.frame':   1599 obs. of  12 variables:
 $ fixed.acidity       : num  7.4 7.8 7.8 11.2 7.4 7.4 7.9 7.3 7.8 7.5 ...
 $ volatile.acidity    : num  0.7 0.88 0.76 0.28 0.7 0.66 0.6 0.65 0.58 0.5 ...
 $ citric.acid         : num  0 0 0.04 0.56 0 0 0.06 0 0.02 0.36 ...
 $ residual.sugar      : num  1.9 2.6 2.3 1.9 1.9 1.8 1.6 1.2 2 6.1 ...
 $ chlorides           : num  0.076 0.098 0.092 0.075 0.076 0.075 0.069 0.065 0.073 0.071 ...
 $ free.sulfur.dioxide : num  11 25 15 17 11 13 15 15 9 17 ...
 $ total.sulfur.dioxide: num  34 67 54 60 34 40 59 21 18 102 ...
 $ density             : num  0.998 0.997 0.997 0.998 0.998 ...
 $ pH                  : num  3.51 3.2 3.26 3.16 3.51 3.51 3.3 3.39 3.36 3.35 ...
 $ sulphates           : num  0.56 0.68 0.65 0.58 0.56 0.56 0.46 0.47 0.57 0.8 ...
 $ alcohol             : num  9.4 9.8 9.8 9.8 9.4 9.4 9.4 10 9.5 10.5 ...
 $ quality             : int  5 5 5 6 5 5 5 7 7 5 ...
str(white_wines)
'data.frame':   4898 obs. of  12 variables:
 $ fixed.acidity       : num  7 6.3 8.1 7.2 7.2 8.1 6.2 7 6.3 8.1 ...
 $ volatile.acidity    : num  0.27 0.3 0.28 0.23 0.23 0.28 0.32 0.27 0.3 0.22 ...
 $ citric.acid         : num  0.36 0.34 0.4 0.32 0.32 0.4 0.16 0.36 0.34 0.43 ...
 $ residual.sugar      : num  20.7 1.6 6.9 8.5 8.5 6.9 7 20.7 1.6 1.5 ...
 $ chlorides           : num  0.045 0.049 0.05 0.058 0.058 0.05 0.045 0.045 0.049 0.044 ...
 $ free.sulfur.dioxide : num  45 14 30 47 47 30 30 45 14 28 ...
 $ total.sulfur.dioxide: num  170 132 97 186 186 97 136 170 132 129 ...
 $ density             : num  1.001 0.994 0.995 0.996 0.996 ...
 $ pH                  : num  3 3.3 3.26 3.19 3.19 3.26 3.18 3 3.3 3.22 ...
 $ sulphates           : num  0.45 0.49 0.44 0.4 0.4 0.44 0.47 0.45 0.49 0.45 ...
 $ alcohol             : num  8.8 9.5 10.1 9.9 9.9 10.1 9.6 8.8 9.5 11 ...
 $ quality             : int  6 6 6 6 6 6 6 6 6 6 ...

Manipulation

I want to create a label that I can use to predict wine color.

# Create a factor column, assign 1 to red wine, and then combine the datasets.
red_wines$is_red <- factor(1, levels = c(0, 1), labels = c("white", "red"))

white_wines$is_red <- factor(0, levels = c(0, 1), labels = c("white", "red"))

wines <- rbind(red_wines, white_wines)
print(head(wines))
  fixed.acidity volatile.acidity citric.acid residual.sugar chlorides
1           7.4             0.70        0.00            1.9     0.076
2           7.8             0.88        0.00            2.6     0.098
3           7.8             0.76        0.04            2.3     0.092
4          11.2             0.28        0.56            1.9     0.075
5           7.4             0.70        0.00            1.9     0.076
6           7.4             0.66        0.00            1.8     0.075
  free.sulfur.dioxide total.sulfur.dioxide density   pH sulphates alcohol
1                  11                   34  0.9978 3.51      0.56     9.4
2                  25                   67  0.9968 3.20      0.68     9.8
3                  15                   54  0.9970 3.26      0.65     9.8
4                  17                   60  0.9980 3.16      0.58     9.8
5                  11                   34  0.9978 3.51      0.56     9.4
6                  13                   40  0.9978 3.51      0.56     9.4
  quality is_red
1       5    red
2       5    red
3       5    red
4       6    red
5       5    red
6       5    red

Here is a summary of the data:

summary(wines)
 fixed.acidity    volatile.acidity  citric.acid     residual.sugar  
 Min.   : 3.800   Min.   :0.0800   Min.   :0.0000   Min.   : 0.600  
 1st Qu.: 6.400   1st Qu.:0.2300   1st Qu.:0.2500   1st Qu.: 1.800  
 Median : 7.000   Median :0.2900   Median :0.3100   Median : 3.000  
 Mean   : 7.215   Mean   :0.3397   Mean   :0.3186   Mean   : 5.443  
 3rd Qu.: 7.700   3rd Qu.:0.4000   3rd Qu.:0.3900   3rd Qu.: 8.100  
 Max.   :15.900   Max.   :1.5800   Max.   :1.6600   Max.   :65.800  
   chlorides       free.sulfur.dioxide total.sulfur.dioxide    density      
 Min.   :0.00900   Min.   :  1.00      Min.   :  6.0        Min.   :0.9871  
 1st Qu.:0.03800   1st Qu.: 17.00      1st Qu.: 77.0        1st Qu.:0.9923  
 Median :0.04700   Median : 29.00      Median :118.0        Median :0.9949  
 Mean   :0.05603   Mean   : 30.53      Mean   :115.7        Mean   :0.9947  
 3rd Qu.:0.06500   3rd Qu.: 41.00      3rd Qu.:156.0        3rd Qu.:0.9970  
 Max.   :0.61100   Max.   :289.00      Max.   :440.0        Max.   :1.0390  
       pH          sulphates         alcohol         quality        is_red    
 Min.   :2.720   Min.   :0.2200   Min.   : 8.00   Min.   :3.000   white:4898  
 1st Qu.:3.110   1st Qu.:0.4300   1st Qu.: 9.50   1st Qu.:5.000   red  :1599  
 Median :3.210   Median :0.5100   Median :10.30   Median :6.000               
 Mean   :3.219   Mean   :0.5313   Mean   :10.49   Mean   :5.818               
 3rd Qu.:3.320   3rd Qu.:0.6000   3rd Qu.:11.30   3rd Qu.:6.000               
 Max.   :4.010   Max.   :2.0000   Max.   :14.90   Max.   :9.000               
numeric_vars <- names(wines)[sapply(wines, is.numeric)]

par(mfrow = c(5, 3), mar = c(3, 3, 2,1))
# Iterate through the list and plot the variables.

for (variable in numeric_vars) {
  hist(
    wines[[variable]],
    main = variable,
    xlab = "",
    col = "skyblue",
    border = "white"
  )
}

barplot(
  table(wines$is_red),
  main = "Wine type",
  xlab = "is_red",
  ylab = "Count",
  col = c("lightyellow", "maroon"))

Tidyverse Comparison

The following summary compares the number of wines, average alcohol content, and average quality for each wine type.

wines |>
  group_by(is_red) |>
  summarise(
    number_of_wines = n(),
    average_alcohol = round(mean(alcohol), 2),
    average_quality = round(mean(quality), 2)
  )
# A tibble: 2 × 4
  is_red number_of_wines average_alcohol average_quality
  <fct>            <int>           <dbl>           <dbl>
1 white             4898            10.5            5.88
2 red               1599            10.4            5.64
ggplot(wines, aes(x = is_red, y = alcohol, fill = is_red)) +
  geom_boxplot() +
  scale_fill_manual(values = c('white' = 'lightyellow', 'red' = 'maroon')) +
  labs(
    title = "Alcohol Content by Wine Type",
    x = "Wine type",
    y = "Alcohol content"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Thoughts


The dataset has no missing values and contains 11 numerical variables for each wine. I added a new variable to indicate whether the wine is red or white. Later, I plan to build a classification model that predicts whether a wine is red or white based on its features. It is important to note that the dataset has a class imbalance between red and white wines, which I will need to account for.
Many of the variables do not follow a normal distribution and would have to be normalized if we would like to do some sort of linear regression modeling.