Shinylive App Comparing SRS (average and ratio) with UPSWR (Hansen–Hurwitz)

SRS (average and ratio) with UPSWR (Hansen–Hurwitz)
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| label: upswr-app
#| standalone: true
#| viewerHeight: 860

library(shiny)

## ---------------------------------------------------------------- population
data_url <- "https://raw.githubusercontent.com/longhaiSK/sampling/main/data/agpop.csv"

raw <- tryCatch({
  download.file(data_url, "agpop.csv")
  read.csv("agpop.csv")
}, error = function(e) NULL)

if (is.null(raw)) {            # fallback so the app runs without network access
  set.seed(1)
  Nf <- 3000
  x1 <- rgamma(Nf, shape = 1.2, scale = 1.8e5)
  raw <- data.frame(
    acres92 = pmax(0, 0.93 * x1 + rnorm(Nf, 0, 0.5 * x1^0.85)),
    acres87 = x1,
    farms92 = pmax(1, round(300 + x1 / 6000 + rnorm(Nf, 0, 250)))
  )
}

raw <- raw[is.finite(raw$acres92) & raw$acres92 != -99, ]
yv  <- raw$acres92
N   <- length(yv)

## every numeric column other than y is a candidate auxiliary variable, both
## for the UPS sizes M and for the ratio estimator's x;
## -99 marks a missing entry and is replaced by the mean of the rest
num <- vapply(raw, is.numeric, logical(1))
X <- raw[, num & names(raw) != "acres92", drop = FALSE]
X <- as.data.frame(lapply(X, function(v) {
  v[!is.finite(v) | v == -99] <- NA
  v[is.na(v)] <- mean(v, na.rm = TRUE)
  v
}))
keep <- vapply(X, function(v) all(is.finite(v)) && all(v >= 0) && var(v) > 0,
               logical(1))
X <- X[, keep, drop = FALSE]
xvars <- names(X)
xdef  <- if ("acres87" %in% xvars) "acres87" else xvars[1]
mdef  <- xdef

ybarU <- mean(yv)
Vy    <- var(yv)                     # S^2 of y, for the SRS average
ZCRIT <- 1.96                        # nominal 95% intervals

set.seed(11)                         # display-only vertical jitter, fixed once
ypl <- yv + rnorm(N, 0, 0.004 * diff(range(yv)))
rgy <- range(ypl)

## ------------------------------------------------------------ estimator specs
EST <- c("srs", "ratio", "hh")
LAB <- c(srs = "SRS average", ratio = "SRS ratio", hh = "UPSWR HH")
COL <- c(srs = "#e07b39", ratio = "#2ca02c", hh = "#9467bd")
PCH <- c(srs = 17, ratio = 16, hh = 15)

col_samp <- "#d62728"                # missed intervals and the population mean
col_pts  <- "blue"                   # sampled observations
col_true <- "black"
col_pop  <- "#9aa0a655"
col_band <- "#eef3f8"

## point size grows with the per-draw selection probability; the scale is
## shared by both designs, so SRS points (all 1/N) show what "equal" looks like
psize <- function(p, pmax) 0.25 + 2.4 * sqrt(p / pmax)

## ------------------------------------------------------- broken-axis mapping
## "top": linear from the minimum up to a cut, then the sparse upper tail
##        squeezed into a thin strip (SRS view: every unit is equally likely).
## "low": the many small values squeezed into a thin strip at the bottom and
##        the large values spread out (UPSWR view: large units dominate).
BRK_THIN <- 0.14
BRK_GAP  <- 0.025
BRK_WIDE <- 1 - BRK_THIN - BRK_GAP

## SRS view: the population mean falls at mid-height
cut_at_centre <- function(rmin, rmax, m) {
  cut <- rmin + (m - rmin) * BRK_WIDE / 0.5
  if (!is.finite(cut) || cut >= rmax) rmin + 0.95 * (rmax - rmin) else cut
}

## UPSWR view: squeeze the values that PPS reaches with only `share` of its
## draws, i.e. the lower `share` of the psi-weighted distribution
cut_psi_share <- function(v, psi, share = 0.2) {
  o <- order(v)
  v[o][which(cumsum(psi[o]) >= share)[1]]
}

