FSAR | Lab #7 | Fall 2025

Difference-in-Differences

Before the Lab

  • Read the summary of the research paper by David Card and Alan Krueger available on Moodle.

Lab Overview

  • Introduction to panel data analysis
  • Difference-in-differences estimation


Minimum Wages and Employment: A case study of the fast-food industry in New Jersey and Pennsylvania

By David Card and Alan Krueger, American Economic Review, 1994

David Edward Card is a Canadian labor economist and Professor of Economics at the University of California, Berkeley.

Alan Bennett Krueger was an American economist, Bendheim Professor of Economics and Public Affairs at Princeton University and Research Associate at the National Bureau of Economic Research.

ABSTRACT: On April 1, 1992, New Jersey’s minimum wage rose from $4.25 to $5.05 per hour. To evaluate the impact of the law we surveyed 410 fast-food restaurants in New Jersey and eastern Pennsylvania before and after the rise. Comparisons of employment growth at stores in New Jersey and Pennsylvania (where the minimum wage was constant) provide simple estimates of the effect of the higher minimum wage. We also compare employment changes at stores in New Jersey that were initially paying high wages (above $5) to the changes at lower-wage stores. We find no indication that the rise in the minimum wage reduced employment. (JEL 530, 523)

See: Nobel Prize in Economics 2021


Overview

  • April \(1^{st}\) 1992: New Jersey (USA) raises minimum wage from $4.25/hour to $5.05/hour.

  • This increase gave New Jersey the highest state-level minimum wage in the US and was strongly opposed by the state’s businesses.

  • Conventional economic theory unambiguously predicts that an increase in the minimum wage will lead perfectly competitive employers to cut down employment.


Research Question: What was the impact of the increase in minimum wage on employment?

1. Getting Started

  1. Download Lab 7’s materials from Moodle:

    • Save provided data set in your data folder in BRM-Labs project folder.
    • Save provided R script in your code folder in BRM-Labs project folder.
  2. Open the provided lab 7’s R script.

  3. Setup your R environment.

# Clean work environment 
rm(list = ls()) # USE with CAUTION: this will delete everything in your environment
# Install packages
# install.packages("lmtest")
# install.packages("sandwich")

# Load packages
library(tidyverse)
library(stargazer)
library(ggthemes)
library(GGally)
library(skimr)
library(corrr)
library(infer)
library(janitor)
library(lmtest)
library(sandwich)
  1. Load the data.
# Load data
load("data/fastfood.RData")
# Check if is tibble
is_tibble(tb.fastfood)
[1] TRUE

Here is the variable description:

  • id: store id
  • time: indicator variable = 1 if after minimum wage increase
  • emptot: number of full-time equivalent (FTE) employees
  • state: indicator variable = 1 if New Jersey
  • chain: anonymous restaurant chain ID (e.g. McDonald’s, Burger King, etc.)
  • co_owned: indicator of company-owned versus franchisee-owned
  • atmin: indicator variable = 1 if wage is at minimum wage level
  • wage: hourly wage
  • hrsopen: number of hours open
  • pmeal: price of a full meal
  • fracft: fraction of full time employees




2. Analytical considerations

  • At what level of analysis would you collect data?

    • Worker level
    • Company level
    • Industry level
  • What data (variables for analysis) would you collect?

    • What are the key variables?
    • What other factors could be impacted by this change besides employment?
  • How would you collect the data?

    • Survey the companies in person, by telephone, or by mail. (Card and Krueger)
    • Get payroll records from each company. (Neumark and Wascher)
    • Use Bureau of Labor Statistics’ (BLS) data. (Card and Krueger)
  • At what point(s) in time would you collect the data?

  • What analysis would you perform?


Card and Krueger use data from fast-food restaurants collected through a telephone survey. Note the following advantages of this sample:

  • They are a leading employer of low-wage workers
  • They comply with minimum wage regulations and thus are expected to comply with the change in legislation
  • Their job requirements and products are relatively homogeneous and thus it is easy to obtain reliable measures of employment, wages, and product prices
  • They are known to have a high response rate to telephone surveys

