#library(haven)
#data <- read_dta("data.dta")
#write.csv(data,"~/path/data.csv", row.names = FALSE)
raw_resume <- read.csv("data/resume.csv")
Resume data is from an experiment that manipulated attributes in fictitious resumes, which they sent out to employers, and measured whether the resumes received a call.
call
is the dependent variable of interest (did the
employer call the fictitious applicant for an interview) (binary)black
is the treatment variable in the data set
(whether the resume has a “Black-sounding” name) (binary)yearsexp
years of experience (integer; 1 - 44)female
gender (binary)computerskills
computer skills (binary)ofjobs
number of previous jobs (integer; 1 - 7)education
applicants education level (categorical; 0-4)
Bertrand, M., & Mullainathan, S. (2004). Are Emily and Greg more employable than Lakisha and Jamal? A field experiment on labor market discrimination. American economic review, 94(4), 991-1013
#install.packages("fastDummies")
library(fastDummies)
resume <- dummy_cols(raw_resume, select_columns = 'education')
Estimate a model, using OLS, that regresses call
on
black
, yearsexp
, education
, and
female
.
m1 <- lm(call ~ black + female + factor(education) + yearsexp, raw_resume)
summary(m1)
##
## Call:
## lm(formula = call ~ black + female + factor(education) + yearsexp,
## data = raw_resume)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.18397 -0.09152 -0.07842 -0.05697 0.96133
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.0654247 0.0408409 1.602 0.109
## black -0.0319980 0.0077781 -4.114 3.96e-05 ***
## female 0.0077546 0.0095458 0.812 0.417
## factor(education)1 0.0038199 0.0586960 0.065 0.948
## factor(education)2 0.0079547 0.0433587 0.183 0.854
## factor(education)3 -0.0005815 0.0411409 -0.014 0.989
## factor(education)4 -0.0013080 0.0403583 -0.032 0.974
## yearsexp 0.0032749 0.0007736 4.233 2.34e-05 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2713 on 4862 degrees of freedom
## Multiple R-squared: 0.007429, Adjusted R-squared: 0.006
## F-statistic: 5.199 on 7 and 4862 DF, p-value: 6.432e-06
m1 <- lm(call ~ black + female + education_1 + education_2 + education_3 + education_4 + yearsexp, resume)
summary(m1)
##
## Call:
## lm(formula = call ~ black + female + education_1 + education_2 +
## education_3 + education_4 + yearsexp, data = resume)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.18397 -0.09152 -0.07842 -0.05697 0.96133
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.0654247 0.0408409 1.602 0.109
## black -0.0319980 0.0077781 -4.114 3.96e-05 ***
## female 0.0077546 0.0095458 0.812 0.417
## education_1 0.0038199 0.0586960 0.065 0.948
## education_2 0.0079547 0.0433587 0.183 0.854
## education_3 -0.0005815 0.0411409 -0.014 0.989
## education_4 -0.0013080 0.0403583 -0.032 0.974
## yearsexp 0.0032749 0.0007736 4.233 2.34e-05 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2713 on 4862 degrees of freedom
## Multiple R-squared: 0.007429, Adjusted R-squared: 0.006
## F-statistic: 5.199 on 7 and 4862 DF, p-value: 6.432e-06
Compares the residual SS of the restricted and unrestricted models.
\[ F = \frac{SSR_r - SSR_{ur} / q}{SSR_{ur} / (N-K-1)} \] Where q is the number of restrictions. The F-statistic measures the ratio of explained variance to unexplained variance.A high F-statistic suggests that the additional parameter(s) significantly improve the model.
Conduct an F-test to see if ofjobs
has an effect on the
dependent variable
# unrestricted
ur <- lm(call ~ black + female + education_1 + education_2 + education_3 + education_4 + yearsexp + ofjobs, resume)
# estimate restricted model on unrestricted data
r <- update(ur, . ~ . - ofjobs, data = ur$model)
# SSR, for restricted and unrestricted models
ssr_r <- sum(r$residuals^2)
ssr_ur <- sum(ur$residuals^2)
# df unrestricted
df_ur <- ur$df.residual
# df difference, # restrictions
q <- r$df.residual - ur$df.residual
# F stat
Fstat <- ((ssr_r - ssr_ur) / q) / (ssr_ur / df_ur)
# p value
pf(Fstat, q, df_ur, lower.tail=F)
## [1] 0.3287994
# compare to anova
stats::anova(r, ur)
## Analysis of Variance Table
##
## Model 1: call ~ black + female + education_1 + education_2 + education_3 +
## education_4 + yearsexp
## Model 2: call ~ black + female + education_1 + education_2 + education_3 +
## education_4 + yearsexp + ofjobs
## Res.Df RSS Df Sum of Sq F Pr(>F)
## 1 4862 357.77
## 2 4861 357.70 1 0.070187 0.9538 0.3288
Conduct a hypothesis test to test the null hypothesis that the effect of having a ‘Black-sounding’ name, compared to having a ‘White-sounding’ name, is equal to the effect of having a ‘female-sounding’ name, compared to having a ‘male-sounding’ name.
library(car)
## Loading required package: carData
car::linearHypothesis(m1, "black - female = 0")
## Linear hypothesis test
##
## Hypothesis:
## black - female = 0
##
## Model 1: restricted model
## Model 2: call ~ black + female + education_1 + education_2 + education_3 +
## education_4 + yearsexp
##
## Res.Df RSS Df Sum of Sq F Pr(>F)
## 1 4863 358.52
## 2 4862 357.77 1 0.75444 10.253 0.001374 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Calculate bootstrapped 95% confidence intervals for
black
. Use the standard bootstrap approach of taking random
draws, with replacement, from the rows of your model’s data matrix. Use
the percentile method to calculate the confidence bounds.
# number of bootstrap samples
boots <- 1000
# number of regression coefficients (K+1)
#ncoef <- summary(m1)$df[1]
ncoef <- 1
# initialize array to store coefs
boots_beta <- array(NA, c(boots, ncoef))
# iterate over samples
for (i in 1:boots){
# sample rows of data w/ replacement
new <- sample(1:nrow(m1$model), nrow(m1$model), replace = T)
# run model with bootstrap sample
boots_beta[i, ] <- coef(lm(formula(m1), data = m1$model[new, ]))[2]
}
# calculate SE for `black`
boots_se <- apply(boots_beta, 2, sd)
sd(boots_beta)
## [1] 0.007737998
# calculate CI
lci <- coef(m1)[2] - qt(0.975, m1$df.residual)*boots_se
uci <- coef(m1)[2] + qt(0.975, m1$df.residual)*boots_se
print (c(lci, uci))
## black black
## -0.04716800 -0.01682805
Now calculate confidence intervals with the BCa method.
# boot package
library(boot)
##
## Attaching package: 'boot'
## The following object is masked from 'package:car':
##
## logit
boot_fn <- function(data, indices) {
d <- data[indices, ] # subset data by randomly generated vector of row indices (w replacement)
fit <- update(m1, . ~ ., data = d)
B <- coef(fit)[2]
se <- sqrt(diag(vcov(fit)))[2]
return(c(B, se))
}
# boot call
m_boot <- boot(data = m1$model, statistic = boot_fn, R = 10000)
# use boot.ci to get CIs
boot.ci(m_boot, type = "bca")
## BOOTSTRAP CONFIDENCE INTERVAL CALCULATIONS
## Based on 10000 bootstrap replicates
##
## CALL :
## boot.ci(boot.out = m_boot, type = "bca")
##
## Intervals :
## Level BCa
## 95% (-0.0471, -0.0169 )
## Calculations and Intervals on Original Scale
Estimate a new model, using OLS, that regresses call
on
black
, yearsexp
, and the interaction between
them.
m2 <- lm(call ~ black*yearsexp, resume)
summary(m2)
##
## Call:
## lm(formula = call ~ black * yearsexp, data = resume)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.17797 -0.09011 -0.07620 -0.05874 0.95695
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.0692625 0.0101234 6.842 8.79e-12 ***
## black -0.0293537 0.0143684 -2.043 0.04111 *
## yearsexp 0.0034682 0.0010822 3.205 0.00136 **
## black:yearsexp -0.0003304 0.0015409 -0.214 0.83025
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2712 on 4866 degrees of freedom
## Multiple R-squared: 0.007231, Adjusted R-squared: 0.006619
## F-statistic: 11.81 on 3 and 4866 DF, p-value: 1.045e-07
Make a plot that shows how the expected value of the dependent
variable changes across years of experience (yearsexp
),
with separate lines for the two race treatment conditions
(black
).
# range of years experience
range_yearsexp <- seq(min(resume$yearsexp), max(resume$yearsexp), 1)
# race values
values_black <- c(0,1)
new_data <- expand.grid(yearsexp = range_yearsexp, black = values_black)
#predict call back based on years experience and race
predicted_call <- predict(m2, newdata = new_data)
#combine new dataset with predicted volume
predicted_df <- cbind(new_data, call = predicted_call)
# install.packages("ggplot2")
library(ggplot2)
# ggplot
ggplot(predicted_df, aes(x = yearsexp, y = predicted_call, color = factor(black))) +
geom_line() +
scale_color_manual(values = c("blue", "red"),
labels = c(paste("White"),
paste("Black")),
name = "Race") +
labs(x = "Years Experience", y = "Predicted Call Back",
title = "Predicted Call vs. Years Experience") +
theme_minimal()
Use the marginaleffects package and the delta method to calculate 95%
confidence bounds for the conditional marginal effect of
black
at each value of yearsexp
.
library(marginaleffects)
## Warning: package 'marginaleffects' was built under R version 4.3.3
options(marginaleffects_print_omit = c("s.value"))
?marginaleffects::slopes # s.value = Shannon information
# estimate ME of black at range of yearsexp
cme <- slopes(m2, variables = c("black"),
newdata = datagrid(yearsexp = range_yearsexp))
# plot ME and SE for range of yearsexp
plot(cme$yearsexp, cme$estimate,
main = "CME of Black Across Years Experience",
ylab="CME of Black", xlab="Years Experience",
type = "l", ylim=c(-0.2,0.2), xlim=c(0,45))
abline(h=0, lty=3)
lines(cme$yearsexp, cme$estimate + 1.96*cme$std.error)
lines(cme$yearsexp, cme$estimate - 1.96*cme$std.error)
The Chow test is a special form of F-test used to test the null hypothesis that a linear model is identical across groups
\[ F = \frac{SSR_{pooled} - (SSR_{male} + SSR_{female})}{SSR_{pooled}} \cdot \frac{N - 2(K+1)}{K+1} \]
Estimate the same model but for male and female respondents. Conduct a Chow test of the null hypothesis that the two groups (for each category of gender) can be combined into a single model (i.e., that the true model is the same across these two groups).
# male <- subset(resume, female == 0)
male <- resume[resume$female == 0, ]
# female <- subset(resume, female == 1)
female <- resume[resume$female == 1, ]
# models (pooled, male, female)
m2p <- lm(call ~ black * yearsexp, data = resume)
m2m <- lm(call ~ black * yearsexp, data = male)
m2f <- lm(call ~ black * yearsexp, data = female)
ssr_p <- sum(m2p$residuals^2)
ssr_m <- sum(m2m$residuals^2)
ssr_f <- sum(m2f$residuals^2)
n <- nrow(m2p$model)
k <- as.numeric(length(m2p$coefficients))
# chow
chow <- ( (ssr_p - (ssr_m + ssr_f)) / ssr_p ) * ( (n - 2*(k+1)) / (k+1) )
# p value
pf(chow, df1 = k, df2 = (n - (2 * k)), lower.tail =F)
## [1] 0.1103738