mk_break <- function(rmin, rmax, cut, mode = c("top", "low")) {
  mode <- match.arg(mode)
  eps  <- 1e-9 * (rmax - rmin)
  cut  <- min(max(cut, rmin + eps), rmax - eps)
  lo_h <- if (mode == "top") BRK_WIDE else BRK_THIN
  hi_h <- 1 - lo_h - BRK_GAP
  tf <- function(v) {
    v <- pmin(pmax(v, rmin), rmax)
    ifelse(v <= cut, (v - rmin) / (cut - rmin) * lo_h,
           lo_h + BRK_GAP + (v - cut) / (rmax - cut) * hi_h)
  }
  list(tf = tf, rmin = rmin, rmax = rmax, cut = cut, mode = mode,
       gap = c(lo_h, lo_h + BRK_GAP))
}

mk_plain <- function(rmin, rmax) {
  list(tf = function(v) (pmin(pmax(v, rmin), rmax) - rmin) / (rmax - rmin),
       rmin = rmin, rmax = rmax, cut = NA, gap = NULL)
}

## counts stay as counts, acreages are shown in thousands
unit_div <- function(rmax) if (rmax >= 1e5) 1000 else 1
unit_lab <- function(div) if (div == 1000) " (thousands)" else ""

axis_at <- function(bk, side, div, rg, nt) {
  v <- pretty(rg, nt)
  v <- v[v >= rg[1] & v <= rg[2]]
  if (!length(v)) return(invisible())
  d <- if (diff(rg) / div < 40) 1 else 0
  axis(side, at = bk$tf(v), labels = formatC(v / div, format = "f", digits = d,
                                             big.mark = ","), las = 1, cex.axis = 0.75)
}

draw_axis <- function(bk, side, div) {
  if (is.null(bk$gap)) {
    axis_at(bk, side, div, c(bk$rmin, bk$rmax), 5)
  } else {
    nt <- if (bk$mode == "top") c(5, 2) else c(2, 5)
    axis_at(bk, side, div, c(bk$rmin, bk$cut), nt[1])
    axis_at(bk, side, div, c(bk$cut + 1e-9, bk$rmax), nt[2])
  }
}

mask_gap <- function(bk, side) {
  if (is.null(bk$gap)) return(invisible())
  usr <- par("usr")
  g <- bk$gap
  m <- mean(g)
  if (side == 2) {
    rect(usr[1], g[1], usr[2], g[2], col = "white", border = NA)
    d <- 0.010 * (usr[2] - usr[1])
    segments(usr[1] - d, c(m - 0.012, m + 0.004), usr[1] + d,
             c(m - 0.004, m + 0.012), xpd = NA, lwd = 1.2)
  } else {
    rect(g[1], usr[3], g[2], usr[4], col = "white", border = NA)
    d <- 0.010 * (usr[4] - usr[3])
    segments(c(m - 0.012, m + 0.004), usr[3] - d, c(m - 0.004, m + 0.012),
             usr[3] + d, xpd = NA, lwd = 1.2)
  }
}

## interval drawn as a capped vertical bar with the estimate marked on it;
## the bar turns red when the interval misses the population mean
ci_bar <- function(x, est, ci, col, pch, covered, tf = identity, cap = 0.012) {
  bcol <- if (covered) col else col_samp
  segments(x, tf(ci[1]), x, tf(ci[2]), col = bcol, lwd = 2)
  segments(x - cap, tf(ci), x + cap, tf(ci), col = bcol, lwd = 2)
  points(x, tf(est), pch = pch, col = col, cex = 1.7)
}

## symmetric density polygon, drawn vertically about x = at
violin <- function(x, at, col, w = 0.32) {
  if (length(x) < 10 || diff(range(x)) <= 0) return(invisible())
  d  <- density(x)
  hw <- w * d$y / max(d$y)
  polygon(c(at + hw, rev(at - hw)), c(d$x, rev(d$x)),
          col = adjustcolor(col, 0.35), border = col)
  m <- median(x)
  segments(at - w * 0.6, m, at + w * 0.6, m, col = col, lwd = 2)
}