The authors collected data in two waves:

  • First wave: a little over one month before the increase in New Jersey’s minimum wage
  • Second wave: eight months after the minimum wage increase


To estimate the impact of the increase in minimum wage on unemployment, consider the following approaches:

2.1 Approach 1

Calculate the difference in New Jersey’s employment level before and after the change in minimum wage

\[employment_t = \beta_0 + \beta_1Time_{t} + \mu_t\]

What are the problems with this approach?

There may be variables in the error term that impact employment and also changed from \(Time_0\) to \(Time_1\). This would result in a biased \(\beta_1\). For instance, if a recession took place from \(Time_0\) to \(Time_1\), this would be related to the variable \(Time\) and also to \(employment\), preventing us from learning the true impact of the minimum wage increase on employment. The coefficient for \(Time\) just shows the difference in employment between the periods after and before, but does not necessarily reveal the effect of the new policy.


to top



2.2 Approach 2

Calculate the difference in New Jersey’s employment level and another state’s employment level (not affected by the minimum wage policy) after the change in minimum wage

\[employment_i = \beta_0 + \beta_1TreatedState_i + \mu_i\]

What are the problems with this approach?

There may be variables in the error term that impact employment and that are state specific. This would result in a biased \(\beta_1\). For instance, if the two states have different political parties in power, the party’s policies may impact both the employment level and the decision to raise minimum wage. This would result in a violation of MLR.4 (zero conditional mean assumption), and our estimate of the impact of the minimum wage increase on employment would suffer from omitted variable bias. Similarly, even if we observe a difference between states, this difference could have already existed in the period before the new law was passed. The coefficient for \(TreatedState\) just shows the difference in employment between the treated State and the control State, but does not necessarily reveal the effect of the new policy.


to top



2.3 Approach 3

Difference-in-differences

\[employment_{it} = \beta_0 + \beta_1TreatedState_i + \beta_2Time_{t} + \beta_3TreatedState_i\times Time_{t} + \mu_{it}\]

To proceed with a Difference-in-differences analysis, the authors collected data for New Jersey (NJ) and Pennsylvania (PA). Pennsylvania shares a border with the state of New Jersey but the minimum wage was unchanged there. It serves as a control group.


to top



3. Exploratory Data Analysis

3.1 Data Structure

How is this data set different from the data sets we have seen so far?

What is our unit of analysis? The fast-food store.

# Print the first rows of the dataset
head(tb.fastfood)

This is a panel data set. Each fast-food store’s id shows up twice in the data, once for time period 0 and once for time period 1.

Is this a balanced panel? Meaning, do we observe each store in each of the time periods?

# Number of observations per time period
tb.fastfood %>% 
  group_by(time) %>%
  summarize(n = n()) %>%
  ungroup()
# Number of time periods per ID
tb.fastfood %>% 
  group_by(id) %>%
  summarize(n = n()) %>%
  filter(n!=2) %>%
  ungroup()


to top



3.2 Data Pre-processing

We will now look at the summary statistics of our numerical variables. Note that some of the variables are affected by missing data.

# Summary statistics
stargazer(  data.frame(tb.fastfood %>% select(-id))
          , type = "text"
          , header = FALSE
          , font.size = "small"
          , title = "Summary Statistics")

Summary Statistics
==========================================
Statistic  N   Mean  St. Dev.  Min   Max  
------------------------------------------
time      714 0.500   0.500     0     1   
emptot    714 20.979  9.508   0.000 85.000
state     714 0.812   0.391     0     1   
chain     714 2.090   1.079     1     4   
co_owned  714 0.356   0.479     0     1   
atmin     714 0.322   0.468     0     1   
wage      708 4.805   0.359   4.250 6.250 
hrsopen   707 14.452  2.821   7.000 24.000
pmeal     672 3.341   0.658   2.140 5.860 
fracft    708 0.344   0.239   0.000 0.904 
------------------------------------------

