Histograms, boxplots, and scatter/line plots in base R, and layered graphics with ggplot2, using AI-assisted prompting
The previous modules covered data structures and data manipulation — identifying what you are working with, then indexing, filtering, and cleaning it. This module turns to a different kind of task: turning a data frame into a picture. Visualization uses different functions and a different logic than manipulation, but the same discipline applies. Every plot below is produced by prompting an AI model for code — using the same role, task, context, and format elements from earlier modules — and every result is checked before it is trusted, not accepted simply because it rendered.
This module works with two different plotting engines. Sections 2 through 4 use base R, where a plot is produced by a single function call with many arguments. Section 5 introduces ggplot2, which builds a plot as a stack of layers added with +. The two engines can produce similar-looking output, but they are structured differently enough that a prompt written for one will not simply drop into the other — part of the point of this module is noticing that difference.
life_expA histogram shows the distribution of a single numeric column — how its values are spread out, and where they cluster. In base R, hist() takes a numeric vector and does the rest.
col = "lightblue" and border = "grey" to the call yourself and re-run it. Confirm the bars changed color.
hist(life_exp$Average_Life_Expectancy,
main = "Histogram of Average Life Expectancy",
xlab = "Average Life Expectancy",
col = "lightblue",
border = "grey")
Average_Life_Expectancy has any missing values, mean() and median() will return NA unless you pass na.rm = TRUE. This is the same argument used in the data manipulation module — it does not remove missing values from the data frame, only from that one calculation.
mean(life_exp$Average_Life_Expectancy, na.rm = TRUE) and median(...) directly, and confirm the two numbers match where the two lines fall on the plot.
mean_le <- mean(life_exp$Average_Life_Expectancy, na.rm = TRUE)
median_le <- median(life_exp$Average_Life_Expectancy, na.rm = TRUE)
hist(life_exp$Average_Life_Expectancy,
main = "Histogram of Average Life Expectancy with Mean and Median",
xlab = "Average Life Expectancy",
col = "lightblue", border = "black")
abline(v = mean_le, col = "blue", lwd = 2)
abline(v = median_le, col = "red", lwd = 2)
legend("topleft", legend = c("Mean", "Median"), col = c("blue", "red"), lwd = 2)
life_expA boxplot summarizes a numeric column's distribution — its median, quartiles, and any outliers — and is most useful when comparing that distribution across the levels of a categorical column, using the formula syntax numeric ~ category.
tapply(life_exp$Average_Life_Expectancy, life_exp$Gender, median, na.rm = TRUE). If tapply() is unfamiliar, ask AI to explain what it is doing before you run it — do not just paste it.
boxplot(Average_Life_Expectancy ~ Gender, data = life_exp,
main = "Boxplot of Life Expectancy by Gender",
xlab = "Gender", ylab = "Average Life Expectancy")
boxplot(Average_Life_Expectancy ~ Gender, data = life_exp,
main = "Boxplot of Life Expectancy by Gender",
xlab = "Gender", ylab = "Average Life Expectancy",
col = c("lightblue", "lightgreen"))
life_expScatter plots and line plots both use plot(), differentiated by the type argument. Both are well suited to a numeric column tracked over time, such as Year.
pch value to a different number and re-run it. Describe what changed about the points visually.
plot(life_exp$Year, life_exp$Average_Life_Expectancy,
xlab = "Year", ylab = "Average Life Expectancy",
main = "Scatter Plot of Life Expectancy Over Time",
pch = 19)
plot(life_exp$Year, life_exp$Average_Life_Expectancy,
xlab = "Year", ylab = "Average Life Expectancy",
main = "Scatter Plot of Life Expectancy Over Time by Gender",
pch = 19, col = ifelse(life_exp$Gender == "Male", "blue", "red"))
xlim and ylim arguments in the code and explain, in your own words, what would go wrong visually if they were removed and the male and female subsets covered different year ranges.
male_data <- subset(life_exp, Gender == "Male")
female_data <- subset(life_exp, Gender == "Female")
plot(male_data$Year, male_data$Average_Life_Expectancy,
type = "l", col = "blue", lwd = 2,
xlab = "Year", ylab = "Average Life Expectancy (years)",
main = "Life Expectancy by Gender Over Time",
lty = 1,
xlim = c(min(life_exp$Year), max(life_exp$Year)),
ylim = c(min(life_exp$Average_Life_Expectancy, na.rm = TRUE),
max(life_exp$Average_Life_Expectancy, na.rm = TRUE)))
lines(female_data$Year, female_data$Average_Life_Expectancy, col = "red", lwd = 2, lty = 2)
legend("bottomright", legend = c("Male", "Female"), col = c("blue", "red"), lwd = 2, lty = c(1, 2))
text(1918, 75, "End of World War I", col = "black")
abline(v = 1918, col = "black", lwd = 1, lty = 3)
plot() call in one pass. It will not tell you whether a line chart, scatter plot, or boxplot is the right choice for what you are trying to show — that decision depends on the shape of your data and the question you are asking, both of which are yours to judge.
insuranceggplot2 follows the Grammar of Graphics: instead of one function with many arguments, a plot is built by stacking layers with + — a base canvas, a geometry (points, lines, bars), and optional layers for scales, facets, labels, and themes. The same visual ideas from Sections 2–4 reappear here, but expressed as layers rather than arguments.
ggplot(data = insurance, aes(x = age, y = expenses)) on its own, with no geom layer added. Describe what appears, and explain in your own words why nothing is plotted without a geom.
library(ggplot2)
ggplot(data = insurance, aes(x = age, y = expenses)) +
geom_point(color = "cornflowerblue", alpha = .5, size = 2)
method = "lm" tells R to fit.
ggplot(data = insurance, aes(x = age, y = expenses)) +
geom_point(color = "cornflowerblue", alpha = .5, size = 2) +
geom_smooth(method = "lm")
aes() mapping placed inside ggplot() applies to every layer that follows. Placed inside a specific geom_ instead, it applies only to that layer. This matters below: mapping color = smoker at the top level colors both the points and the trend line; mapping it only inside geom_point() would color the points alone.
aes() responsible for both the point color and the legend that appears automatically. Then set se = TRUE and describe what the shaded band around each line represents.
ggplot(data = insurance, aes(x = age, y = expenses, color = smoker)) +
geom_point(alpha = .5, size = 2) +
geom_smooth(method = "lm", se = FALSE, linewidth = 1.5)
insurance$obese <- ifelse(insurance$bmi >= 30, "obese", "not obese")
ggplot(data = insurance, aes(x = age, y = expenses, color = smoker)) +
geom_point(alpha = .5) +
geom_smooth(method = "lm", se = FALSE) +
facet_wrap(~obese) +
labs(title = "Relationship between age and medical expenses",
subtitle = "By smoking and obesity status",
x = "Age (years)",
y = "Annual expenses",
color = "Smoker?")
insurance$obese <- ifelse(insurance$bmi >= 30, "obese", "not obese")
ggplot(data = insurance, aes(x = age, y = expenses, color = smoker)) +
geom_point(alpha = .5) +
geom_smooth(method = "lm", se = FALSE) +
facet_wrap(~obese) +
labs(title = "Relationship between age and medical expenses",
subtitle = "By smoking and obesity status",
x = "Age (years)",
y = "Annual expenses",
color = "Smoker?") +
theme_minimal()
ggplot2 call quickly, and it is easy to keep adding layers — a color mapping, a facet, a theme — without stopping to check whether each one is adding clarity or just complexity. Before accepting a plot, ask whether a reader unfamiliar with the data could state its main point in one sentence. If not, the plot needs fewer layers, not more code.
This module covered visualization in two engines. In base R, hist(), boxplot(), and plot() each produce a specific chart type through arguments passed to a single function call, with abline(), legend(), and text() layering on reference lines, legends, and annotations. In ggplot2, the same ideas are expressed as layers added with + — a base mapping, one or more geoms, and optional layers for grouping, scales, faceting, labels, and themes. Each plot was produced by prompting an AI model for code and verifying the result — predicting an outcome before running it, confirming a visual read against a computed value, or explaining what a specific argument does before relying on it.
A separate module extends this ggplot2 grammar with interactive and animated graphics.