pct <- function(v) if (is.na(v)) "--" else sprintf("%.1f%%", 100 * v)

## ---- inline SVG button icons (no font or icon-library dependency) ----------
svg_icon <- function(paths)
  HTML(sprintf(paste0('<svg width="16" height="16" viewBox="0 0 16 16" ',
                      'fill="currentColor" style="vertical-align:-2px">%s</svg>'), paths))
icon_play  <- svg_icon('<path d="M3 2v12l10-6z"/>')
icon_pause <- svg_icon('<rect x="3" y="2" width="4" height="12"/><rect x="9" y="2" width="4" height="12"/>')
icon_skip  <- svg_icon('<path d="M1 2v12l7-6zM8 2v12l6-6z"/><rect x="14" y="2" width="2" height="12"/>')

## ---------------------------------------------------------------------- ui
ui <- fluidPage(
  titlePanel("Shinylive App for UPSWR"),
  tags$style(HTML(
    ".well {padding: 10px 12px; margin-bottom: 10px;}
     .form-group {margin-bottom: 6px;}
     .checkbox, .radio {margin-top: 2px; margin-bottom: 2px;}
     .irs {margin-bottom: 0;}
     .btn {margin-bottom: 6px;}"
  )),
  wellPanel(
    fluidRow(
      column(
        3,
        selectInput("mvar", HTML("Aux. var. for UPS (M<sub>i</sub>)"),
                    choices = xvars, selected = mdef),
        selectInput("xvar", HTML("Aux. var. for estimation (x<sub>i</sub>)"),
                    choices = xvars, selected = xdef),
        checkboxInput("sync", HTML("Same variable (M<sub>i</sub> = x<sub>i</sub>)"), TRUE),
        checkboxInput("brk", "Break the axes", TRUE)
      ),
      column(
        3,
        checkboxGroupInput("show", "Estimators",
                           choices = c("SRS + average" = "srs",
                                       "SRS + ratio" = "ratio",
                                       "UPSWR + HH ratio" = "hh"),
                           selected = EST),
        sliderInput("n", "Sample size (n)", 10, 500, 100, step = 10)
      ),
      column(
        3,
        radioButtons("design", "Show sample from",
                     choices = c("SRS" = "srs", "UPSWR" = "ups"),
                     selected = "ups", inline = TRUE),
        sliderInput("speed", "Samples per second", min = 1, max = 10, value = 5, step = 1),
        numericInput("nrep", "Stop after this many samples",
                     value = 500, min = 10, max = 20000, step = 50)
      ),
      column(
        3,
        actionButton("draw", "Draw new samples", class = "btn-primary", width = "100%"),
        br(), br(),
        fluidRow(
          column(4, actionButton("play",  icon_play,  class = "btn-success", width = "100%",
                                 title = "Play")),
          column(4, actionButton("pause", icon_pause, class = "btn-warning", width = "100%",
                                 title = "Pause")),
          column(4, actionButton("skip",  icon_skip,  class = "btn-info",    width = "100%",
                                 title = "Skip to the end"))
        ),
        actionButton("reset", "Reset", width = "100%"),
        div(style = "font-size: 90%;", textOutput("counter"))
      )
    )
  ),
  plotOutput("mainplot", height = "520px")
)