To make this example simpler for today’s lab we will clean the data set by eliminating stores with missing information:

# Create indicator for missing data by ID
tb.fastfood <- tb.fastfood %>% 
  mutate(missing = as.double(complete.cases(tb.fastfood)==FALSE)) %>%
  group_by(id) %>%
  mutate(missing = max(missing)) %>%
  ungroup()

# Check data
# View(tb.fastfood %>% filter(missing>0))

# Check how many stores have missing data
tb.fastfood %>% 
  filter(missing>0) %>% 
  summarize(n = n_distinct(id))
# Keep only IDs with complete information
tb.fastfood <- tb.fastfood %>% filter(missing==0)
tb.fastfood <- tb.fastfood %>% select(-missing)
# Alternatively you could drop the stores with NAs using:
tb.fastfood <- tb.fastfood %>%
  drop_na %>%
  group_by(id) %>%
  mutate(n = n()) %>%
  filter(n==2) %>%
  select(-n) %>% 
  ungroup()

We will also format the state and time variables as factors:

# Create state and time factors
tb.fastfood <- tb.fastfood %>%
  mutate(  state = factor(state, levels = c(0,1), labels = c("PA", "NJ"))
         , time  = factor(time,  levels = c(0,1), labels = c("Before", "After")))


to top



3.3 Descriptive Statistics

We can now produce a summary statistics table for the clean data set.

# Summary statistics
stargazer(data.frame(tb.fastfood %>% select(-id))
          , type = "text"
          , header = FALSE
          , font.size = "small"
          , title = "Summary Statistics")

Summary Statistics
==========================================
Statistic  N   Mean  St. Dev.  Min   Max  
------------------------------------------
emptot    632 20.691  8.731   5.000 70.500
chain     632 2.092   1.075     1     4   
co_owned  632 0.348   0.477     0     1   
atmin     632 0.326   0.469     0     1   
wage      632 4.804   0.358   4.250 6.250 
hrsopen   632 14.295  2.679   7.000 24.000
pmeal     632 3.355   0.662   2.390 5.860 
fracft    632 0.343   0.240   0.000 0.904 
------------------------------------------



Change in wages

The following plot shows the change in the distribution of wages for the treatment and control group, before and after the change. We can see that both groups had a very similar wage distribution before the change in minimum wage was implemented in New Jersey. After the change, all restaurants in New Jersey that were paying less than $5.05/hour started paying at that rate, complying with the new legislation.

# Plot the distribution of wages per state before and after the policy
ggplot(data = tb.fastfood, aes(x = wage)) + 
  geom_histogram(binwidth=0.2) + 
  facet_wrap(~ state + time) + theme_bw()

Please note that the plot above is not testing the assumption of a common trend prior to treatment. In order to test the common trend assumption, one needs to have data for the control and treatment groups at more than one time point before the treatment takes place. This data set does not allow us to test that assumption as we only have two data points — before and after treatment.



Change in employment

Consider the following questions:

  • What kind of plot does economic theory predict?

  • How does this plot differ from our expectations?

# Boxplot of full time employment per state before and after
ggplot(data = tb.fastfood, aes(x = time, y = emptot, fill = state)) + 
  geom_boxplot() +
  theme_bw() +
  labs(x = "Time", y = "FTE Employment", fill="State") +
  scale_fill_fivethirtyeight()

Economic theory predicts a reduction in employment resulting from the increase in minimum wage. The plot suggests that this did not happen. On the contrary, employment seems to have increased slightly for the treated group (New Jersey).



Means of key variables for PA and NJ before and after policy

We can use the full data set to build a table with the before and after means for treatment and control groups.

# Create a table with the variable means for treatment and control groups before and after the policy
tb.bf.aft <- tb.fastfood %>%
  group_by(state, time) %>%
  summarise(
    mean_emptot     = mean(emptot),
    mean_wage       = mean(wage),
    mean_pmeal      = mean(pmeal),
    mean_hrsopen    = mean(hrsopen),
    mean_fracft     = mean(fracft)) %>%
  ungroup()

