Back to xenophobia and the ESS. Today you ask whether the effect of age on xenophobia is straight-line — or a curve.

  1. Prepare the data (Denmark and Bulgaria, as in Lecture 11).
pacman::p_load(tidyverse, haven, estimatr, modelsummary)
ESS <- read_spss("ESS9e03_1.sav") # from your project folder
ESS <- ESS %>%
  filter(cntry == "DK" | cntry == "BG") %>%          # DK = Denmark, BG = Bulgaria
  mutate(
    cntry  = as_factor(cntry) %>% fct_relevel("Denmark"),
    gndr   = as_factor(gndr),
    eduyrs = case_when(eduyrs < 9 ~ 9, eduyrs > 21 ~ 21, TRUE ~ zap_labels(eduyrs)),
    agea    = zap_labels(agea),
    imwbcnt = 10 - zap_labels(imwbcnt)               # reverse: higher = more xenophobic
  ) %>%
  select(idno, cntry, pspwght, gndr, eduyrs, agea, imwbcnt) %>%
  drop_na()
  1. Predict before you compute. Do you expect the effect of age on xenophobia to be a straight line, or to bend (rise faster at some ages than others)? There is no wrong guess — commit to one, then test it.

  2. Fit a quadratic of age. Use poly(I(agea / 10), 2, raw = TRUE) so the coefficients are per decade, and keep country, gender and education as controls, weighted by pspwght.

lm_robust(imwbcnt ~ cntry + gndr + eduyrs + poly(I(agea / 10), 2, raw = TRUE), data = ESS, weights = pspwght)

ols_poly <- lm_robust(
  imwbcnt ~ cntry + gndr + eduyrs + poly(I(agea / 10), 2, raw = TRUE),
  data = ESS, weights = pspwght
)

modelsummary(list("Xenophobia" = ols_poly),
             coef_rename = c("poly(I(agea/10), 2, raw = TRUE)1" = "Age (per decade)",
                             "poly(I(agea/10), 2, raw = TRUE)2" = "Age² (per decade)"),
             stars = TRUE, gof_map = c("nobs", "r.squared"), output = "kableExtra")

The linear term is and the squared term is . Because the squared term is negative, the age slope .

  1. Where would the curve turn around? A quadratic peaks (or bottoms out) at \(-\beta_1 / (2\beta_2)\). With the coefficients per decade, multiply by 10 to get the age in years. The turning point is at about years.
b1 <- coef(ols_poly)["poly(I(agea/10), 2, raw = TRUE)1"]
b2 <- coef(ols_poly)["poly(I(agea/10), 2, raw = TRUE)2"]

(-b1 / (2 * b2)) * 10   # turning point, in years
  1. That turning point is far outside the ages anyone in the data actually has (15–90). So within the real range, xenophobia .

  2. Write one sentence as a # comment interpreting what the quadratic told you — then compare:

Xenophobia rises with age across the whole observed range, but the negative squared term means the rise decelerates — each additional decade adds a little less than the one before. The curve would technically turn around near age 180, which is meaningless for humans, so in practice this is a gently flattening increase, not a hump. A straight line would have been a defensible simplification here; the curvature is real but mild.

  1. Bonus, for the fast. Plot the fitted curve to see the mild bend. Overlay a straight-line fit for comparison.
ggplot(ESS, aes(x = agea, y = imwbcnt)) +
  geom_jitter(alpha = 0.06, height = 0.2) +
  geom_smooth(method = "lm", formula = y ~ x, se = FALSE, aes(colour = "Straight line")) +
  geom_smooth(method = "lm", formula = y ~ poly(x, 2, raw = TRUE), se = FALSE,
              aes(colour = "Quadratic")) +
  scale_colour_manual(values = c("Straight line" = "#425570", "Quadratic" = "#901A1E"),
                      name = NULL) +
  labs(x = "Age", y = "Xenophobia (0–10)") +
  theme_minimal()

The two lines barely diverge — visual confirmation that the curvature is slight.

  1. Discuss with your neighbour.

    • Why is a turning point outside the observed data a reason not to make much of the "curve"?
    • A colleague reports "xenophobia peaks at age 180." What has gone wrong in how they read the model?
    • When would you expect a genuine hump in age — a real rise-then-fall — for some social outcome?