## ------------------------------------------------------------------- server
server <- function(input, output, session) {

  rv <- reactiveValues(s_srs = NULL, s_ups = NULL, n = 100, ests = NULL)
  running <- reactiveVal(FALSE)

  nrep <- reactive({
    v <- input$nrep
    if (is.null(v) || is.na(v) || v < 1) 1L else min(as.integer(v), 20000L)
  })

  ## everything that depends on the two auxiliary variables: M sets the UPS
  ## selection probabilities, x feeds the SRS ratio estimator
  aux <- reactive({
    mv  <- X[[input$mvar]]
    xv  <- X[[input$xvar]]
    psi <- mv / sum(mv)                  # per-draw selection probabilities
    B   <- sum(yv) / sum(xv)
    pos <- psi > 0
    list(m = mv, x = xv, psi = psi, B = B, xbar = mean(xv),
         ## per-draw variances, so the SE of each estimator with n draws is
         ## sqrt(fpc * V / n) for the SRS ones and sqrt(V / n) for HH
         V = c(srs = Vy, ratio = var(yv - B * xv),
               hh = sum(psi[pos] * (yv[pos] / psi[pos] - sum(yv))^2) / N^2),
         ## horizontal axis of each view: x for SRS, M for UPSWR
         ax = list(
           top = list(v = xv, rg = range(xv), slope = B, name = input$xvar,
                      cut = cut_at_centre(min(xv), max(xv), mean(xv)),
                      cuty = cut_at_centre(rgy[1], rgy[2], ybarU)),
           low = list(v = mv, rg = range(mv), slope = sum(yv) / sum(mv),
                      name = input$mvar, cut = cut_psi_share(mv, psi),
                      cuty = cut_psi_share(ypl, psi))))
  })

  se_theory <- function(a, n)
    sqrt(c(srs = 1 - n / N, ratio = 1 - n / N, hh = 1) * a$V / n)

  ## one replication: an SRS (without replacement) feeding the average and the
  ## ratio estimator, and an independent PPS-with-replacement sample feeding HH
  one_draw <- function(a, n) {
    s   <- sample.int(N, n)
    u   <- sample.int(N, n, replace = TRUE, prob = a$psi)
    ys  <- yv[s]
    xs  <- a$x[s]
    fpc <- 1 - n / N

    Bhat <- if (sum(xs) > 0) sum(ys) / sum(xs) else 0
    z    <- yv[u] / a$psi[u] / N          # each draw's estimate of the mean

    est <- c(srs   = mean(ys),
             ratio = Bhat * a$xbar,
             hh    = mean(z))
    se  <- c(srs   = sqrt(fpc * var(ys) / n),
             ratio = (a$xbar / mean(xs)) * sqrt(fpc * var(ys - Bhat * xs) / n),
             hh    = sd(z) / sqrt(n))

    e <- c(setNames(est, paste0("est_", EST)),
           setNames(est - ZCRIT * se, paste0("lo_", EST)),
           setNames(est + ZCRIT * se, paste0("hi_", EST)),
           setNames(as.numeric(abs(est - ybarU) <= ZCRIT * se), paste0("cov_", EST)),
           Bhat = Bhat, Bhh = mean(yv[u] / a$m[u]))
    list(s = s, u = u, e = e)
  }

  ## draw k replications; only the last one's samples are kept for the left
  ## panel. "Skip" calls this with the full remaining count.
  draw_many <- function(k) {
    a <- isolate(aux())
    n <- isolate(input$n)
    E <- vector("list", k)
    for (i in seq_len(k)) {
      d <- one_draw(a, n)
      E[[i]] <- d$e
    }
    rv$s_srs <- d$s
    rv$s_ups <- d$u
    rv$n     <- n
    rv$ests  <- rbind(rv$ests, do.call(rbind, E))
  }
  draw_one <- function() draw_many(1)

  ## a fresh history always starts from one sample, so the plot is never empty
  reset_history <- function() {
    running(FALSE)
    rv$ests <- NULL
    draw_one()
  }
  observeEvent(list(input$n, input$mvar, input$xvar), reset_history(), ignoreInit = FALSE)

  ## keep M and x the same while "Same variable" is ticked: a change to either
  ## box is copied to the other, and ticking the box copies M to x
  sync_to <- function(id, v) {
    if (isTRUE(input$sync) && !identical(input[[id]], v))
      updateSelectInput(session, id, selected = v)
  }
  observeEvent(input$mvar, sync_to("xvar", input$mvar))
  observeEvent(input$xvar, sync_to("mvar", input$xvar))
  observeEvent(input$sync, sync_to("xvar", input$mvar))
  observeEvent(input$reset, reset_history())

  ## one sample per click; Play repeats until Pause or until the cap is reached
  observeEvent(input$draw,  { running(FALSE); draw_one() })
  observeEvent(input$play,  running(TRUE))
  observeEvent(input$pause, running(FALSE))
  observeEvent(input$skip, {
    running(FALSE)
    k <- nrep() - NROW(rv$ests)
    if (k > 0) draw_many(k)
  })

  observe({
    if (!isTRUE(running())) return()
    if (NROW(isolate(rv$ests)) >= isolate(nrep())) { running(FALSE); return() }
    invalidateLater(1000 / isolate(input$speed), session)
    isolate(draw_one())
  })

  output$counter <- renderText(
    sprintf("%d of %d samples", NROW(rv$ests), nrep())
  )

  output$mainplot <- renderPlot({
    req(rv$s_srs)
    sel <- EST[EST %in% input$show]
    validate(need(length(sel) > 0, "Select at least one estimator."))

    a    <- aux()
    n    <- rv$n
    M    <- rv$ests
    nrep <- NROW(M)
    cur  <- M[nrep, ]
    ups  <- identical(input$design, "ups")
    s    <- if (ups) rv$s_ups else rv$s_srs
    xpos <- setNames(0.02 + 0.04 * (seq_along(sel) - 1), sel)   # CI bar positions

    ## violin window, wide enough for the least precise estimator shown
    half <- 4 * max(se_theory(a, n)[sel])
    lo <- ybarU - half
    hi <- ybarU + half

    md <- if (ups) "low" else "top"
    h  <- a$ax[[md]]
    if (isTRUE(input$brk)) {
      bky <- mk_break(rgy[1], rgy[2], h$cuty, md)
      bkx <- mk_break(h$rg[1], h$rg[2], h$cut, md)
    } else {
      bky <- mk_plain(rgy[1], rgy[2])
      bkx <- mk_plain(h$rg[1], h$rg[2])
    }
    tfy  <- bky$tf
    tfx  <- bkx$tf
    divy <- unit_div(rgy[2])
    divx <- unit_div(h$rg[2])

    xg <- seq(h$rg[1], h$rg[2], length.out = 1500)
    draw_line <- function(yl, ...) {          # stop the line at the frame
      yl[yl < rgy[1] | yl > rgy[2]] <- NA
      lines(tfx(xg), tfy(yl), ...)
    }

    ## point sizes: per-draw selection probability under the design shown
    pmax_ <- max(a$psi)
    cexp  <- if (ups) psize(a$psi, pmax_) else rep(psize(1 / N, pmax_), N)

    layout(matrix(1:2, nrow = 1), widths = c(1, 1))

    ## ---- left: population sized by selection probability, with the sample ----
    par(mar = c(4.2, 4.8, 3.0, 1.0))
    plot(NA, xlim = c(0, 1), ylim = c(0, 1), axes = FALSE,
         xlab = paste0(if (ups) "M: " else "x: ", h$name, unit_lab(divx)),
         ylab = paste0("t: acres92", unit_lab(divy)),
         main = sprintf("Population and %s sample, n = %d",
                        if (ups) "UPSWR" else "SRS", n))
    rect(0, tfy(lo), 1, tfy(hi), col = col_band, border = NA)
    points(tfx(h$v), tfy(ypl), pch = 16, cex = cexp, col = col_pop)

    draw_line(h$slope * xg, col = col_true, lwd = 1.8)
    if (ups) {
      if ("hh" %in% sel)
        draw_line(cur[["Bhh"]] * xg, col = COL["hh"], lwd = 2, lty = 2)
    } else {
      if ("ratio" %in% sel)
        draw_line(cur[["Bhat"]] * xg, col = COL["ratio"], lwd = 2, lty = 2)
      if ("srs" %in% sel)
        abline(h = tfy(cur[["est_srs"]]), col = COL["srs"], lwd = 2, lty = 2)
    }
    ## a unit drawn more than once is drawn once, ringed by its draw count
    tab <- table(s)
    su  <- as.integer(names(tab))
    points(tfx(h$v[su]), tfy(ypl[su]), pch = 16, cex = cexp[su], col = col_pts)
    rep_u <- su[tab > 1]
    if (length(rep_u))
      text(tfx(h$v[rep_u]), tfy(ypl[rep_u]), labels = tab[tab > 1],
           pos = 4, cex = 0.8, col = col_pts, font = 2)

    abline(h = tfy(ybarU), col = col_samp, lty = 3)
    for (k in sel) ci_bar(xpos[[k]], cur[[paste0("est_", k)]],
                          c(cur[[paste0("lo_", k)]], cur[[paste0("hi_", k)]]),
                          COL[[k]], PCH[[k]], cur[[paste0("cov_", k)]] == 1, tfy)

    mask_gap(bky, 2); mask_gap(bkx, 1)
    box(); draw_axis(bky, 2, divy); draw_axis(bkx, 1, divx)

    leg <- data.frame(l = c(if (ups) "size ~ psi = M / M0" else "size ~ 1/N (equal)",
                            "sampled t"),
                      c = c("grey55", col_pts), lt = NA, p = 16, w = NA)
    leg <- rbind(leg, data.frame(l = if (ups) "true t = (t/M0) M" else "true t = (t/tx) x",
                                 c = col_true, lt = 1, p = NA, w = 1.8))
    if (ups && "hh" %in% sel) leg <- rbind(leg, data.frame(
      l = "HH slope mean(t/M)", c = COL[["hh"]], lt = 2, p = NA, w = 2))
    if (!ups && "ratio" %in% sel) leg <- rbind(leg, data.frame(
      l = "ratio fit", c = COL[["ratio"]], lt = 2, p = NA, w = 2))
    if (!ups && "srs" %in% sel) leg <- rbind(leg, data.frame(
      l = "sample average", c = COL[["srs"]], lt = 2, p = NA, w = 2))
    leg <- rbind(leg, data.frame(l = "population mean", c = col_samp, lt = 3, p = NA, w = 1))
    legend("topleft", bty = "n", cex = 0.9, inset = c(0.15, 0.01),
           legend = leg$l, col = leg$c, lty = leg$lt, pch = leg$p, lwd = leg$w)

    ## ---- right: sampling distributions as vertical violins, with current CIs ----
    par(mar = c(4.2, 4.8, 3.0, 1.0))
    k_n <- length(sel)
    at  <- setNames(seq_len(k_n), sel)
    plot(NA, xlim = c(0.4, k_n + 0.6), ylim = c(lo, hi), axes = FALSE,
         xlab = "", ylab = paste0("estimate of mean t (acres92)", unit_lab(divy)),
         main = "Sampling distributions of the estimates")
    usr <- par("usr")
    rect(usr[1], usr[3], usr[2], usr[4], col = col_band, border = NA)
    abline(h = ybarU, col = col_samp, lty = 3)
    for (k in sel) {
      violin(M[, paste0("est_", k)], at[[k]], COL[[k]])
      ci <- c(cur[[paste0("lo_", k)]], cur[[paste0("hi_", k)]])
      bc <- if (cur[[paste0("cov_", k)]] == 1) COL[[k]] else col_samp
      segments(at[[k]], ci[1], at[[k]], ci[2], col = bc, lwd = 2.5)
      segments(at[[k]] - 0.08, ci, at[[k]] + 0.08, ci, col = bc, lwd = 2.5)
      points(at[[k]], cur[[paste0("est_", k)]], pch = PCH[[k]], col = COL[[k]], cex = 1.7)
    }
    box()
    vy <- pretty(c(lo, hi), 5)
    vy <- vy[vy >= lo & vy <= hi]
    axis(2, at = vy, labels = formatC(vy / divy, format = "f",
                                      digits = if ((hi - lo) / divy < 40) 1 else 0,
                                      big.mark = ","), las = 1, cex.axis = 0.75)
    axis(1, at = at, labels = LAB[sel], tick = FALSE, cex.axis = 0.9)

    cov_txt <- function(k) if (nrep >= 10) pct(mean(M[, paste0("cov_", k)])) else "--"
    legend("topleft", bty = "n", cex = 0.9, inset = c(0.02, 0.01),
           title = "95% CI coverage", fill = COL[sel], border = NA,
           legend = paste(LAB[sel], vapply(sel, cov_txt, "")))

    ## reductions are relative to the SRS average, which is always simulated
    cmp <- setdiff(sel, "srs")
    if (length(cmp)) {
      se_th <- se_theory(a, n)
      red <- lapply(cmp, function(k) {
        mc <- if (nrep > 5) 1 - var(M[, paste0("est_", k)]) / var(M[, "est_srs"]) else NA
        th <- 1 - se_th[[k]]^2 / se_th[["srs"]]^2
        list(txt = sprintf("%s %s (theory %s)", LAB[[k]], pct(mc), pct(th)),
             neg = if (is.na(mc)) th < 0 else mc < 0)
      })
      legend("bottomleft", bty = "n", cex = 0.9, inset = c(0.02, 0.01),
             title = "Variance reduction vs. SRS average", title.col = "black",
             fill = COL[cmp], border = NA,
             legend = vapply(red, `[[`, "", "txt"),
             text.col = ifelse(vapply(red, `[[`, TRUE, "neg"), col_samp, "black"))
    }
  })
}

