Exploring Chemical Differences Among Wine Classes

Author

Zixuan Li

Introduction

Wine can vary quite a bit in its chemical composition, even when the samples belong to the same broad category. For this project, I wanted to see whether three classes of wine show noticeable differences in alcohol content, flavanoids, and color intensity. My main question is: How do alcohol content, flavanoid levels, and color intensity differ among the three wine classes, and how are these measurements related?

The dataset contains 178 wine samples, 13 numerical chemical measurements, and one categorical variable identifying the wine class. wine_class identifies the three groups. alcohol records alcohol content, flavanoids is a measurement of a group of phenolic compounds, and color_intensity measures the strength of the wine’s color. I focus on these three measurements because they give us a manageable way to compare the groups and explore relationships among numerical variables.

Data source: UCI Machine Learning Repository, Wine dataset, originally based on chemical analysis of wines from the same region of Italy. Dataset information: https://archive.ics.uci.edu/dataset/109/wine . The CSV used here is a classroom/project copy with descriptive column names and wine classes labeled Class_1, Class_2, and Class_3. UCI is the repository hosting the dataset, not the original laboratory source. Forina et al. are credited in the dataset documentation. The project uses the approved CSV supplied with this submission.

1. Load packages and import the data

The next chunk loads the packages needed for reading the CSV, summarizing observations, and making graphs. The dataset is loaded with readr::read_csv() as required.

# Install required packages automatically if they are not already installed.
# This only installs missing packages, so future renders will be faster.
required_packages <- c("readr", "dplyr", "ggplot2")
missing_packages <- required_packages[!vapply(required_packages, requireNamespace,
                                               logical(1), quietly = TRUE)]
if (length(missing_packages) > 0) {
  install.packages(missing_packages, repos = "https://cloud.r-project.org")
}

# Load packages used for data import, summaries, and visualization.
library(readr)
library(dplyr)
library(ggplot2)

# Read the approved CSV file from the same folder as this .qmd file.
wine <- readr::read_csv("Zixuan_Li_Project1_Wine_Dataset.csv", show_col_types = FALSE)

# Check the number of observations, variable names, and first few rows.
dim(wine)
[1] 178  14
names(wine)
 [1] "wine_class"                   "alcohol"                     
 [3] "malic_acid"                   "ash"                         
 [5] "alcalinity_of_ash"            "magnesium"                   
 [7] "total_phenols"                "flavanoids"                  
 [9] "nonflavanoid_phenols"         "proanthocyanins"             
[11] "color_intensity"              "hue"                         
[13] "od280_od315_of_diluted_wines" "proline"                     
head(wine)
# A tibble: 6 × 14
  wine_class alcohol malic_acid   ash alcalinity_of_ash magnesium total_phenols
  <chr>        <dbl>      <dbl> <dbl>             <dbl>     <dbl>         <dbl>
1 Class_1       14.2       1.71  2.43              15.6       127          2.8 
2 Class_1       13.2       1.78  2.14              11.2       100          2.65
3 Class_1       13.2       2.36  2.67              18.6       101          2.8 
4 Class_1       14.4       1.95  2.5               16.8       113          3.85
5 Class_1       13.2       2.59  2.87              21         118          2.8 
6 Class_1       14.2       1.76  2.45              15.2       112          3.27
# ℹ 7 more variables: flavanoids <dbl>, nonflavanoid_phenols <dbl>,
#   proanthocyanins <dbl>, color_intensity <dbl>, hue <dbl>,
#   od280_od315_of_diluted_wines <dbl>, proline <dbl>

2. Check and clean the dataset

Before making the graphs, I checked for missing values and repeated rows. I also converted the wine class to a factor, since it represents groups rather than a numerical scale. No observations needed to be removed from this copy of the dataset.

# Count missing values and completely duplicated rows before cleaning.
sum(is.na(wine))
[1] 0
sum(duplicated(wine))
[1] 0
# Store the wine classes as an ordered set of categorical labels.
wine <- wine %>%
  mutate(wine_class = factor(wine_class,
                             levels = c("Class_1", "Class_2", "Class_3")))

# Confirm that each group is represented in the dataset.
wine %>% count(wine_class)
# A tibble: 3 × 2
  wine_class     n
  <fct>      <int>
1 Class_1       59
2 Class_2       71
3 Class_3       48

3. Explore the data

To get a clearer picture of the three groups, I calculated the average alcohol content, flavanoid level, and color intensity for each class. These summaries help explain the graphs that follow.

# Group by wine class and calculate descriptive statistics.
wine_summary <- wine %>%
  group_by(wine_class) %>%
  summarize(
    samples = n(),
    mean_alcohol = mean(alcohol, na.rm = TRUE),
    mean_flavanoids = mean(flavanoids, na.rm = TRUE),
    mean_color_intensity = mean(color_intensity, na.rm = TRUE),
    .groups = "drop"
  )

# Display rounded group averages for easier comparison.
wine_summary %>%
  mutate(across(starts_with("mean_"), ~ round(.x, 2)))
# A tibble: 3 × 5
  wine_class samples mean_alcohol mean_flavanoids mean_color_intensity
  <fct>        <int>        <dbl>           <dbl>                <dbl>
1 Class_1         59         13.7            2.98                 5.53
2 Class_2         71         12.3            2.08                 3.09
3 Class_3         48         13.2            0.78                 7.4 

