<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>official statistics | P.A. Parker</title><link>https://www.paparkerstat.com/tag/official-statistics/</link><atom:link href="https://www.paparkerstat.com/tag/official-statistics/index.xml" rel="self" type="application/rss+xml"/><description>official statistics</description><generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>en-us</language><lastBuildDate>Mon, 27 Jul 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.paparkerstat.com/images/icon_hu0b7a4cb9992c9ac0e91bd28ffd38dd00_9727_512x512_fill_lanczos_center_2.png</url><title>official statistics</title><link>https://www.paparkerstat.com/tag/official-statistics/</link></image><item><title>Nonlinear Small Area Estimation in R with nlfh</title><link>https://www.paparkerstat.com/post/nlfh/</link><pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.paparkerstat.com/post/nlfh/</guid><description>
&lt;p>Small area estimation is often described as a solution to a sample size problem. We want reliable estimates for many small geographic areas, but the survey sample size within any given area may be limited. A direct estimate uses only the observations available for that area. That makes it easy to compute, but it can also make it noisy.&lt;/p>
&lt;p>The Fay–Herriot model improves these estimates by combining the direct estimate with information from related covariates and other areas. The basic model does this with a linear regression. That is a useful starting point, but there is no particular reason to expect relationships involving variables like income, poverty, program participation, and population composition to be perfectly linear.&lt;/p>
&lt;p>This was the motivation for &lt;a href="https://CRAN.R-project.org/package=nlfh">&lt;code>nlfh&lt;/code>&lt;/a>, an R package I developed for fitting Bayesian linear and nonlinear Fay–Herriot models. The package currently includes the standard linear model, the random-weight neural network model from &lt;a href="https://doi.org/10.1177/0282423X241244671">Parker (2024)&lt;/a>, and the BART-FH model from &lt;a href="https://doi.org/10.1093/jssam/smaf050">Parker and Eideh (2026)&lt;/a>. In this post, I will use the package to estimate median household income for census tracts in Missouri.&lt;/p>
&lt;div id="installing-nlfh" class="section level2">
&lt;h2>Installing nlfh&lt;/h2>
&lt;p>&lt;code>nlfh&lt;/code> is available on CRAN and can be installed in the usual way.&lt;/p>
&lt;pre class="r">&lt;code>install.packages(&amp;quot;nlfh&amp;quot;)&lt;/code>&lt;/pre>
&lt;p>We will also use a few packages for data manipulation and mapping.&lt;/p>
&lt;pre class="r">&lt;code>library(nlfh)
library(dplyr)
library(ggplot2)
library(sf)
library(tigris)
library(viridis)&lt;/code>&lt;/pre>
&lt;/div>
&lt;div id="the-example-data" class="section level2">
&lt;h2>The example data&lt;/h2>
&lt;p>The package includes &lt;code>acs_dat&lt;/code>, an example data set with 1,617 Missouri census tracts. &lt;code>MedInc&lt;/code> is the direct estimate of median household income and &lt;code>MedIncSE&lt;/code> is its standard error. The remaining columns contain tract-level covariates.&lt;/p>
&lt;pre class="r">&lt;code>data(acs_dat)
tract_geoid &amp;lt;- rownames(acs_dat)
acs_mo &amp;lt;- as.data.frame(acs_dat)
acs_mo$GEOID &amp;lt;- tract_geoid
head(acs_mo)&lt;/code>&lt;/pre>
&lt;pre>&lt;code>## MedInc MedIncSE SNAPRate PovRate White Black Hispanic
## 1 75448 17981.763 0.4723866 0.1992110 0.5925325 0.300324675 0.03246753
## 2 33434 1093.009 0.3852814 0.3160173 0.2774533 0.538551402 0.09988318
## 3 57969 5765.957 0.6087978 0.1496904 0.4821522 0.360104987 0.05564304
## 4 60743 3502.128 0.3817734 0.1190476 0.9178744 0.003360639 0.01554295
## 5 58421 4869.909 0.3766943 0.1019624 0.8944666 0.014261266 0.02129682
## 6 43547 4725.228 0.4859327 0.2281346 0.7823583 0.068202486 0.05365262
## Asian GEOID
## 1 0.019480519 29510123200
## 2 0.008761682 29510124600
## 3 0.046456693 29510125500
## 4 0.004410838 29101960900
## 5 0.002662103 29101960200
## 6 0.015459230 29101960600&lt;/code>&lt;/pre>
&lt;p>For a Fay–Herriot model, we supply the direct estimate and its known sampling variance. Because the data provide a standard error, the sampling variance is &lt;code>MedIncSE^2&lt;/code>.&lt;/p>
&lt;p>It is helpful to first look at the direct estimates on a map. The row names in &lt;code>acs_dat&lt;/code> are census tract GEOIDs, so they can be joined directly to Census Bureau tract boundaries downloaded with &lt;code>tigris&lt;/code>.&lt;/p>
&lt;pre class="r">&lt;code>options(tigris_use_cache = TRUE)
mo_tracts &amp;lt;- tracts(
state = &amp;quot;MO&amp;quot;,
year = 2020,
cb = TRUE,
class = &amp;quot;sf&amp;quot;
)
map_data &amp;lt;- mo_tracts |&amp;gt;
left_join(acs_mo, by = &amp;quot;GEOID&amp;quot;) |&amp;gt;
filter(!is.na(MedInc))&lt;/code>&lt;/pre>
&lt;pre class="r">&lt;code>ggplot(map_data) +
geom_sf(aes(fill = MedInc), color = NA) +
scale_fill_viridis_c(
option = &amp;quot;mako&amp;quot;,
labels = scales::label_dollar(scale = 0.001, suffix = &amp;quot;k&amp;quot;),
name = &amp;quot;Direct\nestimate&amp;quot;
) +
labs(
title = &amp;quot;Direct estimates of median household income&amp;quot;,
subtitle = &amp;quot;Missouri census tracts&amp;quot;
) +
theme_void(base_size = 13) +
theme(
plot.title = element_text(face = &amp;quot;bold&amp;quot;),
legend.position = &amp;quot;right&amp;quot;
)&lt;/code>&lt;/pre>
&lt;p>&lt;img src="https://www.paparkerstat.com/post/2026-07-27-nlfh_files/figure-html/direct-map-1.png" width="100%" />&lt;/p>
&lt;p>There is real geographic structure in the direct estimates, but the estimates are not equally precise. Some of the sharp differences between neighboring tracts may reflect signal, while others may reflect sampling variability. The purpose of the model is not to erase this variation. It is to account for the uncertainty when estimating the underlying tract-level values.&lt;/p>
&lt;/div>
&lt;div id="fitting-a-nonlinear-fayherriot-model" class="section level2">
&lt;h2>Fitting a nonlinear Fay–Herriot model&lt;/h2>
&lt;p>The main user-facing function in the package is &lt;code>fit_fh()&lt;/code>. Its formula interface should look familiar to R users. For nonlinear models, the formula identifies which predictors are available to the model, but it does not restrict their effects to be linear or additive.&lt;/p>
&lt;p>Here I use Bayesian additive regression trees (BART) for the mean function.&lt;/p>
&lt;pre class="r">&lt;code>set.seed(2026)
fit_bart &amp;lt;- fit_fh(
MedInc ~ SNAPRate + PovRate + White + Black + Hispanic + Asian,
sampling_variance = MedIncSE^2,
data = acs_mo,
method = &amp;quot;bart&amp;quot;,
control = list(
n_iter = 1000,
burn_in = 500,
progress = FALSE
)
)
summary(fit_bart)&lt;/code>&lt;/pre>
&lt;pre>&lt;code>## &amp;lt;summary.nlfh_fit&amp;gt;
## Model type: BART Fay-Herriot
## Formula: MedInc ~ SNAPRate + PovRate + White + Black + Hispanic + Asian
## DIC: 36957
##
## MCMC:
## n_iter burn_in posterior_draws areas
## 1000 500 500 1617
##
## Details:
## retained_draws burn_in_fraction progress scale n_trees n_bart_samples
## 500 0.5 FALSE FALSE 50 10
##
## Variance parameters:
## parameter mean sd median q2.5 q97.5
## random_effect_variance 0.3345 0.1418 0.3158 0.1269 0.6531
##
## Area-level estimates theta_i:
## area mean sd median q2.5 q97.5
## 1 47520 3161 47730 41590 53530
## 2 34750 1406 34880 31640 37120
## 3 44190 2906 43840 39480 50350
## 4 54080 1401 53690 52180 57450
## 5 61940 1602 61830 59260 65130
## 6 31260 3048 30730 26190 37860
## ... 1611 more rows
##
## Variable importance:
## SNAPRate PovRate White Black Hispanic Asian
## 0.17216191 0.24776581 0.17140518 0.16129210 0.09538537 0.15198963&lt;/code>&lt;/pre>
&lt;p>The model estimates an unspecified nonlinear function of the six predictors and adds a tract-level random effect. At the same time, it retains the Fay–Herriot sampling model, so direct estimates with large standard errors are allowed to borrow more information than highly precise direct estimates.&lt;/p>
&lt;p>The fitted model contains full posterior draws, not just point estimates. For a quick summary, &lt;code>fitted()&lt;/code> returns the posterior mean for each tract. Setting &lt;code>full = TRUE&lt;/code> also returns posterior standard deviations and credible intervals.&lt;/p>
&lt;pre class="r">&lt;code>tract_estimates &amp;lt;- fitted(fit_bart, full = TRUE)
map_data &amp;lt;- map_data |&amp;gt;
mutate(
model_estimate = tract_estimates$mean[
match(GEOID, acs_mo$GEOID)
],
model_sd = tract_estimates$sd[
match(GEOID, acs_mo$GEOID)
]
)
head(tract_estimates)&lt;/code>&lt;/pre>
&lt;pre>&lt;code>## area mean sd median q2.5 q97.5
## 1 1 47516.53 3160.589 47733.40 41591.80 53530.76
## 2 2 34754.31 1406.342 34881.56 31637.93 37119.98
## 3 3 44193.09 2905.727 43842.86 39478.27 50346.72
## 4 4 54084.83 1400.874 53689.90 52184.31 57447.70
## 5 5 61942.29 1602.468 61833.78 59259.85 65131.50
## 6 6 31256.79 3047.542 30725.50 26186.71 37859.11&lt;/code>&lt;/pre>
&lt;/div>
&lt;div id="what-changed" class="section level2">
&lt;h2>What changed?&lt;/h2>
&lt;p>The most useful comparison is between the noisy direct estimates and the posterior estimates of the underlying tract means.&lt;/p>
&lt;pre class="r">&lt;code>comparison_map &amp;lt;- map_data |&amp;gt;
transmute(
GEOID,
Estimate = &amp;quot;Direct&amp;quot;,
Income = MedInc
)
comparison_map &amp;lt;- bind_rows(
comparison_map,
map_data |&amp;gt;
transmute(
GEOID,
Estimate = &amp;quot;BART-FH&amp;quot;,
Income = model_estimate
)
)
comparison_map$Estimate &amp;lt;- factor(
comparison_map$Estimate,
levels = c(&amp;quot;Direct&amp;quot;, &amp;quot;BART-FH&amp;quot;)
)
ggplot(comparison_map) +
geom_sf(aes(fill = Income), color = NA) +
facet_wrap(~Estimate, ncol = 2) +
scale_fill_viridis_c(
option = &amp;quot;mako&amp;quot;,
limits = range(comparison_map$Income, na.rm = TRUE),
labels = scales::label_dollar(scale = 0.001, suffix = &amp;quot;k&amp;quot;),
name = &amp;quot;Median\nincome&amp;quot;
) +
labs(
title = &amp;quot;From direct estimates to small area estimates&amp;quot;,
subtitle = &amp;quot;The same color scale is used in both maps&amp;quot;
) +
theme_void(base_size = 13) +
theme(
plot.title = element_text(face = &amp;quot;bold&amp;quot;),
strip.text = element_text(face = &amp;quot;bold&amp;quot;, size = 12),
legend.position = &amp;quot;right&amp;quot;
)&lt;/code>&lt;/pre>
&lt;p>&lt;img src="https://www.paparkerstat.com/post/2026-07-27-nlfh_files/figure-html/comparison-map-1.png" width="100%" />&lt;/p>
&lt;p>The broad spatial pattern remains, but some of the most extreme local values move toward estimates supported by the covariates and the rest of the data. This is the usual small area estimation tradeoff: we accept a model in order to reduce the instability of direct estimates.&lt;/p>
&lt;p>The amount of movement is not constant. Tracts with larger direct-estimate standard errors generally have more to gain from the model.&lt;/p>
&lt;pre class="r">&lt;code>ggplot(
map_data,
aes(x = MedIncSE, y = abs(model_estimate - MedInc))
) +
geom_point(
aes(color = model_sd),
alpha = 0.55,
size = 1.8
) +
geom_smooth(method = &amp;quot;loess&amp;quot;, color = &amp;quot;white&amp;quot;, linewidth = 2.4, se = FALSE) +
geom_smooth(method = &amp;quot;loess&amp;quot;, color = &amp;quot;#2c7fb8&amp;quot;, linewidth = 1.1, se = FALSE) +
scale_color_viridis_c(
option = &amp;quot;plasma&amp;quot;,
direction = -1,
name = &amp;quot;Posterior SD&amp;quot;,
labels = scales::label_dollar()
) +
scale_x_continuous(labels = scales::label_dollar()) +
scale_y_continuous(labels = scales::label_dollar()) +
labs(
title = &amp;quot;Less precise direct estimates receive more adjustment&amp;quot;,
x = &amp;quot;Standard error of the direct estimate&amp;quot;,
y = &amp;quot;Absolute change from the direct estimate&amp;quot;
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = &amp;quot;bold&amp;quot;),
panel.grid.minor = element_blank()
)&lt;/code>&lt;/pre>
&lt;p>&lt;img src="https://www.paparkerstat.com/post/2026-07-27-nlfh_files/figure-html/change-plot-1.png" width="100%" />&lt;/p>
&lt;p>For the BART model, &lt;code>variable_importance&lt;/code> gives the proportion of splitting activity attributed to each predictor. This is a useful model diagnostic, although it should not be interpreted as a causal effect.&lt;/p>
&lt;pre class="r">&lt;code>importance &amp;lt;- data.frame(
variable = names(fit_bart$variable_importance),
importance = as.numeric(fit_bart$variable_importance)
) |&amp;gt;
arrange(importance) |&amp;gt;
mutate(variable = factor(variable, levels = variable))
ggplot(importance, aes(x = importance, y = variable)) +
geom_col(fill = &amp;quot;#2c7fb8&amp;quot;, width = 0.7) +
scale_x_continuous(labels = scales::label_percent()) +
labs(
title = &amp;quot;Which variables does BART use?&amp;quot;,
x = &amp;quot;Share of splitting activity&amp;quot;,
y = NULL
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = &amp;quot;bold&amp;quot;),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank()
)&lt;/code>&lt;/pre>
&lt;p>&lt;img src="https://www.paparkerstat.com/post/2026-07-27-nlfh_files/figure-html/importance-1.png" width="100%" />&lt;/p>
&lt;/div>
&lt;div id="one-interface-three-models" class="section level2">
&lt;h2>One interface, three models&lt;/h2>
&lt;p>BART is only one option. Changing the &lt;code>method&lt;/code> argument fits the standard linear Fay–Herriot model or the random-weight neural network model using the same general interface.&lt;/p>
&lt;pre class="r">&lt;code>fit_linear &amp;lt;- fit_fh(
MedInc ~ SNAPRate + PovRate + White + Black + Hispanic + Asian,
sampling_variance = MedIncSE^2,
data = acs_mo,
method = &amp;quot;linear&amp;quot;
)
fit_rnn &amp;lt;- fit_fh(
MedInc ~ SNAPRate + PovRate + White + Black + Hispanic + Asian,
sampling_variance = MedIncSE^2,
data = acs_mo,
method = &amp;quot;rnn&amp;quot;
)&lt;/code>&lt;/pre>
&lt;p>All three methods return objects with a common set of tools. &lt;code>summary()&lt;/code> reports posterior summaries, &lt;code>fitted()&lt;/code> extracts area-level estimates, and &lt;code>posterior_draws()&lt;/code> makes the retained simulations available for custom summaries or uncertainty propagation. The model objects also contain DIC for comparisons among models fit to the same data. As always, I would use model comparison together with diagnostics and substantive knowledge, rather than treating a single criterion as the final answer.&lt;/p>
&lt;/div>
&lt;div id="why-use-nlfh" class="section level2">
&lt;h2>Why use nlfh?&lt;/h2>
&lt;p>The goal of &lt;code>nlfh&lt;/code> is to make nonlinear small area estimation feel like a normal R workflow. A user can start with a formula, a vector of sampling variances, and one call to &lt;code>fit_fh()&lt;/code>. The package handles the model fitting while still returning the posterior quantities needed for a serious analysis.&lt;/p>
&lt;p>More importantly, it provides an alternative to two common estimation choices. We do not have to rely on unstable direct estimates, and we do not have to assume that the relationship between an outcome and its predictors is linear. BART and the random-weight neural network allow the data to support nonlinearities and interactions while retaining the familiar area-level sampling model.&lt;/p>
&lt;p>You can find the package on &lt;a href="https://CRAN.R-project.org/package=nlfh">CRAN&lt;/a> and the source code on &lt;a href="https://github.com/paparker/nlfh">GitHub&lt;/a>.&lt;/p>
&lt;/div></description></item></channel></rss>