shinyApp(ui, server)

About this app

The app compares two ways of using a size variable \(x_i\) to estimate the mean per unit \(\bar{t}_U = t/N\) (here \(t_i\) is acres92 for county \(i\)): at the estimation stage (SRS + ratio) or at the design stage (UPSWR with \(\psi_i = x_i/t_x\)). Choosing the same variable for \(M_i\) and \(x_i\) in the app gives exactly this comparison. The plain SRS average is included as a baseline. Throughout, write \[ B = \frac{t}{t_x}, \qquad r_i = \frac{t_i}{x_i}, \qquad e_i = t_i - B x_i , \] so \(e_i\) is unit \(i\)’s residual from the line through the origin with the population slope. Note that \(\sum_{i=1}^N e_i = t - B t_x = 0\).

SRS + ratio. Use \(x_i\) only at the estimation stage: \[ \hat{\bar{t}}_{r} = \hat{B}\,\bar{x}_U, \quad \hat{B} = \frac{\sum_{i=1}^n t_i}{\sum_{i=1}^n x_i}, \qquad \widehat{\mathrm{SE}}(\hat{\bar{t}}_{r}) = \frac{\bar{x}_U}{\bar{x}}\sqrt{\left(1-\frac{n}{N}\right)\frac{s_e^2}{n}}, \qquad s_e^2 = \frac{1}{n-1}\sum_{i=1}^n \left(t_i - \hat{B}x_i\right)^2 . \]