4. Visualization 1: Alcohol content by wine class

This side-by-side boxplot compares the distribution of alcohol content in the three wine classes. The middle line in each box represents the median, and the box shows the middle half of the values. The colors make the categories easy to tell apart.

# Make a boxplot to compare alcohol distributions across the three groups.
# Use three manually selected colors, a non-default theme, labels, and a legend.
ggplot(wine, aes(x = wine_class, y = alcohol, fill = wine_class)) +
  geom_boxplot(alpha = 0.85, width = 0.62) +
  scale_fill_manual(values = c("Class_1" = "#356A8A",
                               "Class_2" = "#D18A45",
                               "Class_3" = "#7B5C9B"),
                    name = "Wine class") +
  labs(title = "Alcohol Content Across Three Wine Classes",
       x = "Wine class", y = "Alcohol content",
       caption = "Source: UCI Machine Learning Repository, Wine dataset") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "right")

Class 1 has the highest average alcohol content (about 13.75), followed by Class 3 (about 13.15). Class 2 has the lowest average (about 12.28). The distributions overlap, so alcohol alone would not perfectly identify the class, but the differences are noticeable.

5. Visualization 2: Flavanoids and color intensity

The scatterplot below shows one point per wine sample. I used color to identify the class and added a separate fitted trend line for each class. This lets me look at both the overall spread and possible differences between the groups.

# Plot the relationship between flavanoids and color intensity.
# Colored points and fitted lines identify the three categorical groups.
ggplot(wine, aes(x = flavanoids, y = color_intensity,
                 color = wine_class)) +
  geom_point(alpha = 0.75, size = 2) +
  geom_smooth(method = "lm", se = FALSE, linewidth = 0.9) +
  scale_color_manual(values = c("Class_1" = "#356A8A",
                                "Class_2" = "#D18A45",
                                "Class_3" = "#7B5C9B"),
                     name = "Wine class") +
  labs(title = "Flavanoids and Color Intensity by Wine Class",
       x = "Flavanoids", y = "Color intensity",
       caption = "Source: UCI Machine Learning Repository, Wine dataset") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "right")

Class 3 generally has lower flavanoid levels and higher color intensity than the other two groups. Class 2 tends to have lower color intensity, while Class 1 tends to have higher flavanoid levels. The class-specific lines should be interpreted separately: relationships within a class do not necessarily match the pattern across all classes combined.

6. Multiple linear regression

I used a multiple linear regression to see how alcohol and flavanoids are associated with color intensity. The response variable is color_intensity, and the two explanatory variables are alcohol and flavanoids. This is an association model, not evidence that either chemical measurement causes a change in color intensity.

# Fit a multiple linear regression predicting color intensity.
wine_model <- lm(color_intensity ~ alcohol + flavanoids, data = wine)

# Display coefficients, model fit, and statistical test results.
summary(wine_model)

Call:
lm(formula = color_intensity ~ alcohol + flavanoids, data = wine)

Residuals:
    Min      1Q  Median      3Q     Max 
-3.0706 -1.3723 -0.2957  1.0071  5.7647 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) -16.5307     2.2010  -7.510 2.90e-12 ***
alcohol       1.7764     0.1728  10.279  < 2e-16 ***
flavanoids   -0.7420     0.1405  -5.283 3.75e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 1.813 on 175 degrees of freedom
Multiple R-squared:  0.395, Adjusted R-squared:  0.3881 
F-statistic: 57.13 on 2 and 175 DF,  p-value: < 2.2e-16
# Extract the intercept and slopes for the regression equation.
coef(wine_model)
(Intercept)     alcohol  flavanoids 
-16.5307306   1.7764174  -0.7419925 

The fitted equation, rounded to three decimals, is:

[ = -16.531 + 1.776() - 0.742() ]

With flavanoids held constant, a one-unit increase in alcohol is associated with an estimated 1.776-unit increase in color intensity. With alcohol held constant, a one-unit increase in flavanoids is associated with an estimated 0.742-unit decrease in color intensity. The model’s (R^2) is approximately 0.395, meaning these two predictors explain about 39.5% of the variation in color intensity in this sample. Both slope estimates have small p-values in the model output, although the model does not establish causation. Because the wine classes differ substantially, class membership may also be important in understanding the relationship.

7. Final reflection

I started by checking the dataset structure, looking for missing values, and checking whether any complete rows were repeated. There were no missing values or duplicated rows, so I did not have to delete or replace any observations. The main preparation step was converting wine_class from text to a factor so R would treat it as a categorical variable. I also used group_by() and summarize() to calculate the averages for each class before making the graphs.

The boxplot showed me that Class 1 generally has the highest alcohol content and Class 2 has the lowest. I expected some variation, but I was surprised by how different the three groups looked in the scatterplot. Class 3 stood out because its flavanoid levels were usually lower while its color intensity was often higher. The regression gave me another way to study these measurements together. Its (R^2) of about 0.395 suggests alcohol and flavanoids explain part of the variation, but there is still plenty that this simple model does not capture.

If I had more time, I would want to compare additional chemical measurements and investigate whether a model that includes wine class would fit better. I would also like to explore whether the patterns stay similar when the data are analyzed within each class. Overall, this project helped me practice checking a real dataset, choosing graphs that match the variables, and explaining results without assuming that correlation proves causation.