tb.bf.aft

At \(Time_0\), average employment was 23.6 full-time equivalent (FTE) workers per store in Pennsylvania, compared with an average of 20.0 in New Jersey. Despite the increase in wages, FTE employment increased in New Jersey. Whereas New Jersey stores were initially smaller, employment gains in New Jersey coupled with losses in Pennsylvania led to a small and statistically insignificant interstate difference at \(Time_1\).

We can use the data on the table to build a new plot.

# Diff-in-diff plot
ggplot(  data = tb.bf.aft
       , aes(x = time, y = mean_emptot, group = state, color = state)) + 
  geom_point() + geom_line() + theme_bw() + 
  scale_color_fivethirtyeight() + ylim(15, 25)


to top



4. Difference-in-Differences

4.1 Manual Diff-in-Diff

The differences-in-differences strategy amounts to comparing the change in mean FTE in New Jersey to the change in mean FTE in Pennsylvania.

\[\text{Treatment Effect} = (\bar{Y}_{NJ,T1}-\bar{Y}_{NJ,T0})-(\bar{Y}_{PA,T1}-\bar{Y}_{PA,T0})\]

# Difference in differences (manual)
(20.47745 - 20.00588) - (21.54098-23.59836)
[1] 2.5289

Surprisingly, employment rose in New Jersey relative to Pennsylvania after the minimum wage change. New Jersey stores were initially smaller than their Pennsylvania counterparts but grew relative to Pennsylvania stores after the rise in the minimum wage. The relative gain (the “difference in differences” of the changes in employment) is 2.52 FTE employees.

We can also write the treatment effect as:

\[\text{Treatment Effect} = (\bar{Y}_{NJ,T1}-\bar{Y}_{PA,T1})-(\bar{Y}_{NJ,T0}-\bar{Y}_{PA,T0})\]

# Difference in differences (manual)
(20.47745 - 21.54098) - (20.00588-23.59836)
[1] 2.5289


to top



4.2 Regression

We can estimate the difference-in-differences (DiD) estimator in a regression framework using the lm() function. This approach has several advantages:

  • It is easy to calculate standard errors.

  • We can control for other covariates, which may reduce residual variance and lead to more precise estimates.

  • It naturally accommodates extensions to more than two periods.

  • It can be adapted to varying treatment intensities (e.g., different levels of a policy across groups).

Although we are working with panel data, for a two-period DiD setting, the use of lm() is sufficient and preferred for its simplicity and transparency.

However, since the same unit (e.g., store, individual, firm) appears more than once, the assumption of independent errors is violated. To address this, we use robust (heteroskedasticity-consistent) standard errors, which can be computed with the sandwich and lmtest packages.

Note: While lm() is appropriate and convenient for estimating difference-in-differences with two-period panel data, for panels with more than two time periods, it is recommended to use the plm() function from the plm package.

# Estimate model
lm1 <- lm(emptot ~ state + time + state:time, data = tb.fastfood)

# Robust standard errors
robust_se1 <- sqrt(diag(vcovHC(lm1, type = "HC3")))

# Stargazer with robust SEs manually added
stargazer(lm1, lm1,
          se = list(NULL, robust_se1),
          column.labels = c("DiD", "DiD Robust SE"),
          type = "text",
          title = "Difference-in-Differences Results",
          omit.stat = c("ser", "f"),
          no.space = TRUE,
          header = FALSE,
          font.size = "small",
          dep.var.caption = "",
          dep.var.labels.include = FALSE,
          model.names = FALSE)

Difference-in-Differences Results
==============================================
                       DiD       DiD Robust SE
                       (1)            (2)     
----------------------------------------------
stateNJ             -3.592***      -3.592**   
                     (1.238)        (1.688)   
timeAfter             -2.057        -2.057    
                     (1.573)        (1.918)   
stateNJ:timeAfter     2.529          2.529    
                     (1.751)        (2.052)   
Constant            23.598***      23.598***  
                     (1.112)        (1.610)   
