Smoothing Sampling Variances in Python with svy-sae
Generalized variance functions for Fay–Herriot inputs
Tutorials
Area-level
Variance Smoothing
Python
Smooth direct sampling variances with a generalized variance function in Python before fitting a Fay-Herriot model: Gamma and log-linear fits, precision weights, leverage, and leave-one-out checks with svy-sae.
Author
Mamadou S. Diallo, Ph.D.
Modified
November 5, 2026
Keywords
generalized variance function Python, variance smoothing small area estimation, sampling variance Fay-Herriot, GVF survey Python, smooth_variance svy-sae
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.
The Fay–Herriot model treats the sampling variance \(D_i\) of each direct estimate as known(Rao and Molina 2015). In practice it is itself an estimate, and a poor one where the sample is small: a district sampled in a few EAs has a variance estimate with few degrees of freedom, and a district sampled in one EA, or with no poor household in the sample, gets a variance of zero.
Those errors carry straight into the model. Each estimate is a weighted average of the direct estimate and the regression prediction, with weight \(\gamma_i = \sigma^2_u / (\sigma^2_u + D_i)\) on the direct estimate. A variance that is too small pulls the estimate toward the direct one; a variance of zero keeps it there, with an MSE of zero.
A generalized variance function (GVF) replaces the direct variances with values predicted from a model of how the variance depends on domain characteristics, such as the sample size. Every district then gets a usable variance, including those whose direct variance cannot be used.
This tutorial demonstrates how to smooth the district variances with smooth_variance(), check the variance model, and see what smoothing changes in a Fay–Herriot fit.
Setting up the inputs
We continue with the district poverty rates and census covariates built in Preparing inputs:
The variance of an estimated proportion is roughly \(\text{deff} \cdot p(1-p)/n\), so on the log scale it falls linearly with \(\log n\). Add the log of the number of sampled households:
direct = direct.with_columns(pl.col("n").log().alias("log_n"))
Fit the variance model and add the smoothed variances:
smooth_variance() takes any frame with one row per domain and returns it with one more column, variance_smoothed (set by name=). Pass se= instead of variance= to smooth the square of a standard error column.
The model is fitted on the districts whose direct variance carries information: a positive variance with positive df. The others are left out of the fit and predicted, which is what the four districts from Preparing inputs need:
Their smoothed variances are those of any district with the same sample size.
Why df matters
df= weights each district by its design degrees of freedom, the precision of its variance estimate, and leaves out the districts with df of zero. Without it, the two single-EA districts enter the fit with variances of about \(10^{-33}\), which are rounding error, not zero:
On the log scale they sit about 70 units below the other districts and drag the fit with them.
WarningAlways pass df
svy’s estimation output has a df column. Pass it as df= so that districts whose variance is not estimable are predicted rather than fitted.
Choosing a method
The method argument sets how the model is fitted:
Table 1: Variance smoothing methods
method
Fit
Smoothed variance
"gamma"(default)
Gamma GLM with log link, weighted by df
\(\exp(z'\hat\alpha)\), the mean of \(\hat v\)
"loglinear"
Least squares on \(\log \hat v\), weighted by df
\(\exp(z'\hat\alpha)\), the median of \(\hat v\)
"loglinear_ratio"
As "loglinear"
Scaled so the smoothed variances sum to the direct ones
The Gamma model fits the mean of the variance directly, which is what Fay–Herriot needs. The log-linear fit back-transforms to the median, which falls below the mean of a skewed quantity such as a variance estimate. Compare the totals over the districts used in the fit:
The district with the most leverage is the one with the largest sample, alone at that end of \(\log n\). Its smoothed variance barely changes without it, so the fit does not depend on it. The leave-one-out values also compare variance models out of sample: a model that predicts each district’s direct variance well without that district is not just fitting noise. The districts left out of the fit have no leverage, and their leave-one-out value is their smoothed variance, since they never entered the fit.
What smoothing changes
Fit the same Fay–Herriot model, with one covariate, the share of households with piped water (chosen in Choosing covariates), once with the direct variances and once with the smoothed ones, and compare both with the true rates:
def fh_rmse(variance): fit = sae.AreaLevel(smoothed).fh( y="est", x=["piped_water"], area="geo2", variance=variance, pop_data=district_x, ) pred = fit.to_polars("predictions").join(truth, left_on="area", right_on="geo2")return pred, pred.select(((pl.col("pred") - pl.col("true_rate")) **2).mean().sqrt()).item()pred_direct, rmse_direct = fh_rmse("var_direct")pred_smoothed, rmse_smoothed = fh_rmse("variance_smoothed")print(f"RMSE against the truth, direct variances: {rmse_direct:.3f}")print(f"RMSE against the truth, smoothed variances: {rmse_smoothed:.3f}")
RMSE against the truth, direct variances: 0.141
RMSE against the truth, smoothed variances: 0.033
With the direct variances, the four districts keep their direct estimates, with an MSE of zero:
problem = ["geo_02_01", "geo_09_01", "geo_10_01", "geo_10_07"]pred_direct.filter(pl.col("area").is_in(problem)).select("area", "pred", "mse", "true_rate")
shape: (4, 4)
area
pred
mse
true_rate
str
f64
f64
f64
"geo_02_01"
0.12
1.2037e-35
0.572237
"geo_09_01"
0.0
0.0
0.412918
"geo_10_01"
0.0
0.0
0.423086
"geo_10_07"
0.44
8.7750e-33
0.238306
With the smoothed variances, the model moves them toward the regression prediction and reports a real MSE: