19 Difference-in-Differences

In this chapter you will estimate the Garbage Incinerator Difference-In-Differences model discussed in LN6. You can also gain experience working with models that include polynomials.

Solutions are posted in Moodle. The bare minimum you need to do is copy what I have in the solutions and paste it into this file. Doing that will not be considered cheating (in some classes that would be considered cheating, so always make sure you know whether using posted solutions is ok or not). If all you do is copy the solutions, however, you clearly won’t come away with a better understanding of the material, so I strongly encourage you to try to work through the questions on your own before looking at the solutions. But when you’re stuck or just can’t figure out what I’m asking, use the solutions rather than wasting time.

library(tidyverse)
library(pander)
library(stargazer)

## Set how many decimals at which R starts to use scientific notation 
options(scipen=4)

mydata <- read_csv("https://raw.githubusercontent.com/econ380w21/380data/main/garbageIncineratorDD.csv")

19.1 Data

The data has 321 observations on house prices in North Andover, MA. The variables include:

Variable Description
year 1978 or 1981
dist miles from incinerator
rprice price, 1978 dollars (“r” means real price, corrected for inflation)
age age of house in years
rooms # rooms in house
baths # bathrooms
area house size in square feet
land land area in square feet

The relevant background is covered in LN6, but I’ll summarize it again. We have house prices in 1978 and 1981 in North Andover, MA. In 1978 no one knows about any garbage incinerator, so prices in 1978 cannot be affected by the garbage incinerator. In 1979 it was announced publicly that a new garbage incinerator would be built and operational by 1981. The variable “dist” has the distance in miles from the future location of the incinerator. We want to estimate if knowledge of the future incinerator lowered house prices for houses near the incinerator. We’ll define “near” as within 3 miles of the incinerator location.

stargazer(as.data.frame(mydata), type = "html",summary.stat = c("n","mean","sd", "min", "median", "max"))
Statistic N Mean St. Dev. Min Median Max
year 321 1,979.327 1.492 1,978 1,978 1,981
dist 321 3.923 1.611 0.947 3.769 7.576
rprice 321 83,721.350 33,118.790 26,000.000 82,000.000 300,000.000
age 321 18.009 32.566 0 4 189
rooms 321 6.586 0.901 4 7 10
baths 321 2.340 0.771 1 2 4
area 321 2,106.729 694.958 735 2,056 5,136
land 321 39,629.890 39,514.390 1,710 43,560 544,500

Note that I put mydata inside as.data.frame(). Sometimes stargazer doesn’t work otherwise (as some of you found out in the CP).

19.2 Model 1

Estimate the following model and store the results in model1

\[rprice=\beta_0+\beta_1near+\beta_2y81+\beta_3near\cdot y81+u\] where

Variable Description
near =1 if house is 3 miles or less from incinerator location, = 0 o/w
y81 =1 if house sold in 1981, = 0 o/w

Fill in your code here. You need to generate the near and y81 variables a variable for the interaction term (i.e., y81near=y81*near).

