Choosing Covariates for a Fay–Herriot Model in Python with svy-sae
Stepwise selection by AIC, BIC, or KIC
Tutorials
Area-level
Model Selection
Python
Choose the covariates of a Fay-Herriot small area model in Python with svy-sae: stepwise selection by AIC, BIC, or KIC, search direction, collinearity, and categorical candidates.
Author
Mamadou S. Diallo, Ph.D.
Modified
November 5, 2026
Keywords
covariate selection Fay-Herriot, stepwise selection small area estimation, AIC BIC KIC Python, variance inflation factor, area-level model selection Python
WarningAbout these examples
This tutorial illustrates the svy-sae API; it is not a rigorous small area analysis. The models are kept simple so each step stays readable, and in many places, from the choice of covariates to the variance model, better choices could be made to improve them.
A census offers many candidate covariates, and with a few dozen areas the model can afford only a few of them. Too many covariates fit the noise in the direct estimates, and census shares that measure the same thing, such as household assets, are close to collinear: their coefficients become unstable and the estimates with them.
Information criteria trade fit against the number of parameters. Stepwise selection searches the candidates one term at a time, dropping or adding whichever term lowers the criterion most, and stops when no move does.
This tutorial demonstrates how to choose the covariates of the Fay–Herriot model with AreaLevel.stepwise(), compare criteria and search directions, and check the choice against the true district poverty rates.
NoteNot a search for the best model
This tutorial does not try to find the best model for these data. It shows the options svy-sae offers to evaluate a model and the ways to improve it; the choices made below are examples, not recommendations.
Setting up the inputs
We continue with the district poverty rates, smoothed variances, and census covariates from the previous tutorials:
A VIF of 10 is a common warning level; most of these are in the hundreds. Districts with more refrigerators also have more TVs and bank accounts, so the model cannot tell their effects apart. The fit flags it as SAE_COLLINEARITY (see Checking the model).
Stepwise selection
stepwise() takes the same inputs as fh(), with the candidates as x:
Selected ['piped_water'] by BIC = -71.1715 in 10 steps, 59 areas
It returns a Selection. Its path records each move and the criterion after it:
selection.to_polars()
shape: (11, 3)
step
change
criterion
i64
str
f64
0
""
-38.012504
1
"-flush_toilet"
-42.051917
2
"-bank"
-46.045849
3
"-refrigerator"
-50.054227
4
"-tv"
-54.072561
…
…
…
6
"-cell"
-61.683902
7
"-car"
-65.146713
8
"-hhsize"
-67.983168
9
"-electricity"
-70.599501
10
"-urban"
-71.171537
With the default direction="both", the search starts from all the candidates; each step drops one term or adds back one already dropped. Here it drops ten in turn and keeps piped water, the covariate used in The Fay–Herriot model.
Candidates are compared by maximum likelihood fits without an MSE, so the search is fast. The criteria therefore differ from those that fh() reports for its REML fit.
Table 1: Options of stepwise()
Argument
Values
Description
criterion
"aic"(default), "bic", "kic"
Penalty per parameter: \(2\), \(\log m\) for \(m\) areas, or \(3\)
direction
"both"(default), "backward", "forward"
Moves allowed: drop and add, drop only, or add only
start
list of terms
Terms of the first model; by default all of x, or none with direction="forward"
vif_max
number
Reject an addition that would put any VIF above it
Comparing criteria
The criteria differ in how much a parameter costs. With 59 districts, BIC charges \(\log 59 \approx 4.1\) per parameter, more than AIC (2) or KIC (3):
pl.DataFrame( {"criterion": c,"terms": ", ".join(model.stepwise( y="est", x=candidates, area="geo2", variance="variance_smoothed", pop_data=district_x, criterion=c, ).terms), }for c in ["aic", "bic", "kic"])
shape: (3, 2)
criterion
terms
str
str
"aic"
"urban, piped_water"
"bic"
"piped_water"
"kic"
"urban, piped_water"
AIC and KIC also keep the share of urban households.
Search direction
A stepwise search is greedy: each step takes the best single move, so different starting points can end in different models. Search forward from no covariates instead:
Selected ['cell'] by BIC = -71.6490 in 1 steps, 59 areas
The forward search adds mobile phone ownership and stops. Its BIC is slightly lower than piped water’s. The first search never reached it: once down to piped water alone, adding mobile phones does not lower the BIC, and no single move swaps one term for another.
A difference of less than about 2 in an information criterion is commonly read as no real difference. Both covariates measure how well-off a district is; run both directions and choose between near-ties on substance, such as which covariate is measured more reliably in every district. We keep piped water.
Tipvif_max
vif_max=10 stops the search from adding a term that would push any VIF above 10. It only constrains additions: a backward search from a collinear start still reaches the collinear models on its way down.
Categorical candidates
A candidate can be a categorical variable, which enters and leaves the model as one term with all its levels. Add the region as a candidate: