This report explores the relationship between secondary school enrollment and fertility rates using a series of visualizations. The dataset was cleaned and prepared beforehand, and the visualizations aim to highlight key patterns in the data.

Before we dive into the graphs, it’s worth taking a moment to talk about the data itself and why I think it is important!

First, you’ll notice the dataset includes both fertility rate and birth rate. They sound similar, but they capture different things. Fertility rate tells us how many children, on average, a woman is expected to have over her lifetime — it’s a slower-moving, long-term measure. Birth rate, by contrast, counts how many babies are born each year per 1,000 people — so it’s more sensitive to things like population age structure. In this project, I focus mostly on fertility rate because it’s better for seeing the effects of education.

Second, the education enrollment rate might seem strange at first — some countries show more than 100%. That’s not a mistake! This is gross enrollment, which includes all students enrolled in secondary school, even if they’re older, younger, or repeating grades. So when a country reports 110%, it means more students are attending than the official school-age population — often a good sign of access and inclusiveness.

So why put this all together?

Because how much education people get — especially women — has a huge influence on fertility rates, life opportunities, and long-term development. This isn’t just a chart exercise. It’s about understanding how something as fundamental as access to school can ripple through societies and shape our futures. That’s why I wanted to visualize it clearly and thoughtfully.

1. Classic Scatterplot: More Education, Fewer Babies?

This first graph is the classic scatterplot. I improved it by first benchmarking the secondary enrollment school data to percentage because I think people tend to associate percent of completion to education better than numerical. Then I also added a trendline!

final_data <- read.csv("/Users/selenezeng/Desktop/R visu/tidy_fertility_birth_education.csv")

final_data$EnrollmentRatio <- final_data$EducationEnrollment / 100
final_data$EnrollmentRatio <- round(final_data$EducationEnrollment / 100, 2)

library(ggplot2)

ggplot(final_data, aes(x = EducationEnrollment, y = FertilityRate)) +
  geom_point(color = "#FF7F0E", size = 3, alpha = 0.75) +
  geom_smooth(method = "lm", se = FALSE, color = "#1F77B4", linetype = "dashed") +
  geom_vline(xintercept = 100, color = "gray40", linetype = "dotted", linewidth = 0.8) +
  labs(
    title = "More Education, Fewer Babies?",
    subtitle = "100% = full enrollment for official school-age group; >100% includes older, younger, or repeating students",
    x = "Secondary School Enrollment (%)",
    y = "Children per Woman"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    plot.title = element_text(face = "bold"),
    panel.grid.minor = element_blank()
  )

2. Ridgeline Plot: Distribution by Education Bands

This is the Ridgeline graph! I kept this visual because I really like how it shows some women will have around 1.5~ baby regardless of the education level of their country, and mostly only less educated countries have women having more than 4 kids.

I used the cut function to break our education data into four different high school enrollment bands and adjusted the height / solved the N/A issue. Also added a note.

library(ggridges)
library(dplyr)

final_data$EducationBand <- cut(
  final_data$EducationEnrollment,
  breaks = c(0, 60, 90, 110, Inf),
  labels = c("30–60%", "60–90%", "90–110%", "110%+"),
  right = FALSE
)

# For each education band, compute a density estimate for FertilityRate
density_data <- final_data %>%
  filter(!is.na(FertilityRate)) %>%
  group_by(EducationBand) %>%
  do({
    d <- density(.$FertilityRate, na.rm = TRUE)
    data.frame(
      x = d$x,
      dens = d$y,
      EducationBand = unique(.$EducationBand)
    )
  }) %>%
  ungroup() %>%
  mutate(BandNum = as.numeric(EducationBand))

# I experimented with bolding/dashing the 2 babies/ 4 babies benchmark, but the visual is just distracting.
# Plotting the peak with a dot to emphasize the mode is more visually clear and precise
mode_data <- density_data %>%
  group_by(EducationBand) %>%
  filter(dens == max(dens)) %>%
  ungroup() %>%
  mutate(BandNum = as.numeric(EducationBand))

scale_value <- 1.4

