Surveys are often designed for large areas (provinces, regions) but the estimates are needed for the sub-areas inside them (districts, municipalities). The sub-areas of one area share the area’s characteristics, so their estimates should borrow strength not only from the regression on auxiliary variables, but also from the other sub-areas of the same area.
The two-fold sub-area level model of Torabi and Rao (2014) does this with two nested random effects. fastsae fits it in two ways:
| Function | Approach | Uncertainty | Area aggregates |
|---|---|---|---|
eblup_twofold() |
EBLUP, REML or ML | Analytical (Prasad-Rao type) or parametric bootstrap MSE | df_area, weighted by
weight |
hb_twofold() |
Hierarchical Bayes with INLA | Posterior SD and credible intervals | df_area, weighted by
weight |
hb_twofold() also fits binomial and Poisson responses
and a spatial (BYM2, Besag) area effect.
For sub-area \(j\) (\(j = 1, \dots, N_i\)) of area \(i\) (\(i = 1, \dots, m\)), the direct estimator \(y_{ij}\) with known sampling variance \(\psi_{ij}\) follows
\[y_{ij} = \theta_{ij} + e_{ij}, \qquad \theta_{ij} = x_{ij}^\top \beta + v_i + u_{ij},\]
with \(v_i \sim N(0, \sigma_v^2)\) the area effect, \(u_{ij} \sim N(0, \sigma_u^2)\) the sub-area effect and \(e_{ij} \sim N(0, \psi_{ij})\) the sampling error, all independent.
Compared with the Fay-Herriot model, which treats every sub-area as unrelated, the area effect \(v_i\) is estimated from all the sampled sub-areas of area \(i\). This matters most for sub-areas that are not sampled: they get \(x_{ij}^\top \hat\beta + \hat v_i\) instead of the synthetic \(x_{ij}^\top \hat\beta\) alone. Only a sub-area of an area without any sample is predicted synthetically.
The output follows the notation above: random_effect_var
holds sigma2_v (area) and sigma2_u (sub-area),
and df_eblup (or df_hb) holds
random_effect_area and
random_effect_subarea.
We simulate 30 areas with 3 to 6 sub-areas each, so that the true
sub-area means \(\theta_{ij}\) are
known. Fifteen sub-areas have no sample (their direct estimate is
NA), and all the sub-areas of area A30 are
unsampled.
library(fastsae)
set.seed(2024)
m <- 30
dat <- do.call(rbind, lapply(seq_len(m), function(i) {
ni <- sample(3:6, 1)
data.frame(
area = sprintf("A%02d", i),
subarea = sprintf("A%02d-%d", i, seq_len(ni)),
x1 = rnorm(ni, mean = 2),
pop = sample(200:2000, ni)
)
}))
v <- rnorm(m, sd = 1)
names(v) <- unique(dat$area)
dat$theta <- 10 + 2 * dat$x1 + v[dat$area] + rnorm(nrow(dat), sd = sqrt(0.5))
dat$vardir <- runif(nrow(dat), 0.5, 2)
dat$y <- dat$theta + rnorm(nrow(dat), sd = sqrt(dat$vardir))
# Unsampled sub-areas: 15 at random, plus every sub-area of A30
unsampled <- union(sample(which(dat$area != "A30"), 15), which(dat$area == "A30"))
dat$y[unsampled] <- NA
c(subareas = nrow(dat), sampled = sum(!is.na(dat$y)))
#> subareas sampled
#> 131 111
head(dat)
#> area subarea x1 pop theta vardir y
#> 1 A01 A01-1 1.5347257 1857 13.78128 1.663801 12.20118
#> 2 A01 A01-2 2.5191534 1961 15.03099 1.105026 14.48113
#> 3 A01 A01-3 2.5284898 1222 14.79953 1.301555 15.01471
#> 4 A01 A01-4 1.4848072 1252 11.84648 1.406429 11.36094
#> 5 A02 A02-1 1.8729653 1255 13.61192 1.986447 12.02304
#> 6 A02 A02-2 0.7751246 924 12.23727 1.268523 NAEach row is one sub-area. Unsampled sub-areas stay in the data with
y = NA and their covariates, so that they are
predicted.
fit_tf <- eblup_twofold(
y ~ x1,
vardir = "vardir",
domain = "area",
subarea = "subarea",
data = dat,
weight = "pop",
print_result = FALSE
)
fit_tf$random_effect_var
#> sigma2_v sigma2_u
#> 1.1067031 0.1486881
fit_tf$estcoef
#> beta std.error stderr_beta zvalue pvalue
#> (Intercept) 9.605159 0.3223129 0.3223129 29.80073 3.822665e-195
#> x1 2.104314 0.1144109 0.1144109 18.39260 1.505751e-75
head(fit_tf$df_eblup)
#> domain subarea y eblup vardir random_effect_area
#> 1 A01 A01-1 12.20118 12.39678 1.663801 -0.4204473
#> 2 A01 A01-2 14.48113 14.48525 1.105026 -0.4204473
#> 3 A01 A01-3 15.01471 14.55766 1.301555 -0.4204473
#> 4 A01 A01-4 11.36094 12.21855 1.406429 -0.4204473
#> 5 A02 A02-1 12.02304 12.66335 1.986447 -0.8351927
#> 6 A02 A02-2 NA 10.40107 1.268523 -0.8351927
#> random_effect_subarea mse rse
#> 1 -0.0174796591 0.4205889 5.231427
#> 2 -0.0005543296 0.3941804 4.334326
#> 3 0.0522123369 0.4063272 4.378712
#> 4 -0.0906664052 0.4122688 5.254976
#> 5 -0.0479275040 0.4428309 5.254973
#> 6 0.0000000000 0.4782345 6.648787The default mse_type = "analytical" gives the Prasad-Rao
type MSE of the REML fit (for ML it adds the bias correction). Sub-area
A01-3 is not sampled: it still gets the area effect of
A01, while its sub-area effect is 0.
weight (here the population of each sub-area) aggregates
the sub-area EBLUPs to area means, \(\tilde\theta_i = \sum_j w_{ij}\tilde\theta_{ij} /
\sum_j w_{ij}\), in df_area. Their MSE accounts for
the errors the sub-areas of an area share (the estimated area effect and
\(\hat\beta\)), so it is larger than
\(\sum_j w_{ij}^2\,\mathrm{mse}_{ij}\)
would suggest.
head(fit_tf$df_area)
#> domain eblup_area mse_area rse_area n_subareas n_sampled
#> 1 A01 13.43189 0.2748473 3.903090 4 4
#> 2 A02 11.19056 0.3146821 5.012844 5 4
#> 3 A03 14.07817 0.2208185 3.337885 3 3
#> 4 A04 16.64890 0.3174408 3.384120 6 4
#> 5 A05 13.25981 0.4443843 5.027383 4 3
#> 6 A06 12.79993 0.2838182 4.162101 3 3Because the data are simulated, we can compare each estimator with
the true \(\theta_{ij}\). The
Fay-Herriot model, fitted with eblup_fh() on the same rows,
ignores the areas.
fit_fh <- eblup_fh(y ~ x1, vardir = "vardir", data = dat, print_result = FALSE)
sampled <- !is.na(dat$y)
in_sampled_area <- !sampled & dat$area != "A30"
emp_mse <- function(est, rows) mean((est[rows] - dat$theta[rows])^2)
data.frame(
rows = c("sampled sub-areas", "unsampled sub-areas of sampled areas"),
direct = c(emp_mse(dat$y, sampled), NA),
fay_herriot = c(
emp_mse(fit_fh$df_eblup$eblup, sampled),
emp_mse(fit_fh$df_eblup$eblup, in_sampled_area)
),
two_fold = c(
emp_mse(fit_tf$df_eblup$eblup, sampled),
emp_mse(fit_tf$df_eblup$eblup, in_sampled_area)
)
)
#> rows direct fay_herriot two_fold
#> 1 sampled sub-areas 1.211463 0.5087563 0.5291281
#> 2 unsampled sub-areas of sampled areas NA 1.5247133 0.7748664Both models improve on the direct estimates of the sampled sub-areas, and in this data set they are about equally accurate there: a sampled sub-area has its own direct estimate, so the area effect adds little. The difference is in the unsampled sub-areas of sampled areas. The Fay-Herriot model can only give them the synthetic estimate, while the two-fold model adds the estimated area effect and roughly halves the error. This is one simulated data set, not a simulation study: the gain depends on how large \(\sigma_v^2\) is relative to \(\sigma_u^2\).
The parametric bootstrap refits the model on B data sets
generated from the fitted model and predicts with the same estimator as
the point estimate. seed makes it reproducible without
changing the random number stream of the session.
fit_boot <- eblup_twofold(
y ~ x1,
vardir = "vardir",
domain = "area",
subarea = "subarea",
data = dat,
mse_type = "bootstrap",
B = 100,
seed = 1,
print_result = FALSE
)
summary(fit_boot$df_eblup$mse / fit_tf$df_eblup$mse)
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 0.6322 0.8257 0.9056 0.9205 0.9971 1.4684The two MSE estimators agree on average; with B = 100
single bootstrap values are still noisy. Use
mse_type = "none" when only the point estimates are
needed.
The usual tools of the package work on the result:
diagnose(fit_tf)
#> ── fastsae Small Area Estimation Diagnostic Report ─────────────────────────────
#> Model: "TWOFOLD" | Domains: 131 (Sampled: 111, Unsampled: 20)
#>
#> ── 1. Precision & Efficiency Gain ──
#>
#> ✔ Domains with RSE < 25%: 100% (High precision)
#> ✔ Average RSE reduction: Direct "8.54%" -> SAE "4.97%" (Gain: 3.57%)
#> ✔ Variance reduction in 100% of areas (MSE ratio median: 3.2, max: 5.34)
#>
#> ── 2. Brown et al. (2001) Diagnostics ──
#>
#> ✔ Goodness-of-Fit Statistic W: W = 60.62 (df = 111), p-value = 1 [Consistent with direct estimates]
#> ℹ Bias regression (direct ~ model, descriptive): alpha = -0.7789 (SE 0.5383), beta = 1.0544 (SE 0.0388), R^2 = 0.872
#> A slope above 1 is expected for shrinkage estimators; see ?diagnose.
#> ────────────────────────────────────────────────────────────────────────────────
#> ✔ Final Assessment: PASS: Model estimates are consistent with the direct estimates (Brown W test) and reliable.
#> ────────────────────────────────────────────────────────────────────────────────hb_twofold()hb_twofold() fits the same model with INLA. It needs the
INLA package, which is not on CRAN; see https://www.r-inla.org/download/. The chunks below are
only run when INLA is installed.
The argument weight (here the population of each
sub-area) aggregates the sub-area posteriors to area estimates, \(\hat\theta_i = \sum_j w_{ij} \theta_{ij} / \sum_j
w_{ij}\), with their posterior uncertainty in
df_area. For the Gaussian model these are linear
combinations of the sub-area predictors, whose posterior INLA computes
exactly; the binomial and Poisson aggregates come from posterior draws
(inla_engine(draws = )).
fit_hb <- hb_twofold(
y ~ x1,
vardir = "vardir",
domain = "area",
subarea = "subarea",
weight = "pop",
data = dat,
print_result = FALSE
)
fit_hb$random_effect_var
#> sigma2_v sigma2_u
#> 1.2361948 0.1876566
head(fit_hb$df_hb)
#> domain subarea y hb linear_pred vardir sd mse
#> 1 A01 A01-1 12.20118 12.39284 12.39284 1.663801 0.6266608 0.3927038
#> 2 A01 A01-2 14.48113 14.48299 14.48299 1.105026 0.6021869 0.3626291
#> 3 A01 A01-3 15.01471 14.56048 14.56048 1.301555 0.6148532 0.3780445
#> 4 A01 A01-4 11.36094 12.20728 12.20728 1.406429 0.6221135 0.3870252
#> 5 A02 A02-1 12.02304 12.65276 12.65276 1.986447 0.6498179 0.4222633
#> 6 A02 A02-2 NA 10.39822 10.39822 1.268523 0.7104407 0.5047260
#> rse ci_lower ci_upper random_effect_area random_effect_subarea
#> 1 5.056637 11.15158 13.62591 -0.4212193 -2.070355e-02
#> 2 4.157891 13.29671 15.66933 -0.4212193 -1.328942e-03
#> 3 4.222755 13.36344 15.78856 -0.4212193 5.675650e-02
#> 4 5.096251 10.95402 13.40851 -0.4212193 -9.928924e-02
#> 5 5.135780 11.35408 13.91965 -0.8382561 -5.528423e-02
#> 6 6.832333 8.99364 11.81438 -0.8382561 -3.841197e-11
head(fit_hb$df_area)
#> domain hb_area sd_area mse_area rse_area ci_lower_area ci_upper_area
#> 1 A01 13.42833 0.5162319 0.2664954 3.844349 12.41277 14.43825
#> 2 A02 11.18124 0.5549241 0.3079408 4.962993 10.08415 12.26186
#> 3 A03 14.08170 0.4637863 0.2150978 3.293539 13.17472 14.99413
#> 4 A04 16.66077 0.5774092 0.3334014 3.465681 15.53889 17.80449
#> 5 A05 13.24774 0.6585491 0.4336869 4.971031 11.94801 14.53404
#> 6 A06 12.79906 0.5225056 0.2730121 4.082374 11.77389 13.82419
#> n_subareas
#> 1 4
#> 2 5
#> 3 3
#> 4 6
#> 5 4
#> 6 3With the default priors the posterior means are very close to the EBLUP (mean difference, limits of agreement and Lin’s concordance correlation below):
compare_sae(fit_tf, fit_hb, names = c("EBLUP two-fold", "HB two-fold"))
#>
#> ── Small Area Estimation Model Concordance & Comparison ────────────────────────
#> Model 1: EBLUP two-fold
#> Model 2: HB two-fold
#> Matched Domains: 131
#> ────────────────────────────────────────────────────────────────────────────────
#>
#> Agreement, Ranking & Efficiency Metrics:
#> Group Metric
#> Domains Matched Domains
#> Agreement Mean Difference (HB two-fold - EBLUP two-fold)
#> Agreement 95% Limits of Agreement
#> Agreement Mean Absolute Difference (MAE)
#> Agreement Root Mean Squared Difference (RMSD)
#> Agreement Lin's Concordance Correlation (CCC)
#> Uncertainty Median |Difference| / RMSE of EBLUP two-fold
#> Uncertainty % of domains with |Difference| > RMSE of EBLUP two-fold
#> Ranking Spearman Rank Correlation (rho)
#> Ranking Mean Absolute Rank Change
#> Ranking Shared Top 13 (highest estimates)
#> Ranking Shared Bottom 13 (lowest estimates)
#> Efficiency Mean MSE Ratio (EBLUP two-fold / HB two-fold)
#> Efficiency Median MSE Ratio (EBLUP two-fold / HB two-fold)
#> Efficiency % of domains where HB two-fold reports lower uncertainty
#> Efficiency Mean RSE % (EBLUP two-fold)
#> Efficiency Mean RSE % (HB two-fold)
#> Value
#> 131
#> -0.0004
#> [-0.0208, 0.0200]
#> 0.0076
#> 0.0104
#> 1.0000
#> 0.008
#> 0.0%
#> 0.9999
#> 0.18
#> 13 / 13
#> 13 / 13
#> 1.0440
#> 1.0528
#> 78.6%
#> 4.97%
#> 4.89%
#> ℹ One model is Hierarchical Bayes: its posterior variance and the frequentist MSE of the other are not the same quantity, so a smaller value does not show a more accurate estimator.compare_sae() reports smaller posterior variances than
EBLUP MSEs. The two are not the same quantity, and here the posterior
mean of \(\sigma_u^2\) is also smaller
than the REML estimate (both are below the true value 0.5, which is hard
to separate from sampling variances of 0.5 to 2). A smaller reported
uncertainty is therefore not evidence of a more accurate estimator. With
simulated data, the error against the truth settles it; here the two are
practically equal:
c(
eblup_two_fold = emp_mse(fit_tf$df_eblup$eblup, sampled | in_sampled_area),
hb_two_fold = emp_mse(fit_hb$df_hb$hb, sampled | in_sampled_area)
)
#> eblup_two_fold hb_two_fold
#> 0.5583827 0.5571915For counts or proportions, use family = "poisson" with
exposure or family = "binomial" with
trials; with spatial = "bym2" and a
neighbourhood graph W between areas (for example from
create_weights()), the area effects are spatially
correlated.
engine = jax_engine()The same model can be sampled by MCMC (NumPyro’s No-U-Turn sampler on
JAX) instead of INLA. It needs Python with jax and
numpyro (see ?setup_numpyro_env), so this
chunk is not run here. The result has the same structure, plus the
posterior draws in draws and the largest split R-hat in
rhat_max; the area aggregates in df_area come
from the MCMC draws.
fit_jax <- hb_twofold(
y ~ x1,
vardir = "vardir",
domain = "area",
subarea = "subarea",
weight = "pop",
data = dat,
engine = jax_engine(warmup = 1000, samples = 1000, chains = 4),
print_result = FALSE
)
fit_jax$rhat_max
head(fit_jax$df_area)Both engines use the same priors by default
(jax_engine(priors = "inla")), so their estimates differ
only by INLA’s approximation and the Monte Carlo error.
A proportion \(\hat p_{ij}\) estimated from a clustered, weighted sample is less precise than one from a simple random sample of the same size \(n_{ij}\). The binomial/logit-normal model with effective sample sizes (Chen, Wakefield and Lumley 2014; Franco and Bell 2015) takes this into account: with the design effect \(\mathrm{deff}_{ij}\), the effective sample size is \(n^*_{ij} = n_{ij} / \mathrm{deff}_{ij}\) and the effective number of cases \(y^*_{ij} = n^*_{ij} \hat p_{ij}\), and
\[y^*_{ij} \mid p_{ij} \sim \text{Binomial}(n^*_{ij}, p_{ij}), \qquad \text{logit}(p_{ij}) = x_{ij}^\top \beta + v_i + u_{ij},\]
so that the binomial variance \(p_{ij}(1 -
p_{ij}) / n^*_{ij}\) matches the design variance of \(\hat p_{ij}\). Neither \(n^*_{ij}\) nor \(y^*_{ij}\) is a whole number.
hb_twofold() (and hb_area()) take the
proportions as the response and the effective sample sizes as
trials, and do not round them: INLA then uses its
"xbinomial" likelihood, which equals the binomial
likelihood on whole numbers. A direct estimate of 0 is a sampling zero
and enters the model as such.
set.seed(7)
prov <- rep(1:12, times = sample(4:8, 12, replace = TRUE))
svy <- data.frame(
prov = prov,
kab = seq_along(prov),
x1 = rnorm(length(prov)),
pop = round(runif(length(prov), 2e4, 2e5))
)
p_true <- plogis(-2.5 + 0.5 * svy$x1 + rnorm(12, sd = 0.4)[prov] +
rnorm(nrow(svy), sd = 0.3))
n <- sample(30:200, nrow(svy), replace = TRUE)
svy$deff <- runif(nrow(svy), 1.3, 2.5) # from the survey, e.g. design-based variance estimates
svy$n_eff <- n / svy$deff
# clustered sample: the direct estimate varies like a binomial proportion on n_eff
svy$p_hat <- rbinom(nrow(svy), round(svy$n_eff), p_true) / round(svy$n_eff)
sum(svy$p_hat == 0) # districts with a direct estimate of 0
#> [1] 7fit_neff <- hb_twofold(
p_hat ~ x1,
domain = "prov",
subarea = "kab",
weight = "pop",
data = svy,
family = "binomial",
trials = "n_eff",
print_result = FALSE
)
#> ℹ Response `y` appears to be proportions; using `y * trials` as the number of cases (not rounded).
head(fit_neff$df_hb[, c("domain", "subarea", "y", "hb", "sd", "ci_lower", "ci_upper")])
#> domain subarea y hb sd ci_lower ci_upper
#> 1 1 1 0.41836735 0.37409714 0.04307312 0.29454171 0.4627799
#> 2 1 2 0.05128205 0.13067668 0.03276752 0.07144100 0.1984512
#> 3 1 3 0.29310345 0.28807520 0.03513040 0.22249246 0.3603284
#> 4 1 4 0.18181818 0.16933173 0.03333476 0.11034737 0.2415796
#> 5 1 5 0.07692308 0.08946033 0.02336449 0.05011325 0.1413181
#> 6 2 6 0.11009174 0.11499289 0.02219531 0.07582830 0.1629797
head(fit_neff$df_area)
#> domain hb_area sd_area mse_area rse_area ci_lower_area
#> 1 1 0.21368585 0.02102955 0.0004422421 9.84134 0.17544730
#> 2 2 0.17879583 0.01825154 0.0003331186 10.20803 0.14510813
#> 3 3 0.09158928 0.01079945 0.0001166281 11.79117 0.07157852
#> 4 4 0.05061304 0.01078522 0.0001163210 21.30917 0.03198272
#> 5 5 0.12943218 0.01795375 0.0003223370 13.87116 0.09692217
#> 6 6 0.10547244 0.02022086 0.0004088831 19.17170 0.07000754
#> ci_upper_area n_subareas
#> 1 0.25581826 5
#> 2 0.21663284 6
#> 3 0.11395483 7
#> 4 0.07390814 5
#> 5 0.16706448 5
#> 6 0.15002950 6
# the districts with a direct estimate of 0
fit_neff$df_hb[svy$p_hat == 0, c("subarea", "hb", "ci_lower", "ci_upper")]
#> subarea hb ci_lower ci_upper
#> 24 24 0.08050287 0.03983269 0.13450008
#> 32 32 0.05449982 0.02616682 0.09442714
#> 37 37 0.07686584 0.03966416 0.12537198
#> 61 61 0.04219197 0.02001805 0.07550120
#> 62 62 0.03536521 0.01692659 0.06207803
#> 63 63 0.02568126 0.01187169 0.04668581
#> 65 65 0.03351536 0.01613033 0.05817551The model is only as good as the design effects: they are not
estimated by the model, so effective sample sizes that are too large
give intervals that are too narrow. Design effects of small areas are
themselves noisy, and are often smoothed (for example, pooled within a
larger area or modeled) before use. The default priors are those of the
other families (vignette("hb-priors")); a \(N(0, 1)\) prior on the coefficients, for
example, is set with
inla_engine(control.fixed = list(mean = 0, prec = 1)).
domain column identifies
the area and is required.vardir must be finite and positive for every sampled
sub-area; unsampled sub-areas can have NA. Covariates must
be complete for all rows, sampled or not.eblup_twofold() has self_benchmark = TRUE
for estimates that add up to a known total (see
?eblup_twofold).