Moderation and Heterogeneous Effects
Welcome
Introduction!
Welcome to our tenth tutorial for the Statistics II: Statistical Modeling & Causal Inference (with R) course.
During this week’s lecture you were introduced to Moderation and Heterogeneous Effects.
In this lab session we will:
- Learn how to perform interaction models with
lm()
- Learn how to extract marginal/partial effects with
margins::margins()
and predictive margins withggeffects::::ggeffect()
- Learn how to vectorize multiple ifelse() statements with
dplyr::case_when()
Measuring the effect of an information intervention on peace agreement support
The country of Absurdistan has had an ongoing civil conflict for the past 60 years. The fighting between national government forces and guerrilla members from the National Revolutionary Army (NRA) has lead to more than 200,000 deaths, 8 million internally displaced persons, and countless victims between 1960 to 2020.
The civil war in Absurdistan has been a low-intensity asymmetric war. The legacies of the conflict have been bore largely by regions in the periphery of the country. Large cities and industrial regions have been spared from most of the fighting.
The government and the leadership of the NRA reached a peace agreement a couple of months ago; however, the agreement has been received poorly by the general population of Absurdistan. The opposition party the Undemocratic Center (UC) has allegedly ran campaigns misrepresenting the contents of the agreement in partisan media outlets and social media.
The Special Envoy for Peace has established a taskforce to design a strategy to increase support for the peace agreement. Many in the taskforce suspect that if the public were properly informed about the agreement reached, the levels of support would be higher.
You are hired as a scientific consultant for the taskforce. You run a survey experiment on a sample of 1000 respondents. You randomly assign respondents to watch an educational 2 minute video about the peace agreement.
Packages
set.seed(42) #for consistent results
library(dplyr) # to wrangle our data
library(tidyr) # to wrangle our data - pivot_longer()
library(ggplot2) # to render our graphs
library(readr) # for loading the .csv data
library(janitor) # for data management and tabyl()
library(kableExtra) # to render better formatted tables
library(stargazer) # for formatting your model output
library(margins) #for calculating MARGINAL/PARTIAL EFFECT
library(ggeffects) # easily calculating PREDICTIVE MARGINS
Exploratory analysis
moderation_df <- readr::read_csv("https://raw.githubusercontent.com/seramirezruiz/hertiestats2/master/data/moderation_df.csv") # simulated data
names(moderation_df) # to check the names of the variables in our data
## [1] "subject_id" "treatment" "victim_verbose"
## [4] "victim" "female" "female_verbose"
## [7] "support_thermometer"
Our dataset moderation_df, contains the following information:
subject_id
: A unique numeric identification for each respondenttreatment
: A binary marker for treatmentvictim_verbose
: A verbose binary marker of the respondents’ victimhood status (Not a Victim-Victim)victim
: A numeric binary marker of the respondents’ victimhood status (0-1)female
: A numeric binary marker of the respondents’ sex (0-1)female_verbose
: A verbose binary marker of the respondents’ sex (Male-Female)support_thermometer
: A continuous measure of support for the agreement (0-100)
Let’s explore who is in our sample
We can use what we have learned about the janitor::tabyl() function, to check who was in our sample:
moderation_df %>%
janitor::tabyl(treatment) %>%
knitr::kable(col.names = c("Treatment", "N", "Percent, %")) %>% # create kable table
kableExtra::kable_styling() # view kable table
Treatment | N | Percent, % |
---|---|---|
0 | 500 | 0.5 |
1 | 500 | 0.5 |
moderation_df %>%
janitor::tabyl(treatment, victim_verbose) %>%
janitor::adorn_totals(c("row", "col")) %>%
knitr::kable(col.names = c("Treatment", "Not a victim", "Victim", "Total")) %>% # create kable table
kableExtra::kable_styling() %>% # view kable table
kableExtra::add_header_above(c("", "Victimization status" = 2, ""))
Treatment | Not a victim | Victim | Total |
---|---|---|---|
0 | 250 | 250 | 500 |
1 | 250 | 250 | 500 |
Total | 500 | 500 | 1000 |
Let’s explore visually the observed levels of public support for the agreement
ggplot(moderation_df, aes(x = support_thermometer)) +
geom_density(fill = "#af8dc3", alpha = 0.5) +
theme_minimal() +
labs(x = "Support thermometer",
y = "Density")
What do we see in this graph?
Let’s explore visually the observed levels of public support for the agreement conditional on the treatment status
ggplot(moderation_df, aes(x = support_thermometer, fill = factor(treatment))) +
geom_density(alpha = 0.5) +
theme_minimal() +
labs(x = "Support thermometer",
y = "Density",
fill = "Treatment") +
theme(legend.position = "bottom") +
scale_fill_manual(name = " ", # changes to fill dimension
values = c("#a7a8aa", "#cc0055"),
labels = c("Control", "Treatment"))
What do we see through these distributions?
Let’s explore visually the observed levels of public support for the agreement conditional on the treatment and victimhood status
ggplot(moderation_df, aes(x = support_thermometer, fill = factor(treatment))) +
geom_density(alpha = 0.5) +
facet_grid(treatment~victim_verbose) +
theme_bw() +
labs(x = "Support thermometer",
y = "Density",
fill = "Treatment") +
theme(legend.position = "bottom") +
scale_fill_manual(name = " ", # changes to fill dimension
values = c("#a7a8aa", "#cc0055"),
labels = c("Control", "Treatment"))
What patterns do we see here?
Modeling and estimating
During this week’s lecture, we learned that we can explicitly model heterogeneity in treatment effects for subgroups. Thus, we can address the tension between having to do inference at the group level, and the recognition of individual differences.
The analysis of heterogeneity can be very important for the design of our strategy. There are competing theories about the effects of conflict victimization on political behavior and attitudes. Some of the literature points to victims developing pro-social attitudes, while others suggest that victims become intransigent towards out-groups.
Could factual information about the peace agreement be received differently by victims and non-victims?
How to estimate heterogenous treatment effects
Heterogeneous treatment effects are usually estimated with regression models that include an interaction between the treatment and the moderator. In our case, the formula would look like this:
\[Y_{i} = β_0 + β_1D_{i} + β_2Victim_{i} + β_3D_i * Victim_{i}+ ϵ_i\]
- \(β_0\): Constant
- \(β_1\): Effect of \(D_i\) on \(Y_i\) if \(Victim_i\) is zero
- \(β_2\): Effect of \(Victim_i\) on \(Y_i\) if \(D_i\) is zero
- \(β_3\): Difference in treatment effects of \(D_i\) depending on \(Victim_i\)
or in lm()
terms (same result):
lm(outcome ~ treatment + moderator + (treatment*moderator))
lm(outcome ~ treatment + moderator + treatment:moderator)
lm(outcome ~ treatment * moderator)
Think of the switch logic posed by Prof. Munzert
Let’s model our results
We will move forward by creating two models:
- A naive model, where we will regress
support_thermometer
ontreatment
. - An interaction model, where we will add an interaction between
treatment
andvictim
Naive modeling
naive_model <- lm(support_thermometer ~ treatment, data = moderation_df)
stargazer::stargazer(naive_model, type = "html")
Dependent variable: | |
support_thermometer | |
treatment | 18.898*** |
(1.026) | |
Constant | 35.055*** |
(0.725) | |
Observations | 1,000 |
R2 | 0.254 |
Adjusted R2 | 0.253 |
Residual Std. Error | 16.220 (df = 998) |
F Statistic | 339.360*** (df = 1; 998) |
Note: | p<0.1; p<0.05; p<0.01 |
What does this model tell us?
The naive model suggests that the subjects that the support for the peace agreement is about 18.9 percentage points higher on average for the subjects that watched the educational video. We suspect, however, that the video may affect differently victims from non-victims of the conflict.
Here is a visual illustration of the values rendered by the naive regression
ggplot(moderation_df, aes(x = support_thermometer, fill = factor(treatment))) +
geom_density(alpha = 0.5) +
theme_minimal() +
geom_vline(xintercept = 35.055, linetype = "longdash", color = "#a7a8aa") + #D=0 (just beta0)
geom_vline(xintercept = 35.055 + 18.898, linetype = "longdash", color = "#cc0055") + #D=1 (beta0+beta1) +
geom_text(aes(label = "ß0", x = 35.055 + 3, y = 0.04), color = "#a7a8aa") + # we add the 3 to repel from the line
geom_text(aes(label = "ß0 + ß1", x = 35.055 + 18.898 + 6, y = 0.04 ), color = "#cc0055") + # we add the 6 to repel from the line
labs(x = "Support thermometer",
y = "Density",
fill = "Treatment") +
theme(legend.position = "bottom") +
scale_fill_manual(name = " ", # changes to fill dimension
values = c("#a7a8aa", "#cc0055"),
labels = c("Control", "Treatment"))
Interaction model
Following any of the different formats renders the same results.
interaction_model <- lm(support_thermometer ~ treatment + victim + (treatment*victim), data = moderation_df)
interaction_model_2 <- lm(support_thermometer ~ treatment + victim + treatment:victim, data = moderation_df)
interaction_model_3 <- lm(support_thermometer ~ treatment*victim, data = moderation_df)
stargazer::stargazer(interaction_model, interaction_model_2, interaction_model_3, type = "html")
Dependent variable: | |||
support_thermometer | |||
(1) | (2) | (3) | |
treatment | 8.013*** | 8.013*** | 8.013*** |
(0.776) | (0.776) | (0.776) | |
victim | 14.256*** | 14.256*** | 14.256*** |
(0.776) | (0.776) | (0.776) | |
treatment:victim | 21.771*** | 21.771*** | 21.771*** |
(1.097) | (1.097) | (1.097) | |
Constant | 27.927*** | 27.927*** | 27.927*** |
(0.549) | (0.549) | (0.549) | |
Observations | 1,000 | 1,000 | 1,000 |
R2 | 0.787 | 0.787 | 0.787 |
Adjusted R2 | 0.786 | 0.786 | 0.786 |
Residual Std. Error (df = 996) | 8.673 | 8.673 | 8.673 |
F Statistic (df = 3; 996) | 1,227.193*** | 1,227.193*** | 1,227.193*** |
Note: | p<0.1; p<0.05; p<0.01 |
What does this model tell us?
- \(β_0\): Constant = The average support for non-victims who were not exposed to the video was 27.92
- \(β_1\): Effect of \(D_i\) on \(Y_i\) if \(Victim_i\) is zero = The educational video results in an increase of about 8 percentage points of the peace agreement for the non-victims
- \(β_2\): Effect of \(Victim_i\) on \(Y_i\) if \(D_i\) is zero = On average, victims’ support for the peace agreement is 14.3 percentage points higher than that of the non-victims in the control group
- \(β_3\): Difference in treatment effects of \(D_i\) depending on \(Victim_i\) = The educational video results in an additional increase for victims of about 21.8 percentage points, compared to the effect for non-victims
Here is a visual illustration of the values rendered by the model with interaction terms
moderation_df %>%
dplyr::group_by(treatment,victim_verbose) %>%
dplyr::mutate(avg_support = mean(support_thermometer)) %>%
ggplot(., aes(x = support_thermometer, fill = factor(treatment))) +
geom_density(alpha = 0.5) +
geom_vline(aes(xintercept = avg_support), linetype = "longdash") +
facet_grid(treatment~victim_verbose) +
theme_bw() +
labs(x = "Support thermometer",
y = "Density",
fill = "Treatment") +
theme(legend.position = "bottom") +
scale_fill_manual(name = " ", # changes to fill dimension
values = c("#a7a8aa", "#cc0055"),
labels = c("Control", "Treatment"))
Extracting marginal/partial effects from our interaction models
For this portion, we are interested in marginal/partial effects, which we will extract through the margins()
function from the margins
package.
Some packages in R aimed at rendering marginal effects do render the predictive margins instead. For the purposes of the class, when asked to render marginal or partial effects you are expected to render them as introduced in the lecture (i.e., \(\frac{\partial Y_i}{\partial {D}_i}\)). When asked for this, you will utilize margins::margins()
.
You can check the documentation here. The syntax for the margins()
function for extracting partial effects of the treatment at different levels of the moderator is the following:
margins::margins(name_of_your_model, variables = "treatment_var", at = list(moderator_variable = c("value1", "value2", "value3"))
Let’s say we are interested in the marginal/partial effect of our video treatment for victims and non-victims. We would do:
summary(margins::margins(interaction_model, variables = "treatment", at = list(victim = c(0,1))))
## factor victim AME SE z p lower upper
## treatment 0.0000 8.0125 0.7757 10.3288 0.0000 6.4921 9.5330
## treatment 1.0000 29.7831 0.7758 38.3895 0.0000 28.2626 31.3037
Let’s plot the marginal/partial effect of the treatment for victims and non-victims
pe_margins <- margins::margins(interaction_model, variables = "treatment", at = list(victim = c(0,1)))
pe_plotting <- summary(pe_margins) %>% #NOTE we use the summary output, instead of the margins object
dplyr::select(victim, AME, lower, upper) %>% # you will need to adapt this based your moderator
dplyr::mutate(victim_labels = ifelse(victim == 1, "Victim", "No Victim")) # you may not need this to create labels for the assignment
ggplot(pe_plotting, aes(x = victim_labels,
y = AME)) +
geom_point(size = 1.5) +
geom_segment(aes(x = victim_labels, xend = victim_labels, y = lower, yend = upper), size = 0.5) + # render whiskers from confidence intervals
theme_minimal() +
scale_y_continuous(limits = c(0,45)) + # you may need to change the limits for your plots based on the specific effects of your application
labs(x = "Respondent status\n",
y = "\nPartial effect of educational video",
caption = "Note: You can utilize it to describe what the plot illustrates during your assignment.") +
coord_flip() # to flip the plot
Extracting predictive margins from our interaction models
For this portion, we are interested in predictive margins. In here, our interest is to return the expectation for each level of a predictor. We will extract this through the ggeffects()
function from the ggeffects
package. You can check the documentation here. The syntax is the following:
ggeffects::ggeffect(name_of_your_model, terms = c("termA", "termB"))
Let’s say we are interested in the predictive margins for all out victim and treatment permutations. We would do:
ggeffects::ggeffect(interaction_model, terms = c("victim","treatment"))
## # Predicted values of support_thermometer
## # x = victim
##
## # treatment = 0
##
## x | Predicted | 95% CI
## ------------------------------
## 0 | 27.93 | [26.85, 29.00]
## 1 | 42.18 | [41.11, 43.26]
##
## # treatment = 1
##
## x | Predicted | 95% CI
## ------------------------------
## 0 | 35.94 | [34.86, 37.02]
## 1 | 71.97 | [70.89, 73.04]
Let’s plot these predictive margins
In this exercise we will meet a very important function from dplyr
, dplyr::case_when()
. This function allows us to vectorize multiple ifelse() statements. The syntax is the following:
dplyr::case_when(condition ~ what to do if met)
Let’s see it at play.
extracted_me <- ggeffects::ggeffect(interaction_model, terms = c("victim","treatment")) %>%
dplyr::mutate(group_labels = dplyr::case_when(x == 1 & group == 1 ~ "Victim (1) - Treatment (1)",
x == 1 & group == 0 ~ "Victim (1) - Treatment (0)",
x == 0 & group == 1 ~ "Victim (0) - Treatment (1)",
x == 0 & group == 0 ~ "Victim (0) - Treatment (0)"
)) # adding labels to (x,group) combinations for the plot
extracted_me
ggplot(extracted_me, aes(x = group_labels,
y= predicted,
fill = x,
alpha = group
)) +
geom_bar(stat = "identity") +
geom_point(size = 1.5) +
geom_segment(aes(x = group_labels,
xend = group_labels,
y = conf.high,
yend = conf.low), size = 0.5) + # render whiskers from confidence intervals
theme_minimal() +
labs(x = "\nRespondent status",
y = "Predictive margins",
fill = "Treatment",
caption = "Note: You can utilize it to describe what the plot illustrates during your assignment.") +
theme(legend.position = "note") +
scale_alpha_manual(values=c(0.6, 1))
Drafting some brief recommedations
After conducting your experiment, you report back to the taskforce. Based on your results, you suggest that the educational videos may be a useful tool to encourage the wider public to hold a warmer opinion about the peace agreement. You also tell the taskforce that, although the videos have an average positive effect, they affect with a higher intensity victims of the conflict. You suggest to develop alternative strategies to tackle the non-victims, so that they do not fall through the cracks.