Demographics

Gender

df_cbzs_elg %>% 
  mutate(gender = ifelse(is.na(gender) | gender == "","other",gender)) %>% 
  group_by(gender) %>% 
  summarise(N = n()) %>% 
  ungroup() %>% 
  mutate(Perc = round(100*(N/sum(N)),2)) %>% 
  ungroup() %>% 
  arrange(desc(Perc)) %>% 
  kbl() %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
gender N Perc
man 141 49.47
woman 140 49.12
other 4 1.40

Race

race N Perc
White 193 67.72
Black or African American 34 11.93
multiracial 24 8.42
Asian 21 7.37
Hispanic, Latino, or Spanish origin 8 2.81
Middle Eastern or North African 2 0.70
NA 2 0.70
Other (please specify) 1 0.35

Age

Mean age: 41.49.

Income

median_income_num <- df_cbzs_elg %>% 
  mutate(income_num = as.numeric(income)) %>% 
  summarise(median = median(income_num, na.rm = TRUE)) %>% 
  pull(median)

df_cbzs_elg %>% 
  ggplot(aes(x = income)) +
  geom_bar() +
  geom_vline(xintercept = median_income_num, 
             color = "lightblue", linetype = "dashed") +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black", face = "bold"),
        axis.title.x = element_blank(),
        axis.title.y = element_blank()) +
  coord_flip()

Education

edu N Perc
GED 66 23.16
2yearColl 23 8.07
4yearColl 127 44.56
MA 51 17.89
PHD 15 5.26
NA 3 1.05

SES

ses N Perc
Lower Class 38 13.33
Lower Middle Class 75 26.32
Middle Class 124 43.51
Upper Middle Class 46 16.14
Upper Class 2 0.70

Politics

Political ideology

Participants were asked about the extent to which they subscribe to the following ideologies on a scale of 1-7 (select NA if unfamiliar): Conservatism, Liberalism, Democratic Socialism, Libertarianism, Progressivism.

means <- df_cbzs_elg %>%
  dplyr::select(PID,ideo_con:ideo_prog) %>% 
  pivot_longer(-PID,
               names_to = "ideo",
               values_to = "score") %>% 
  filter(!is.na(score)) %>% 
  group_by(ideo) %>% 
  summarise(score = mean(score)) %>% 
  ungroup()

