pps  ·  sequential  ·  exact sample size

A survey design
in one call.

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.

a design, reproducible from its seed
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

Expected hits accumulate; crossings release selections.

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.

Six units whose expected hits sum to three; the cumulative path crosses three integer levels, and each crossing releases one selection.

the api

Eight functions, one engine.

functionwhat 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

A seed names the design.

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.

bring your own generator, if you prefer
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

One wheel, no dependencies.

PyPI
pip install pychromy
from source (Rust ≥ 1.77)
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.