ggplot() +
  geom_ridgeline(
    data = density_data,
    aes(x = x, y = BandNum, height = dens, group = EducationBand, fill = EducationBand),
    stat = "identity",
    scale = scale_value,
    alpha = 0.85,
    color = "white"
  ) +
  geom_point(
    data = mode_data,
    aes(x = x, y = BandNum + scale_value * dens),
    color = "black",
    size = 3,
    inherit.aes = FALSE
  ) +
  geom_text(
    data = mode_data,
    aes(x = x + 0.2, y = BandNum + scale_value * dens, label = round(x, 2)),
    color = "black",
    size = 3,
    hjust = 0,
    inherit.aes = FALSE
  ) +
  scale_fill_viridis_d(option = "plasma") +
  scale_y_continuous(
    breaks = 1:4,
    labels = c("30–60%", "60–90%", "90–110%", "110%+"),
    name = "Secondary School Enrollment Band (%)"
  ) +
  labs(
    title = "How Many Kids Do Women Have? An Exploration by Education Level",
    subtitle = "Ridgeline distributions of fertility rates by enrollment band; black dots mark the peak (mode) for each group.",
    x = "Fertility Rate (Children per Woman)",
    caption = "Note: '110%+' enrollment means the total number of students exceeds the official school-age population.\nThis happens because the count includes all learners—even those who are younger, older, or repeating."
  ) +
  theme_minimal(base_size = 14) +
  theme(
    legend.position = "none",
    plot.title = element_text(face = "bold"),
    panel.grid.minor = element_blank(),
    panel.grid.major.y = element_blank()
  )

3. Overlay Graph: Enrollment vs. Fertility by Continent

This one I wanted to make an overlay graph, and I replaced the spiral! As suggested in discussion, I grouped the countries into continents, and used visually contrasting bars and lines.

This section required installing additional packages — I used online help for this one!

if (!requireNamespace("pacman", quietly=TRUE)) install.packages("pacman")
pacman::p_load(countrycode, dplyr, ggplot2, stringr)

df <- read.csv(
  "/Users/selenezeng/Desktop/R visu/tidy_fertility_birth_education.csv",
  stringsAsFactors = FALSE
)

# Normalize names
names(df) <- gsub("([a-z])([A-Z])", "\\1_\\2", names(df))
names(df) <- tolower(names(df))

df <- df %>%
  mutate(
    country_clean = str_remove(country, "\\s*\\(.*?\\)$"),
    continent     = countrycode(sourcevar = country_clean, origin = "country.name", destination = "continent")
  ) %>%
  filter(!is.na(continent))

continent_summary <- df %>%
  group_by(continent) %>%
  summarise(
    avg_enrollment = mean(education_enrollment, na.rm = TRUE),
    avg_fertility  = mean(fertility_rate,       na.rm = TRUE),
    .groups        = "drop"
  )

scale_factor <- 20

ggplot(continent_summary, aes(x = continent)) +
  geom_col(aes(y = avg_enrollment), fill = "#FFB600", width = 0.6, alpha = 0.85) +
  geom_line(aes(y = avg_fertility * scale_factor, group = 1), color = "#1F77B4", size = 1) +
  geom_point(aes(y = avg_fertility * scale_factor), color = "#1F77B4", size = 3) +
  scale_y_continuous(
    name = "Secondary School Enrollment (%)",
    sec.axis = sec_axis(~ . / scale_factor, name = "Fertility Rate\n(Children per Woman)")
  ) +
  labs(
    title = "Average Enrollment vs. Fertility by Continent",
    subtitle = "Orange bars = Enrollment (%)  |  Blue line & dots = Fertility Rate"
  ) +
  theme_classic(base_size = 14) +
  theme(
    plot.title = element_text(face = "bold"),
    plot.subtitle = element_text(face = "italic"),
    axis.title.y.left = element_text(face = "bold", color = "#FFB600"),
    axis.title.y.right = element_text(face = "bold", color = "#1F77B4"),
    axis.text.x = element_text(angle = 45, hjust = 1)
  )