UPSWR + ratio. Use \(x_i\) at the design stage, with \(\psi_i = x_i/t_x\). Every draw then has \(x_i/\psi_i = t_x\). So the HH ratio estimator, scaled by \(\bar{x}_U\), is exactly the HH estimator of the mean, \(\hat{t}_{\mathrm{HH}}/N\), and it reduces to \(\bar{x}_U\) times the average of the sampled ratios \(r_i\): \[ \hat{\bar{t}}_{\mathrm{HH}} = \bar{x}_U\,\hat{\bar{y}}_{\mathrm{HH},r} = \bar{x}_U\,\frac{\sum_{i=1}^n t_i/\psi_i}{\sum_{i=1}^n x_i/\psi_i} = \frac{\hat{t}_{\mathrm{HH}}}{N} = \bar{x}_U\,\frac{1}{n}\sum_{i=1}^n r_i , \] \[ \widehat{\mathrm{SE}}(\hat{\bar{t}}_{\mathrm{HH}}) = \bar{x}_U\,\frac{s_r}{\sqrt{n}}, \qquad s_r^2 = \frac{1}{n-1}\sum_{i=1}^n \left(r_i - \frac{1}{n}\sum_{j=1}^n r_j\right)^2 . \]

This app accompanies Shinylive App Comparing SRS and UPSWR in Elements of Sampling Survey.