pps · sequential · exact sample size
pychromy draws
probability-proportional-to-size samples by Chromy's (1979) sequential
method — the procedure of PROC
SURVEYSELECT METHOD=PPS_SEQ — with every unit's realised selections
within one of its expectation and the sample size always exact. Lists in,
lists out: nothing to install besides the wheel, pandas optional by
construction. The engine is the dependency-free Rust crate
chromy-core,
shared with the R package.
import pychromy
s = pychromy.sample(
size=pupils, n=40,
keys=[region, pupils], # serpentine order: region, then size
seed=20261022,
)
sum(s["hits"]) 40 # always: exact, not expected
y = [[outcome[u] for u in s["unit"]]]
pychromy.estimate_total(y, s["position"],
s["expected_hits"], s["hits"])
{"total": [37623.4], "se": [5010.2], "random_draws": 38}
The weights are 1 / expected_hits, already on the sample — ready for any design-based analysis.
what runs underneath
The expected hits accumulate down the ordered list; every time the running sum crosses an integer, one selection is released, and a single uniform per unit decides where it lands. Each unit's hits come out the floor or the ceiling of its expectation — never further — which is what probability minimum replacement means.
the api
| function | what it does |
| sample(size, n, keys=, seed=) | a whole design: size → expected hits → order → head → selection → weights |
| serpentine_order(keys, method=) | hierarchical serpentine or nested order; string keys rank by byte order |
| mpps_size(columns, n_j=) | the maximal size measure over several auxiliary variables |
| eqcv_allocation(columns, n, …) | the factors that equalise anticipated CVs across variables |
| expected_hits(size, n, min_replacement=) | sizes into expected hits; certainties resolved, or hits above one allowed |
| select(lam, order=, head=, seed=…) | the full procedure with every draw under your control |
| pmr(lam, uniforms, priority=) | the sequential selection itself, deterministic given the uniforms |
| estimate_total(y, positions, expected, hits) | the multiplicity estimator with successive-difference variance |
Matrices are lists of columns: a pandas user writes
[df[c].tolist() for c in cols],
a plain-list user writes the lists directly. Every refusal is loud — expected
hits that do not sum to an integer are refused, never rescaled, because a
selection whose inclusion probabilities nobody knows estimates nothing.
randomness, stated plainly
The engine takes its uniforms as arguments, so a selection is a pure
function of its inputs. sample(seed=…)
draws them from a fixed, documented generator (SplitMix64): a published
design can name its seed and be replayed, by anyone, years later.
R and Python reproduce themselves, not each other — each language consumes its own generator — but both run the same arithmetic, pinned bit for bit to one reference implementation by the golden fixtures the three packages share.
import numpy as np
rng = np.random.default_rng(2026)
lam = pychromy.expected_hits(size, 40)
s = pychromy.select(
lam,
u_head=rng.uniform(),
uniforms=rng.uniform(size=len(lam)).tolist(),
)
# the engine never draws: you did
install
pip install pychromy
pip install maturin
maturin develop --release
One abi3 wheel per platform serves every Python from 3.9 up. The name is
pychromy because
chromy was taken on PyPI by an
unrelated browser tool; everywhere else the ecosystem is
chromy.