---
title: "Getting Started with sondage"
output: rmarkdown::html_vignette
bibliography: references.bib
vignette: >
  %\VignetteIndexEntry{Getting Started with sondage}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup, echo = FALSE}
library(sondage)
set.seed(1)
```

## Overview

`sondage` provides fast implementations of survey sampling algorithms for finite populations. Every sampling function returns a *design object* that carries the selected sample alongside the design metadata (inclusion probabilities or expected hits, sample size, method). A set of generics then operates on these objects to compute quantities needed for variance estimation.

For the full object contract and field definitions, see `?sondage_sample`.

For without-replacement designs, the stored `pik` vector is the design-defining target inclusion probability vector. For methods with exact first-order guarantees, this equals the true first-order inclusion probabilities. For order-sampling methods such as `sps` and `pareto`, the stored vector remains the target `pik`, while the true finite-population first-order inclusion probabilities are only approximately equal to that target.

The five entry-point functions are organised by two dimensions, plus a dedicated dispatcher for balanced sampling:

|                | Without replacement | With replacement |
|----------------|---------------------|------------------|
| Equal prob.    | `equal_prob_wor()`  | `equal_prob_wr()`  |
| Unequal prob.  | `unequal_prob_wor()`| `unequal_prob_wr()`|
| Balanced       | `balanced_wor()`    |                    |

## Equal probability sampling

### Without replacement

The simplest case: draw `n` units from a population of size `N` with equal probabilities.

```{r}
library(sondage)
s <- equal_prob_wor(N = 50, n = 5)
s
```

The returned object stores the sampled unit indices and the inclusion probabilities:

```{r}
s$sample
s$pik[1:10]  # all equal to n/N = 0.1
```

Alternative methods are available. Systematic sampling provides implicit stratification based on the ordering of units:

```{r}
s_sys <- equal_prob_wor(N = 50, n = 5, method = "systematic")
s_sys
```

Bernoulli sampling selects each unit independently with probability `p = n/N`, producing a random sample size. The parameter `n` is the expected sample size not the realized sample size.

```{r}
s_ber <- equal_prob_wor(N = 50, n = 5, method = "bernoulli")
s_ber
length(s_ber$sample)  # may differ from 5
```

### With replacement

```{r}
s_wr <- equal_prob_wr(N = 50, n = 5)
s_wr
```

With-replacement designs track **hits** (how many times each unit was selected) instead of inclusion probabilities:

```{r}
s_wr$hits  # length-N vector of selection counts
```

## Unequal probability sampling

When units have different sizes (employees, revenue, area, ...), probability-proportional-to-size (PPS) designs give larger units higher selection probabilities, which reduces the variance of Horvitz--Thompson estimators [@horvitz1952].

### Computing design quantities

Start from a vector of positive size measures and compute inclusion probabilities (WOR) or expected hits (WR):

```{r}
x <- c(10, 20, 5, 40, 25)
n <- 3

# Without replacement: inclusion probabilities (capped at 1)
pik <- inclusion_prob(x, n)
pik
sum(pik)  # equals n

# With replacement: expected hits (can exceed 1)
eh <- expected_hits(x, n)
eh
sum(eh)   # equals n
```

### Without replacement

Pass the inclusion probability vector to `unequal_prob_wor()`. The default method is Conditional Poisson Sampling [CPS, @chen1994], which has maximum entropy among fixed-size designs:

```{r}
s <- unequal_prob_wor(pik, method = "cps")
s
s$sample
inclusion_prob(s)  # extract the stored design-defining target pik
```

Other fixed-size methods include `"brewer"` (draw-by-draw, order-invariant), `"sampford"` [@sampford1967], `"systematic"` (O(N), implicit stratification), `"sps"` [Sequential Poisson sampling, @ohlsson1998, supports PRN], and `"pareto"` [Pareto sampling, @rosen1997, supports PRN]. The `"poisson"` method produces a random sample size.

For `sps` and `pareto`, `inclusion_prob(s)` still returns the stored target `pik` vector used to define the design. Those methods do not, in general, provide exact finite-population first-order inclusion probabilities equal to that target.

### With replacement

Pass the expected hits vector to `unequal_prob_wr()`. @chromy1979's method is the default:

```{r}
s_wr <- unequal_prob_wr(eh, method = "chromy")
s_wr
s_wr$hits  # realized selection counts
```

The `"multinomial"` method draws each selection independently.

## Balanced sampling

When auxiliary information is available at the design stage, balanced sampling ensures that Horvitz--Thompson estimates of auxiliary totals are (approximately) exact: $\sum_{k \in S} x_k / \pi_k \approx \sum_{k \in U} x_k$. This reduces variance for any study variable correlated with the balancing variables.

The `balanced_wor()` function implements the cube method [@deville2004]:

```{r}
pik <- inclusion_prob(c(10, 20, 5, 40, 25), n = 3)
x <- matrix(c(10, 20, 5, 40, 25))  # auxiliary variable to balance on
s_bal <- balanced_wor(pik, aux = x)
s_bal
```

The cube method proceeds in two phases: a *flight phase* that moves probabilities toward 0 or 1 while maintaining all balancing constraints, and a *landing phase* that resolves the remaining undecided units by progressively relaxing constraints (starting from the last column of `aux`). The sample size constraint is always placed first and never relaxed.

Users should order auxiliary variables by importance (most important first) in the `aux` matrix.

For ill-conditioned or collinear auxiliary variables, `condition_aux = TRUE` applies weighted centering and scaling followed by rank pruning before running the cube algorithm.

### Stratified balanced sampling

When strata are known, the stratified cube method [@chauvet2009] preserves within-stratum sample sizes while balancing on auxiliary variables. Exact preservation requires `sum(pik)` within each stratum to be close to an integer; when this condition is not met, within-stratum and total sample sizes will vary around their targets and the design is treated as random-size (`fixed_size = FALSE`):

```{r}
N <- 20
pik_strat <- rep(0.4, N)
x_strat <- matrix(as.double(1:N), ncol = 1)
strata <- rep(1:4, each = 5)

