Install all required libraries

#install.packages(c("ggplot2", "patchwork", "knitr"))

1. Introduction

We use the built-in airquality dataset in R.
The goal is to predict Ozone levels using the other weather variables.

Variable Meaning
Ozone Ozone concentration (ppb) — outcome
Solar.R Solar radiation (lang)
Wind Wind speed (mph)
Temp Temperature (°F)
Month Month (5 = May … 9 = September)
Day Day of the month

2. Load and Explore the Data

# Load the airquality dataset that is built into R
data(airquality)

# Show the first 6 rows so we can see what the data looks like
head(airquality)
##   Ozone Solar.R Wind Temp Month Day
## 1    41     190  7.4   67     5   1
## 2    36     118  8.0   72     5   2
## 3    12     149 12.6   74     5   3
## 4    18     313 11.5   62     5   4
## 5    NA      NA 14.3   56     5   5
## 6    28      NA 14.9   66     5   6
# Show basic statistics (min, max, mean, missing values) for each column
summary(airquality)
##      Ozone           Solar.R           Wind             Temp      
##  Min.   :  1.00   Min.   :  7.0   Min.   : 1.700   Min.   :56.00  
##  1st Qu.: 18.00   1st Qu.:115.8   1st Qu.: 7.400   1st Qu.:72.00  
##  Median : 31.50   Median :205.0   Median : 9.700   Median :79.00  
##  Mean   : 42.13   Mean   :185.9   Mean   : 9.958   Mean   :77.88  
##  3rd Qu.: 63.25   3rd Qu.:258.8   3rd Qu.:11.500   3rd Qu.:85.00  
##  Max.   :168.00   Max.   :334.0   Max.   :20.700   Max.   :97.00  
##  NAs    :37       NAs    :7                                       
##      Month            Day      
##  Min.   :5.000   Min.   : 1.0  
##  1st Qu.:6.000   1st Qu.: 8.0  
##  Median :7.000   Median :16.0  
##  Mean   :6.993   Mean   :15.8  
##  3rd Qu.:8.000   3rd Qu.:23.0  
##  Max.   :9.000   Max.   :31.0  
## 

3. Handle Missing Values

# Count how many missing values exist in each column
colSums(is.na(airquality))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##      37       7       0       0       0       0
# Remove rows that have any missing value so the model can run cleanly
air_clean <- na.omit(airquality)

# Confirm how many rows we have after removing missing values
nrow(air_clean)
## [1] 111

4. Explore the Data Visually

# Load ggplot2 for nicer charts
library(ggplot2)

# Load patchwork so we can display multiple charts side by side
library(patchwork)

# Chart 1: Ozone vs Temperature — we expect a positive relationship
p1 <- ggplot(air_clean, aes(x = Temp, y = Ozone)) +
  geom_point(color = "steelblue", alpha = 0.7) +   # draw scatter points
  geom_smooth(method = "lm", color = "red", se = TRUE) + # add a trend line
  labs(title = "Ozone vs Temperature",
       x = "Temperature (°F)", y = "Ozone (ppb)") +
  theme_minimal()

# Chart 2: Ozone vs Wind — we expect a negative relationship
p2 <- ggplot(air_clean, aes(x = Wind, y = Ozone)) +
  geom_point(color = "darkorange", alpha = 0.7) +
  geom_smooth(method = "lm", color = "red", se = TRUE) +
  labs(title = "Ozone vs Wind",
       x = "Wind Speed (mph)", y = "Ozone (ppb)") +
  theme_minimal()

# Chart 3: Ozone vs Solar Radiation
p3 <- ggplot(air_clean, aes(x = Solar.R, y = Ozone)) +
  geom_point(color = "forestgreen", alpha = 0.7) +
  geom_smooth(method = "lm", color = "red", se = TRUE) +
  labs(title = "Ozone vs Solar Radiation",
       x = "Solar Radiation (lang)", y = "Ozone (ppb)") +
  theme_minimal()

# Chart 4: Histogram of Ozone — check if it is roughly bell-shaped
p4 <- ggplot(air_clean, aes(x = Ozone)) +
  geom_histogram(bins = 20, fill = "purple", alpha = 0.7, color = "white") +
  labs(title = "Distribution of Ozone", x = "Ozone (ppb)", y = "Count") +
  theme_minimal()

# Display all four charts in a 2x2 grid
(p1 | p2) / (p3 | p4)


5. Check Correlations

# Compute the correlation between Ozone and every other numeric variable
# Values close to +1 or -1 show a strong relationship
cor(air_clean[, c("Ozone", "Solar.R", "Wind", "Temp")],
    use = "complete.obs")
