1 Introduction

This document works through the five independent exercises from the Data Visualization Practicum. Each section calculates or prepares the data needed, builds the requested plot with ggplot2, and closes with a short, plain language read of what the plot is actually telling us not just a restatement of the numbers.


2 Exercise 1 — Comparison: Average City Fuel Efficiency by Manufacturer (mpg)

Task: Calculate the average cty for each manufacturer, keep the top 10, build a comparison plot, and summarize the finding.

mpg_cty <- mpg %>%
  group_by(manufacturer) %>%
  summarise(
    average_cty = mean(cty),
    n_models = n(),
    .groups = "drop"
  ) %>%
  arrange(desc(average_cty)) %>%
  slice_head(n = 10)

mpg_cty
## # A tibble: 10 × 3
##    manufacturer average_cty n_models
##    <chr>              <dbl>    <int>
##  1 honda               24.4        9
##  2 volkswagen          20.9       27
##  3 subaru              19.3       14
##  4 hyundai             18.6       14
##  5 toyota              18.5       34
##  6 nissan              18.1       13
##  7 audi                17.6       18
##  8 pontiac             17          5
##  9 chevrolet           15         19
## 10 ford                14         25
ggplot(data = mpg_cty,
       aes(x = reorder(manufacturer, average_cty),
           y = average_cty)) +
  geom_col(fill = "#2C7FB8") +
  geom_text(aes(label = round(average_cty, 1)),
            hjust = -0.2, size = 3.5) +
  coord_flip(clip = "off") +
  labs(
    title = "Top 10 Manufacturers by Average City Fuel Efficiency",
    subtitle = "Based on the mpg dataset",
    x = NULL,
    y = "Average city mileage (mpg)"
  ) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"))

What this is actually telling us:

Honda’s clearly out in front here — nobody else in the top 10 gets close to 24 mpg. After that it’s a pretty steady slide down to Ford at 14. That makes sense - the top of the list is dominated by compact, economy focused lineups, and the further down you go the more trucks and SUVs start pulling the average down. One thing to keep in mind, this is just an average of whatever models each brand happens to have in the dataset, so a manufacturer with only a couple of small cars in the sample can look better than it “really” is.


3 Exercise 2 — Distribution: Diamond Carat by Cut Quality (diamonds)

Task: Choose one numerical variable, compare its distribution across one categorical variable, improve the plot’s appearance, and interpret the pattern.

Here we look at carat (diamond weight) split by cut quality, using a ridgeline plot so the shapes of the distributions are easy to line up against each other.

ggplot(data = diamonds,
       aes(x = carat,
           y = cut,
           fill = cut)) +
  geom_density_ridges(
    alpha = 0.8,
    scale = 1.1,
    color = "white",
    show.legend = FALSE
  ) +
  scale_fill_viridis_d(option = "magma", begin = 0.15, end = 0.9) +
  scale_x_continuous(limits = c(0, 3)) +
  labs(
    title = "Distribution of Diamond Carat Weight by Cut Quality",
    subtitle = "Heavier stones are noticeably rarer among the best cuts",
    x = "Carat",
    y = "Cut quality"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold"),
    panel.grid.minor = element_blank()
  )

What this is actually telling us:

Ideal of cut diamonds are clearly bunched up under 1 carat sharp peak, quick drop off. As you move down toward Good and Fair, that peak spreads out and slides right, meaning more of the big stones end up with lower cut grades. That tracks with how cutting actually works, turning a big rough diamond into a perfect Ideal-cut means losing more material, so cutters working with larger stones often accept a slightly worse cut to hang onto carat weight (and value). Cut and carat aren’t really independent this plot is basically that trade off in picture form.


4 Exercise 3 — Relationship: Carat vs. Price (diamonds_sample)

Task: Visualize the relationship between carat and price, add at least one relevant aesthetic, apply suitable customization, and explain the relationship shown.

#creating sample
set.seed(123)
diamonds_sample <- diamonds %>%
  slice_sample(n=3000)

ggplot(data = diamonds_sample,
       aes(x = carat,
           y = price,
           color = clarity)) +
  geom_point(alpha = 0.55, size = 1.6) +
  geom_smooth(
    aes(group = 1),
    method = "loess",
    color = "black",
    linewidth = 0.9,
    se = FALSE
  ) +
  scale_color_viridis_d(option = "plasma") +
  scale_y_continuous(labels = scales::dollar_format()) +
  labs(
    title = "Diamond Price Rises Sharply with Carat Weight",
    subtitle = "Color shows clarity grade; black line shows the overall trend",
    x = "Carat",
    y = "Price (USD)",
    color = "Clarity"
  ) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"))

What this is actually telling us:

Price obviously climbs with carat, and it’s not linear, it accelerates, so the jump from 1.5 to 2 carats adds a lot more than the jump from 0.5 to 1. That lines up with how diamonds get priced in general, big stones are rarer, so price per carat jumps at certain size thresholds. Clarity adds some spread at any given carat size, but I wouldn’t read too much into which direction it goes from the color alone, the diamonds dataset has a known quirk where clarity, color, and carat are tangled together (bigger stones tend to get cut for weight over clarity), so a clean “better clarity = higher price at the same size” story isn’t something I’d claim without actually checking the correlation. Carat is clearly doing most of the work here; clarity is a secondary factor worth digging into further rather than something I can confidently characterize from the scatter alone.