s_strat <- balanced_wor(pik_strat, aux = x_strat, strata = strata)
s_strat

# Check per-stratum sample sizes
table(strata[s_strat$sample])
```

Joint inclusion probabilities for the cube use the high-entropy approximation, since the cube produces a near-maximum-entropy design.

### Balanced sampling with inequalities

The cube method also accepts linear inequality constraints through `bounds = list(B, lower, upper)` [@tripet2026]. These constrain raw realized-sample sums, $\mathtt{lower}_j \leq \sum_{k \in S} B_{kj} \leq \mathtt{upper}_j$, while preserving the supplied first-order inclusion probabilities. A common use is to keep category counts between the integers immediately below and above their expectations:

```{r}
pik_control <- rep(0.5, 12)
group <- rep(c("a", "b", "c"), each = 4)
B <- sapply(unique(group), function(g) as.double(group == g))
expected_counts <- colSums(B * pik_control)

s_control <- balanced_wor(
  pik_control,
  bounds = list(
    B = B,
    lower = floor(expected_counts),
    upper = ceiling(expected_counts)
  )
)
colSums(B[s_control$sample, , drop = FALSE])
```


### Spatially balanced sampling

It's possible to select well-spread, spatially balanced samples using `lpm2` [local pivotal method 2, @grafstrom2012] and `scps` [spatially correlated poisson sampling, @grafstrom2012scps].


```{r}
coordinates <- as.matrix(expand.grid(x = 1:4, y = 1:4))
pik_spatial <- rep(0.25, nrow(coordinates))

s_lpm2 <- balanced_wor(
  pik_spatial,
  spread = coordinates,
  method = "lpm2"
)
s_scps <- balanced_wor(
  pik_spatial,
  spread = coordinates,
  method = "scps"
)
s_lpm2$sample
s_scps$sample
```


These built-in methods are spread-only: they do not accept `aux`, `strata`, or `bounds`. They preserve the supplied first-order inclusion probabilities, but intentionally produce low-entropy designs, so `joint_inclusion_prob()` and `sampling_cov()` are not available for them.

## Design queries

Once you have a design object, generics compute the quantities needed for variance estimation.

### Joint inclusion probabilities (WOR)

```{r}
pik <- inclusion_prob(c(10, 20, 5, 40, 25), n = 3)
s <- unequal_prob_wor(pik, method = "cps")

pikl <- joint_inclusion_prob(s)
pikl
```

The diagonal holds the first-order probabilities $\pi_i$ and the off-diagonal entries are $\pi_{ij} = P(i \in S \text{ and } j \in S)$. For `sps` and `pareto`, the approximation is built from the stored target `pik` vector, not from unknown exact finite-population first-order inclusion probabilities.

### Joint expected hits (WR)

```{r}
eh <- expected_hits(c(10, 20, 5, 40, 25), n = 3)
s_wr <- unequal_prob_wr(eh, method = "chromy")

joint_expected_hits(s_wr)
```

### Submatrices for large populations

By default, `joint_inclusion_prob()` and `joint_expected_hits()` return the full N x N matrix, which is prohibitive when N > 10,000 (~0.8 GB). In practice, variance estimators only need the submatrix for sampled units. The `sampled_only = TRUE` argument returns this n x n submatrix directly:

```{r}
pik_large <- inclusion_prob(runif(500), n = 10)
s_large <- unequal_prob_wor(pik_large, method = "brewer")

