Skip to main content

weightpipe

CI Python 3.11+ License: MIT

Survey weighting in Python: define a sampling design, apply a weighting recipe, then estimate means, totals, proportions, ratios, medians, and design-based GLMs with bootstrap, jackknife, or linearized standard errors.

The point is one API for the whole path — design, eligibility, nonresponse, calibration, trim, estimates, and sample-size planning — instead of stitching specialized raking, weighting, and variance packages together.

Install

Install from GitHub (latest main):

pip install "git+https://github.com/sdaza/weightpipe.git"
# or
uv add "git+https://github.com/sdaza/weightpipe.git"

Requires Python 3.11+. Not published on PyPI yet.

Quickstart

WeightPipe is the single entry point: describe the sample, chain any adjustments, then read weights or estimates. Weights are computed on first use, so estimating with no adjustment steps works too.

from weightpipe import WeightPipe, population_totals

totals = population_totals(pop, "~ region + sex + age")

pipe = (
    WeightPipe(df, weight="pw", psu="psu", strata="stratum")
    .unknown_eligibility(unknown="unknown", by=["region"], cluster="hh")
    .drop_ineligible(ineligible="ineligible")
    .nonresponse(
        respondent="responded",
        method="propensity",
        engine="logit",  # or "gbm" / "forest"
        formula="~ region + sex",
        num_classes=5,
        cluster="hh",
    )
    .calibrate(
        method="linear",
        formula="~ region + sex + age",
        totals=totals,
        # engine="forest", population=pop,  # tree-embedding GREG (needs population microdata)
        # assist="propensity",              # add p̂ to linear calibration
        # assist="propensity_class",        # rake/calibrate while keeping class mass
    )
    .trim(max_ratio=4.0, reference="median", redistribute=True)
)

pipe.weights  # final weights
pipe.collect_weights(keep_intermediate=True)  # weights and per-step factors
pipe.diagnostics  # per-step diagnostics and alerts

pipe.estimate.mean(["income", "food_share"], by="urban_rural")
pipe.estimate.ratio(["food", "miles"], ["income", "trips"], by="urban_rural")
pipe.estimate.median("y", variance="bootstrap", replicates=200, seed=1)
pipe.estimate.glm("employed ~ region + age", family="binomial", variance="linearization")

Design weights alone are enough to estimate:

WeightPipe(df, N=10_000).estimate.mean("y", variance="bootstrap", seed=1)

Each step returns a new pipe, so you can branch from a common base and compare.

Sampling designs

Pass the sampling inputs; the design type is inferred automatically.

WeightPipe(df)                                            # no weight → base_weight=1 (logged)
WeightPipe(df, N=10_000)                                  # SRS: w = N / n
WeightPipe(df, strata="region", N_h={"North": 5000, ...}) # stratified SRS: w = N_h / n_h
WeightPipe(df, weight="pw", psu="psu")                    # cluster
WeightPipe(df, weight="pw", psu="psu", strata="stratum")  # stratified cluster
WeightPipe(df, probabilities=["p1", "p2"], psu="psu")     # multi-stage: w = 1 / (p1 * p2)
WeightPipe(df, stage_weights=["w1", "w2"], psu="psu")     # multi-stage: w = w1 * w2
WeightPipe(df, weight="pw")                               # existing design weights

Check what was inferred with pipe.kind.

Multi-stage clusters (one psu)

You can draw several nested stages in the field. You still name one psu for variance: the first-stage unit (the ultimate cluster). Later stages go into the weight, not into extra PSU columns.

Example: sample schools, then classes inside those schools, then students inside those classes. That is a three-stage cluster sample.

Stage Unit What it is for
1 School PSU — independent draws; use this in psu=
2 Class Nested in school — inclusion goes in the weight
3 Student Row in the data — inclusion goes in the weight
# w = 1 / (p_school * p_class * p_student); SEs resample schools
WeightPipe(
    df,
    probabilities=["p_school", "p_class", "p_student"],
    psu="school",
    # strata="district",  # if schools were drawn within strata
)

If you already have per-stage weights (w_k = 1/π_k), use stage_weights=["w_school", "w_class", "w_student"] with the same psu="school". If the product is already in one column, WeightPipe(df, weight="pw", psu="school") is enough for variance (kind is then "cluster" rather than "multistage").

