Agricultural surveys often combine an area frame (a grid of square cells
classified by intensity of agricultural use from satellite imagery, with
sample points inside the selected cells identifying the fields and so the
establishments) and a list frame (large and specialised establishments,
cheap to reach). This vignette walks through such a design with fieldopt:
stratified selection of cells, points inside them, the multiplicity
estimator, the cost of the field work, and the dual-frame and two-stage
allocations that put the pieces together. The data are synthetic; the
structure is the usual one of area frames built on a regular grid (square
segments of about 1 km²) with a list of large producers alongside.
library(fieldopt) library(dplyr, warn.conflicts = FALSE) library(tidyr) set.seed(2026) side <- 30 cells <- expand_grid(x = seq_len(side), y = seq_len(side)) |> mutate( unit = paste0("c", row_number()), use = 0.5 * x / side + 0.3 * sin(y / 4) + rnorm(n(), sd = 0.15), intensity = cut(use, c(-Inf, 0.25, 0.5, Inf), c("low", "mid", "high")), size = c(low = 1, mid = 2, high = 4)[as.character(intensity)] ) |> select(-use) cells |> count(intensity) #> # A tibble: 3 × 2 #> intensity n #> <fct> <int> #> 1 low 380 #> 2 mid 284 #> 3 high 236
Each cell holds a few establishments; the agricultural area of an establishment inside a cell is what the point sampling needs. Here every cell has two establishments with 40 % and 20 % of its area, and a crop value per establishment that follows the intensity.
areas <- bind_rows( cells |> transmute(unit, establishment = paste0(unit, "-1"), area = 0.4), cells |> transmute(unit, establishment = paste0(unit, "-2"), area = 0.2) ) |> left_join(cells |> select(unit, size), by = "unit") |> mutate(y = round(rgamma(n(), shape = 4, rate = 4 / (20 * area * size)), 1)) |> select(-size) truth <- sum(areas$y) truth #> [1] 22447.9
Cells are selected within intensity strata, more where use is intense, with
the local pivotal method so that the sample spreads over the territory; then
3, 6 or 9 points per cell depending on the stratum, laid out as a regular
grid with one random offset per cell (layout = "systematic"), which keeps
each point uniform over the cell while spreading them.
n_h <- c(low = 20, mid = 35, high = 45) cells_s <- select_units(cells, n = n_h, strata = "intensity", seed = 7) cells_s #> Sample of 100 units from 900 by "lpm", 3 strata. #> # A tibble: 900 × 7 #> x y unit intensity size pi sampled #> * <int> <int> <chr> <fct> <dbl> <dbl> <lgl> #> 1 1 1 c1 low 1 0.0526 FALSE #> 2 1 2 c2 low 1 0.0526 FALSE #> 3 1 3 c3 low 1 0.0526 FALSE #> 4 1 4 c4 mid 2 0.123 FALSE #> 5 1 5 c5 low 1 0.0526 FALSE #> 6 1 6 c6 low 1 0.0526 FALSE #> 7 1 7 c7 low 1 0.0526 FALSE #> 8 1 8 c8 low 1 0.0526 FALSE #> 9 1 9 c9 mid 2 0.123 TRUE #> 10 1 10 c10 low 1 0.0526 FALSE #> # ℹ 890 more rows ppc <- c(low = 3, mid = 6, high = 9) points <- cells_s |> select_points(points_per_cell = ppc, cell_size = 1, layout = "systematic", seed = 7) nrow(points) #> [1] 675
In the field each point is visited and the establishment it falls on is interviewed about its whole operation. Here the field is simulated: a point hits establishment 1 with probability 0.4, establishment 2 with probability 0.2, and otherwise falls on non-agricultural land.
set.seed(8) hits <- points |> mutate(u = runif(n()), which = case_when(u < 0.4 ~ "-1", u < 0.6 ~ "-2")) |> filter(!is.na(which)) |> transmute(unit, establishment = paste0(unit, which)) |> left_join(areas |> select(establishment, y), by = "establishment") c(hits = nrow(hits), establishments = n_distinct(hits$establishment)) #> hits establishments #> 401 164
An establishment can be hit several times; the estimator divides the value of every hit by the expected number of hits of that establishment under the design, which depends on the cells it spans, their inclusion probabilities and their point densities.
eh <- expected_hits(areas, cells_s, points_per_cell = ppc, cell_size = 1) hits <- hits |> left_join(eh |> select(establishment, expected_hits), by = "establishment") point_estimator(hits, cells_s) #> # A tibble: 1 × 11 #> total variance se cv variance_srs n N variance_method df #> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <int> <chr> <int> #> 1 22034. 1178527. 1086. 0.0493 3243984. 100 900 stratified local-… 97 #> # ℹ 2 more variables: n_hits <int>, n_establishments <int> truth #> [1] 22447.9
The variance uses the cell design (local-mean estimator on the cell contributions). The same cells selected as a spatially ordered systematic sample with four interpenetrating replicates give a replicate-based variance instead:
cells <- cells |> left_join(areas |> group_by(unit) |> summarise(crop = sum(y)), by = "unit") cells_r <- cells |> select_units( n = n_h, strata = "intensity", method = "systematic", replicates = 4, seed = 7 ) design_variance(cells_r, "crop") #> # A tibble: 1 × 9 #> total variance se cv variance_srs n N variance_method df #> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <int> <chr> <int> #> 1 22562. 517031. 719. 0.0319 2488872. 92 900 replicates 3
The cells are visited from a depot; the routing gives the travel, the cost model the money. The cost per visited cell is the input the allocations below need, and it depends on how many cells are visited and how spread they are, which is why it is computed rather than assumed.
frame <- bind_rows( tibble(unit = "depot", x = side / 2, y = side / 2), cells |> select(unit, x, y) ) model <- field_cost_model( per_travel = 2.5, per_unit = 60, per_interview = 25, interviews_per_unit = 4, currency = "BRL" ) routed_unit_cost( frame, "depot", model, n = sum(n_h), method = "euclidean", n_rep = 3, iterations = 60 ) #> # A tibble: 1 × 6 #> n cost_per_unit travel_per_unit cost_mean routes_mean n_rep #> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> #> 1 100 167. 2.67 16667. 1 3
The dual-frame allocation below needs the design effect of the area sample
relative to simple random sampling of establishments. point_design_effect()
simulates the whole area design (cells, points, hits, multiplicity
estimator) and returns the bias, the empirical variance, the mean of the
variance estimates, the number of interviews and the design effect.
deff <- point_design_effect( cells, areas, n = n_h, strata = "intensity", points_per_cell = ppc, cell_size = 1, n_sim = 60, seed = 9 ) deff #> # A tibble: 1 × 10 #> n_sim total mean_estimate bias variance cv mean_variance_estimate #> * <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> #> 1 60 22448. 22355. -93.1 1554792. 0.0555 1349171. #> # ℹ 3 more variables: interviews <dbl>, variance_srs <dbl>, deff <dbl>
The bias is negligible and the variance estimator of point_estimator()
tracks the empirical variance; the design effect is what enters deff_a.
With the between-cell and within-cell variance components (from a pilot or
from the frame) and the costs per cell and per point, Cochran's rule gives
the number of points per cell and then the number of cells for a target
precision. The cost per cell is not a constant: a route shares its travel
among the cells it visits, so the routed cost of n cells grows like
c0 + a n + b sqrt(n). two_stage_design() routes samples at a few sizes,
fits that curve and allocates with the marginal cost of a cell, which is
what Cochran's rule needs when the cost is not linear (the average cost per
cell would give too few cells), iterating until the number of cells settles.
This is the travel-cost model of Hansen, Hurwitz and Madow (1953), whose term
C0 sqrt(m) they derived from points on a regular grid; here b plays the
role of C0 and is measured by routing the frame itself. The history shows
the marginal cost used in each round next to the average, and
routed_cost_curve() gives the curve on its own.
ts <- two_stage_design( frame, "depot", model, m_secondary = 9, s2_between = var(cells$crop / 9), s2_within = var(areas$y) * 2, target_cv = 0.05, mean = mean(cells$crop) / 9, method = "euclidean", n_rep = 3, iterations = 60 ) ts #> #> ── Two-stage allocation ──────────────────────────────────────────────────────── #> n = 158 primary units with m = 9 secondary units each (optimal m 9): cost #> 45860, variance of the total 1256000 (SE 1121, CV 4.99%). ts$history #> # A tibble: 2 × 7 #> round c1 c1_average n m cost variance_total #> <int> <dbl> <dbl> <int> <int> <dbl> <dbl> #> 1 1 90.1 65.3 158 9 45863. 1255934. #> 2 2 62.4 65.3 158 9 45863. 1255934. ts$cost_curve #> # A tibble: 5 × 3 #> n cost_mean cost_sd #> <int> <dbl> <dbl> #> 1 95 6360. 4.19 #> 2 126 8310. 12.0 #> 3 158 10312. 11.8 #> 4 198 12805. 11.7 #> 5 253 16209. 12.0
The list frame holds the large establishments, which are also on the area
frame (the overlap ab); small establishments are on the area frame only
(a); a few specialised units are on the list only (b). Given the sizes,
means and standard deviations of the three domains, the cost per list unit
and the design effect from the simulation, dual_frame_design() runs
Hartley's allocation with the marginal routed cost of an area unit, in the
same way, iterating until the area sample size settles; theta = "screening" gives the design in
which the area sample does not use the overlap.
domains <- tibble( domain = c("a", "ab", "b"), size = c(1500, 250, 50), mean = c(6, 30, 45), sd = c(5, 20, 35) ) df <- dual_frame_design( frame, "depot", model, domains, cost_b = 45, deff_a = deff$deff, target_cv = 0.05, method = "euclidean", n_rep = 3, iterations = 60 ) df #> #> ── Dual-frame allocation ─────────────────────────────────────────────────────── #> Minimum cost for target variance 878900: n_A = 75 (frame A, cost 163.2/unit), #> n_B = 129 (frame B, cost 45/unit), theta = 0.163 (optimised). #> Cost 18390; variance 874000 (frame A 679000, frame B 195000); CV 4.99% of the #> total 18750. #> Expected overlap units: 10.7 in the A sample, 107.5 in the B sample. df$history #> # A tibble: 3 × 8 #> round cost_a cost_a_average n_a n_b theta cost variance #> <int> <dbl> <dbl> <int> <int> <dbl> <dbl> <dbl> #> 1 1 190. 168. 73 136 0.161 18375. 875540. #> 2 2 163. 168. 75 129 0.163 18386. 874037. #> 3 3 163. 168. 75 129 0.163 18386. 873980. tidy(df) #> # A tibble: 2 × 8 #> frame n cost_per_unit cost deff variance overlap_units #> <chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl> #> 1 A 75 163. 12239. 0.753 679399. 10.7 #> 2 B 129 45 5805 1 194581. 108. #> # ℹ 1 more variable: weight_on_overlap <dbl> dual_frame_allocation( domains, cost_a = df$cost_a, cost_b = 45, deff_a = deff$deff, theta = "screening", target_cv = 0.05 ) #> #> ── Dual-frame allocation ─────────────────────────────────────────────────────── #> Minimum cost for target variance 878900: n_A = 82 (frame A, cost 163.2/unit), #> n_B = 144 (frame B, cost 45/unit), theta = 0 (fixed). #> Cost 19860; variance 876100 (frame A 692000, frame B 184000); CV 4.99% of the #> total 18750. #> Expected overlap units: 11.7 in the A sample, 120 in the B sample.
The domains come from the overlap between the frames: frame_overlap()
places each listed establishment in the cell it falls in (by coordinates,
by polygons, or by a table of pairs from record linkage, for example with
the reclin2 package) and marks it ab or b, and tells each cell which
listed establishments it holds, which is what the teams use to screen the
overlap out of the area sample.
Once both samples are in, dual_frame_estimator() combines them. Each
sampled unit carries its domain (a or ab on the area frame, b or ab
on the list); Hartley's estimator mixes the two estimates of the overlap with
a weight theta that can be fixed, set to zero (screening) or estimated to
minimise the variance, and the Fuller-Burmeister estimator adds the
difference between the two estimates of the overlap size. The variances come
from the variance estimator of each design.
cells <- cells |> mutate( domain = if_else(intensity == "high" & runif(n()) < 0.6, "ab", "a"), crop_est = if_else(domain == "ab", 60, 12) + rnorm(n(), sd = 3) ) overlap <- cells |> filter(domain == "ab") # the same holdings, seen from the list list_frame <- bind_rows( overlap |> transmute(unit = paste0("l", row_number()), domain, crop_est), tibble(unit = paste0("b", 1:12), domain = "b", crop_est = 90 + rnorm(12, sd = 5)) ) |> mutate(x = runif(n()), y = runif(n())) sa <- cells |> select_units(n = 40, strata = "intensity", seed = 3) sb <- list_frame |> select_units(n = 20, method = "srs", seed = 4) dual_frame_estimator(sa, sb, y = "crop_est", domain = "domain") #> # A tibble: 1 × 13 #> total variance se cv estimator theta theta_fixed y_a y_ab_a y_ab_b #> <dbl> <dbl> <dbl> <dbl> <chr> <dbl> <lgl> <dbl> <dbl> <dbl> #> 1 20078. 186923. 432. 0.0215 hartley 0.217 FALSE 9797. 10005. 9470. #> # ℹ 3 more variables: y_b <dbl>, n_a <int>, n_b <int> dual_frame_estimator(sa, sb, y = "crop_est", domain = "domain", theta = "screening") #> # A tibble: 1 × 13 #> total variance se cv estimator theta theta_fixed y_a y_ab_a y_ab_b #> <dbl> <dbl> <dbl> <dbl> <chr> <dbl> <lgl> <dbl> <dbl> <dbl> #> 1 19962. 425106. 652. 0.0327 hartley 0 TRUE 9797. 10005. 9470. #> # ℹ 3 more variables: y_b <dbl>, n_a <int>, n_b <int> dual_frame_estimator( sa, sb, y = "crop_est", domain = "domain", estimator = "fuller-burmeister" ) #> # A tibble: 1 × 13 #> total variance se cv estimator beta_1 beta_2 y_a y_ab_a y_ab_b y_b #> <dbl> <dbl> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> #> 1 20077. 186851. 432. 0.0215 fuller-b… 0.253 -2.26 9797. 10005. 9470. 695. #> # ℹ 2 more variables: n_a <int>, n_b <int>
The selection scales to national frames: the local pivotal method and the systematic and cube methods run on a million cells in about a second (a grid index keeps the nearest-neighbour searches local), and the selection is done within strata, so the size of the whole frame is not a constraint.
For everything else the estimation needs, calibration to known totals,
domain estimates, nonresponse adjustment, as_svydesign() hands a sample
to the survey package as a design object with the right weights, strata
and, for replicated systematic samples, the replicate weights.
d <- as_svydesign(sa) survey::svytotal(~crop_est, d) #> total SE #> crop_est 19802 1632.6 survey::svyby(~crop_est, ~intensity, d, survey::svytotal) #> intensity crop_est se #> low low 4873.614 212.3148 #> mid mid 3815.881 160.8595 #> high high 11112.423 1610.7470
Area frames come with auxiliaries known for every cell: the area of farmland
from the land-cover map, the number of holdings from the last census. The
ratio estimator uses one such total at the estimation stage, and the cube
method uses several at the selection stage, drawing a sample whose
Horvitz-Thompson estimates of the auxiliaries match their totals.
spatial_balance() measures how evenly a sample covers the territory
(Stevens and Olsen's Voronoi measure: zero is perfect).
cells <- cells |> mutate( farmland = size * runif(n(), 15, 35), crop_r = 0.5 * farmland + rnorm(n(), sd = 3) ) s_lpm <- cells |> select_units(n = 40, strata = "intensity", seed = 5) ratio_estimator(s_lpm, y = "crop_r", x = "farmland") |> select(total, se, variance, variance_ht) #> # A tibble: 1 × 4 #> total se variance variance_ht #> <dbl> <dbl> <dbl> <dbl> #> 1 23722. 411. 169014. 956341. s_cube <- cells |> select_units(n = 40, strata = "intensity", method = "cube", balance = "farmland", seed = 5) s_srs <- cells |> select_units(n = 40, strata = "intensity", method = "srs", seed = 5) tibble( design = c("lpm", "cube", "srs"), balance = c(spatial_balance(s_lpm), spatial_balance(s_cube), spatial_balance(s_srs)) ) #> # A tibble: 3 × 2 #> design balance #> <chr> <dbl> #> 1 lpm 0.129 #> 2 cube 0.368 #> 3 srs 0.275
Cochran, W. G. (1977). Sampling Techniques, third edition. Wiley.
Hartley, H. O. (1974). Multiple frame methodology and selected applications. Sankhya C, 36, 99--118.
Lohr, S. L. and Rao, J. N. K. (2006). Estimation in multiple-frame surveys. Journal of the American Statistical Association, 101(475), 1019--1030.
Nealon, J. P. (1984). Review of the multiple and area frame estimators. USDA Statistical Reporting Service, Staff Report 80.
Deville, J.-C. and Tillé, Y. (2004). Efficient balanced sampling: the cube method. Biometrika, 91(4), 893--912.
Fuller, W. A. and Burmeister, L. F. (1972). Estimators for samples selected from two overlapping frames. Proceedings of the Social Statistics Section, American Statistical Association, 245--249.
Stevens, D. L. and Olsen, A. R. (2004). Spatially balanced sampling of natural resources. Journal of the American Statistical Association, 99, 262--278.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.