Load library tidyverse in order to access dplyr and ggplot2
library(tidyverse)
The source for this dataset is the New York State Department of Conservation and the National Weather Service of 1973 for five months from May to September recorded daily.
Because airquality is a pre-built dataset, we can write it to our data directory to store it for later use.
data("airquality")
str(airquality)
## 'data.frame': 153 obs. of 6 variables:
## $ Ozone : int 41 36 12 18 NA 28 23 19 8 NA ...
## $ Solar.R: int 190 118 149 313 NA NA 299 99 19 194 ...
## $ Wind : num 7.4 8 12.6 11.5 14.3 14.9 8.6 13.8 20.1 8.6 ...
## $ Temp : int 67 72 74 62 56 66 65 59 61 69 ...
## $ Month : int 5 5 5 5 5 5 5 5 5 5 ...
## $ Day : int 1 2 3 4 5 6 7 8 9 10 ...
The function, head, will only display the first 6 rows of the dataset. There are 153 observations (rows) and 6 variables.
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
Notice that all the variables are classified as either integers
If you want to look at specific statistics, here are some variations on coding. Here are 2 different ways to calculate “mean.”
mean(airquality$Temp)
## [1] 77.88235
mean(airquality[,4])
## [1] 77.88235
For the second way to calculate the mean, the matrix \[row,column\] is looking for column #4, which is the Temp column and we use all rows
median(airquality$Temp)
## [1] 79
sd(airquality$Wind)
## [1] 3.523001
var(airquality$Wind)
## [1] 12.41154
Sometimes we prefer the months to be numerical, but here, we need them as the month names. There are MANY ways to do this. Here is one way to convert numbers 5 - 9 to May through September
airquality1 <- airquality |>
mutate(month_name = case_when(Month == 4 ~ "April",
Month == 5 ~ "May",
Month == 6 ~ "June",
Month == 7 ~ "July",
Month == 8 ~ "August",
Month == 9 ~ "September"))
See how Month has changed to have characters instead of numbers (it is now classified as “character” rather than “integer”) in the new airquality1 dataframe.
summary(airquality1$month_name)
## Length N.unique N.blank Min.nchar Max.nchar
## 153 5 0 3 9
This is one way to reorder the Months so they do not default to alphabetical (you will see another way to reorder DIRECTLY in the chunk that creates the plot below in Plot #1
airquality1$Month <- factor(airquality1$month_name,
levels=c("May", "June","July", "August",
"September"))
Here is a first attempt at viewing a histogram of temperature by the months May through September. We will see that temperatures increase over these months. The median temperature appears to be about 75 degrees.
fill = Month colors the histogram by months between May - Sept.
scale_fill_discrete(name = “Month”…) provides the month names on the right side as a legend in chronological order. This is a different way to order than what was shown above.
labs allows us to add a title, axes labels, and a caption for the data source
p1 <- airquality1 |>
ggplot(aes(x=Temp, fill=month_name)) +
geom_histogram(position="identity")+
scale_fill_discrete(name = "Month",
labels = c("May", "June","July", "August", "September")) +
labs(x = "Monthly Temperatures from May - Sept",
y = "Frequency of Temps",
title = "Histogram of Monthly Temperatures from May - Sept, 1973",
caption = "New York State Department of Conservation and the National Weather Service")
p1
Is this plot useful in answering questions about monthly temperature
values? Not very. With position = "identity" the five
months are drawn directly on top of one another with no transparency, so
the bars in front hide the bars behind them and it is impossible to read
any single month’s distribution.
Outline the bars in white using the color = “white” command
Use alpha to add some transparency (values between 0 and 1)
Change the binwidth
Add some transparency and white borders around the histogram bars.
p2 <- airquality1 |>
ggplot(aes(x=Temp, fill=month_name)) +
geom_histogram(position="identity", alpha=0.5, binwidth = 5, color = "white")+
scale_fill_discrete(name = "Month", labels = c("May", "June","July", "August", "September")) +
labs(x = "Monthly Temperatures from May - Sept",
y = "Frequency of Temps",
title = "Histogram of Monthly Temperatures from May - Sept, 1973",
caption = "New York State Department of Conservation and the National Weather Service")
p2
Here July stands out for having high frequency of 85 degree temperatures. The dark purple color indicates overlaps of months due to the transparency.
Did this improve the readability of the plot? Yes — the transparency and the wider bins make the overlapping months much easier to separate than in Plot 1.
We can see that August has the highest temperatures based on the boxplot distribution.
p3 <- airquality1 |>
ggplot(aes(Month, Temp, fill = month_name)) +
labs(x = "Months from May through September", y = "Temperatures",
title = "Side-by-Side Boxplot of Monthly Temperatures",
caption = "New York State Department of Conservation and the National Weather Service") +
geom_boxplot() +
scale_fill_discrete(name = "Month", labels = c("May", "June","July", "August", "September"))
p3
Notice that the points above and below the boxplots in June and July are outliers.
Make the same side-by-side boxplots, but in grey-scale
Use the scale_fill_grey command for the grey-scale legend, and again, use fill=Month in the aesthetics.
Here we just changed the color palette to gray scale using scale_fill_grey
p4 <- airquality1 |>
ggplot(aes(Month, Temp, fill = month_name)) +
labs(x = "Monthly Temperatures", y = "Temperatures",
title = "Side-by-Side Boxplot of Monthly Temperatures",
caption = "New York State Department of Conservation and the National Weather Service") +
geom_boxplot()+
scale_fill_grey(name = "Month", labels = c("May", "June","July", "August", "September"))
p4
While reproducing the plots above I found a bug that runs through all four of them, and fixing it is the meaningful change I am submitting.
Every plot maps fill = month_name, which is a
character variable. ggplot2 sorts a character variable
alphabetically, so the fill groups are ordered:
levels(factor(airquality1$month_name))
## [1] "August" "July" "June" "May" "September"
That is August, July, June, May, September — not chronological. The
tutorial code then overrides the legend text with
labels = c("May", "June", "July", "August", "September"),
which does not reorder anything. It simply renames the
alphabetical groups in that order, so the August group gets labeled
“May,” the July group gets labeled “June,” and so on. Every legend in
Plots 1 through 4 is wrong, and the colors do not match the months they
claim to.
The fix is to map fill to the Month factor
we already built with the correct chronological levels, and then delete
the manual labels argument so ggplot2 uses the real level
names:
p2_fixed <- airquality1 |>
ggplot(aes(x = Temp, fill = Month)) +
geom_histogram(position = "identity", alpha = 0.5, binwidth = 5, color = "white") +
scale_fill_brewer(name = "Month", palette = "YlOrRd") +
labs(x = "Temperature (degrees Fahrenheit)",
y = "Number of days",
title = "Daily Temperatures by Month, May - September 1973",
subtitle = "Legend now in chronological order, with a palette that runs cool to warm",
caption = "New York State Department of Conservation and the National Weather Service")
p2_fixed
Two further improvements are included here. The legend is now in
calendar order, which lets a reader track the seasonal progression down
the legend instead of hunting through an alphabetical list. And
scale_fill_brewer(palette = "YlOrRd") replaces the default
rainbow hues with a sequential yellow-to-red ramp, so warmer months are
literally warmer colors. Month is ordered, so a sequential palette
matches the data better than unordered categorical colors.
The same correction applied to the boxplots:
p3_fixed <- airquality1 |>
ggplot(aes(x = Month, y = Temp, fill = Month)) +
geom_boxplot() +
scale_fill_brewer(name = "Month", palette = "YlOrRd") +
labs(x = "Month", y = "Temperature (degrees Fahrenheit)",
title = "Side-by-Side Boxplots of Monthly Temperatures, 1973",
subtitle = "July and August are the warmest months; May is both coolest and most variable",
caption = "New York State Department of Conservation and the National Weather Service")
p3_fixed
Now the box colors and the legend agree with the x-axis.
For my own plot I moved away from temperature-by-month and looked
instead at the relationship between two numerical variables we had not
yet explored: Ozone (parts per billion) and
Temp, with Wind speed added as a third
dimension.
Ozone has missing values, so I filtered them out first
and reported how many were dropped.
sum(is.na(airquality$Ozone))
## [1] 37
p5 <- airquality1 |>
filter(!is.na(Ozone)) |>
ggplot(aes(x = Temp, y = Ozone)) +
geom_point(aes(color = Month, size = Wind), alpha = 0.75) +
geom_smooth(method = "loess", se = TRUE, color = "grey25", linewidth = 0.8) +
scale_color_brewer(name = "Month", palette = "YlOrRd") +
scale_size_continuous(name = "Wind (mph)", range = c(1.5, 6)) +
labs(x = "Temperature (degrees Fahrenheit)",
y = "Ozone (parts per billion)",
title = "Ozone Rises Sharply with Temperature in New York, 1973",
subtitle = "Each point is one day; larger points are windier days. 37 days with missing ozone readings excluded.",
caption = "New York State Department of Conservation and the National Weather Service")
p5
## `geom_smooth()` using formula = 'y ~ x'
The plot type. Plot 5 is a scatterplot of two continuous numerical variables, ozone concentration against temperature, with a LOESS smoothing curve laid over it. It is meaningfully different from Plots 1 through 4: those were univariate displays of a single variable (temperature) broken out by a categorical variable (month), using histograms and boxplots. This plot shows the relationship between two numerical variables, which neither a histogram nor a boxplot can do. It also encodes four variables at once — temperature on the x-axis, ozone on the y-axis, month as color, and wind speed as point size — which follows the “show multivariate data” principle from the Unit 1 notes.
Insights. The relationship is clearly positive but not linear. Below roughly 80 degrees, ozone stays low and fairly flat, mostly under 50 ppb. Above 80 degrees the curve bends sharply upward and the spread widens dramatically, with the hottest days reaching over 150 ppb. That accelerating pattern is why the LOESS curve is more honest here than a straight regression line would be: a line would understate ozone on the hottest days and overstate it on cool ones.
The color and size channels add a second finding. The high-ozone points are almost entirely July and August, which is what the earlier boxplots would predict since those were the warmest months. The size channel shows the opposite relationship for wind: the large points, meaning windy days, cluster along the bottom of the plot at low ozone, while the extreme ozone days are small points, meaning nearly still air. That makes physical sense, since wind disperses pollutants while stagnant hot air lets ozone accumulate. Heat alone does not produce a bad air day; heat plus calm does.
One honest limitation: 37 of the 153 days have no ozone reading and are dropped here. If those readings failed disproportionately on particular kinds of days, the pattern could be biased in a way this plot cannot reveal.
Special code. Several things beyond the tutorial’s toolkit:
filter(!is.na(Ozone)) removes the 37 missing ozone
readings up front, so the count is reported explicitly rather than
appearing as a silent ggplot2 warning.geom_smooth(method = "loess", se = TRUE) fits a locally
weighted curve with a shaded confidence band. Unlike
geom_histogram or geom_boxplot, this geom
computes a model from the data and plots its predictions.size = Wind inside aes() adds a
continuous third variable, and
scale_size_continuous(range = c(1.5, 6)) controls the
point-size span so the smallest points stay visible while the largest do
not swamp the plot.alpha = 0.75 keeps overlapping points readable in the
dense low-temperature cluster.scale_color_brewer(palette = "YlOrRd") reuses the
cool-to-warm palette from my corrected plots so that color means the
same thing throughout the document.