# Full 500 x 500 matrix
pikl_full <- joint_inclusion_prob(s_large)
dim(pikl_full)

# Only the 10 x 10 submatrix for sampled units
pikl_sub <- joint_inclusion_prob(s_large, sampled_only = TRUE)
dim(pikl_sub)
```

All methods compute the submatrix directly without allocating the N x N matrix, so arbitrarily large populations are supported. The `sampling_cov()` generic also accepts `sampled_only`:

```{r}
delta_sub <- sampling_cov(s_large, sampled_only = TRUE)
dim(delta_sub)
```

### Sampling covariance

The `sampling_cov()` generic computes $\Delta_{ij} = \pi_{ij} - \pi_i \pi_j$ for WOR designs (or the analogous expression for WR designs):

```{r}
sampling_cov(s)
```

With `weighted = TRUE`, it returns the Sen--Yates--Grundy check quantities $1 - \pi_i \pi_j / \pi_{ij}$ [@sen1953; @yatesandgrundy1953]. For well-behaved WOR designs, these should be non-positive off-diagonal:

```{r}
sampling_cov(s, weighted = TRUE)
```

## Batch sampling

All five dispatchers accept an `nrep` parameter for drawing multiple independent samples in one call, useful for simulation studies. The result is always a design object (the same class as `nrep = 1`) but with `$sample` holding all replicates:

```{r}
sim <- equal_prob_wor(N = 50, n = 5, nrep = 4)
sim  # design object
dim(sim$sample)  # 5 x 4 matrix: each column is one sample