Why psu is school, not class. Classes are selected independently inside a sampled school, not from a national list of all classes. A class can appear only if its school was selected first; two classes in the same school share that school draw. Bootstrap and jackknife therefore resample whole schools (every selected class and student in that school moves together). Setting psu to class would treat classes as independent first-stage units and understate SEs; setting it to student would treat the sample like an SRS of students.

strata= is separate (for example district). That is the groups within which schools were drawn, not a second PSU.

If you take every student in the selected classes, stage 3 has π = 1 and the sample is two-stage (schools → classes). psu is still "school".

Weighting steps

Chain adjustments on a pipe, in order:

Step What it does
unknown_eligibility Redistribute unknown eligibility within cells; optional household cluster=
drop_ineligible Set ineligible units to weight 0
nonresponse Weighting-class or propensity (engine="logit", "gbm", or "forest"); optional cluster=
calibrate Raking, post-stratification, or linear/GREG; engine="forest"/"gbm" for tree-embedding GREG (population= required); optional assist= (propensity_class auto-converts proportions= → absolute margins before attaching class totals)
trim Cap extreme weights by ratio to median/base/value
trim_weights Automatic Tukey or Potter trimming

After calibrate, step diagnostics include a tidy margin_table (target vs achieved). You can also check weighted category margins anytime:

pipe.margins("sex")  # current weighted totals / proportions
pipe.margins(targets="calibrate")  # reuse last calibrate targets
pipe.margins("sex", proportions={"sex": {"M": 0.5, "F": 0.5}})
pipe.diagnostics["steps"]["calibrate"]["margin_table"]

Covariate balance compares base vs final weights to population targets via standardized mean differences (SMD), for continuous and categorical covariates:

report = pipe.balance(
    ["age", "sex", "region"],
    means={"age": 40.0},
    proportions={"sex": {"M": 0.5, "F": 0.5}, "region": {"N": 0.5, "S": 0.5}},
)
report.table  # before / after / target / smd_* / balanced
report.summary  # max |SMD|, n_imbalanced, ESS before/after
# or: pipe.balance(["age", "sex"], target=pop_df, target_weight="N")

Estimation

