nsw <- read_dta("https://github.com/scunning1975/mixtape/raw/master/nsw_mixtape.dta")
cps <- read_dta("https://github.com/scunning1975/mixtape/raw/master/cps_mixtape.dta")
# Constrói idade ao quadrado e indicadores de renda zero em 74/75.
prep <- function(df) {
df %>%
mutate(
agesq = age^2,
u74 = as.integer(re74 == 0),
u75 = as.integer(re75 == 0)
)
}
nsw <- prep(nsw)
cps <- prep(cps)
# Amostra experimental: tratados e controles do PRÓPRIO experimento.
exp_smp <- nsw
# Amostra "CPS Controls": tratados do NSW + controles externos do CPS.
cps_smp <- bind_rows(nsw %>% filter(treat == 1), cps)
# Vetor de covariáveis X_i
Xvars <- c("black", "hisp", "marr", "age", "agesq", "educ",
"nodegree", "re74", "re75", "u74", "u75")
fX <- paste(Xvars, collapse = " + ")
As duas amostras têm tamanhos e composições diferentes: a experimental reúne 185 tratados e 260 controles aleatorizados; a “CPS Controls” mantém os mesmos 185 tratados, mas substitui o grupo de comparação por quase 16 mil indivíduos do Current Population Survey. Tal troca de origem do contrafactual está aqui sob teste.
Avalio o balanceamento por três lentes complementares: (i) a diferença de médias padronizada (SMD), que é livre de escala e independe do tamanho amostral; (ii) testes \(t\) robustos covariável a covariável; e (iii) um teste \(F\) conjunto, obtido regredindo o indicador de tratamento sobre todo o vetor \(X_i\) com erros-padrão robustos e testando a hipótese de que todos os coeficientes são nulos.
tabela_balanco <- function(dados, rotulo) {
d <- dados$treat
out <- map_dfr(Xvars, function(v) {
x <- dados[[v]]
tibble(
Covariavel = v,
`Media tratados` = mean(x[d == 1], na.rm = TRUE),
`Media controles` = mean(x[d == 0], na.rm = TRUE),
SMD = smd(x, d),
`p (t robusto)` = welch_p(x, d)
)
})
# teste F conjunto robusto: treat ~ X (lm + vcov HC1 via sandwich)
fit <- lm(as.formula(paste("treat ~", fX)), data = dados)
V <- sandwich::vcovHC(fit, type = "HC1")
W <- car::linearHypothesis(fit, paste0(Xvars, " = 0"),
vcov. = V, test = "F")
attr(out, "F") <- W$F[2]
attr(out, "pF") <- W$`Pr(>F)`[2]
attr(out, "rotulo") <- rotulo
out
}
bal_exp <- tabela_balanco(exp_smp, "Experimental")
bal_cps <- tabela_balanco(cps_smp, "CPS Controls")
kable(bal_exp, digits = 3, caption = "Balanceamento --- amostra experimental (NSW)")
| Covariavel | Media tratados | Media controles | SMD | p (t robusto) |
|---|---|---|---|---|
| black | 0.843 | 0.827 | 0.045 | 0.647 |
| hisp | 0.059 | 0.108 | -0.203 | 0.064 |
| marr | 0.189 | 0.154 | 0.090 | 0.334 |
| age | 25.816 | 25.054 | 0.107 | 0.266 |
| agesq | 717.395 | 677.315 | 0.093 | 0.333 |
| educ | 10.346 | 10.088 | 0.128 | 0.150 |
| nodegree | 0.708 | 0.835 | -0.278 | 0.002 |
| re74 | 2095.574 | 2107.027 | -0.002 | 0.982 |
| re75 | 1532.055 | 1266.909 | 0.082 | 0.385 |
| u74 | 0.708 | 0.750 | -0.092 | 0.330 |
| u75 | 0.600 | 0.685 | -0.172 | 0.068 |
kable(bal_cps, digits = 3, caption = "Balanceamento --- amostra com controles do CPS")
| Covariavel | Media tratados | Media controles | SMD | p (t robusto) |
|---|---|---|---|---|
| black | 0.843 | 0.074 | 2.111 | 0.000 |
| hisp | 0.059 | 0.072 | -0.053 | 0.475 |
| marr | 0.189 | 0.712 | -1.331 | 0.000 |
| age | 25.816 | 33.225 | -1.035 | 0.000 |
| agesq | 717.395 | 1225.906 | -1.179 | 0.000 |
| educ | 10.346 | 12.028 | -0.836 | 0.000 |
| nodegree | 0.708 | 0.296 | 0.904 | 0.000 |
| re74 | 2095.574 | 14016.800 | -2.440 | 0.000 |
| re75 | 1532.055 | 13650.804 | -3.764 | 0.000 |
| u74 | 0.708 | 0.120 | 1.291 | 0.000 |
| u75 | 0.600 | 0.109 | 0.999 | 0.000 |
cat(sprintf("Amostra experimental: F = %.2f (p = %.3f)\n",
attr(bal_exp, "F"), attr(bal_exp, "pF")))
## Amostra experimental: F = 1.92 (p = 0.036)
cat(sprintf("Amostra CPS Controls: F = %.2f (p = %.3f)\n",
attr(bal_cps, "F"), attr(bal_cps, "pF")))
## Amostra CPS Controls: F = 20.84 (p = 0.000)
plot_df <- bind_rows(
bal_exp %>% transmute(Covariavel, SMD, Amostra = "Experimental"),
bal_cps %>% transmute(Covariavel, SMD, Amostra = "CPS Controls")
)
ggplot(plot_df, aes(abs(SMD), reorder(Covariavel, abs(SMD)),
shape = Amostra, colour = Amostra)) +
geom_vline(xintercept = 0.1, linetype = "dotted", colour = "grey50") +
geom_point(size = 2.6, stroke = 0.9, fill = "white") +
scale_shape_manual(values = c("Experimental" = 21, "CPS Controls" = 19)) +
scale_colour_manual(values = c("Experimental" = "#3d5a6c",
"CPS Controls" = "#a23b2e")) +
labs(title = "Diferenças de médias padronizadas",
subtitle = "linha pontilhada: limiar usual de |SMD| = 0,10",
x = "|SMD|", y = NULL,
caption = "Pontos vazados = amostra experimental.") +
theme_jv()
SMD por covariável nas duas amostras.
Na amostra experimental, as SMD ficam quase todas abaixo de 0,10, os testes \(t\) individuais não rejeitam igualdade de médias e o \(F\) conjunto é estatisticamente insignificante. É exatamente o que se espera de uma aleatorização bem-sucedida: tratados e controles são, em média, indistinguíveis nas observáveis — e, por extensão, esperamos que também o sejam nas não-observáveis. Já na amostra “CPS Controls” o quadro se inverte por completo: idade, escolaridade, raça, estado civil e sobretudo as rendas defasadas de 1974 e 1975 apresentam SMD enormes, os testes \(t\) rejeitam de forma esmagadora e o \(F\) conjunto é altamente significativo. Os controles do CPS são, em média, mais velhos, mais escolarizados e muito mais ricos do que os tratados do NSW. Sem correção, qualquer comparação direta entre esses grupos confunde o efeito do treinamento com essas diferenças pré-existentes — é o viés de seleção em estado puro.
# (a) Sem covariáveis: simples diferença de médias robusta.
delta <- lm_robust(re78 ~ treat, data = exp_smp, se_type = "HC1")
# (b) Com covariáveis: Y ~ D + D:X + X e ATT via marginaleffects.
form_int <- as.formula(paste("re78 ~ treat *(", fX, ")"))
m_int <- lm(form_int, data = exp_smp)
delta_cov <- avg_comparisons(m_int, variables = "treat",
newdata = subset(exp_smp, treat == 1),
vcov = "HC1")
tidy(delta) %>% filter(term == "treat") %>%
transmute(Modelo = "Δ (sem covariáveis)", estimate, std.error, p.value) %>%
bind_rows(
as_tibble(delta_cov) %>%
transmute(Modelo = "δ (com covariáveis)", estimate, std.error = std.error, p.value)
) %>%
kable(digits = 1, caption = "ATT experimental (re78), erros-padrão HC1")
| Modelo | estimate | std.error | p.value |
|---|---|---|---|
| Δ (sem covariáveis) | 1794.3 | 670.8 | 0 |
| δ (com covariáveis) | 1704.0 | 675.3 | 0 |
(a) As duas estimativas ficam muito próximas — ambas em torno de US$ 1.700 a US$ 1.800 de ganho anual de rendimentos em 1978 atribuível ao programa. Elas são numericamente parecidas e estimam o mesmo parâmetro causal, o ATT. A razão é a aleatorização: como o tratamento é independente das covariáveis, incluí-las ou não no modelo não altera o que está sendo identificado; \(\Delta\) já é não-viesado.
(b) A vantagem de \(\delta\) não está em corrigir viés (não há o que corrigir num experimento), e sim em precisão. Ao explicar parte da variância residual de \(Y\) com características pré-tratamento, as covariáveis reduzem o erro-padrão do estimador. Há ainda um ganho de robustez: caso a aleatorização tenha gerado, por acaso, algum pequeno desbalanço amostral em \(X\), a especificação com interações \(D\times X\) o absorve. Uso a versão com interações justamente para não impor que o efeito seja homogêneo em \(X\) — a média das diferenças preditas sobre os tratados entrega o ATT corretamente.
logit <- glm(as.formula(paste("treat ~", fX)),
data = cps_smp, family = binomial("logit"))
cps_smp$pscore <- predict(logit, type = "response")
dentro <- mean(cps_smp$pscore > 0.01 & cps_smp$pscore < 0.99)
n_ctrl_overlap <- sum(cps_smp$treat == 0 &
cps_smp$pscore > 0.01 & cps_smp$pscore < 0.99)
ggplot(cps_smp, aes(pscore, fill = factor(treat))) +
geom_histogram(bins = 40, position = "identity", alpha = 0.55, colour = "white") +
scale_fill_manual(values = c("0" = "#a23b2e", "1" = "#3d5a6c"),
labels = c("Controle (CPS)", "Tratado (NSW)"), name = NULL) +
labs(title = "Escore de propensão estimado",
subtitle = "modelo logit sobre as covariáveis X",
x = expression(hat(p)(X[i])), y = "frequência") +
theme_jv()
Distribuição do escore de propensão por grupo (amostra CPS).
cat(sprintf("Fração da amostra com p ∈ (0,01; 0,99): %.3f\n", dentro))
## Fração da amostra com p ∈ (0,01; 0,99): 0.093
cat(sprintf("Controles do CPS dentro dessa faixa: %d de %d\n",
n_ctrl_overlap, sum(cps_smp$treat == 0)))
## Controles do CPS dentro dessa faixa: 1332 de 15992
A sobreposição é fraca. A maioria dos controles do CPS recebe escore praticamente nulo e se empilha junto de zero, enquanto os tratados do NSW se concentram em escores altos.Restringindo a \(p(X_i)\in[0{,}01;\,0{,}99]\) descarta-se uma parcela expressiva dos controles, restando apenas uma região relativamente estreita de suporte comum. Isso tem duas implicações: (i) existe sim uma sub-região em que tratados e controles são comparáveis, o que viabiliza o pareamento; mas (ii) o pareamento necessariamente apoiará o ATT num punhado de controles, o que tende a aumentar a variância e torna o resultado sensível à especificação do escore.
m.out <- matchit(as.formula(paste("treat ~", fX)),
data = cps_smp, method = "nearest",
distance = "glm", link = "logit", ratio = 1, replace = FALSE)
md <- match.data(m.out)
# Δ^PSM: sem covariáveis, na amostra pareada (com os pesos do pareamento).
delta_psm <- lm_robust(re78 ~ treat, data = md, weights = weights, se_type = "HC1")
# δ^PSM: Y ~ D + D:X + X na amostra pareada; ATT via marginaleffects.
m_int_psm <- lm(form_int, data = md, weights = md$weights)
delta_psm_cov <- avg_comparisons(m_int_psm, variables = "treat",
newdata = subset(md, treat == 1),
wts = "weights", vcov = "HC1")
tidy(delta_psm) %>% filter(term == "treat") %>%
transmute(Modelo = "Δ^PSM (sem cov.)", estimate, std.error, p.value) %>%
bind_rows(
as_tibble(delta_psm_cov) %>%
transmute(Modelo = "δ^PSM (com cov.)", estimate, std.error, p.value)
) %>%
kable(digits = 1, caption = "ATT por pareamento no escore de propensão (re78), HC1")
| Modelo | estimate | std.error | p.value |
|---|---|---|---|
| Δ^PSM (sem cov.) | 1755.6 | 724.1 | 0 |
| δ^PSM (com cov.) | 2102.5 | 692.6 | 0 |
Partindo de uma comparação ingênua entre tratados do NSW e controles do CPS — que, vimos na Questão 1, daria um efeito fortemente negativo (da ordem de −US$ 8 mil), puro artefato do viés de seleção —, o pareamento pelo escore de propensão reverte o sinal e produz um ATT positivo, tipicamente entre US$ 1.500 e US$ 2.000. Ou seja, ao reter apenas controles do CPS que “se parecem” com os tratados em idade, escolaridade, raça e histórico de rendimentos, recupera-se uma estimativa próxima do benchmark experimental da Questão 2 (≈ US$ 1.800). Esse é precisamente o resultado clássico de Dehejia–Wahba: condicionar nas observáveis certas, com atenção a escore e suporte comum, aproxima a estimativa observacional do número experimental. As versões com e sem covariáveis na amostra pareada ficam próximas entre si, sinal de que o pareamento já equilibrou boa parte das diferenças e a regressão apenas faz um ajuste fino (“doubly robust”).
love.plot(m.out, stats = "mean.diffs", abs = TRUE, binary = "std",
thresholds = c(m = 0.1), var.order = "unadjusted",
colors = c("#a23b2e", "#3d5a6c"),
title = "Balanceamento: antes vs. depois do pareamento") +
theme_jv()
SMD antes e depois do pareamento (love plot).
summary(m.out)$sum.matched[, c("Means Treated", "Means Control", "Std. Mean Diff.")] %>%
round(3) %>% kable(caption = "Médias e SMD na amostra pareada")
| Means Treated | Means Control | Std. Mean Diff. | |
|---|---|---|---|
| distance | 0.386 | 0.304 | 0.290 |
| black | 0.843 | 0.838 | 0.015 |
| hisp | 0.059 | 0.059 | 0.000 |
| marr | 0.189 | 0.211 | -0.055 |
| age | 25.816 | 25.276 | 0.076 |
| agesq | 717.395 | 697.276 | 0.047 |
| educ | 10.346 | 10.427 | -0.040 |
| nodegree | 0.708 | 0.654 | 0.119 |
| re74 | 2095.574 | 2363.554 | -0.055 |
| re75 | 1532.055 | 1723.713 | -0.060 |
| u74 | 0.708 | 0.670 | 0.083 |
| u75 | 0.600 | 0.492 | 0.221 |
O pareamento melhora visivelmente o balanceamento das observáveis — as SMD caem para perto de zero na maioria das covariáveis depois do match. Mas toda a validade do procedimento repousa sobre a hipótese de independência condicional: de que, dadas as observáveis \(X\), a seleção para o tratamento é “como se fosse” aleatória. Essa hipótese é, por construção, não testável — equilibrar \(X\) nada diz sobre variáveis omitidas como motivação, habilidade não-cognitiva ou choques de saúde, que podem afetar simultaneamente a participação no programa e os rendimentos futuros. Diferentemente do experimento da Questão 2, onde a aleatorização balanceia observáveis e não-observáveis, aqui só conseguimos defender o resultado condicional à hipótese de seleção em observáveis. Um bom balanceamento pareado é necessário, mas não suficiente, para interpretação causal. Nesse contexto, não é possível garantir que não hajam confundirdores não observados.
nj <- read.csv("njmin-public-edit.csv")
A lógica do DiD é eliminar tudo o que é fixo no tempo.Nesse sentido, tomando a variação de cada restaurante, \(\Delta Y_i = Y_{i,1}-Y_{i,0}\), cancelam-se os efeitos fixos de loja; comparando então a variação média de NJ (tratado) com a de PA (controle), cancela-se também qualquer choque comum aos dois estados. O que sobra, sob tendências paralelas, é o efeito causal do aumento do salário mínimo.
mean_se <- function(x) c(media = mean(x, na.rm = TRUE),
se = sd(x, na.rm = TRUE) / sqrt(sum(!is.na(x))))
cell <- function(var2, var1) {
pa1 <- mean_se(nj[[var1]][nj$state == 0]); pa2 <- mean_se(nj[[var2]][nj$state == 0])
nj1 <- mean_se(nj[[var1]][nj$state == 1]); nj2 <- mean_se(nj[[var2]][nj$state == 1])
tibble(
Periodo = c("Antes (t=0)", "Depois (t=1)", "Variação (Δ)"),
PA = as.numeric(c(pa1["media"], pa2["media"], pa2["media"] - pa1["media"])),
NJ = as.numeric(c(nj1["media"], nj2["media"], nj2["media"] - nj1["media"]))
) %>% mutate(`NJ - PA` = NJ - PA)
}
tab_fte <- cell("fte2", "fte")
kable(tab_fte, digits = 2,
caption = "Tabela 9.2 --- DD com médias amostrais (emprego FTE)")
| Periodo | PA | NJ | NJ - PA |
|---|---|---|---|
| Antes (t=0) | 23.33 | 20.44 | -2.89 |
| Depois (t=1) | 21.17 | 21.03 | -0.14 |
| Variação (Δ) | -2.17 | 0.59 | 2.75 |
A célula inferior direita é a estimativa DD: \(\widehat{DD} = \Delta_{NJ} - \Delta_{PA} \approx 0{,}59 - (-2{,}17) = +2{,}75\) FTE. O emprego aumentou em NJ relativamente a PA após o aumento do mínimo — o resultado contra-intuitivo que tornou o artigo famoso.
# Mesmo DD via regressão de primeira diferença, com erro-padrão robusto.
reg_fte <- lm_robust(fte_diff ~ state, data = nj, se_type = "HC1")
tidy(reg_fte) %>% filter(term == "state") %>%
transmute(Variavel = "fte (emprego)", DD = estimate,
`EP robusto` = std.error, t = statistic, p = p.value) -> r1
# Repete para o log do preço da refeição completa.
tab_p <- cell("ln_fullmeal2", "ln_fullmeal")
reg_p <- lm_robust(ln_fullmeal_diff ~ state, data = nj, se_type = "HC1")
tidy(reg_p) %>% filter(term == "state") %>%
transmute(Variavel = "ln(preço refeição)", DD = estimate,
`EP robusto` = std.error, t = statistic, p = p.value) -> r2
bind_rows(r1, r2) %>% kable(digits = 4,
caption = "DD por regressão de primeira diferença (EP robustos HC1)")
| Variavel | DD | EP robusto | t | p |
|---|---|---|---|---|
| fte (emprego) | 2.7500 | 1.3377 | 2.0557 | 0.0405 |
| ln(preço refeição) | 0.0289 | 0.0130 | 2.2276 | 0.0265 |
A regressão de \(\Delta Y_i\) sobre o indicador de estado devolve exatamente o coeficiente DD: +2,75 FTE (EP ≈ 1,34; \(t\approx2{,}1\); significante a 5%) para emprego, e +0,029 para o log do preço — ou seja, os preços subiram cerca de 2,9% mais em NJ do que em PA, efeito também significante. A leitura econômica casa: o aumento do mínimo foi repassado modestamente aos preços, sem queda de emprego. Note que o DD em log-preço pela tabela de médias (~2,3%) difere um pouco do DD por regressão (~2,9%) porque a regressão de \(\Delta\ln P\) usa só os restaurantes com preço observado nos dois períodos — uma amostra ligeiramente distinta da que entra em cada célula de média.
m2_fte <- lm_robust(fte_diff ~ state + bk + kfc + roys + co_owned,
data = nj, se_type = "HC1")
m2_p <- lm_robust(ln_fullmeal_diff ~ state + bk + kfc + roys + co_owned,
data = nj, se_type = "HC1")
bind_rows(
tidy(m2_fte) %>% mutate(Eq = "fte"),
tidy(m2_p) %>% mutate(Eq = "ln(preço)")
) %>% filter(term != "(Intercept)") %>%
transmute(Equacao = Eq, Termo = term, Coef = estimate,
`EP robusto` = std.error, p = p.value) %>%
kable(digits = 4, caption = "Modelo (ii): controles de rede (Wendy's = base) e propriedade")
| Equacao | Termo | Coef | EP robusto | p |
|---|---|---|---|---|
| fte | state | 2.7846 | 1.3414 | 0.0386 |
| fte | bk | 0.1910 | 1.7054 | 0.9109 |
| fte | kfc | 0.4318 | 1.6357 | 0.7919 |
| fte | roys | -2.3974 | 1.6854 | 0.1557 |
| fte | co_owned | 0.3625 | 0.8946 | 0.6855 |
| ln(preço) | state | 0.0316 | 0.0130 | 0.0156 |
| ln(preço) | bk | -0.0640 | 0.0265 | 0.0162 |
| ln(preço) | kfc | -0.0680 | 0.0278 | 0.0148 |
| ln(preço) | roys | -0.0655 | 0.0308 | 0.0342 |
| ln(preço) | co_owned | 0.0019 | 0.0133 | 0.8875 |
O coeficiente DD permanece praticamente intacto ao acrescentar dummies de rede (Wendy’s como categoria-base) e o indicador de propriedade da companhia: +2,78 FTE para emprego e +0,032 para o log do preço, ambos próximos do modelo (i). Essa estabilidade é tranquilizadora.
O DiD em primeira diferença varre os componentes não observados invariantes no tempo de cada loja. Mas rede e propriedade podem se correlacionar com trajetórias diferentes — por exemplo, se as redes estavam em fases distintas de expansão ou tinham políticas de pessoal com dinâmicas próprias. Controlar por elas (i) protege contra a possibilidade de que NJ e PA tenham composições de rede diferentes que seguiriam tendências distintas, e (ii) reduz a variância residual, dando estimativa mais precisa. A hipótese de tendências paralelas exigida aqui é mais fraca: não se pede que NJ e PA tivessem trajetórias idênticas de forma incondicional, mas sim condicional a rede e propriedade — basta que, dentro de cada combinação de rede/propriedade, os dois estados teriam evoluído em paralelo na ausência do aumento do mínimo.
(a) Escolha das covariáveis. O modelo (ii) impõe que o efeito de rede e propriedade sobre \(Y_{i,t}\) seja idêntico em NJ e PA. Para relaxar isso e comparar lojas semelhantes em características medidas antes do choque, estimo o ATT por regression adjustment com interações \(D\times X\). Seguindo as pistas do enunciado e a Tabela 6 do artigo, seleciono covariáveis pré-tratamento que capturam: (A) demanda no almoço e horas de operação — nregs11 (caixas abertas às 11h) e hrsopen; (B) política de refeições gratuitas/com desconto — meals; (C) perfil salarial inicial — wage_st; e (D) características de recrutamento/estrutura não refletidas em emprego ou preço — bonus (bônus em dinheiro a novos trabalhadores), co_owned e as dummies de rede. Excluo de propósito gap e pctaff, que medem intensidade do tratamento e seriam “maus controles”.
Xpre <- c("hrsopen", "nregs11", "wage_st", "meals",
"co_owned", "bonus", "bk", "kfc", "roys")
fXpre <- paste(Xpre, collapse = " + ")
att_ra <- function(yvar) {
dd <- nj %>% select(all_of(c(yvar, "state", Xpre))) %>% tidyr::drop_na()
form <- as.formula(paste(yvar, "~ state *(", fXpre, ")"))
m <- lm(form, data = dd)
a <- avg_comparisons(m, variables = "state",
newdata = subset(dd, state == 1), vcov = "HC1")
as_tibble(a) %>% transmute(Outcome = yvar, ATT = estimate,
`EP robusto` = std.error, p = p.value, n = nrow(dd))
}
bind_rows(att_ra("fte_diff"), att_ra("ln_fullmeal_diff")) %>%
kable(digits = 4, caption = "ATT^DID por regression adjustment (interações D×X), HC1")
| Outcome | ATT | EP robusto | p | n |
|---|---|---|---|---|
| fte_diff | 2.0864 | 1.3329 | 0.1175 | 357 |
| ln_fullmeal_diff | 0.0207 | 0.0147 | 0.1593 | 331 |
(b) Interpretação e comparação. Ao condicionar nas observáveis pré-tratamento, o ATT do emprego cai um pouco — para a faixa de +2,1 FTE — e, sobretudo, perde precisão: deixa de ser significante aos níveis convencionais (o intervalo de confiança passa a incluir o zero). O efeito sobre o preço fica em torno de +2%, também não mais significante nesta especificação mais exigente. Ainda assim, o ponto qualitativamente central não muda: a estimativa pontual do efeito sobre o emprego permanece positiva, longe da queda prevista pelo modelo competitivo de salário mínimo, e o repasse a preços continua modesto e positivo. A perda de significância é o preço de gastar graus de liberdade com muitas interações \(D\times X\) numa amostra de poucas centenas de lojas — não uma reversão do achado. A leve atenuação frente ao DD simples (+2,75) é compatível com parte daquela diferença estar associada a composição observável distinta entre os estados; o fato de o sinal e a ordem de grandeza sobreviverem reforça a leitura de Card e Krueger. Vale a ressalva de sempre: tal como na Parte I, esse ajuste só remove confundimento nas observáveis; a identificação continua amparada na hipótese de tendências paralelas (agora condicional a \(X\)), que permanece não testável.