Tip about creating dummy variables: If you use something like dist<=3 to create near, it will create a column of TRUE and FALSE. If you put the condition in parentheses and multiply by 1, it will turn TRUE into andFALSEinto 0. Thus,(dist<=3)*1is equivalent toifelse(dist<=3,1,0)`

Tip (or, more of a requirement for 380) about interaction terms: R will let you include y81*near in the regression directly, but do not do this in 380 (and I suggest not doing it after 380 either, at least unless you check to make sure it’s doing what you think it’s doing). Frequently people make mistakes when they rely on R to include the interactions for them. To be sure that you know what the interaction terms are, you need to create them yourself. So create the interaction term yourself. You can name it y81near.

After you create the three variables, estimate the regression, store the regression results in model1, and display the results using pander.

Then, use group_by() and summarize to calculate the mean price for the four groups (far 1978, far 1981, near 1978, near 1981)

Finally, display the combinations of regression coefficients that equal the four group averages. These should match with the values shown and discussed in the LN6 slides.

## Write your code here

## Create Variables
mydata <- mydata %>% mutate(near = (dist <=3) * 1,
                            y81 = (year == 1981) * 1,
                            y81near = y81 * near)

## Estimate model
model1 <- lm(rprice~near+y81+y81near,data=mydata)

pander(summary(model1))
  Estimate Std. Error t value Pr(>|t|)
(Intercept) 82517 2727 30.26 1.709e-95
near -18824 4875 -3.861 0.0001368
y81 18790 4050 4.64 0.000005117
y81near -11864 7457 -1.591 0.1126
Fitting linear model: rprice ~ near + y81 + y81near
Observations Residual Std. Error \(R^2\) Adjusted \(R^2\)
321 30243 0.1739 0.1661
## 4 group averages using group_by() and summarize()

mydata %>% 
   group_by(near,y81) %>%
   summarize(priceAvg=mean(rprice))
## `summarise()` regrouping output by 'near' (override with `.groups` argument)
## # A tibble: 4 x 3
## # Groups:   near [2]
##    near   y81 priceAvg
##   <dbl> <dbl>    <dbl>
## 1     0     0   82517.
## 2     0     1  101308.
## 3     1     0   63693.
## 4     1     1   70619.
## far 1978

coef(model1)["(Intercept)"]
## (Intercept) 
##    82517.23
## far 1981

coef(model1)["(Intercept)"]+coef(model1)["y81"]
## (Intercept) 
##    101307.5
## near 1978

coef(model1)["(Intercept)"]+coef(model1)["near"]
## (Intercept) 
##    63692.86
## near 1981

coef(model1)["(Intercept)"]+coef(model1)["near"]+coef(model1)["y81"]+coef(model1)["y81near"]
## (Intercept) 
##    70619.24

19.2.1 Equivalent model 1

In LN6 we discuss an equivalent model that defines dummy variables for each group. Create the set of group dummy variables and estimate the model. Stote it in model1equiv. Calculate the conditional expectation for the four groups and show that each is the same as in the version of model 1 estimated above.

## Write your code here

## Create Variables

mydata <- mydata %>% mutate(y78near = (1-y81)*near,
                            y81far = y81 * (1-near))


## Estimate model

model1equiv <- lm(rprice~y81near+y78near+y81far,data=mydata)

pander(summary(model1equiv))
  Estimate Std. Error t value Pr(>|t|)
(Intercept) 82517 2727 30.26 1.709e-95
y81near -11898 5505 -2.161 0.03141
y78near -18824 4875 -3.861 0.0001368
y81far 18790 4050 4.64 0.000005117
Fitting linear model: rprice ~ y81near + y78near + y81far
Observations Residual Std. Error \(R^2\) Adjusted \(R^2\)
321 30243 0.1739 0.1661
## display conditional expectations for each model, and subtract model1equiv estimate from model 1 to show they are o (all are within 1e-10)

## far 1978

coef(model1)["(Intercept)"]
## (Intercept) 
##    82517.23
## far 1981

coef(model1equiv)["(Intercept)"]
## (Intercept) 
##    82517.23
## near 1978

coef(model1)["(Intercept)"]-coef(model1equiv)["(Intercept)"]
##   (Intercept) 
## -7.275958e-11
## near 1981

coef(model1)["(Intercept)"]+coef(model1)["y81"]
## (Intercept) 
##    101307.5
coef(model1equiv)["(Intercept)"]+coef(model1equiv)["y81far"]
## (Intercept) 
##    101307.5
coef(model1)["(Intercept)"]+coef(model1)["y81"]-(coef(model1equiv)["(Intercept)"]+coef(model1equiv)["y81far"])
##   (Intercept) 
## -1.018634e-10
coef(model1)["(Intercept)"]+coef(model1)["near"]
## (Intercept) 
##    63692.86
coef(model1equiv)["(Intercept)"]+coef(model1equiv)["y78near"]
## (Intercept) 
##    63692.86
coef(model1)["(Intercept)"]+coef(model1)["near"]-(coef(model1equiv)["(Intercept)"]+coef(model1equiv)["y78near"])
##  (Intercept) 
## 2.473826e-10
coef(model1)["(Intercept)"]+coef(model1)["near"]+coef(model1)["y81"]+coef(model1)["y81near"]
## (Intercept) 
##    70619.24
coef(model1equiv)["(Intercept)"]+coef(model1equiv)["y81near"]
## (Intercept) 
##    70619.24
coef(model1)["(Intercept)"]+coef(model1)["near"]+coef(model1)["y81"]+coef(model1)["y81near"]-(coef(model1equiv)["(Intercept)"]+coef(model1equiv)["y81near"])
##   (Intercept) 
## -2.910383e-11

19.3 Model 2

Now estimate the following model

\[ \begin{align} rprice=\beta_0+\beta_1near+\beta_2y81+\beta_3near\cdot y81 &+\beta_4age+\beta_5age^2 + \beta_6rooms \\ &+ \beta_7baths+\beta_8area+\beta_9land+u \end{align} \]

where \(age^2\) is the variable age squared. Store your regression results in model2. Note that if you want to use \(age^2\) inside lm() instead of creating the agesq variable first, you need to put the age^2 inside I(). But I suggest creating it by hand so you’re positive you’re using what you want to use.

##Write your code here

## model2 <- lm(...)

##SOLUTION:
mydata$agesq <- mydata$age^2
model2 <- lm(rprice~near+y81+y81near+age+agesq+rooms+baths+area+land,data=mydata)
pander(summary(model2))
  Estimate Std. Error t value Pr(>|t|)
(Intercept) 2189 10440 0.2097 0.8341
near 9965 3881 2.568 0.0107
y81 15210 2788 5.455 0.00000009997
y81near -15021 5030 -2.986 0.003048
age -657.2 129 -5.095 0.0000006063
agesq 3.083 0.8099 3.806 0.0001698
rooms 3281 1679 1.954 0.05156
baths 7489 2601 2.879 0.004267
area 17.83 2.328 7.658 2.405e-13
land 0.1156 0.02993 3.863 0.0001364
Fitting linear model: rprice ~ near + y81 + y81near + age + agesq + rooms + baths + area + land
Observations Residual Std. Error \(R^2\) Adjusted \(R^2\)
321 19824 0.6518 0.6417

19.4 Comparison of models

Here we display the two models in a stargazer table. First we’ll display them with the p-values. Then we’ll display them with standard errors, next with the t-statistic, and finally with the 95% confidence interval. It’s common for results to be presenting with one of these four statistics, so it’s important for you to be comfortable with each choice. I’ve included the stargazer tables for you. Just un-comment-them out (i.e., remove the hash tags…which you can do by selecting all of them and using ctrl/cmd-shift-C).

## Un-comment-out the following once you have estimated model1 and model2

stargazer(model1, model2,
          type = "text",
          report=('vc*p'),
          keep.stat = c("n","rsq","adj.rsq"),
          notes = "p-value reported in parentheses,  <em>&#42;p&lt;0.1;&#42;&#42;p&lt;0.05;&#42;&#42;&#42;p&lt;0.01</em>",
          notes.append = FALSE)

================================================================================================================== Dependent variable:
—————————————————————————————————– rprice
(1) (2)
—————————————————————————————————————— near -18,824.370*** 9,964.964**
p = 0.0002 p = 0.011

y81 18,790.290*** 15,210.440***
p = 0.00001 p = 0.00000

y81near -11,863.900 -15,020.680***
p = 0.113 p = 0.004

age -657.159***
p = 0.00000

agesq 3.083***
p = 0.0002

rooms 3,280.510*
p = 0.052

baths 7,489.360***
p = 0.005

area 17.830***
p = 0.000

land 0.116***
p = 0.0002

Constant 82,517.230*** 2,189.081
p = 0.000 p = 0.835

Observations 321 321 R2 0.174 0.652 Adjusted R2 0.166 0.642 ================================================================================================================== Note: p-value reported in parentheses, *p<0.1;**p<0.05;***p<0.01
r stargazer(model1, model2, type = "text", report=('vc*s'), keep.stat = c("n","rsq","adj.rsq"), notes = "standard error reported in parentheses, <em>&#42;p&lt;0.1;&#42;&#42;p&lt;0.05;&#42;&#42;&#42;p&lt;0.01</em>", notes.append = FALSE)
========================================================================================================================= Dependent variable: ———————————————————————————————————— rprice (1) (2)

near -18,824.370*** 9,964.964**
(4,875.322) (3,880.777)

y81 18,790.290*** 15,210.440***
(4,050.065) (2,788.266)

y81near -11,863.900 -15,020.680***
(7,456.646) (5,029.757)

age -657.159***
(128.983)

agesq 3.083***
(0.810)

rooms 3,280.510*
(1,678.566)

baths 7,489.360***
(2,601.438)

area 17.830***
(2.328)

land 0.116***
(0.030)

Constant 82,517.230*** 2,189.081
(2,726.910) (10,440.190)

Observations 321 321 R2 0.174 0.652 Adjusted R2 0.166 0.642 ========================================================================================================================= Note: standard error reported in parentheses, *p<0.1;**p<0.05;***p<0.01
r stargazer(model1, model2, type = "text", report=('vc*t'), keep.stat = c("n","rsq","adj.rsq"), notes = "t-statistic reported for each coefficient, <em>&#42;p&lt;0.1;&#42;&#42;p&lt;0.05;&#42;&#42;&#42;p&lt;0.01</em>", notes.append = FALSE)
============================================================================================================================ Dependent variable: ————————————————————————————————————— rprice (1) (2)

near -18,824.370*** 9,964.964**
t = -3.861 t = 2.568

y81 18,790.290*** 15,210.440***
t = 4.640 t = 5.455

y81near -11,863.900 -15,020.680***
t = -1.591 t = -2.986

age -657.159***
t = -5.095

agesq 3.083***
t = 3.806

rooms 3,280.510*
t = 1.954

baths 7,489.360***
t = 2.879

area 17.830***
t = 7.658

land 0.116***
t = 3.863

Constant 82,517.230*** 2,189.081
t = 30.260 t = 0.210

Observations 321 321 R2 0.174 0.652 Adjusted R2 0.166 0.642 ============================================================================================================================ Note: t-statistic reported for each coefficient, *p<0.1;**p<0.05;***p<0.01
r stargazer(model1, model2, type = "text", ci = TRUE, ci.level = 0.95, keep.stat = c("n","rsq","adj.rsq"), notes = "95% confidence interval reported in parentheses, <em>&#42;p&lt;0.1;&#42;&#42;p&lt;0.05;&#42;&#42;&#42;p&lt;0.01</em>", notes.append = FALSE)
================================================================================================================================== Dependent variable: ——————————————————————————————————————— rprice (1) (2)

near -18,824.370*** 9,964.964**
(-28,379.830, -9,268.915) (2,358.781, 17,571.150)

y81 18,790.290*** 15,210.440***
(10,852.310, 26,728.270) (9,745.545, 20,675.350)

y81near -11,863.900 -15,020.680***
(-26,478.660, 2,750.855) (-24,878.830, -5,162.541)

age -657.159***
(-909.960, -404.357)

agesq 3.083***
(1.496, 4.670)

rooms 3,280.510*
(-9.419, 6,570.438)

baths 7,489.360***
(2,390.634, 12,588.080)

area 17.830***
(13.267, 22.393)

land 0.116***
(0.057, 0.174)

Constant 82,517.230*** 2,189.081
(77,172.580, 87,861.870) (-18,273.310, 22,651.470)


Observations 321 321
R2 0.174 0.652
Adjusted R2 0.166 0.642
================================================================================================================================== Note: 95% confidence interval reported in parentheses, *p<0.1;**p<0.05;***p<0.01

19.5 Additional questions

19.5.1 Question 1

Based on model 1, what effect did the garbage incinerator have on house prices for houses “near” (within 3 miles of) the incinerator? Is this effect statistically significant?


Based on this regression, the incinerator decreased prices by $11863.90, on average. It is not statistically significant at any standard level of significance (based on the p-value of 0.113).


19.5.2 Question 2

Here is how you can easily report the confidence intervals for model1 for the 3 standard (i.e., commonly used/reported) levels of significance:

## Un-comment out:
pander(confint(model1,level = 0.99))
  0.5 % 99.5 %
(Intercept) 75451 89584
near -31458 -6190
y81 8295 29286
y81near -31187 7459
pander(confint(model1,level = 0.95))
  2.5 % 97.5 %
(Intercept) 77152 87882
near -28416 -9232
y81 10822 26759
y81near -26535 2807
pander(confint(model1,level = 0.90))
  5 % 95 %
(Intercept) 78019 87016
near -26867 -10782
y81 12109 25472
y81near -24165 437.1

To reach the same conclusion about statistical significance of the effect of the garbage incinerator as your answer to the previous question (for which you looked at the p-value), what level confidence interval do we need to look at? Why?


In the previous question, we concluded that the effect of the garbage incinerator (-11864) is not statistically significant at any standard level. We reached this conclusion because the p-value (0.113) is larger than 0.10. When the p-value is larger than 0.10, that implies that the 90% confidence interval will contain 0. Any time the 90% confidence interval contains 0, we know for sure that the 95% and 99% confidence intervals must also contain 0 (just as we know that when a p-value is larger than 0.10, it obviously must also be larger than 0.05 and 0.01). So, to conclude that a coefficient is not statistically significant at any standard level, you need to look at the 90% confidence interval and see if it includes 0. If the 90% confidence interval includes 0, that means we fail to reject that the coefficient might be 0 at the 10% level (which means we also fail to reject at 5% and 1%, and thus, at any standard level).


19.5.3 Question 3

Now look at the second model. Based on model2, what effect did the garbage incinerator have on house prices for houses “near” (within 3 miles of) the incinerator? Is this effect statistically significant?


Based on this regression, the incinerator decreased prices by $15,020.68, on average, holding constant characteristics of the house (number of rooms and bathrooms, size, land, and a quadratic in age). It is statistically significant at the 1% level (based on the p-value of 0.003).


19.5.4 Question 4

Calculate the confidence interval needed to reach the same conclusion about statistical significance of the effect of the garbage incinerator as your answer to the previous question (for which you looked at the p-value)? Explain why this is the level you need to look at to reach that conclusion.


In the previous question, we concluded that the effect of the garbage incinerator (-15021) is statistically significant at the 1% level. We reached this conclusion because the p-value (0.003) is smaller than 0.01. If we calculate the 99% confidence interval, it will not contain zero. Thus, it’s less than 1% likely that the true coefficient is 0, so we can reject at the 1% level that the true coefficient is 0. The 95% and 90% confidence intervals will be narrower, and further away from 0, so they won’t contain 0 either. But to conclude that the coefficient is statistically significant at the 1% level, we need to look at the 99% confidence interval. As expected, it doesn’t include 0:


19.6 Polynomials

For this part I’ve left in all the code, so there’s nothing you have to do here other than comment out the code I give you. However, I suggest you go through the code and experiment a bit until you understand what’s going on with polynomial models.

In model 2 you included a second order polynomial in age (i.e., you included age and age squared). What this allows for is the regression line to have a parabolic shape instead of being a straight line. To see an example of this, consider the following two models, the first that only includes age and the second that includes age and age squared.

mydata$agesq <- mydata$age^2
modelAge <- lm(rprice~age,data=mydata)
mydata$yHatAge <- fitted(modelAge)


modelAge2 <- lm(rprice~age+agesq,data=mydata)
mydata$yHatAge2 <- fitted(modelAge2)

ggplot(data=mydata,aes(y=rprice,x=age)) +
  geom_point(shape=4) +
  geom_point(aes(y=yHatAge,x=age),color="blue",size=2)  +
  geom_line(aes(y=yHatAge,x=age),color="blue") +
  geom_point(aes(y=yHatAge2,x=age),color="green",size=2)  +
  geom_line(aes(y=yHatAge2,x=age),color="green")

To see a slightly more complete model, also including near, y81, and y81near. Let’s define a factor variable that will indicate the four groups by color. We’ll first display the graph for the model that only includes age (plus near, y81, and y81near). Then we’ll display the graph for the model that also includes age squared.

mydata$group <- ifelse(mydata$near==0 & mydata$year==1978,"y78far",
                       ifelse(mydata$near==1 & mydata$year==1978,"y78near",
                              ifelse(mydata$near==0 & mydata$year==1981,"y81far",
                                     "y81near"
                       )))

modelAgeWithGroups <- lm(rprice~age+near+y81+y81near,data=mydata)
mydata$yHatAgeWithGroups <- fitted(modelAgeWithGroups)

ggplot(data=mydata,aes(y=rprice,x=age,color=group)) +
  geom_point(shape=4) +
  geom_point(aes(y=yHatAgeWithGroups,x=age,color=group),size=2)  +
  geom_line(aes(y=yHatAgeWithGroups,x=age,color=group))

modelAge2WithGroups <- lm(rprice~age+agesq+near+y81+y81near,data=mydata)
mydata$yHatAge2WithGroups <- fitted(modelAge2WithGroups)

ggplot(data=mydata,aes(y=rprice,x=age,color=group)) +
  geom_point(shape=4) +
  geom_point(aes(y=yHatAge2WithGroups,x=age,color=group),size=2)  +
  geom_line(aes(y=yHatAge2WithGroups,x=age,color=group))

What you see in the polynomial graphs is that include age squared allows for a nonlinear relationship. Here, initially house price goes down as the house gets older. However, after about 100 years, older houses start to sell for more. This makes sense. Initially getting older is bad because older houses are more likely to have problems, require repairs, etc. But a hundred+ year old house is probably a pretty nice house if it’s survived that long, it might be considered “historic”, etc. We can’t possibly capture that relationship unless we specify our model in a way that allows for a nonlinear relationship. Sometimes you will also see a cubic term. That allows for three turns in the relationship (down, up, down, or up, down, up).

Note that in the final graph, the only reason the lines overlap is because ggplot simply connects the dots. If there were more points the bottom wouldn’t be flat and the lines wouldn’t cross.