df_cbzs_elg %>%
  dplyr::select(PID,ideo_con:ideo_prog) %>% 
  pivot_longer(-PID,
               names_to = "ideo",
               values_to = "score") %>% 
  filter(!is.na(score)) %>%  
  ggplot() +
  geom_density(aes(x = score), fill = "lightblue",color = NA) +
  scale_x_continuous(limits = c(1,7),
                     breaks = seq(1,7,1)) +
  geom_vline(data = means,mapping = aes(xintercept = score),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold")) +
  facet_wrap(~ideo,nrow = 2)

Party affiliation

party_id N Perc
Independent 97 34.04
Democrat 95 33.33
Republican 93 32.63

Measures

Class-based Zero-Sum Beliefs

  1. If the upper class becomes richer, this comes at the expense of the working class
  2. If the upper class makes more money, then the working class makes less money
  3. If the upper class does better economically, this does NOT come at the expense of the working class [R]

alpha = 0.93

df_cbzs_elg %>% 
  ggplot(aes(x = zs_class)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$zs_class,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Race-based Zero-Sum Beliefs

participants racial ingroup and outgroup was determined by the demographic questionnaire they completed upon starting the study. If “white” was selected, their ingroup shows as “white people” and their outgroup shows as “racial minorities.” If not, their ingroup shows as “racial minorities” and their outgroup shows as “white people.” The idea was to always have the ingroup as the losing side of the zsb.

  1. If [OUTGROUP] become richer, this comes at the expense of [INGROUP]
  2. If [OUTGROUP] make more money, then [INGROUP] make less money
  3. If [OUTGROUP] do better economically, this does NOT come at the expense of [INGROUP] [R]

alpha = 0.95

df_cbzs_elg %>% 
  ggplot(aes(x = zs_race)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$zs_race,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Broken down by white/non-white

means <- df_cbzs_elg %>% 
  group_by(ingroup) %>% 
  summarise(score = mean(zs_race,na.rm = T)) %>% 
  ungroup()

df_cbzs_elg %>% 
  ggplot(aes(x = zs_race)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(data = means,mapping = aes(xintercept = score),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold")) +
  facet_wrap(~ingroup,nrow = 2)

Linked Fate

White Working Class

  1. Issues that affect me also affect white working-class Americans
  2. What happens to white working-class Americans in this country will have something to do with what happens to me
  3. White working-class Americans and me share a common destiny
  4. Progress for white working-class Americans also means progress for me

alpha = 0.9

df_cbzs_elg %>% 
  ggplot(aes(x = lf_wwc)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$lf_wwc,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Non-White Working Class

  1. Issues that affect me also affect non-white working-class Americans
  2. What happens to non-white working-class Americans in this country will have something to do with what happens to me
  3. Non-white working-class Americans and me share a common destiny
  4. Progress for non-white working-class Americans also means progress for me

alpha = 0.88

df_cbzs_elg %>% 
  ggplot(aes(x = lf_nwwc)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$lf_nwwc,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

White Upper Class

  1. Issues that affect me also affect white upper-class Americans
  2. What happens to white upper-class Americans in this country will have something to do with what happens to me
  3. White upper-class Americans and me share a common destiny
  4. Progress for white upper-class Americans also means progress for me

alpha = 0.91

df_cbzs_elg %>% 
  ggplot(aes(x = lf_wuc)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$lf_wuc,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Non-White Upper Class

  1. Issues that affect me also affect non-white upper-class Americans
  2. What happens to non-white upper-class Americans in this country will have something to do with what happens to me
  3. Non-white upper-class Americans and me share a common destiny
  4. Progress for non-white upper-class Americans also means progress for me

alpha = 0.91

df_cbzs_elg %>% 
  ggplot(aes(x = lf_nwuc)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$lf_nwuc,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Class Solidarity

  1. I feel a sense of solidarity with the working class
  2. I support policy that helps the working class
  3. I stand united with the working class
  4. Policies negatively affecting the working class should be changed
  5. More people should know about how the working class are negatively affected by economic issues
  6. It’s important to challenge the power structures that disadvantage the working class

alpha = 0.92

df_cbzs_elg %>% 
  ggplot(aes(x = soli)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$soli,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Cross-Race Class Solidarity

Mean score of the out-group items in the following scale (we exclude each participant’s own self-reported in-group). If they are not affiliated with any of the following racial groups, we take the mean score of all items. If they are affiliated with more than one group, we take only the items of the groups they are not affiliated with.

  1. I feel a sense of solidarity with working-class White people
  2. I feel a sense of solidarity with working-class Black people
  3. I feel a sense of solidarity with working-class Asian people
  4. I feel a sense of solidarity with working-class Hispanic people

alpha (of all items) = 0.88

df_cbzs_elg %>% 
  ggplot(aes(x = crs)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$crs,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Support for redistributive policy

Participants saw one of the following policies and indicated their support for it (1 = Strongly Oppose to 7 = Strongly Support)

Minimum wage increase

Congress has not increased the federal minimum wage, currently set at $7.25, since 2009. Some Congresspeople are proposing a policy that would gradually raise the federal minimum wage to $20 an hour by 2028. After 2028, the minimum wage would be adjusted each year to keep pace with growth in the median wage, a measure of wages for typical workers.

Student debt relief

Some Congresspeople are proposing a policy that would help to address the student loan debt crisis by forgiving up to $50,000 in loans per borrower. Approximately 42 million Americans, or about 1 in 6 American adults, owe a cumulative $1.6 trillion in student loans. Student loans are now the second-largest slice of household debt after mortgages, bigger than credit card debt.

Housing

Some Congresspeople are proposing a housing affordability policy that would help ensure that every American has a place to live. The policy would allow for smaller, lower cost homes like duplexes, townhouses, and garden apartments to be built and developed, allowing new nonprofit homes and reducing overall housing prices.

Climate change

Some Congresspeople are proposing a Green New Deal bill which would phase out the use of fossil fuels, with the government providing clean energy jobs for people who can’t find employment in the private sector. All jobs would pay at least $20 an hour, and include healthcare benefits and collective bargaining rights.

df_cbzs_elg %>% 
  ggplot(aes(x = support)) +
  geom_histogram(fill = "lightblue",
                 binwidth = 1,
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(0,8)) +
  ylab("count") +
  geom_vline(xintercept = mean(df_cbzs_elg$support,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Relative Deprivation

  1. When I think about what I have compared to others, I feel deprived
  2. I feel privileged compared to other people like me [R]
  3. I feel resentful when I see how prosperous other people seem to be
  4. When I compare what I have with others, I realize that I am quite well off [R]

alpha = 0.72

df_cbzs_elg %>% 
  ggplot(aes(x = reldep)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$reldep,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Inclusion of Other-in-Self

Participants saw one of two sets of five images with differing degrees of overlapping circles (1 = no overlap to 5 = full overlap). Those who selected “White” in the demographic section saw “YOU” and “Non-White People” on the circles. Those who did not select “White” in the demographic section saw “YOU” and “White People” on the circles.

Please select the image that best represents how close you feel to [white/non-white] people in America.

df_cbzs_elg %>% 
  ggplot(aes(x = IOS)) +
  geom_histogram(fill = "lightblue",
                 color = NA,
                 binwidth = 1) +
  scale_x_continuous(breaks = seq(1,5,1),
                     limits = c(0,6)) +
  ylab("count") +
  geom_vline(xintercept = mean(df_cbzs_elg$IOS,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Broken down by white/non-white

means <- df_cbzs_elg %>% 
  group_by(ingroup) %>% 
  summarise(score = mean(IOS,na.rm = T)) %>% 
  ungroup()

df_cbzs_elg %>% 
  ggplot(aes(x = IOS)) +
  geom_histogram(fill = "lightblue",
                 color = NA,
                 binwidth = 1) +
  scale_x_continuous(breaks = seq(1,5,1),
                     limits = c(0,6)) +
  ylab("count") +
  geom_vline(data = means,mapping = aes(xintercept = score),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold")) +
  facet_wrap(~ingroup,nrow = 2)

Working-class identification

To what extent do you identify as working-class? (0 = Not at All Working-Class; 50 = Moderately Working-Class; 100 = Very Strongly Working-Class)

df_cbzs_elg %>% 
  ggplot(aes(x = class_id)) +
  geom_histogram(fill = "lightblue",
                 binwidth = 5,
                 color = NA) +
  scale_x_continuous(breaks = seq(0,100,10),
                     limits = c(-10,110)) +
  ylab("count") +
  geom_vline(xintercept = mean(df_cbzs_elg$class_id,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Correlations

Correlations: All

We’re gonna break this by racial ingroup (white people AND everybody else)

Correlations: White people

Correlations: Non-White people

Analysis

Mediations

Mediation Model 1

Predictor: Class ZSB

Mediator: Linked fate (for white participants, I take linked fate with non-white working-class; for non-white participants, I take linked fate with white working-class)

Outcome: Solidarity

Bootstraps: 10,000

form.m <- reformulate("zs_class", response = "lf_combined")

form.y <- reformulate(c("zs_class", "lf_combined"), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate("zs_class", response = "soli"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf_combined",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0240202 0.0052322 0.0515167 0.0066
ADE (direct) 0.2327523 0.1765107 0.2875782 0.0000
Total Effect 0.2567725 0.1940591 0.3211096 0.0000
Prop. Mediated 0.0935466 0.0225452 0.1817874 0.0066
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf_combined"]
b_p      <- summary(y.fit)$coefficients["lf_combined", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf_combined",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Mediation Model 2

Predictor: Class ZSB

Mediator: Linked fate with non-white working class

Outcome: Solidarity

Sample: White people

Bootstraps: 10,000

form.m <- reformulate("zs_class", response = "lf_nwwc")

form.y <- reformulate(c("zs_class", "lf_nwwc"), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg %>% filter(ingroup == "white people"))        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg %>% filter(ingroup == "white people"))        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate("zs_class", response = "soli"),
  data = df_cbzs_elg %>% filter(ingroup == "white people")
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf_nwwc",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0185807 0.0033663 0.0384383 0.0148
ADE (direct) 0.2127029 0.1575950 0.2668865 0.0000
Total Effect 0.2312836 0.1757540 0.2843681 0.0000
Prop. Mediated 0.0803374 0.0148292 0.1689617 0.0148
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf_nwwc"]
b_p      <- summary(y.fit)$coefficients["lf_nwwc", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf_nwwc",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Mediation Model 3

Predictor: Class ZSB

Mediator: Linked fate (for white participants, I take linked fate with non-white working-class; for non-white participants, I take linked fate with white working-class)

Outcome: Cross-Race Solidarity

Bootstraps: 10,000

form.m <- reformulate("zs_class", response = "lf_combined")

form.y <- reformulate(c("zs_class", "lf_combined"), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg %>% filter(!is.na(crs)))        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg %>% filter(!is.na(crs)))        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate("zs_class", response = "crs"),
  data = df_cbzs_elg %>% filter(!is.na(crs))
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf_combined",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0680548 0.0195127 0.1222030 0.0058
ADE (direct) 0.0737728 -0.0214105 0.1718166 0.1280
Total Effect 0.1418276 0.0316086 0.2520887 0.0118
Prop. Mediated 0.4798416 0.1656788 1.3907277 0.0144
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf_combined"]
b_p      <- summary(y.fit)$coefficients["lf_combined", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf_combined",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Mediation Model 4

Predictor: Class ZSB

Mediator: Linked fate with non-white working class

Outcome: Cross-Race Solidarity

Sample: White people

Bootstraps: 10,000

form.m <- reformulate("zs_class", response = "lf_nwwc")

form.y <- reformulate(c("zs_class", "lf_nwwc"), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg %>% filter(ingroup == "white people") %>% filter(!is.na(crs)))        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg %>% filter(ingroup == "white people") %>% filter(!is.na(crs)))        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate("zs_class", response = "crs"),
  data = df_cbzs_elg %>% filter(ingroup == "white people") %>% filter(!is.na(crs))
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf_nwwc",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0660911 0.0166123 0.1194175 0.0092
ADE (direct) 0.0725522 -0.0422206 0.1910918 0.2034
Total Effect 0.1386433 0.0098357 0.2677465 0.0322
Prop. Mediated 0.4766987 0.0971616 1.9637991 0.0378
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf_nwwc"]
b_p      <- summary(y.fit)$coefficients["lf_nwwc", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf_nwwc",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Serial Mediation 1

Predictor: Class ZSB

Mediator 1: Linked fate (for white participants, I take linked fate with non-white working-class; for non-white participants, I take linked fate with white working-class)

Mediator 2: Solidarity

Outcome: Support for policy

Bootstraps: 10,000

df <- df_cbzs_elg

model_serial <- '
  # structural paths (the chain)
  lf_combined ~ a*zs_class
  soli   ~ b*lf_combined + c1*zs_class
  support~ d*soli    + e*lf_combined + c2*zs_class

  # defined (derived) effects
  ind_zs_to_soli      := a*b
  ind_zs_to_support_1 := a*b*d        # zs -> lf -> soli -> support
  ind_zs_to_support_2 := a*e          # zs -> lf -> support
  ind_zs_to_support   := ind_zs_to_support_1 + ind_zs_to_support_2

  total_zs_to_soli    := c1 + ind_zs_to_soli
  total_zs_to_support := c2 + ind_zs_to_support
'

fit_serial <- lavaan::sem(
  model_serial,
  data      = df,
  se        = "bootstrap",
  bootstrap = 10000
)

pe <- parameterEstimates(fit_serial, ci = TRUE, level = 0.95) %>%
  as_tibble()

# Effects table (for the defined ones)
effects_tbl <- pe %>%
  filter(op == ":=") %>%
  transmute(
    Effect   = lhs,
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects_tbl, digits = 3) %>%
  kable_styling(bootstrap_options = "hover",
                full_width = FALSE,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ind_zs_to_soli 0.024 0.005 0.052 0.048
ind_zs_to_support_1 0.018 0.003 0.042 0.078
ind_zs_to_support_2 -0.011 -0.043 0.014 0.447
ind_zs_to_support 0.007 -0.019 0.036 0.604
total_zs_to_soli 0.257 0.194 0.322 0.000
total_zs_to_support 0.195 0.064 0.331 0.004
library(ggplot2)
library(grid)
library(dplyr)

p_stars <- function(p) {
  if (is.na(p)) return("")
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# assumes `pe` exists from lavaan::parameterEstimates(fit_serial, ci=TRUE)
get_path <- function(lhs, rhs) {
  row <- pe %>% filter(op == "~", lhs == !!lhs, rhs == !!rhs)
  list(est = row$est[1], p = row$pvalue[1])
}

# paths
a  <- get_path("lf_combined", "zs_class")
b  <- get_path("soli", "lf_combined")
d  <- get_path("support", "soli")
c1 <- get_path("soli", "zs_class")      # zs -> soli (diagonal)
e  <- get_path("support", "lf_combined")    # lf -> support (diagonal)
c2 <- get_path("support", "zs_class")   # zs -> support (bottom horizontal)

# labels
a_lab  <- paste0("a = ",   round(a$est, 3),  p_stars(a$p))
b_lab  <- paste0("b = ",   round(b$est, 3),  p_stars(b$p))
d_lab  <- paste0("d = ",   round(d$est, 3),  p_stars(d$p))
c1_lab <- paste0("c'1 = ", round(c1$est, 3), p_stars(c1$p))
e_lab  <- paste0("e = ",   round(e$est, 3),  p_stars(e$p))
c2_lab <- paste0("c'2 = ", round(c2$est, 3), p_stars(c2$p))

# node positions (moved zs left, support right)
pos <- tibble::tibble(
  node  = c("zs_class","lf_combined","soli","support"),
  x     = c(0.7, 1.0, 4.2, 4.6),
  y     = c(0.6, 2.2, 2.2, 0.6)
)

# box size
w <- 2.1
h <- 0.65

# helpers
node_xy <- function(name) {
  row <- pos[pos$node == name, ]
  list(x = row$x[1], y = row$y[1])
}
edge_right <- function(x) x + w/2
edge_left  <- function(x) x - w/2
edge_top   <- function(y) y + h/2
edge_bot   <- function(y) y - h/2

# draw a boxed node
draw_node <- function(name, label) {
  p <- node_xy(name)
  list(
    annotate("rect",
             xmin = p$x - w/2, xmax = p$x + w/2,
             ymin = p$y - h/2, ymax = p$y + h/2,
             fill = "white", color = "black", linewidth = 0.5),
    annotate("text", x = p$x, y = p$y, label = label,
             fontface = "bold", size = 3.8)
  )
}

# label text (no bubble, like the reference)
lbl <- function(x, y, txt) annotate("text", x = x, y = y, label = txt, size = 3.5)

# convenience
Pzs <- node_xy("zs_class")
Plf <- node_xy("lf_combined")
Pso <- node_xy("soli")
Psu <- node_xy("support")

ggplot() +
  xlim(-0.8, 6.1) + ylim(0.0, 2.8) +   # widened x-lims for new layout

  # nodes
  draw_node("zs_class", "zs_class") +
  draw_node("lf_combined",  "lf_combined") +
  draw_node("soli",     "soli") +
  draw_node("support",  "support") +

  # zs -> lf (vertical-ish)
  annotate("segment",
           x = Pzs$x, xend = Plf$x,
           y = edge_top(Pzs$y), yend = edge_bot(Plf$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pzs$x - 0.40, (Pzs$y + Plf$y)/2, a_lab) +

  # lf -> soli (top horizontal)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Pso$x),
           y = Plf$y, yend = Pso$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl((Plf$x + Pso$x)/2, Plf$y + 0.22, b_lab) +

  # soli -> support (vertical-ish down)
  annotate("segment",
           x = Pso$x, xend = Psu$x,
           y = edge_bot(Pso$y), yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pso$x + 0.40, (Pso$y + Psu$y)/2, d_lab) +

  # zs -> soli (diagonal up-right)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Pso$x),
           y = edge_top(Pzs$y),   yend = edge_bot(Pso$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Pso$x)/2, (Pzs$y + Pso$y)/2 + 0.12, c1_lab) +

  # lf -> support (diagonal down-right)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Psu$x),
           y = edge_bot(Plf$y),   yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Plf$x + Psu$x)/2, (Plf$y + Psu$y)/2 - 0.12, e_lab) +

  # zs -> support (bottom horizontal)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Psu$x),
           y = Pzs$y, yend = Psu$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Psu$x)/2, Pzs$y - 0.22, c2_lab) +

  theme_void()

Serial Mediation 2

Predictor: Class ZSB

Mediator 1: Linked fate with non-white working class

Mediator 2: Solidarity

Outcome: Support for policy

Sample: White people

Bootstraps: 10,000

df_w <- df_cbzs_elg %>%
  filter(ingroup == "white people")

model_serial <- '
  # structural paths (the chain)
  lf_nwwc ~ a*zs_class
  soli   ~ b*lf_nwwc + c1*zs_class
  support~ d*soli    + e*lf_nwwc + c2*zs_class

  # defined (derived) effects
  ind_zs_to_soli      := a*b
  ind_zs_to_support_1 := a*b*d        # zs -> lf -> soli -> support
  ind_zs_to_support_2 := a*e          # zs -> lf -> support
  ind_zs_to_support   := ind_zs_to_support_1 + ind_zs_to_support_2

  total_zs_to_soli    := c1 + ind_zs_to_soli
  total_zs_to_support := c2 + ind_zs_to_support
'

fit_serial <- lavaan::sem(
  model_serial,
  data      = df_w,
  se        = "bootstrap",
  bootstrap = 10000
)

pe <- parameterEstimates(fit_serial, ci = TRUE, level = 0.95) %>%
  as_tibble()

# Effects table (for the defined ones)
effects_tbl <- pe %>%
  filter(op == ":=") %>%
  transmute(
    Effect   = lhs,
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects_tbl, digits = 3) %>%
  kable_styling(bootstrap_options = "hover",
                full_width = FALSE,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ind_zs_to_soli 0.019 0.003 0.038 0.039
ind_zs_to_support_1 0.011 0.001 0.027 0.107
ind_zs_to_support_2 -0.007 -0.044 0.025 0.690
ind_zs_to_support 0.004 -0.029 0.035 0.783
total_zs_to_soli 0.231 0.175 0.286 0.000
total_zs_to_support 0.269 0.110 0.431 0.001
library(ggplot2)
library(grid)
library(dplyr)

p_stars <- function(p) {
  if (is.na(p)) return("")
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# assumes `pe` exists from lavaan::parameterEstimates(fit_serial, ci=TRUE)
get_path <- function(lhs, rhs) {
  row <- pe %>% filter(op == "~", lhs == !!lhs, rhs == !!rhs)
  list(est = row$est[1], p = row$pvalue[1])
}

# paths
a  <- get_path("lf_nwwc", "zs_class")
b  <- get_path("soli", "lf_nwwc")
d  <- get_path("support", "soli")
c1 <- get_path("soli", "zs_class")      # zs -> soli (diagonal)
e  <- get_path("support", "lf_nwwc")    # lf -> support (diagonal)
c2 <- get_path("support", "zs_class")   # zs -> support (bottom horizontal)

# labels
a_lab  <- paste0("a = ",   round(a$est, 3),  p_stars(a$p))
b_lab  <- paste0("b = ",   round(b$est, 3),  p_stars(b$p))
d_lab  <- paste0("d = ",   round(d$est, 3),  p_stars(d$p))
c1_lab <- paste0("c'1 = ", round(c1$est, 3), p_stars(c1$p))
e_lab  <- paste0("e = ",   round(e$est, 3),  p_stars(e$p))
c2_lab <- paste0("c'2 = ", round(c2$est, 3), p_stars(c2$p))

# node positions (moved zs left, support right)
pos <- tibble::tibble(
  node  = c("zs_class","lf_nwwc","soli","support"),
  x     = c(0.7, 1.0, 4.2, 4.6),
  y     = c(0.6, 2.2, 2.2, 0.6)
)

# box size
w <- 2.1
h <- 0.65

# helpers
node_xy <- function(name) {
  row <- pos[pos$node == name, ]
  list(x = row$x[1], y = row$y[1])
}
edge_right <- function(x) x + w/2
edge_left  <- function(x) x - w/2
edge_top   <- function(y) y + h/2
edge_bot   <- function(y) y - h/2

# draw a boxed node
draw_node <- function(name, label) {
  p <- node_xy(name)
  list(
    annotate("rect",
             xmin = p$x - w/2, xmax = p$x + w/2,
             ymin = p$y - h/2, ymax = p$y + h/2,
             fill = "white", color = "black", linewidth = 0.5),
    annotate("text", x = p$x, y = p$y, label = label,
             fontface = "bold", size = 3.8)
  )
}

# label text (no bubble, like the reference)
lbl <- function(x, y, txt) annotate("text", x = x, y = y, label = txt, size = 3.5)

# convenience
Pzs <- node_xy("zs_class")
Plf <- node_xy("lf_nwwc")
Pso <- node_xy("soli")
Psu <- node_xy("support")

ggplot() +
  xlim(-0.8, 6.1) + ylim(0.0, 2.8) +   # widened x-lims for new layout

  # nodes
  draw_node("zs_class", "zs_class") +
  draw_node("lf_nwwc",  "lf_nwwc") +
  draw_node("soli",     "soli") +
  draw_node("support",  "support") +

  # zs -> lf (vertical-ish)
  annotate("segment",
           x = Pzs$x, xend = Plf$x,
           y = edge_top(Pzs$y), yend = edge_bot(Plf$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pzs$x - 0.40, (Pzs$y + Plf$y)/2, a_lab) +

  # lf -> soli (top horizontal)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Pso$x),
           y = Plf$y, yend = Pso$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl((Plf$x + Pso$x)/2, Plf$y + 0.22, b_lab) +

  # soli -> support (vertical-ish down)
  annotate("segment",
           x = Pso$x, xend = Psu$x,
           y = edge_bot(Pso$y), yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pso$x + 0.40, (Pso$y + Psu$y)/2, d_lab) +

  # zs -> soli (diagonal up-right)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Pso$x),
           y = edge_top(Pzs$y),   yend = edge_bot(Pso$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Pso$x)/2, (Pzs$y + Pso$y)/2 + 0.12, c1_lab) +

  # lf -> support (diagonal down-right)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Psu$x),
           y = edge_bot(Plf$y),   yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Plf$x + Psu$x)/2, (Plf$y + Psu$y)/2 - 0.12, e_lab) +

  # zs -> support (bottom horizontal)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Psu$x),
           y = Pzs$y, yend = Psu$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Psu$x)/2, Pzs$y - 0.22, c2_lab) +

  theme_void()

Serial Mediation 3

Predictor: Class ZSB

Mediator 1: Linked fate (for white participants, I take linked fate with non-white working-class; for non-white participants, I take linked fate with white working-class)

Mediator 2: Cross-Race Solidarity

Outcome: Support for policy

Bootstraps: 10,000

df <- df_cbzs_elg

model_serial <- '
  # structural paths (the chain)
  lf_combined ~ a*zs_class
  crs   ~ b*lf_combined + c1*zs_class
  support~ d*crs    + e*lf_combined + c2*zs_class

  # defined (derived) effects
  ind_zs_to_crs      := a*b
  ind_zs_to_support_1 := a*b*d        # zs -> lf -> crs -> support
  ind_zs_to_support_2 := a*e          # zs -> lf -> support
  ind_zs_to_support   := ind_zs_to_support_1 + ind_zs_to_support_2

  total_zs_to_crs    := c1 + ind_zs_to_crs
  total_zs_to_support := c2 + ind_zs_to_support
'

fit_serial <- lavaan::sem(
  model_serial,
  data      = df,
  se        = "bootstrap",
  bootstrap = 10000
)

pe <- parameterEstimates(fit_serial, ci = TRUE, level = 0.95) %>%
  as_tibble()

# Effects table (for the defined ones)
effects_tbl <- pe %>%
  filter(op == ":=") %>%
  transmute(
    Effect   = lhs,
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects_tbl, digits = 3) %>%
  kable_styling(bootstrap_options = "hover",
                full_width = FALSE,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ind_zs_to_crs 0.068 0.018 0.123 0.010
ind_zs_to_support_1 0.021 0.004 0.047 0.054
ind_zs_to_support_2 -0.014 -0.047 0.010 0.316
ind_zs_to_support 0.007 -0.019 0.036 0.603
total_zs_to_crs 0.142 0.030 0.253 0.013
total_zs_to_support 0.343 0.216 0.471 0.000
library(ggplot2)
library(grid)
library(dplyr)

p_stars <- function(p) {
  if (is.na(p)) return("")
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# assumes `pe` exists from lavaan::parameterEstimates(fit_serial, ci=TRUE)
get_path <- function(lhs, rhs) {
  row <- pe %>% filter(op == "~", lhs == !!lhs, rhs == !!rhs)
  list(est = row$est[1], p = row$pvalue[1])
}

# paths
a  <- get_path("lf_combined", "zs_class")
b  <- get_path("crs", "lf_combined")
d  <- get_path("support", "crs")
c1 <- get_path("crs", "zs_class")      # zs -> crs (diagonal)
e  <- get_path("support", "lf_combined")    # lf -> support (diagonal)
c2 <- get_path("support", "zs_class")   # zs -> support (bottom horizontal)

# labels
a_lab  <- paste0("a = ",   round(a$est, 3),  p_stars(a$p))
b_lab  <- paste0("b = ",   round(b$est, 3),  p_stars(b$p))
d_lab  <- paste0("d = ",   round(d$est, 3),  p_stars(d$p))
c1_lab <- paste0("c'1 = ", round(c1$est, 3), p_stars(c1$p))
e_lab  <- paste0("e = ",   round(e$est, 3),  p_stars(e$p))
c2_lab <- paste0("c'2 = ", round(c2$est, 3), p_stars(c2$p))

# node positions (moved zs left, support right)
pos <- tibble::tibble(
  node  = c("zs_class","lf_combined","crs","support"),
  x     = c(0.7, 1.0, 4.2, 4.6),
  y     = c(0.6, 2.2, 2.2, 0.6)
)

# box size
w <- 2.1
h <- 0.65

# helpers
node_xy <- function(name) {
  row <- pos[pos$node == name, ]
  list(x = row$x[1], y = row$y[1])
}
edge_right <- function(x) x + w/2
edge_left  <- function(x) x - w/2
edge_top   <- function(y) y + h/2
edge_bot   <- function(y) y - h/2

# draw a boxed node
draw_node <- function(name, label) {
  p <- node_xy(name)
  list(
    annotate("rect",
             xmin = p$x - w/2, xmax = p$x + w/2,
             ymin = p$y - h/2, ymax = p$y + h/2,
             fill = "white", color = "black", linewidth = 0.5),
    annotate("text", x = p$x, y = p$y, label = label,
             fontface = "bold", size = 3.8)
  )
}

# label text (no bubble, like the reference)
lbl <- function(x, y, txt) annotate("text", x = x, y = y, label = txt, size = 3.5)

# convenience
Pzs <- node_xy("zs_class")
Plf <- node_xy("lf_combined")
Pso <- node_xy("crs")
Psu <- node_xy("support")

ggplot() +
  xlim(-0.8, 6.1) + ylim(0.0, 2.8) +   # widened x-lims for new layout

  # nodes
  draw_node("zs_class", "zs_class") +
  draw_node("lf_combined",  "lf_combined") +
  draw_node("crs",     "crs") +
  draw_node("support",  "support") +

  # zs -> lf (vertical-ish)
  annotate("segment",
           x = Pzs$x, xend = Plf$x,
           y = edge_top(Pzs$y), yend = edge_bot(Plf$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pzs$x - 0.40, (Pzs$y + Plf$y)/2, a_lab) +

  # lf -> crs (top horizontal)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Pso$x),
           y = Plf$y, yend = Pso$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl((Plf$x + Pso$x)/2, Plf$y + 0.22, b_lab) +

  # crs -> support (vertical-ish down)
  annotate("segment",
           x = Pso$x, xend = Psu$x,
           y = edge_bot(Pso$y), yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pso$x + 0.40, (Pso$y + Psu$y)/2, d_lab) +

  # zs -> crs (diagonal up-right)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Pso$x),
           y = edge_top(Pzs$y),   yend = edge_bot(Pso$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Pso$x)/2, (Pzs$y + Pso$y)/2 + 0.12, c1_lab) +

  # lf -> support (diagonal down-right)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Psu$x),
           y = edge_bot(Plf$y),   yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Plf$x + Psu$x)/2, (Plf$y + Psu$y)/2 - 0.12, e_lab) +

  # zs -> support (bottom horizontal)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Psu$x),
           y = Pzs$y, yend = Psu$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Psu$x)/2, Pzs$y - 0.22, c2_lab) +

  theme_void()

Serial Mediation 4

Predictor: Class ZSB

Mediator 1: Linked fate with non-white working class

Mediator 2: Cross-Race Solidarity

Outcome: Support for policy

Sample: White people

Bootstraps: 10,000

df_w <- df_cbzs_elg %>%
  filter(ingroup == "white people")

model_serial <- '
  # structural paths (the chain)
  lf_nwwc ~ a*zs_class
  crs   ~ b*lf_nwwc + c1*zs_class
  support~ d*crs    + e*lf_nwwc + c2*zs_class

  # defined (derived) effects
  ind_zs_to_crs      := a*b
  ind_zs_to_support_1 := a*b*d        # zs -> lf -> crs -> support
  ind_zs_to_support_2 := a*e          # zs -> lf -> support
  ind_zs_to_support   := ind_zs_to_support_1 + ind_zs_to_support_2

  total_zs_to_crs    := c1 + ind_zs_to_crs
  total_zs_to_support := c2 + ind_zs_to_support
'

fit_serial <- lavaan::sem(
  model_serial,
  data      = df_w,
  se        = "bootstrap",
  bootstrap = 10000
)

pe <- parameterEstimates(fit_serial, ci = TRUE, level = 0.95) %>%
  as_tibble()

# Effects table (for the defined ones)
effects_tbl <- pe %>%
  filter(op == ":=") %>%
  transmute(
    Effect   = lhs,
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects_tbl, digits = 3) %>%
  kable_styling(bootstrap_options = "hover",
                full_width = FALSE,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ind_zs_to_crs 0.066 0.016 0.121 0.013
ind_zs_to_support_1 0.014 0.000 0.037 0.135
ind_zs_to_support_2 -0.010 -0.048 0.020 0.553
ind_zs_to_support 0.004 -0.029 0.036 0.789
total_zs_to_crs 0.139 0.014 0.266 0.032
total_zs_to_support 0.379 0.229 0.526 0.000
library(ggplot2)
library(grid)
library(dplyr)

p_stars <- function(p) {
  if (is.na(p)) return("")
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# assumes `pe` exists from lavaan::parameterEstimates(fit_serial, ci=TRUE)
get_path <- function(lhs, rhs) {
  row <- pe %>% filter(op == "~", lhs == !!lhs, rhs == !!rhs)
  list(est = row$est[1], p = row$pvalue[1])
}

# paths
a  <- get_path("lf_nwwc", "zs_class")
b  <- get_path("crs", "lf_nwwc")
d  <- get_path("support", "crs")
c1 <- get_path("crs", "zs_class")      # zs -> crs (diagonal)
e  <- get_path("support", "lf_nwwc")    # lf -> support (diagonal)
c2 <- get_path("support", "zs_class")   # zs -> support (bottom horizontal)

# labels
a_lab  <- paste0("a = ",   round(a$est, 3),  p_stars(a$p))
b_lab  <- paste0("b = ",   round(b$est, 3),  p_stars(b$p))
d_lab  <- paste0("d = ",   round(d$est, 3),  p_stars(d$p))
c1_lab <- paste0("c'1 = ", round(c1$est, 3), p_stars(c1$p))
e_lab  <- paste0("e = ",   round(e$est, 3),  p_stars(e$p))
c2_lab <- paste0("c'2 = ", round(c2$est, 3), p_stars(c2$p))

# node positions (moved zs left, support right)
pos <- tibble::tibble(
  node  = c("zs_class","lf_nwwc","crs","support"),
  x     = c(0.7, 1.0, 4.2, 4.6),
  y     = c(0.6, 2.2, 2.2, 0.6)
)

# box size
w <- 2.1
h <- 0.65

# helpers
node_xy <- function(name) {
  row <- pos[pos$node == name, ]
  list(x = row$x[1], y = row$y[1])
}
edge_right <- function(x) x + w/2
edge_left  <- function(x) x - w/2
edge_top   <- function(y) y + h/2
edge_bot   <- function(y) y - h/2

# draw a boxed node
draw_node <- function(name, label) {
  p <- node_xy(name)
  list(
    annotate("rect",
             xmin = p$x - w/2, xmax = p$x + w/2,
             ymin = p$y - h/2, ymax = p$y + h/2,
             fill = "white", color = "black", linewidth = 0.5),
    annotate("text", x = p$x, y = p$y, label = label,
             fontface = "bold", size = 3.8)
  )
}

# label text (no bubble, like the reference)
lbl <- function(x, y, txt) annotate("text", x = x, y = y, label = txt, size = 3.5)

# convenience
Pzs <- node_xy("zs_class")
Plf <- node_xy("lf_nwwc")
Pso <- node_xy("crs")
Psu <- node_xy("support")

ggplot() +
  xlim(-0.8, 6.1) + ylim(0.0, 2.8) +   # widened x-lims for new layout

  # nodes
  draw_node("zs_class", "zs_class") +
  draw_node("lf_nwwc",  "lf_nwwc") +
  draw_node("crs",     "crs") +
  draw_node("support",  "support") +

  # zs -> lf (vertical-ish)
  annotate("segment",
           x = Pzs$x, xend = Plf$x,
           y = edge_top(Pzs$y), yend = edge_bot(Plf$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pzs$x - 0.40, (Pzs$y + Plf$y)/2, a_lab) +

  # lf -> crs (top horizontal)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Pso$x),
           y = Plf$y, yend = Pso$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl((Plf$x + Pso$x)/2, Plf$y + 0.22, b_lab) +

  # crs -> support (vertical-ish down)
  annotate("segment",
           x = Pso$x, xend = Psu$x,
           y = edge_bot(Pso$y), yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pso$x + 0.40, (Pso$y + Psu$y)/2, d_lab) +

  # zs -> crs (diagonal up-right)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Pso$x),
           y = edge_top(Pzs$y),   yend = edge_bot(Pso$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Pso$x)/2, (Pzs$y + Pso$y)/2 + 0.12, c1_lab) +

  # lf -> support (diagonal down-right)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Psu$x),
           y = edge_bot(Plf$y),   yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Plf$x + Psu$x)/2, (Plf$y + Psu$y)/2 - 0.12, e_lab) +

  # zs -> support (bottom horizontal)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Psu$x),
           y = Pzs$y, yend = Psu$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Psu$x)/2, Pzs$y - 0.22, c2_lab) +

  theme_void()

Serial Mediation 5

Predictor: Class ZSB

Mediator 1: Solidarity

Mediator 2: Linked fate (for white participants, I take linked fate with non-white working-class; for non-white participants, I take linked fate with white working-class)

Outcome: Support for policy

Bootstraps: 10,000

df <- df_cbzs_elg

model_serial <- '
  # structural paths (the chain)
  soli ~ a*zs_class
  lf_combined   ~ b*soli + c1*zs_class
  support~ d*lf_combined    + e*soli + c2*zs_class

  # defined (derived) effects
  ind_zs_to_lf_combined      := a*b
  ind_zs_to_support_1 := a*b*d        # zs -> lf -> lf_combined -> support
  ind_zs_to_support_2 := a*e          # zs -> lf -> support
  ind_zs_to_support   := ind_zs_to_support_1 + ind_zs_to_support_2

  total_zs_to_lf_combined    := c1 + ind_zs_to_lf_combined
  total_zs_to_support := c2 + ind_zs_to_support
'

fit_serial <- lavaan::sem(
  model_serial,
  data      = df,
  se        = "bootstrap",
  bootstrap = 10000
)

pe <- parameterEstimates(fit_serial, ci = TRUE, level = 0.95) %>%
  as_tibble()

# Effects table (for the defined ones)
effects_tbl <- pe %>%
  filter(op == ":=") %>%
  transmute(
    Effect   = lhs,
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects_tbl, digits = 3) %>%
  kable_styling(bootstrap_options = "hover",
                full_width = FALSE,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ind_zs_to_lf_combined 0.109 0.046 0.181 0.002
ind_zs_to_support_1 -0.009 -0.033 0.012 0.423
ind_zs_to_support_2 0.188 0.110 0.275 0.000
ind_zs_to_support 0.179 0.105 0.259 0.000
total_zs_to_lf_combined 0.129 0.037 0.224 0.008
total_zs_to_support 0.367 0.237 0.496 0.000
library(ggplot2)
library(grid)
library(dplyr)

p_stars <- function(p) {
  if (is.na(p)) return("")
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# assumes `pe` exists from lavaan::parameterEstimates(fit_serial, ci=TRUE)
get_path <- function(lhs, rhs) {
  row <- pe %>% filter(op == "~", lhs == !!lhs, rhs == !!rhs)
  list(est = row$est[1], p = row$pvalue[1])
}

# paths
a  <- get_path("soli", "zs_class")
b  <- get_path("lf_combined", "soli")
d  <- get_path("support", "lf_combined")
c1 <- get_path("lf_combined", "zs_class")      # zs -> lf_combined (diagonal)
e  <- get_path("support", "soli")    # lf -> support (diagonal)
c2 <- get_path("support", "zs_class")   # zs -> support (bottom horizontal)

# labels
a_lab  <- paste0("a = ",   round(a$est, 3),  p_stars(a$p))
b_lab  <- paste0("b = ",   round(b$est, 3),  p_stars(b$p))
d_lab  <- paste0("d = ",   round(d$est, 3),  p_stars(d$p))
c1_lab <- paste0("c'1 = ", round(c1$est, 3), p_stars(c1$p))
e_lab  <- paste0("e = ",   round(e$est, 3),  p_stars(e$p))
c2_lab <- paste0("c'2 = ", round(c2$est, 3), p_stars(c2$p))

# node positions (moved zs left, support right)
pos <- tibble::tibble(
  node  = c("zs_class","soli","lf_combined","support"),
  x     = c(0.7, 1.0, 4.2, 4.6),
  y     = c(0.6, 2.2, 2.2, 0.6)
)

# box size
w <- 2.1
h <- 0.65

# helpers
node_xy <- function(name) {
  row <- pos[pos$node == name, ]
  list(x = row$x[1], y = row$y[1])
}
edge_right <- function(x) x + w/2
edge_left  <- function(x) x - w/2
edge_top   <- function(y) y + h/2
edge_bot   <- function(y) y - h/2

# draw a boxed node
draw_node <- function(name, label) {
  p <- node_xy(name)
  list(
    annotate("rect",
             xmin = p$x - w/2, xmax = p$x + w/2,
             ymin = p$y - h/2, ymax = p$y + h/2,
             fill = "white", color = "black", linewidth = 0.5),
    annotate("text", x = p$x, y = p$y, label = label,
             fontface = "bold", size = 3.8)
  )
}

# label text (no bubble, like the reference)
lbl <- function(x, y, txt) annotate("text", x = x, y = y, label = txt, size = 3.5)

# convenience
Pzs <- node_xy("zs_class")
Plf <- node_xy("soli")
Pso <- node_xy("lf_combined")
Psu <- node_xy("support")

ggplot() +
  xlim(-0.8, 6.1) + ylim(0.0, 2.8) +   # widened x-lims for new layout

  # nodes
  draw_node("zs_class", "zs_class") +
  draw_node("soli",  "soli") +
  draw_node("lf_combined",     "lf_combined") +
  draw_node("support",  "support") +

  # zs -> lf (vertical-ish)
  annotate("segment",
           x = Pzs$x, xend = Plf$x,
           y = edge_top(Pzs$y), yend = edge_bot(Plf$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pzs$x - 0.40, (Pzs$y + Plf$y)/2, a_lab) +

  # lf -> lf_combined (top horizontal)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Pso$x),
           y = Plf$y, yend = Pso$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl((Plf$x + Pso$x)/2, Plf$y + 0.22, b_lab) +

  # lf_combined -> support (vertical-ish down)
  annotate("segment",
           x = Pso$x, xend = Psu$x,
           y = edge_bot(Pso$y), yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pso$x + 0.40, (Pso$y + Psu$y)/2, d_lab) +

  # zs -> lf_combined (diagonal up-right)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Pso$x),
           y = edge_top(Pzs$y),   yend = edge_bot(Pso$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Pso$x)/2, (Pzs$y + Pso$y)/2 + 0.12, c1_lab) +

  # lf -> support (diagonal down-right)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Psu$x),
           y = edge_bot(Plf$y),   yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Plf$x + Psu$x)/2, (Plf$y + Psu$y)/2 - 0.12, e_lab) +

  # zs -> support (bottom horizontal)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Psu$x),
           y = Pzs$y, yend = Psu$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Psu$x)/2, Pzs$y - 0.22, c2_lab) +

  theme_void()

Serial Mediation 6

Predictor: Class ZSB

Mediator 1: Cross-Race Solidarity

Mediator 2: Linked fate (for white participants, I take linked fate with non-white working-class; for non-white participants, I take linked fate with white working-class)

Outcome: Support for policy

Bootstraps: 10,000

df <- df_cbzs_elg

model_serial <- '
  # structural paths (the chain)
  crs ~ a*zs_class
  lf_combined   ~ b*crs + c1*zs_class
  support~ d*lf_combined    + e*crs + c2*zs_class

  # defined (derived) effects
  ind_zs_to_lf_combined      := a*b
  ind_zs_to_support_1 := a*b*d        # zs -> lf -> lf_combined -> support
  ind_zs_to_support_2 := a*e          # zs -> lf -> support
  ind_zs_to_support   := ind_zs_to_support_1 + ind_zs_to_support_2

  total_zs_to_lf_combined    := c1 + ind_zs_to_lf_combined
  total_zs_to_support := c2 + ind_zs_to_support
'

fit_serial <- lavaan::sem(
  model_serial,
  data      = df,
  se        = "bootstrap",
  bootstrap = 10000
)

pe <- parameterEstimates(fit_serial, ci = TRUE, level = 0.95) %>%
  as_tibble()

# Effects table (for the defined ones)
effects_tbl <- pe %>%
  filter(op == ":=") %>%
  transmute(
    Effect   = lhs,
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects_tbl, digits = 3) %>%
  kable_styling(bootstrap_options = "hover",
                full_width = FALSE,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ind_zs_to_lf_combined 0.051 0.010 0.099 0.024
ind_zs_to_support_1 -0.006 -0.020 0.004 0.336
ind_zs_to_support_2 0.045 0.008 0.093 0.039
ind_zs_to_support 0.039 0.007 0.081 0.041
total_zs_to_lf_combined 0.128 0.035 0.225 0.008
total_zs_to_support 0.375 0.241 0.504 0.000
library(ggplot2)
library(grid)
library(dplyr)

p_stars <- function(p) {
  if (is.na(p)) return("")
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# assumes `pe` exists from lavaan::parameterEstimates(fit_serial, ci=TRUE)
get_path <- function(lhs, rhs) {
  row <- pe %>% filter(op == "~", lhs == !!lhs, rhs == !!rhs)
  list(est = row$est[1], p = row$pvalue[1])
}

# paths
a  <- get_path("crs", "zs_class")
b  <- get_path("lf_combined", "crs")
d  <- get_path("support", "lf_combined")
c1 <- get_path("lf_combined", "zs_class")      # zs -> lf_combined (diagonal)
e  <- get_path("support", "crs")    # lf -> support (diagonal)
c2 <- get_path("support", "zs_class")   # zs -> support (bottom horizontal)

# labels
a_lab  <- paste0("a = ",   round(a$est, 3),  p_stars(a$p))
b_lab  <- paste0("b = ",   round(b$est, 3),  p_stars(b$p))
d_lab  <- paste0("d = ",   round(d$est, 3),  p_stars(d$p))
c1_lab <- paste0("c'1 = ", round(c1$est, 3), p_stars(c1$p))
e_lab  <- paste0("e = ",   round(e$est, 3),  p_stars(e$p))
c2_lab <- paste0("c'2 = ", round(c2$est, 3), p_stars(c2$p))

# node positions (moved zs left, support right)
pos <- tibble::tibble(
  node  = c("zs_class","crs","lf_combined","support"),
  x     = c(0.7, 1.0, 4.2, 4.6),
  y     = c(0.6, 2.2, 2.2, 0.6)
)

# box size
w <- 2.1
h <- 0.65

# helpers
node_xy <- function(name) {
  row <- pos[pos$node == name, ]
  list(x = row$x[1], y = row$y[1])
}
edge_right <- function(x) x + w/2
edge_left  <- function(x) x - w/2
edge_top   <- function(y) y + h/2
edge_bot   <- function(y) y - h/2

# draw a boxed node
draw_node <- function(name, label) {
  p <- node_xy(name)
  list(
    annotate("rect",
             xmin = p$x - w/2, xmax = p$x + w/2,
             ymin = p$y - h/2, ymax = p$y + h/2,
             fill = "white", color = "black", linewidth = 0.5),
    annotate("text", x = p$x, y = p$y, label = label,
             fontface = "bold", size = 3.8)
  )
}

# label text (no bubble, like the reference)
lbl <- function(x, y, txt) annotate("text", x = x, y = y, label = txt, size = 3.5)

# convenience
Pzs <- node_xy("zs_class")
Plf <- node_xy("crs")
Pso <- node_xy("lf_combined")
Psu <- node_xy("support")

ggplot() +
  xlim(-0.8, 6.1) + ylim(0.0, 2.8) +   # widened x-lims for new layout

  # nodes
  draw_node("zs_class", "zs_class") +
  draw_node("crs",  "crs") +
  draw_node("lf_combined",     "lf_combined") +
  draw_node("support",  "support") +

  # zs -> lf (vertical-ish)
  annotate("segment",
           x = Pzs$x, xend = Plf$x,
           y = edge_top(Pzs$y), yend = edge_bot(Plf$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pzs$x - 0.40, (Pzs$y + Plf$y)/2, a_lab) +

  # lf -> lf_combined (top horizontal)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Pso$x),
           y = Plf$y, yend = Pso$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl((Plf$x + Pso$x)/2, Plf$y + 0.22, b_lab) +

  # lf_combined -> support (vertical-ish down)
  annotate("segment",
           x = Pso$x, xend = Psu$x,
           y = edge_bot(Pso$y), yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pso$x + 0.40, (Pso$y + Psu$y)/2, d_lab) +

  # zs -> lf_combined (diagonal up-right)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Pso$x),
           y = edge_top(Pzs$y),   yend = edge_bot(Pso$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Pso$x)/2, (Pzs$y + Pso$y)/2 + 0.12, c1_lab) +

  # lf -> support (diagonal down-right)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Psu$x),
           y = edge_bot(Plf$y),   yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Plf$x + Psu$x)/2, (Plf$y + Psu$y)/2 - 0.12, e_lab) +

  # zs -> support (bottom horizontal)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Psu$x),
           y = Pzs$y, yend = Psu$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Psu$x)/2, Pzs$y - 0.22, c2_lab) +

  theme_void()

Serial Mediation 7

Predictor: Class ZSB

Mediator 1: Class ID

Mediator 2: Solidarity

Outcome: Support for policy

Bootstraps: 10,000

df <- df_cbzs_elg

model_serial <- '
  # structural paths (the chain)
  class_id ~ a*zs_class
  soli   ~ b*class_id + c1*zs_class
  support~ d*soli    + e*class_id + c2*zs_class

  # defined (derived) effects
  ind_zs_to_soli      := a*b
  ind_zs_to_support_1 := a*b*d        # zs -> lf -> soli -> support
  ind_zs_to_support_2 := a*e          # zs -> lf -> support
  ind_zs_to_support   := ind_zs_to_support_1 + ind_zs_to_support_2

  total_zs_to_soli    := c1 + ind_zs_to_soli
  total_zs_to_support := c2 + ind_zs_to_support
'

fit_serial <- lavaan::sem(
  model_serial,
  data      = df,
  se        = "bootstrap",
  bootstrap = 10000
)

pe <- parameterEstimates(fit_serial, ci = TRUE, level = 0.95) %>%
  as_tibble()

# Effects table (for the defined ones)
effects_tbl <- pe %>%
  filter(op == ":=") %>%
  transmute(
    Effect   = lhs,
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects_tbl, digits = 3) %>%
  kable_styling(bootstrap_options = "hover",
                full_width = FALSE,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ind_zs_to_soli 0.017 0.002 0.037 0.065
ind_zs_to_support_1 0.013 0.002 0.030 0.081
ind_zs_to_support_2 -0.016 -0.046 0.004 0.211
ind_zs_to_support -0.004 -0.030 0.019 0.763
total_zs_to_soli 0.257 0.193 0.323 0.000
total_zs_to_support 0.185 0.054 0.321 0.007
library(ggplot2)
library(grid)
library(dplyr)

p_stars <- function(p) {
  if (is.na(p)) return("")
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# assumes `pe` exists from lavaan::parameterEstimates(fit_serial, ci=TRUE)
get_path <- function(lhs, rhs) {
  row <- pe %>% filter(op == "~", lhs == !!lhs, rhs == !!rhs)
  list(est = row$est[1], p = row$pvalue[1])
}

# paths
a  <- get_path("class_id", "zs_class")
b  <- get_path("soli", "class_id")
d  <- get_path("support", "soli")
c1 <- get_path("soli", "zs_class")      # zs -> soli (diagonal)
e  <- get_path("support", "class_id")    # lf -> support (diagonal)
c2 <- get_path("support", "zs_class")   # zs -> support (bottom horizontal)

# labels
a_lab  <- paste0("a = ",   round(a$est, 3),  p_stars(a$p))
b_lab  <- paste0("b = ",   round(b$est, 3),  p_stars(b$p))
d_lab  <- paste0("d = ",   round(d$est, 3),  p_stars(d$p))
c1_lab <- paste0("c'1 = ", round(c1$est, 3), p_stars(c1$p))
e_lab  <- paste0("e = ",   round(e$est, 3),  p_stars(e$p))
c2_lab <- paste0("c'2 = ", round(c2$est, 3), p_stars(c2$p))

# node positions (moved zs left, support right)
pos <- tibble::tibble(
  node  = c("zs_class","class_id","soli","support"),
  x     = c(0.7, 1.0, 4.2, 4.6),
  y     = c(0.6, 2.2, 2.2, 0.6)
)

# box size
w <- 2.1
h <- 0.65

# helpers
node_xy <- function(name) {
  row <- pos[pos$node == name, ]
  list(x = row$x[1], y = row$y[1])
}
edge_right <- function(x) x + w/2
edge_left  <- function(x) x - w/2
edge_top   <- function(y) y + h/2
edge_bot   <- function(y) y - h/2

# draw a boxed node
draw_node <- function(name, label) {
  p <- node_xy(name)
  list(
    annotate("rect",
             xmin = p$x - w/2, xmax = p$x + w/2,
             ymin = p$y - h/2, ymax = p$y + h/2,
             fill = "white", color = "black", linewidth = 0.5),
    annotate("text", x = p$x, y = p$y, label = label,
             fontface = "bold", size = 3.8)
  )
}

# label text (no bubble, like the reference)
lbl <- function(x, y, txt) annotate("text", x = x, y = y, label = txt, size = 3.5)

# convenience
Pzs <- node_xy("zs_class")
Plf <- node_xy("class_id")
Pso <- node_xy("soli")
Psu <- node_xy("support")

ggplot() +
  xlim(-0.8, 6.1) + ylim(0.0, 2.8) +   # widened x-lims for new layout

  # nodes
  draw_node("zs_class", "zs_class") +
  draw_node("class_id",  "class_id") +
  draw_node("soli",     "soli") +
  draw_node("support",  "support") +

  # zs -> lf (vertical-ish)
  annotate("segment",
           x = Pzs$x, xend = Plf$x,
           y = edge_top(Pzs$y), yend = edge_bot(Plf$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pzs$x - 0.40, (Pzs$y + Plf$y)/2, a_lab) +

  # lf -> soli (top horizontal)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Pso$x),
           y = Plf$y, yend = Pso$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl((Plf$x + Pso$x)/2, Plf$y + 0.22, b_lab) +

  # soli -> support (vertical-ish down)
  annotate("segment",
           x = Pso$x, xend = Psu$x,
           y = edge_bot(Pso$y), yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.6) +
  lbl(Pso$x + 0.40, (Pso$y + Psu$y)/2, d_lab) +

  # zs -> soli (diagonal up-right)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Pso$x),
           y = edge_top(Pzs$y),   yend = edge_bot(Pso$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Pso$x)/2, (Pzs$y + Pso$y)/2 + 0.12, c1_lab) +

  # lf -> support (diagonal down-right)
  annotate("segment",
           x = edge_right(Plf$x), xend = edge_left(Psu$x),
           y = edge_bot(Plf$y),   yend = edge_top(Psu$y),
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Plf$x + Psu$x)/2, (Plf$y + Psu$y)/2 - 0.12, e_lab) +

  # zs -> support (bottom horizontal)
  annotate("segment",
           x = edge_right(Pzs$x), xend = edge_left(Psu$x),
           y = Pzs$y, yend = Psu$y,
           arrow = arrow(length = unit(0.18, "cm")), linewidth = 0.55) +
  lbl((Pzs$x + Psu$x)/2, Pzs$y - 0.22, c2_lab) +

  theme_void()

Mediation Model 5

Predictor: Class ZSB

Mediator: Linked fate with white working class

Outcome: Solidarity

Sample: Non-White people

Bootstraps: 10,000

form.m <- reformulate("zs_class", response = "lf_wwc")

form.y <- reformulate(c("zs_class", "lf_wwc"), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg %>% filter(ingroup == "racial minorities"))        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg %>% filter(ingroup == "racial minorities"))        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate("zs_class", response = "soli"),
  data = df_cbzs_elg %>% filter(ingroup == "racial minorities")
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf_wwc",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0286635 -0.0290598 0.1252338 0.3918
ADE (direct) 0.3030031 0.1395502 0.4336693 0.0006
Total Effect 0.3316666 0.1446846 0.5113423 0.0008
Prop. Mediated 0.0864228 -0.1228025 0.2881147 0.3918
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf_wwc"]
b_p      <- summary(y.fit)$coefficients["lf_wwc", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf_wwc",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Mediation Model 6

Predictor: Class ZSB

Mediator: Linked fate with white working class

Outcome: Cross-race solidarity

Sample: Non-White people

Bootstraps: 10,000

form.m <- reformulate("zs_class", response = "lf_wwc")

form.y <- reformulate(c("zs_class", "lf_wwc"), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg %>% filter(ingroup == "racial minorities"))        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg %>% filter(ingroup == "racial minorities"))        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate("zs_class", response = "crs"),
  data = df_cbzs_elg %>% filter(ingroup == "racial minorities")
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf_wwc",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0588758 -0.0595016 0.2022429 0.3484
ADE (direct) 0.0840953 -0.0983181 0.2594606 0.3468
Total Effect 0.1429711 -0.0916900 0.3700589 0.2280
Prop. Mediated 0.4118020 -1.9810774 2.9148363 0.3256
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf_wwc"]
b_p      <- summary(y.fit)$coefficients["lf_wwc", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf_wwc",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Mediation Model 7

Predictor: Class ZSB

Mediator: Linked fate (for white participants, I take linked fate with non-white working-class; for non-white participants, I take linked fate with white working-class)

Outcome: Solidarity

Controls: Relative deprivation

Bootstraps: 10,000

controls = c("reldep")

form.m <- reformulate(c("zs_class",controls), response = "lf_combined")

form.y <- reformulate(c("zs_class", "lf_combined",controls), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class",controls), response = "soli"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf_combined",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0275596 0.0075406 0.0579074 0.0022
ADE (direct) 0.2505302 0.1949334 0.3054687 0.0000
Total Effect 0.2780898 0.2151542 0.3455880 0.0000
Prop. Mediated 0.0991033 0.0297521 0.1863427 0.0022
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf_combined"]
b_p      <- summary(y.fit)$coefficients["lf_combined", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf_combined",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Mediation Model 8

Predictor: Class ZSB

Mediator: Linked fate (for white participants, I take linked fate with non-white working-class; for non-white participants, I take linked fate with white working-class)

Outcome: Cross-Race Solidarity

Controls: Relative deprivation

Bootstraps: 10,000

controls = c("reldep")

form.m <- reformulate(c("zs_class",controls), response = "lf_combined")

form.y <- reformulate(c("zs_class", "lf_combined",controls), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg %>% filter(!is.na(crs)))        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg %>% filter(!is.na(crs)))        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class",controls), response = "crs"),
  data = df_cbzs_elg %>% filter(!is.na(crs))
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf_combined",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0780018 0.0295362 0.1332398 0.0018
ADE (direct) 0.1244007 0.0238862 0.2244199 0.0124
Total Effect 0.2024025 0.0917412 0.3135033 0.0000
Prop. Mediated 0.3853796 0.1737683 0.7649324 0.0018
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf_combined"]
b_p      <- summary(y.fit)$coefficients["lf_combined", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf_combined",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Mediation Model 9

Predictor: Class ZSB

Mediator: Relative deprivation

Outcome: Solidarity

Bootstraps: 10,000

form.m <- reformulate("zs_class", response = "reldep")

form.y <- reformulate(c("zs_class", "reldep"), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate("zs_class", response = "soli"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "reldep",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) -0.0213173 -0.0413804 -0.0048421 0.0094
ADE (direct) 0.2780898 0.2166643 0.3449410 0.0000
Total Effect 0.2567725 0.1959893 0.3224623 0.0000
Prop. Mediated -0.0830202 -0.1743534 -0.0182783 0.0094
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["reldep"]
b_p      <- summary(y.fit)$coefficients["reldep", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "reldep",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Mediation Model 10

Predictor: Class ZSB

Mediator: Relative deprivation

Outcome: Cross-Race Solidarity

Bootstraps: 10,000

form.m <- reformulate("zs_class", response = "reldep")

form.y <- reformulate(c("zs_class", "reldep"), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg %>% filter(!is.na(crs)))        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg %>% filter(!is.na(crs)))        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate("zs_class", response = "crs"),
  data = df_cbzs_elg %>% filter(!is.na(crs))
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "reldep",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) -0.0605750 -0.1036552 -0.0240729 0.0004
ADE (direct) 0.2024025 0.0904033 0.3127376 0.0002
Total Effect 0.1418276 0.0286232 0.2520236 0.0142
Prop. Mediated -0.4271030 -1.9936329 -0.1192909 0.0146
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["reldep"]
b_p      <- summary(y.fit)$coefficients["reldep", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "reldep",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +
  theme_void()

Interactions

LM 1A

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Solidarity

Sample: White people

m1 <- lm(soli ~ zs_class*zs_race,data = df_cbzs_elg %>% filter(ingroup == "white people"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 5.35 [4.80, 5.89] 19.28 212 < .001 NA
Zs class 0.20 [0.10, 0.31] 3.77 212 < .001 0.251
Zs race -0.23 [-0.48, 0.02] -1.85 212 .065 0.077
Zs class \(\times\) Zs race 0.02 [-0.03, 0.07] 0.73 212 .469 0.002
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 1B

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Solidarity

Controls: Class ID, Relative deprivation

Sample: White people

m1 <- lm(soli ~ zs_class*zs_race + class_id + reldep,data = df_cbzs_elg %>% filter(ingroup == "white people"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 5.35 [4.78, 5.93] 18.35 210 < .001 NA
Zs class 0.19 [0.08, 0.29] 3.54 210 < .001 0.263
Zs race -0.28 [-0.53, -0.04] -2.27 210 .024 0.081
Class id 0.01 [0.00, 0.01] 3.17 210 .002 0.037
Reldep -0.08 [-0.16, 0.00] -2.05 210 .042 0.018
Zs class \(\times\) Zs race 0.03 [-0.02, 0.08] 1.16 210 .247 0.006
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 2A

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Cross-race Solidarity

Sample: White people

m1 <- lm(crs ~ zs_class*zs_race,data = df_cbzs_elg %>% filter(ingroup == "white people"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 3.88 [2.79, 4.97] 7.00 209 < .001 NA
Zs class 0.41 [0.20, 0.62] 3.80 209 < .001 0.030
Zs race 0.22 [-0.28, 0.71] 0.86 209 .392 0.163
Zs class \(\times\) Zs race -0.13 [-0.23, -0.04] -2.72 209 .007 0.034
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 2B

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Cross-race Solidarity

Controls: Class ID, relative deprivation

Sample: White people

m1 <- lm(crs ~ zs_class*zs_race + class_id + reldep,data = df_cbzs_elg %>% filter(ingroup == "white people"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.07 [2.94, 5.20] 7.11 207 < .001 NA
Zs class 0.39 [0.18, 0.59] 3.69 207 < .001 0.033
Zs race 0.11 [-0.37, 0.59] 0.45 207 .657 0.176
Class id 0.01 [0.01, 0.02] 3.53 207 < .001 0.051
Reldep -0.25 [-0.41, -0.10] -3.24 207 .001 0.054
Zs class \(\times\) Zs race -0.11 [-0.20, -0.01] -2.27 207 .024 0.024
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 3A

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Linked Fate with Non-White Working-Class

Sample: White people

m1 <- lm(lf_nwwc ~ zs_class*zs_race,data = df_cbzs_elg %>% filter(ingroup == "white people"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 5.20 [4.33, 6.06] 11.85 212 < .001 NA
Zs class 0.13 [-0.03, 0.30] 1.57 212 .119 0.036
Zs race -0.31 [-0.70, 0.09] -1.54 212 .126 0.127
Zs class \(\times\) Zs race 0.00 [-0.08, 0.08] 0.01 212 .991 0.000
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 3B

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Linked Fate with Non-White Working-Class

Controls: Class ID + relative deprivation

Sample: White people

m1 <- lm(lf_nwwc ~ zs_class*zs_race + class_id + reldep,data = df_cbzs_elg %>% filter(ingroup == "white people"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 5.22 [4.30, 6.15] 11.13 210 < .001 NA
Zs class 0.12 [-0.05, 0.29] 1.40 210 .162 0.037
Zs race -0.35 [-0.75, 0.04] -1.78 210 .077 0.130
Class id 0.01 [0.00, 0.01] 1.98 210 .049 0.015
Reldep -0.09 [-0.22, 0.04] -1.40 210 .163 0.009
Zs class \(\times\) Zs race 0.01 [-0.07, 0.09] 0.28 210 .777 0.000
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 4A

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Linked Fate with White Upper-Class

Sample: White people

m1 <- lm(lf_wuc ~ zs_class*zs_race,data = df_cbzs_elg %>% filter(ingroup == "white people"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 6.18 [5.10, 7.26] 11.27 212 < .001 NA
Zs class -0.55 [-0.76, -0.34] -5.14 212 < .001 0.183
Zs race -0.33 [-0.82, 0.16] -1.33 212 .185 0.012
Zs class \(\times\) Zs race 0.09 [-0.01, 0.19] 1.85 212 .066 0.016
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 4B

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Linked Fate with White Upper-Class

Controls: Class ID + relative deprivation

Sample: White people

m1 <- lm(lf_wuc ~ zs_class*zs_race + class_id + reldep,data = df_cbzs_elg %>% filter(ingroup == "white people"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 7.32 [6.23, 8.41] 13.23 210 < .001 NA
Zs class -0.47 [-0.67, -0.27] -4.64 210 < .001 0.205
Zs race -0.29 [-0.75, 0.18] -1.22 210 .223 0.014
Class id -0.01 [-0.01, 0.00] -1.63 210 .105 0.030
Reldep -0.38 [-0.53, -0.23] -5.07 210 < .001 0.103
Zs class \(\times\) Zs race 0.10 [0.01, 0.19] 2.17 210 .031 0.022
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 5A

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Linked Fate with White Working-Class

Sample: Non-White people

m1 <- lm(lf_wwc ~ zs_class*zs_race,data = df_cbzs_elg %>% filter(ingroup == "racial minorities"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 1.85 [0.07, 3.63] 2.07 65 .042 NA
Zs class 0.68 [0.24, 1.13] 3.08 65 .003 0.020
Zs race 0.66 [0.12, 1.19] 2.47 65 .016 0.002
Zs class \(\times\) Zs race -0.16 [-0.27, -0.05] -2.86 65 .006 0.112
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 5B

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Linked Fate with White Working-Class

Controls: Class ID + relative deprivation

Sample: Non-White people

m1 <- lm(lf_wwc ~ zs_class*zs_race + class_id + reldep,data = df_cbzs_elg %>% filter(ingroup == "racial minorities"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 2.08 [-0.18, 4.35] 1.84 63 .071 NA
Zs class 0.62 [0.15, 1.09] 2.61 63 .011 0.021
Zs race 0.57 [0.00, 1.15] 1.98 63 .052 0.002
Class id 0.01 [-0.01, 0.02] 0.92 63 .361 0.022
Reldep -0.09 [-0.43, 0.25] -0.52 63 .603 0.036
Zs class \(\times\) Zs race -0.14 [-0.26, -0.02] -2.29 63 .026 0.077
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 6A

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Linked Fate with Non-White Upper-Class

Sample: Non-White people

m1 <- lm(lf_nwuc ~ zs_class*zs_race,data = df_cbzs_elg %>% filter(ingroup == "racial minorities"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 2.43 [0.77, 4.09] 2.92 65 .005 NA
Zs class 0.55 [0.14, 0.96] 2.66 65 .010 0.015
Zs race 0.56 [0.06, 1.05] 2.25 65 .028 0.000
Zs class \(\times\) Zs race -0.13 [-0.23, -0.03] -2.53 65 .014 0.089
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)

LM 6B

Predictor: Class ZSB

Moderator: Race ZSB

Outcome: Linked Fate with Non-White Upper-Class

Controls: Class ID + relative deprivation

Sample: Non-White people

m1 <- lm(lf_nwuc ~ zs_class*zs_race + class_id + reldep,data = df_cbzs_elg %>% filter(ingroup == "racial minorities"))

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 3.87 [1.87, 5.87] 3.87 63 < .001 NA
Zs class 0.51 [0.09, 0.93] 2.44 63 .017 0.017
Zs race 0.48 [-0.03, 0.99] 1.88 63 .064 0.000
Class id -0.01 [-0.02, 0.00] -1.48 63 .143 0.051
Reldep -0.29 [-0.60, 0.01] -1.93 63 .058 0.119
Zs class \(\times\) Zs race -0.11 [-0.21, 0.00] -1.96 63 .054 0.058
interact_plot(m1,
              pred = "zs_class",
              modx = "zs_race",
              interval = T)