# Generics still work on batch objects
inclusion_prob(sim)
```

For fixed-size designs `$sample` is a matrix (`n x nrep`). For random-size designs (Bernoulli, Poisson) it is a list of integer vectors, and realized sample sizes are available via `lengths(sim$sample)`.

## Design properties and statistical guarantees

The table below summarises what `sondage` computes for each method. Understanding these properties is essential for choosing the right variance estimator.

```{r, echo = FALSE}
props <- data.frame(
  Method = c(
    "SRS WOR", "Systematic (EP)", "Bernoulli",
    "SRS WR",
    "CPS", "Brewer", "Sampford", "Systematic PPS", "Poisson",
    "SPS", "Pareto",
    "Chromy", "Multinomial",
    "Cube", "LPM2", "SCPS"
  ),
  Type = c(
    rep("EP WOR", 3), "EP WR",
    rep("UP WOR", 7),
    rep("UP WR", 2),
    rep("Balanced", 3)
  ),
  `Fixed n` = c(
    "Yes", "Yes", "No",
    "Yes",
    "Yes", "Yes", "Yes", "Yes", "No",
    "Yes", "Yes",
    "Yes", "Yes",
    "Yes", "Yes", "Yes"
  ),
  `Exact design marginals?` = c(
    "Yes", "Yes", "Yes",
    "Yes",
    "Yes", "Yes", "Yes", "Yes", "Yes",
    "Target only*", "Target only*",
    "Yes", "Yes",
    "Yes", "Yes", "Yes"
  ),
  `Joint probs` = c(
    "Exact", "Exact (zeros)", "Exact",
    "Exact",
    "Exact", "HE approx", "Exact", "Exact (zeros)", "Exact",
    "HE approx", "HE approx",
    "Simulated", "Exact",
    "HE approx", "Not available", "Not available"
  ),
  check.names = FALSE
)
knitr::kable(props, align = "llccl",
  caption = paste(
    "HE approx = high-entropy approximation (Brewer & Donadio, 2003).",
    "For WOR methods, design marginals are first-order inclusion probabilities pi_k;",
    "for WR methods, they are expected hits E(N_k).",
    "Target only* = the stored design-defining pik is the target vector;",
    "for sps and pareto the true finite-population first-order probabilities",
    "are only approximately equal to that target, with the discrepancy",
    "vanishing asymptotically.",
    "Simulated = Monte Carlo via nsim parameter."
  )
)
```

### Notes on specific methods

**Equal-probability systematic sampling.** In `sondage`, `joint_inclusion_prob()` returns exact joint inclusion probabilities computed via C code, using the same algorithm as systematic PPS with uniform $\pi_k = n/N$. Some $\pi_{ij}$ are structurally zero (pairs of units that can never co-occur in the same systematic sample), so the SYG estimator is generally inapplicable. Consider replication or successive-differences variance estimators for systematic designs.

**SPS and Pareto (order sampling).** Both are implemented via @rosen1997's order sampling framework. SPS uses ranking key $\xi_k = u_k / \pi_k$ [equivalent to @ohlsson1998's sequential threshold adjustment]; Pareto uses $\xi_k = [u_k/(1-u_k)] / [\pi_k/(1-\pi_k)]$. The $n$ units with the smallest $\xi_k$ are selected. Design objects store the supplied `pik` as the design-defining target vector, and `inclusion_prob(s)` returns that stored target. The true finite-population first-order inclusion probabilities are approximately (not exactly) equal to the target $\pi_k$ for finite populations; the discrepancy vanishes as $N$ grows. Both support permanent random numbers (PRN) for sample coordination.

**Systematic PPS.** Joint inclusion probabilities are exact (computed via C code) but some $\pi_{ij}$ are structurally zero e.g for pairs of units that
can never co-occur. `sampling_cov(s, weighted = TRUE)` returns `NA` for those entries and issues a warning, because the SYG estimator is undefined
for such pairs. Consider successive-differences, Hartley--Rao, or replication methods instead.

**Chromy.** Pairwise expectations $E(n_i n_j)$ are estimated by Monte Carlo
simulation in `joint_expected_hits()` (controlled by `nsim`, default 10 000).

**Cube (balanced sampling).** The cube method [@deville2004] produces near-maximum-entropy samples, so `joint_inclusion_prob()` uses the high-entropy approximation. The stratified variant [@chauvet2009] preserves exact within-stratum sample sizes. Order auxiliary variables by importance (most important first) since the landing phase relaxes constraints from last to first. Inequality bounds provide controlled selection, but tight bounds can make the high-entropy approximation less accurate.

**LPM2 and SCPS (spatially balanced sampling).** Both methods preserve exact first-order inclusion probabilities and spread the sample using the variables supplied in `spread`. Their low-entropy spatial dependence is deliberate; no joint inclusion probability or sampling covariance approximation is exposed. Use a local-neighbourhood variance estimator instead.

### Choosing a method

- **CPS**: Use when exactness matters more than speed. It is the maximum-entropy fixed-size unequal-probability design, with exact first- and second-order inclusion probabilities.
- **Brewer**: Use when you want exact first-order inclusion probabilities with lower computational cost than `cps`, and can work with approximate second-order quantities in `sondage`.
- **Sampford**: Use for fixed-size PPS with exact first- and second-order inclusion probabilities and fast typical draws.
- **Systematic**: Use when very fast sampling and ordering or implicit stratification are central, and structural zeros in some joint inclusion probabilities are acceptable (low-entropy).
- **Poisson**: Use when a random sample size is acceptable and independent selection is desirable.
- **SPS / Pareto**: Use as fast high-entropy order-sampling alternatives when approximate first- and second-order quantities are acceptable.
- **Cube**: Use when balancing on auxiliary variables is important.
- **LPM2 / SCPS**: Use when spatial spread is the priority and a local-neighbourhood variance strategy is available.

### Choosing a variance strategy

- **SYG with exact $\pi_{ij}$**: CPS, Sampford and SRS WOR provide exact joint probabilities, so the Sen--Yates--Grundy variance estimator [@sen1953; @yatesandgrundy1953] is unbiased.
- **SYG with approximate $\pi_{ij}$**: For Brewer, SPS, Pareto, and cube, joint probabilities use the high-entropy approximation [@brewer2003, eq. 18]. This guarantees symmetry, non-negativity, and $\pi_{ij} \le \min(\pi_i, \pi_j)$, but does *not* satisfy the fixed-size marginal identity $\sum_{j \neq i}\pi_{ij} = (n-1)\pi_i$ exactly. The marginal defect is typically small but can be non-trivial for highly skewed $\pi_k$ vectors when $n$ is small (e.g., $n = 2$); a warning is issued when it exceeds 5% of $n$. For `sps` and `pareto`, this approximation is built from the stored target `pik` vector rather than the unknown exact finite-population first-order probabilities. Use `method = "cps"` when exact second-order structure is required.
- **Independent designs** (Poisson, Bernoulli): The covariance $\Delta_{ij} = 0$ for $i \neq j$, so the variance simplifies to $\text{Var}(\hat{Y}_{HT}) = \sum_i (1 - \pi_i)\, y_i^2 / \pi_i$.
- **Systematic designs with $\pi_{ij} = 0$**: The SYG estimator is undefined. Use successive-differences, Hartley--Rao approximations, or replication methods.
- **With-replacement designs** (Chromy, multinomial): Use the @hansen1943 estimator, or the generalised Horvitz--Thompson framework of @chromy2009 which unifies WOR and WR variance estimation via `sampling_cov()`.

## Extending sondage

`register_method()` lets you plug custom unequal-probability sampling algorithms into the existing dispatchers and generics. Registered methods work with `unequal_prob_wor()`, `joint_inclusion_prob()`, `sampling_cov()`, batch mode, and all downstream tooling. See `vignette("custom-methods")` for worked examples.

## References