----------------------------------------------
Observations           632            632     
R2                    0.014          0.014    
Adjusted R2           0.010          0.010    
==============================================
Note:              *p<0.1; **p<0.05; ***p<0.01

Notes:

  • state, time, and state:time represent the classic DiD model:

    • state: treated vs control
    • time: before vs after
    • state:time: the DiD interaction (your estimate of interest)
  • We use vcovHC() to correct standard errors (HC3 is a good robust default).

Are the coefficients statistically significant? How do we interpret the regression coefficients?

  • emptot00: average FTE employment in Pennsylvania at T0 (\(\beta_0\))

  • emptot10: average FTE employment in New Jersey at T0 (\(\beta_0\) + \(\beta_1\))

  • emptot01: average FTE employment in Pennsylvania at T1 (\(\beta_0\) + \(\beta_2\))

  • emptot11: average FTE employment in New Jersey at T1 (\(\beta_0\) + \(\beta_1\) + \(\beta_2\) + \(\beta_3\))

What is the correspondence between the betas and the values from the table?

\(\beta_0 = 23.59836\)

# Store vector with diff-in-diff coefficients
coefs <- coefficients(lm1)

# Intercept
coefs[1]
(Intercept) 
     23.598 

\(\beta_1 = emptot10 - emptot00 =\)

20.00588 - 23.59836
[1] -3.5925
# Coefficient of state
coefs[2]
stateNJ 
-3.5925 

\(\beta_2 = emptot01 - emptot00 =\)

21.54098 - 23.59836
[1] -2.0574
# Coefficient of time
coefs[3]
timeAfter 
  -2.0574 

\(\beta_3 = (emptot11 - emptot10)-(emptot01 - emptot00) =\)

(20.47745 - 20.00588) - (21.54098 - 23.59836)
[1] 2.5289
# Coefficient of interaction term state*time
coefs[4]
stateNJ:timeAfter 
           2.5289 


to top



4.3 Adding Controls

We can control for other factors to get a better estimate of the treatment effect. Note that the introduction of the co-ownership and chain controls to the model, did not impact the estimated coefficients of timeAfter and stateNJ:timeAfter. This happens when the additional explanatory variables are uncorrelated with the variables that were already included in the model.

# Extended model with controls
lm2 <- update(lm1, . ~ . + factor(co_owned) + factor(chain))

# Robust standard errors (HC3)
robust_se2 <- sqrt(diag(vcovHC(lm2, type = "HC3")))

# Output regression table with robust SEs
stargazer(lm1, lm2,
          se = list(robust_se1, robust_se2),
          type = "text",
          title = "Difference-in-Differences with and without Controls",
          column.labels = c("Simple", "With Controls"),
          dep.var.caption = "",
          dep.var.labels.include = FALSE,
          model.names = FALSE,
          no.space = TRUE,
          omit.stat = c("ser", "f"),
          header = FALSE,
          font.size = "small")

Difference-in-Differences with and without Controls
==============================================
                      Simple     With Controls
                       (1)            (2)     
----------------------------------------------
stateNJ              -3.592**      -2.906**   
                     (1.688)        (1.475)   
timeAfter             -2.057        -2.057    
                     (1.918)        (1.629)   
factor(co_owned)1                   -0.859    
                                    (0.604)   
factor(chain)2                    -10.680***  
                                    (0.671)   
factor(chain)3                     -3.187***  
                                    (0.800)   
factor(chain)4                      -1.823*   
                                    (1.086)   
stateNJ:timeAfter     2.529          2.529    
                     (2.052)        (1.752)   
Constant            23.598***      26.770***  
                     (1.610)        (1.572)   
----------------------------------------------
Observations           632            632     
R2                    0.014          0.251    
Adjusted R2           0.010          0.242    
==============================================
Note:              *p<0.1; **p<0.05; ***p<0.01

After considering other factors, the diff-in-diff estimate stateNJ:timeAfter (treatment effect) is still not statistically significant. There is no significant difference between the relative changes in employment between states.


to top