##              Ozone    Solar.R       Wind       Temp
## Ozone    1.0000000  0.3483417 -0.6124966  0.6985414
## Solar.R  0.3483417  1.0000000 -0.1271835  0.2940876
## Wind    -0.6124966 -0.1271835  1.0000000 -0.4971897
## Temp     0.6985414  0.2940876 -0.4971897  1.0000000

6. Fit the Multiple Linear Regression Model

# Build the multiple linear regression model
# Ozone is the outcome (left of ~)
# Solar.R, Wind, and Temp are the predictors (right of ~)
model <- lm(Ozone ~ Solar.R + Wind + Temp, data = air_clean)

# Show the full model results (coefficients, p-values, R-squared, etc.)
summary(model)
## 
## Call:
## lm(formula = Ozone ~ Solar.R + Wind + Temp, data = air_clean)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -40.485 -14.219  -3.551  10.097  95.619 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -64.34208   23.05472  -2.791  0.00623 ** 
## Solar.R       0.05982    0.02319   2.580  0.01124 *  
## Wind         -3.33359    0.65441  -5.094 1.52e-06 ***
## Temp          1.65209    0.25353   6.516 2.42e-09 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 21.18 on 107 degrees of freedom
## Multiple R-squared:  0.6059, Adjusted R-squared:  0.5948 
## F-statistic: 54.83 on 3 and 107 DF,  p-value: < 2.2e-16

7. Interpret the Results

Coefficients Table

Predictor Coefficient Plain-English Meaning
Intercept -64.342 Predicted Ozone when all predictors = 0
Solar.R 0.06 Each extra lang of solar radiation adds 0.06 ppb of Ozone
Wind -3.334 Each extra mph of wind reduces Ozone by 3.334 ppb
Temp 1.652 Each extra °F of temperature adds 1.652 ppb of Ozone

Key take-aways from the summary output:

  • Solar.R — small positive effect; statistically significant (p < 0.05).
    More sunshine → slightly more Ozone.

  • Wind — strong negative effect; highly significant (p < 0.001).
    Stronger winds blow Ozone away, lowering its concentration.

  • Temp — strong positive effect; highly significant (p < 0.001).
    Hotter days produce more Ozone (heat drives chemical reactions in the air).

  • R² ≈ 0.61 — the three predictors together explain about 61 % of the variation in Ozone levels.


8. Check Model Assumptions

# The four standard diagnostic plots for a linear regression model
# They help us check whether the model assumptions are met
par(mfrow = c(2, 2))   # arrange 4 plots in a 2x2 grid

# Plot 1 – Residuals vs Fitted: checks if the relationship is truly linear
# Plot 2 – Q-Q plot: checks if residuals are normally distributed
# Plot 3 – Scale-Location: checks if variance is roughly constant
# Plot 4 – Residuals vs Leverage: spots influential outliers
plot(model)

# Reset the plot layout back to a single panel
par(mfrow = c(1, 1))

What to look for:

Plot What we want to see
Residuals vs Fitted Points scattered randomly around the 0 line
Normal Q-Q Points lying close to the diagonal line
Scale-Location A roughly flat red line (constant spread)
Residuals vs Leverage No points far outside Cook’s distance lines

9. Make Predictions

# Create a small table of new weather conditions we want to predict for
new_days <- data.frame(
  Solar.R = c(150, 200, 250),   # three different solar radiation levels
  Wind    = c(10,   8,   5),    # three different wind speeds
  Temp    = c(75,  80,  90)     # three different temperatures
)

# Use the fitted model to predict Ozone for these new days
predictions <- predict(model,
                       newdata  = new_days,
                       interval = "confidence",  # add 95 % confidence bounds
                       level    = 0.95)

# Combine the new conditions with the predictions for easy reading
result_table <- cbind(new_days, round(predictions, 1))

# Rename columns so they are easy to understand
colnames(result_table) <- c("Solar.R", "Wind", "Temp",
                            "Predicted Ozone", "Lower 95%", "Upper 95%")

# Print the final prediction table
knitr::kable(result_table, caption = "Predicted Ozone levels for new weather conditions")
Predicted Ozone levels for new weather conditions
Solar.R Wind Temp Predicted Ozone Lower 95% Upper 95%
150 10 75 35.2 30.8 39.6
200 8 80 53.1 48.6 57.7
250 5 90 82.6 75.1 90.1

10. Conclusion

Using multiple linear regression on the airquality dataset, we found that:

  1. Temperature is the strongest predictor — hotter days lead to much higher Ozone.
  2. Wind has a strong negative effect — windy days clear the air.
  3. Solar radiation has a small but significant positive effect.
  4. Together, these three variables explain roughly 61 % of the variation in Ozone, which is a reasonably good fit for real-world environmental data.

The model diagnostics show acceptable (though not perfect) behaviour — there is slight non-linearity and a few high-leverage points, which is normal for atmospheric data.