sequential · probability minimum replacement · Chromy (1979)
Chromy's method selects units one at a time down an ordered list, each with
probability proportional to its size, so that a unit expected 0.3 times is
selected 0 or 1 times, a unit expected 2.4 times is selected 2 or 3 — never
further — and the sample size is always exact. It is how many large surveys
draw their samples, and how PROC SURVEYSELECT
METHOD=PPS_SEQ works. Until now it lived inside proprietary software
and one Stata module. chromy
is the method in the open: one Rust engine, bound to R and Python.
s <- chromy_sample(frame, n = 40, size = "pupils",
order_by = c("region", "chromy_size"), seed = 123)
sum(s$sample$hits) 40 # always: exact, not expected
sum(s$expected_hits) 40
estimate_total(s, "outcome") total 37623, SE 5010 (CV 13.3%)
The same seed gives the same sample, down to the last draw — the randomness comes from R's own generator, never from inside the engine.
the method
The expected hits accumulate down the ordered list. Every time the running sum crosses an integer, one selection is released; a single uniform per unit decides, conditionally on the walk so far, whether the pending selection lands here or carries forward. The conditionals are built so that each unit's hits come out with exactly the probability its expectation demands — a sequential implementation of the design, not an approximation to it.
exact sample size
The hits sum to n in every realisation. Not on average: always.
minimum replacement
Hits are the floor or the ceiling of the expectation — a unit can be selected more than once, but never drift from what the design promised it.
implicit stratification
Units that sit together in the list are selected apart, so the sample spreads over whatever the sort keys encode.
the toolkit
A selection procedure alone is half a design. Around it, the pieces the surveys that rely on Chromy's method actually use — each one a function of the engine, each one tested against the same reference.
serpentine sorting
The hierarchical sort that reverses direction at every group boundary, so the list meanders through the frame and neighbouring units are always alike. Categorical keys rank by byte order, never by locale: the same frame sorts the same way on every machine.
maximal size measure
For surveys that must estimate several totals from one sample: each unit's size is the largest of its scaled shares across the auxiliary variables (Folsom, Potter and Williams, 1987), so every variable gets at least its intended coverage.
equal-CV allocation
The factors that decide how much sample each variable commands, searched until every variable's total is anticipated with the same coefficient of variation — no estimate quietly sacrificed to another.
multiplicity estimation
The estimator that matches the design — values divided by the effective expected hits, weighted by the hits received — with the successive-difference variance read in list order, where the serpentine left genuinely similar neighbours.
one engine, three languages
chromy-core has no dependencies
and no generator: the uniforms arrive as arguments, so a selection is a pure
function of its inputs. R draws them with R's generator — which is why
set.seed() reproduces the
original prototype draw for draw — and Python draws them from a documented
seed. Three languages, one arithmetic, one reference.
R · chromy
Data frames in, tibbles out; the whole design in one call; four vignettes from the walk to the validation.
Python · pychromy
Lists of columns in, dicts of parallel lists out — nothing to install besides the wheel; pandas optional by construction.
Rust · chromy-core
The engine itself, for a production system that draws its own samples; slices in, structs out.
fidelity
Expected hits that should sum to an integer rarely do in doubles, and a remainder that lands at 1 − 10−16 either is or is not a crossing — the answer changes who can be selected. The engine measures the drift with compensated summation, refuses it beyond 10−8 rather than rescaling the caller's probabilities, assigns what remains to a single unit, and reports every adjustment it made. The effective expected hits it returns are the denominators the estimator divides by.
The whole ecosystem is pinned to one reference implementation by golden fixtures: integer outputs exact, floating point at machine precision — and the R package goes further, replaying the reference seed for seed.
s$accounting
$eps_sum -8.43e-15 # the measured drift
$corrected_unit 512 # who absorbed it
$adjusted_crossings 0
$max_adjustment 8.43e-15
# and a refusal is a refusal:
Error: the expected hits sum 3.2e-2 away from an
integer, beyond the 1e-8 tolerance; they do not
define a sample size
install
pak::pak(
"castlaboratory/chromy")
pip install pychromy
[dependencies]
chromy-core = "0.1"
The R package builds from source with a Rust toolchain (≥ 1.77) until the CRAN binaries are up; the crate itself has no dependencies at all.