5 Exercise 4 — Time Series: Personal Savings Rate Over Time (economics)

Task: Visualize psavert over time, use clear labels and a suitable theme, highlight or annotate a noticeable change, and interpret it.

lowest_point <- economics |>
  filter(psavert == min(psavert))

ggplot(data = economics,
       aes(x = date, y = psavert)) +
  geom_line(color = "#2C7FB8", linewidth = 0.7) +
  geom_point(data = lowest_point,
             aes(x = date, y = psavert),
             color = "firebrick", size = 2.5) +
  annotate(
    "text",
    x = lowest_point$date,
    y = lowest_point$psavert + 1.5,
    label = paste0("Lowest point: ", round(lowest_point$psavert, 1),
                    "%\n(", format(lowest_point$date, "%b %Y"), ")"),
    color = "firebrick",
    size = 3.3,
    hjust = 0.15
  ) +
  scale_y_continuous(labels = scales::percent_format(scale = 1)) +
  labs(
    title = "U.S. Personal Savings Rate, 1967–2015",
    subtitle = "Savings habits shifted dramatically over five decades",
    x = "Year",
    y = "Personal savings rate"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold"),
    panel.grid.minor = element_blank()
  )

What this is actually telling us:

From the graph the whole shift in American saving habits in one line. Through the late 60s and 70s people were regularly saving 10–15% of income — sounds crazy by today’s standards. From there it just grinds downward through the 80s and 90s, bottoming out at 2.2% in mid-2005. That timing isn’t a coincidence — it’s right in the middle of the cheap-credit, rising-home-equity period leading into the 2008 crash, when people leaned on debt and home value instead of savings. After the crash there’s a real rebound as households got more cautious. So the line isn’t just noisy data — it’s basically a proxy for how nervous or confident people were about money over 50 years.


6 Exercise 5 — Improve a Visualization

Task: Create one visualization with at least three presentation problems, then redesign it with improvements, and briefly explain the changes.

6.1 5a. The “Before” Plot (Deliberately Flawed)

mpg_class_summary <- mpg |>
  group_by(class) |>
  summarise(mean_hwy = mean(hwy), .groups = "drop")
ggplot(data = mpg_class_summary,
       aes(x = class, y = mean_hwy, fill = class)) +
  geom_col(show.legend = TRUE) +
  coord_cartesian(ylim = c(15, 30)) +   # problem 1: truncated axis exaggerates differences
  theme(
    axis.text.x = element_text(size = 6),  # problem 2: tiny, unreadable labels, no rotation
    legend.position = "right"              # problem 3: redundant legend duplicating the x-axis
  ) +
  labs(x = "", y = "")                     # problem 4: no title, no axis labels/units

Problems with this plot:

  1. Truncated y-axis (ylim = c(15, 30)) — this exaggerates the differences between classes, making a gap of a few mpg look like a two- or three-fold difference.
  2. No title, no axis labels, no units — a reader has no idea what they’re looking at without already knowing the dataset.
  3. A legend that just repeats the x-axisclass is already labeled on the x-axis, so mapping it to fill as well adds visual noise (and a legend) without adding any new information.
  4. Tiny, non-rotated axis text — the class names are barely legible.

6.2 5b. The “After” Plot (Redesigned)

ggplot(data = mpg_class_summary,
       aes(x = reorder(class, mean_hwy), y = mean_hwy)) +
  geom_col(fill = "#2C7FB8", width = 0.7) +
  geom_text(aes(label = round(mean_hwy, 1)), vjust = -0.6, size = 3.5) +
  coord_cartesian(ylim = c(0, max(mpg_class_summary$mean_hwy) * 1.15)) +
  labs(
    title = "Average Highway Fuel Efficiency by Vehicle Class",
    subtitle = "Full y-axis scale, from zero, for an honest comparison",
    x = NULL,
    y = "Average highway mileage (mpg)"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0, size = 10),
    panel.grid.major.x = element_blank()
  )

What changed, and why:

  • The y-axis now starts at zero. This is the single biggest fix — bar charts encode value through bar length, so truncating the axis breaks that encoding and visually overstates how different the classes are. Starting from zero gives an honest sense of scale.
  • Dropped the redundant fill = class legend and replaced it with a single consistent bar color, since the categories were already labeled on the x-axis. One less thing competing for attention.
  • Added a real title, subtitle, and a properly labeled, unit-carrying y-axis, so the chart can stand on its own without needing the code or a caption to explain it.
  • Reordered the bars by value (reorder()) instead of leaving them in whatever order the factor levels happened to be in, so the reader can immediately see the ranking from lowest to highest efficiency.
  • Labeled the bars directly with their values and cleaned up the gridlines, so the exact numbers are visible at a glance without forcing the reader to trace a bar back to the axis.

Small changes, but together they turn a chart that technically shows the data into one that actually communicates it clearly and honestly.