Pass one variable or a list. by= splits into domains (same idea as R svyby or svy's sample.estimation.mean(..., by=...)). Replicate weights are built once and reused.

pipe.estimate.mean(["income", "food_share"], by="urban_rural")
pipe.estimate.mean("y", variance="jackknife")
pipe.estimate.proportion("employed", by="region", variance="bootstrap", replicates=200, seed=1)
pipe.estimate.total("y", variance="linearization")
pipe.estimate.ratio("y", "x", variance="jackknife")
pipe.estimate.ratio(["food", "housing"], "income")  # one denominator
pipe.estimate.ratio(["food", "miles"], ["income", "trips"], by="urban_rural")  # paired
pipe.estimate.median("y", variance="jackknife")

# still valid
pipe.estimate("y", estimand="mean", variance="jackknife")
pipe.estimation.mean("y")  # alias of estimate

The result is one row per variable × domain, with estimate, se, cv, and a confidence interval.

Supported estimands: mean, total, proportion, ratio, median.
Variance: bootstrap (Rao–Wu; default) and jackknife (delete-a-PSU) re-run the recipe on each replicate so SEs include estimated weights. linearization is the fast ultimate-cluster Taylor SE that treats the fitted weights as fixed (survey-style). Median has no linearized SE.

Design-based GLM

pipe.estimate.glm is survey::svyglm-style regression: survey-weighted IRLS for the coefficients, then design-based SEs. It is not a statsmodels / sklearn wrapper with weights passed through.

pipe.estimate.glm("employed ~ region + age", family="binomial", variance="linearization")
pipe.estimate.glm("y ~ region", family="gaussian", variance="jackknife")
pipe.estimate.glm("count ~ age", family="poisson")
pipe.estimate.glm("y ~ 1", family="gaussian", variance="linearization")  # intercept = Hájek mean
family= Link Outcome
gaussian (normal) identity continuous
binomial (logit, logistic, quasibinomial) logit 0/1
poisson (log, quasipoisson) log counts

The result is one row per coefficient (term), with the same estimate / se / cv / CI columns as other estimands. Categorical predictors use sorted levels and drop the first (R treatment contrasts with alphabetical levels). An intercept-only gaussian matches pipe.estimate.mean; an intercept-only binomial is logit of the weighted proportion, not svymean.

variance= is the same as for means: linearization is the Binder sandwich (ultimate-cluster, weights fixed); bootstrap / jackknife re-fit β on recipe-aware replicate weights. Gold matches survey::svyglm. There is no by= on GLM, and no tabs / Rao–Scott.

Sample-size planning

Plan sample sizes before fieldwork, then build a pipe after data collection:

from weightpipe import (
    WeightPipe,
    allocate_strata,
    allocation_table,
    margin_of_error,
    sample_size,
    stratified_margin_of_error,
)

sample_size(0.05)  # n for ±5 percentage points at 95% confidence
margin_of_error(384)

populations = {"North": 5_000, "South": 15_000}
plan = allocation_table(populations, sample=400, method="mixed")
stratified_margin_of_error(plan["sample"], population=plan["population"])

# After drawing the planned cases:
pipe = WeightPipe(df, strata="region", N_h=populations)

Allocation methods: mixed, root, neyman, stdev, and error.

Logging

weightpipe stays silent unless you ask for messages. Turn them on to see notes such as a missing design weight:

import weightpipe as wp

wp.setup_logging("INFO")  # 2026-08-17 12:03:13  No design weight provided; ...
wp.set_log_level("WARNING")  # quieter

Messages go to stderr through the weightpipe logger, so your own logging config keeps working if you'd rather configure it yourself.

Lower-level API

WeightPipe wraps two objects you can also use directly: Design (sampling inputs and base weights) and Recipe (the adjustment steps, run with prep()). Reach for them when you want to hold a design or an unfitted recipe on its own, for example to build replicate weights by hand.

from weightpipe import Design, Recipe, estimate, estimate_glm

design = Design(df, weight="pw", psu="psu", strata="stratum")
recipe = Recipe.from_design(design).step_calibrate(method="raking", proportions=props)
fitted = recipe.prep()
estimate(recipe, ["y", "x"], by="region", fitted=fitted, variance="jackknife")
estimate_glm(recipe, "employed ~ region", family="binomial", fitted=fitted, variance="linearization")

Examples

Interactive scripts (run cell-by-cell or with Python):

Comparisons

weightpipe is meant to integrate the steps you usually assemble from several tools, and to make that cascade easier in Python: one WeightPipe, in order, with diagnostics and estimates at the end.

Specialized packages still do one slice well.

Raking / IPF only. Weightipy 0.4.2 is a fast RIM engine (dicts or census tables, nested/segmented RIM, Kish efficiency). ipfn is a small Python IPF helper. R anesrake is ANES-style raking. Quantipy3 is the market-research stack Weightipy was forked from.

Weighting recipes. R weightflow is the closest recipe-style analogue (eligibility → NR → calibrate → trim, plus recipe-aware bootstrap/jackknife). R icarus and ReGenesees are calibration-focused (raking, linear/GREG, official-statistics workflows).

Design-based analysis. The current Python peer is svy (docs): one svy.Design + svy.Sample, then .estimation, .weighting, .glm, tabs, and BRR/SDR. It is the successor of archived samplics (same author) and is validated against R survey. In R, survey is still the usual toolkit once weights exist (svydesign, svymean, svyby, svyglm, calibrate, rake, as.svrepdesign); srvyr is a dplyr front end on it.

weightpipe is the recipe-first path: eligibility → NR → calibrate → trim on one WeightPipe, then the same estimate shape (pipe.estimate.mean(["income", "food_share"], by="urban_rural"), pipe.estimate.glm("y ~ x", family="binomial")). Use svy or R survey when you need sample selection, categorical tests, BRR/SDR, or small-area estimation (svy-sae).

Planning and selection. svy draws probability samples (SRS, systematic, PPS, multi-stage) and plans sizes. R sampler is weightpipe's planning gold. R PracTools, surveyplanning, and sampling (Tillé) are other R kits. weightpipe plans sizes, then you attach the collected microdata — it does not draw the field sample.

Diagnostics. Meta balance is covariate balance (SMD / Love plots), not a weighting recipe. weightpipe's balance() is the SMD before/after check; margins() checks calibration targets.

Marks mean the package has a first-class API for that row. "via survey" means weightflow builds the weights (or replicate weights) and you estimate in survey / srvyr.

weightpipe svy Weightipy 0.4.2 R survey R weightflow R sampler
Language Python Python Python R R R
Design object (strata / PSU / weights) base weights¹
Multi-stage inclusion weights
Sample selection (draw PPS / SRS)
Unknown eligibility / drop ineligible
Nonresponse class + propensity class class + propensity
Raking (RIM / IPF)
Nested / segmented RIM
Post-stratification
Linear / GREG calibration
Trim
One chained recipe fluent steps raking only
Recipe-aware replicate weights
Taylor linearization SE via survey
Bootstrap / jackknife SE
BRR / SDR
Domain estimates (by=) via survey
Several variables at once via survey
Design-based GLM via survey
Tabs / Rao–Scott via survey
Covariate balance (SMD)
Sample-size planning / allocation
Small-area estimation svy-sae

¹ weightflow takes design weights you already computed (weighting_spec(..., base_weights=)). It uses strata/PSU when it resamples replicates. It does not infer SRS / stratified / cluster from N / N_h the way WeightPipe / svy.Design / survey::svydesign do.

Weightipy stays a focused raking library. weightpipe uses the same class of iterative raking as one calibrate(method="raking") step, then continues with eligibility, nonresponse, GREG, trim, balance, estimates (including design-based GLM), and planning. svy and R survey remain broader for sample selection, categorical tests, tabs, BRR/SDR, and SAE.

Numerical gold

Shared methods are checked on the same toy frames against frozen CSVs in tests/gold/ (CI) and optionally live R / svy. These are correctness checks, not runtime speed benchmarks.

Method R survey R weightflow svy² R sampler
Unknown eligibility
Drop ineligible
Weighting-class NR
Raking
Post-stratification
Linear / GREG
Trim (value cap, no redistribute)
NR → raking cascade
Full cascade (eligibility → trim)
Mean, total, ratio, median
Design-based GLM
Sample size / allocation / MOE

² Frozen *_svy.csv plus live checks from uv sync --extra gold.

Tolerances are tight (1e-121e-6 depending on the solver). Median vs survey::svyquantile may differ by one unique y value because quantile definitions can differ. Design-based GLM coefficients and linearized SEs match survey::svyglm (gaussian / quasibinomial / quasipoisson). Logit propensity uses the same 1/p adjustment as weightflow, but sklearn vs R glm coefficients are not bit-matched.

Gold testing

Gold tests compare weightpipe to frozen reference outputs (and optionally live svy / R) on the same toy inputs. CSVs live in tests/gold/. Python weighting gold is svy; estimands and GLM gold are R survey.

Run the gold suite (same idea as CI):

uv sync --all-groups --extra gold --locked
uv run --extra gold pytest -m gold -q

Or run everything (unit tests + gold):

uv run --extra gold pytest -q

Frozen CSV checks always run when the files are present. Live svy checks need the gold extra. Live R checks need R packages and optionally uv sync --extra r-gold.

Regenerate frozen CSVs (local only — not CI). Do this when a reference tool or gold scenario intentionally changes, then commit the updated files under tests/gold/:

uv run --extra gold python tests/gold/generate_svy_gold.py
Rscript tests/gold/generate_r_gold.R          # needs R packages survey + weightflow
Rscript tests/gold/generate_sampler_gold.R    # needs R package sampler (or SAMPLER_R_DIR)

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

weightpipe-0.1.0.tar.gz (90.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

weightpipe-0.1.0-py3-none-any.whl (75.1 kB view details)

Uploaded Python 3

File details

Details for the file weightpipe-0.1.0.tar.gz.

File metadata

  • Download URL: weightpipe-0.1.0.tar.gz
  • Upload date:
  • Size: 90.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for weightpipe-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3e0f84081f02d2877e7d6e1ad5bf6fde4d5aef30afb292a390bc6476abdff4f0
MD5 eb7b1b07257e53b7796a85a1c6fba014
BLAKE2b-256 56bac713e87fff7543a5216e9629756d834811ae9e4ac305dd370b5f01815bc2

See more details on using hashes here.

Provenance

The following attestation bundles were made for weightpipe-0.1.0.tar.gz:

Publisher: publish-to-pypi.yml on sdaza/weightpipe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file weightpipe-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: weightpipe-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 75.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for weightpipe-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c7f5a358773cbad3f0bbdb66b81fa86f2d3b916f3f2e095e00ce79b46dee00e5
MD5 0603e68a150be703ccd60dbf111df599
BLAKE2b-256 b1c1a0f3fa5bb648eb362127512951c4027f5045fe821ce1d7409733f924b697

See more details on using hashes here.

Provenance

The following attestation bundles were made for weightpipe-0.1.0-py3-none-any.whl:

Publisher: publish-to-pypi.yml on sdaza/weightpipe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page