panglm ships one
simulated and five real panel datasets so that every estimator family
can be taught on data where it makes substantive sense. The simulated
copd panel has known data-generating values, so students
can first check that an estimator recovers the truth; the real panels
then pose the same questions on public data, and their build scripts
(data-raw/ in the GitHub repository) reproduce published
numbers to show that the data were assembled correctly. Each help page
(for example ?docvisits) lists every variable, the source
and licence, the validation, and a “Teaching use” section with research
questions.
| Dataset | Panel | Outcome types | Estimators | Research question |
|---|---|---|---|---|
copd (simulated) |
60 patients x 5 visits | continuous, count, binary, cost with zeros, proportion, ordinal | all families, twopart(), panqr(),
attgt(), etwfe(),
eventstudy() |
Do the estimators recover the known effects, including that of a staggered care-management programme on cost? |
docvisits |
7,293 persons x 1984-1994 (7 waves, unbalanced) | count, binary, ordinal (0-10) | Poisson / negbin within and random, panhausman(),
probit with dynamic = TRUE, FE ordered logit,
panfmm(), panhurdle(), paniv()
Hausman-Taylor |
Does add-on insurance raise doctor visits; is doctor use state dependent? |
meps |
3,000 US adults x 2018-2019 | expenditure with zeros, counts, binary, ordinal | twopart(), family = "gamma",
"tobit", panqr(), panselect(),
negbin, cre = TRUE |
How much less do the uninsured spend, and how much of the gap is selection? |
medicaid |
51 states x 2009-2019 | rates (continuous) | attgt(), etwfe(),
eventstudy(), did(), two-way within |
Did the ACA Medicaid expansion reduce uninsurance? |
healthspend |
193 countries x 2000-2022 | continuous (log spending), shares in (0, 1) | within / random / CRE, panhausman(),
pangmm(), paniv() Hausman-Taylor,
family = "beta", random slopes, panqr() |
Is health care a luxury good? |
cigar |
46 US states x 1963-1992 | continuous (log demand) | pooled vs within vs pangmm(),
dynamic = TRUE |
How addictive is cigarette demand, and what is the long-run price elasticity? |
Each exercise below states a question, fits the model and interprets the numbers it prints.
Question. Clinics 1 and 2 adopted a care-management
programme at visit 3, clinics 3 and 4 at visit 4, and clinics 5 and 6
never did. By construction, the programme cuts the cost of users by
1 - exp(-0.5), about 39 percent, and its sample average
effect on the treated visits is -406 euros (see ?copd). Do
the estimators find it?
idx <- c("id", "visit")
att <- attgt(cost ~ 1, data = copd, index = idx, treat = "adopt",
control = "notyet")
summary(att)
#>
#> Callaway and Sant'Anna group-time ATT
#> Aggregation: overall (simple weighted average)
#> Control group: not yet treated Estimator: unconditional Individuals: 60
#>
#> ATT: -184 (SE 199.1, 95% CI -574.2 to 206.2)
#>
#> Pre-trend test (pre-treatment ATT(g, t) = 0): Chi-squared = 2.116, df = 3, p-value = 0.5487
tp <- twopart(cost ~ exacerbations + hospitalized + smoker + treatment + crp +
visit + programme + factor(clinic),
binary_formula = ~ exacerbations + hospitalized + smoker +
treatment + crp,
data = copd, index = idx, positive = "lognormal")
coef(tp$positive)[["programme"]]
#> [1] -0.5178754Interpretation. With only 60 patients the Callaway
and Sant’Anna estimate is imprecise (about -184 euros, standard error
about 200): its confidence interval covers the true -406 euros, and the
pre-trend test does not reject parallel trends, but a single small panel
cannot pin the effect down. The two-part model is far more precise
because it conditions on exacerbations and admissions, which explain
most of the cost variation: its programme coefficient on the log
positive cost is about -0.52 (standard error 0.13) against a true value
of -0.5. Rerunning the design on a large simulated panel (as
data-raw/copd.R does) brings both estimators close to the
truth; that is the point of a known-truth teaching dataset.
Question. People who saw a doctor last year are more
likely to see one this year. Is that habit (true state dependence) or
persistent ill health (unobserved heterogeneity)? We use the consecutive
waves 1984 to 1988 and the Wooldridge initial-conditions approach
(dynamic = TRUE).
early <- subset(docvisits, year <= 1988)
f <- doctor ~ age + hhninc + hhkids + married + working
pooled <- panglm(f, data = early, index = c("id", "year"), model = "pooling",
family = "probit", dynamic = TRUE, vcov = "cluster")
random <- panglm(f, data = early, index = c("id", "year"), model = "random",
family = "probit", dynamic = TRUE, R = 8)
cbind(pooled = coef(pooled)[c("lag_doctor", "initial_doctor")],
random = coef(random)[c("lag_doctor", "initial_doctor")])
#> pooled random
#> lag_doctor 0.6453130 0.2824974
#> initial_doctor 0.5457217 0.9103464Interpretation. The pooled dynamic probit gives a
lagged-visit coefficient of about 0.65, which mixes true state
dependence with persistent unobserved health. The random-effects probit
with initial conditions shrinks it to about 0.28 (roughly 8 percentage
points on the probability scale, see partialeffects()),
while the initial-condition coefficient and the random-effect variance
absorb the rest: most of the apparent persistence is heterogeneity, not
habit.
Question. How much less do uninsured adults spend on health care, and how much of the cross-sectional gap survives once we compare the same person before and after a change of coverage?
tp <- twopart(totexp ~ insurance + I(age / 10) + sex + nchronic + povcat,
data = meps, index = c("id", "year"), positive = "gamma")
partialeffects(tp, variables = "insurance")
#>
#> Average partial effects (total mean)
#> Delta-method standard errors, vcov: classical
#>
#> term contrast estimate std.error statistic p.value conf.low
#> insurance public - private -503.2 728.1 -0.691 4.896e-01 -1930
#> insurance uninsured - private -6194.0 563.4 -10.990 4.086e-28 -7299
#> conf.high
#> 924
#> -5090
pos <- subset(meps, totexp > 0)
pooled <- panglm(totexp ~ insurance + povcat + I(age / 10) + sex + nchronic,
data = pos, index = c("id", "year"), family = "gamma",
model = "pooling")
within <- panglm(totexp ~ insurance + povcat, data = pos,
index = c("id", "year"), family = "gamma", model = "within")
exp(c(pooled = coef(pooled)[["insuranceuninsured"]],
within = coef(within)[["insuranceuninsured"]])) - 1
#> pooled within
#> -0.6795660 -0.3096233Interpretation. The two-part model puts the average partial effect of being uninsured rather than privately insured at about -6,200 dollars per person-year, while public-only coverage differs little from private. Among people with positive spending, the pooled gamma model says the uninsured spend about 68 percent less; the within estimate, identified only from people who gained or lost coverage between 2018 and 2019, shrinks this to about 31 percent less. Much of the cross-sectional gap reflects who is uninsured rather than the effect of losing coverage.
Question. Did the Affordable Care Act Medicaid
expansion reduce uninsurance among adults below the poverty line?
Adoption was staggered (2014 to 2019 within the panel; later adopters
and the ten non-adopters serve as controls), so the single-coefficient
two-way fixed effects regression may compare late adopters with
already-treated early adopters. attgt() builds clean
comparisons with never-treated states.
ix <- c("state", "year")
cs <- attgt(uninspov ~ 1, data = medicaid, index = ix, treat = "adopt",
control = "never")
summary(cs, type = "dynamic")
#>
#> Callaway and Sant'Anna group-time ATT
#> Aggregation: event study (by time since treatment)
#> Control group: never treated Estimator: unconditional Individuals: 51
#>
#> ATT (mean of e >= 0): -6.562 (SE 1.317, 95% CI -9.143 to -3.98)
#>
#> event estimate se lower upper
#> -9 0.97529 0.8140 -0.62013 2.5707
#> -8 -0.09853 0.6997 -1.46999 1.2729
#> -7 1.16922 1.0103 -0.81093 3.1494
#> -6 -1.12859 1.1047 -3.29372 1.0365
#> -5 1.48015 0.7483 0.01347 2.9468
#> -4 -0.63116 0.4610 -1.53479 0.2725
#> -3 -0.29785 0.5167 -1.31050 0.7148
#> -2 0.40279 0.3635 -0.30965 1.1152
#> -1 -0.38889 0.5469 -1.46071 0.6829
#> 0 -5.15564 0.9010 -6.92165 -3.3896
#> 1 -6.39822 1.2670 -8.88146 -3.9150
#> 2 -7.08836 1.3521 -9.73834 -4.4384
#> 3 -7.59013 1.4220 -10.37726 -4.8030
#> 4 -6.52274 1.5487 -9.55810 -3.4874
#> 5 -6.61563 1.8257 -10.19386 -3.0374
tw <- did(uninspov ~ 1, data = medicaid, index = ix, treat = "adopt")
ew <- etwfe(uninspov ~ 1, data = medicaid, index = ix, treat = "adopt")
c(attgt = summary(cs)$overall$estimate, etwfe = summary(ew)$overall$estimate,
twfe = coef(tw)[["treated"]])
#> attgt etwfe twfe
#> -6.539795 -7.116279 -7.206276
# first stage: Medicaid coverage of the same adults
summary(attgt(medicaidpov ~ 1, data = medicaid, index = ix,
treat = "adopt", control = "never"))$overall
#> estimate se lower upper
#> 1 10.2355 1.40579 7.480201 12.9908Interpretation. Leads before expansion are small and scattered around zero, which supports parallel trends. The uninsured share of adults below the poverty line falls by about 5 percentage points in the year of expansion and by 6 to 8 points after two to three years; the overall group-time ATT is about -6.5 points (SE about 1.3). Medicaid coverage of the same adults rises by about 10 points, more than uninsurance falls: part of the gain replaces other coverage (crowd-out). Here the two-way fixed effects estimate (about -7.2) stays close to the robust ones (extended TWFE about -7.1), because most treated states adopted in 2014 and the effect is fairly flat after two years; with more spread-out adoption and stronger dynamics the gap can be large.
Question. Does health spending rise more than proportionally with national income (an income elasticity above 1)?
hs <- transform(healthspend, lche = log(chereal), lgdp = log(gdppc))
ix <- c("iso3c", "year")
fe <- panglm(lche ~ lgdp, data = hs, index = ix, model = "within")
re <- panglm(lche ~ lgdp, data = hs, index = ix, model = "random")
fe2 <- panglm(lche ~ lgdp, data = hs, index = ix, model = "within",
effect = "twoways", vcov = "cluster")
cbind(within = coef(fe), random = coef(re)[-1], twoways = coef(fe2))
#> within random twoways
#> lgdp 1.14926 1.135463 0.8396004
panhausman(fe, method = "mundlak", data = hs)
#>
#> Mundlak specification test (fixed vs. random effects), vcov: cluster
#>
#> F = 1.463, df = 1, 185, p-value = 0.228
#> alternative hypothesis: one model is inconsistentInterpretation. Within countries, real health spending grows by about 1.15 percent for each 1 percent of income, which looks like a luxury good, and the Mundlak test does not reject random effects. Adding year effects brings the elasticity down to about 0.84: much of the apparent luxury effect is a common upward trend in spending across all countries.
Question. Cigarette demand is addictive: how strong is the habit, and what is the long-run price elasticity? The lagged dependent variable makes pooled OLS biased upwards and the within estimator biased downwards (Nickell bias); difference GMM should lie in between.
ix <- c("state", "year")
f <- lnc ~ lag(lnc, 1) + lnp + lnpn + lny
ols <- panglm(f, data = cigar, index = ix, model = "pooling")
fe <- panglm(f, data = cigar, index = ix, model = "within", effect = "twoways")
ab <- pangmm(lnc ~ lag(lnc, 1) + lnp + lnpn + lny | lag(lnc, 2:99),
data = cigar, index = ix, transformation = "d", model = "onestep")
#> Warning: 437 instruments for 46 individuals: too many instruments bias GMM
#> toward OLS and weaken the Hansen test; use collapse = TRUE or a smaller maxlag
tab <- rbind(OLS = coef(ols)[-1], Within = coef(fe), GMM = coef(ab)[1:4])
round(tab, 3)
#> lag(lnc, 1) lnp lnpn lny
#> OLS 0.969 -0.090 0.024 -0.031
#> Within 0.833 -0.299 0.034 0.100
#> GMM 0.843 -0.377 -0.016 0.139
round(tab[, "lnp"] / (1 - tab[, 1]), 2) # long-run price elasticity
#> OLS Within GMM
#> -2.96 -1.79 -2.40Interpretation. The habit coefficient from difference GMM (0.84) lies between the within (0.83) and OLS (0.97) estimates, as theory predicts, and the three rows reproduce Table 8.1 of Baltagi’s Econometric Analysis of Panel Data. The short-run price elasticity is about -0.38 and the long-run one about -2.4.