Skip to content

statspai.survey

survey

Survey design and weighted estimation — StatsPAI's answer to R's survey package and Stata's svy: prefix.

Supports stratified, clustered, and weighted survey designs with design-corrected standard errors for means, totals, and regression.

import numpy as np import pandas as pd import statspai as sp rng = np.random.default_rng(0) n = 200 df = pd.DataFrame({'stratum': rng.integers(0, 4, n), ... 'education': rng.integers(8, 18, n), ... 'age': rng.integers(20, 65, n), ... 'pw': rng.uniform(1, 3, n)}) df['psu'] = df['stratum'] * 10 + rng.integers(0, 5, n) df['income'] = (10 + 2 * df['education'] + 0.3 * df['age'] ... + rng.normal(0, 5, n)) design = sp.svydesign(data=df, weights='pw', strata='stratum', ... cluster='psu') # doctest: +SKIP design.mean('income') # doctest: +SKIP design.total('income') # doctest: +SKIP design.glm('income ~ education + age') # doctest: +SKIP

SurveyDesign

Declare a complex survey design.

Parameters:

Name Type Description Default
data DataFrame

Survey microdata.

required
weights str or array - like

Sampling weights (inverse probability). If str, column name in data.

required
strata str or None

Stratification variable (column name).

None
cluster str or None

Primary sampling unit (PSU) variable (column name).

None
fpc str or None

Finite population correction, as in R survey::svydesign(fpc=) and Stata svyset, fpc(): either the population number of PSUs in the stratum (all values >= 1, at least one > 1) or the first-stage sampling fraction (all values <= 1). For an element-sampled design (cluster=None) the PSUs are the rows, so the count form is the population number of elements. Mixing the two forms, or a count smaller than the number of sampled PSUs in the stratum, raises ValueError.

None
nest bool

If True, PSU ids are relabelled to be unique within strata. PSUs are always identified within their stratum for variance and degrees-of-freedom purposes (Stata svyset convention), so with nest=False ids that repeat across strata only trigger a warning (R's svydesign refuses them without nest=TRUE).

False
lonely_psu ('remove', 'certainty', 'adjust', 'average', 'fail')

How a stratum with a single sampled PSU enters the variance, with the semantics of R's options(survey.lonely.psu=) for a single-stage design:

  • "remove" (default) / "certainty" -- the stratum contributes zero (Stata singleunit(certainty));
  • "adjust" -- the single PSU total is centred at the grand mean of PSU totals instead of its stratum mean (Stata singleunit(centered));
  • "average" -- the variance summed over strata with >1 PSU is scaled by #strata / #strata-with->1-PSU (Stata singleunit(scaled));
  • "fail" -- raise ValueError (R's default).

Unless lonely_psu is given explicitly, a lonely stratum emits a UserWarning (Stata's default reports a missing SE instead).

"remove"

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 300
>>> df = pd.DataFrame({
...     "stratum": rng.integers(0, 3, size=n),
...     "psu": rng.integers(0, 30, size=n),
...     "wt": rng.uniform(0.5, 2.0, size=n),
...     "income": rng.normal(50, 10, size=n),
... })
>>> design = sp.SurveyDesign(df, weights="wt", strata="stratum",
...                          cluster="psu", nest=True)
>>> design.n
300
>>> m = design.mean("income")
>>> type(m).__name__
'SurveyResult'

mean

mean(variables: Union[str, List[str]], alpha: float = 0.05, **kwargs: Any) -> SurveyResult

Design-corrected weighted mean(s); kwargs go to sp.svymean.

total

total(variables: Union[str, List[str]], alpha: float = 0.05, **kwargs: Any) -> SurveyResult

Design-corrected weighted total(s); kwargs go to sp.svytotal.

glm

glm(formula: str, family: str = 'gaussian', alpha: float = 0.05, **kwargs: Any) -> SurveyResult

Survey-weighted GLM; kwargs go to sp.svyglm.

calibrate

calibrate(*, margins: Optional[Dict[str, Dict]] = None, totals: Optional[Dict[str, float]] = None, variance: str = 'greg', max_iter: int = 100, tol: float = 1e-10) -> 'SurveyDesign'

Calibrated design whose standard errors account for calibration.

Raking to margins (as :func:sp.rake) or linear GREG calibration to totals (as :func:sp.linear_calibration). Unlike feeding the calibrated weights to a new design -- which treats them as fixed -- the returned design keeps the auxiliary variables, and every estimator's linearisation scores are replaced by their residuals from the weighted regression on them before the design variance is formed. Estimates of the calibration totals themselves then have zero variance, as they should.

Parameters:

Name Type Description Default
margins dict

{column: {category: population count}} -- raking. Weights are put on the population scale (sum = the margins' total), as R rake / calibrate and Stata svycal. Proportions are accepted; the weights then keep the design-weight total.

None
totals dict

{column: population total} -- linear (chi-squared distance) calibration, no intercept added (include a column of ones with total N for one).

None
variance ('greg', 'stata')

Weights of the residualising regression. "greg" uses the design weights and multiplies the residuals by the calibrated weights -- the g-weighted residual variance of Särndal, Swensson & Wretman, as R survey::calibrate computes it. "stata" uses the calibrated weights throughout, as Stata svyset, rake() / regress(). The two are asymptotically equivalent and differ at O(1/n) (0.5 % on the reference data).

"greg"
max_iter int

Raking controls (see :func:sp.rake).

100
tol int

Raking controls (see :func:sp.rake).

100

Returns:

Type Description
SurveyDesign

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 400
>>> df = pd.DataFrame({
...     "psu": rng.integers(0, 40, n), "stratum": rng.integers(0, 4, n),
...     "d": rng.uniform(20, 40, n), "sex": rng.choice(["F", "M"], n),
... })
>>> df["y"] = 5 + (df["sex"] == "M") + rng.normal(size=n)
>>> des = sp.svydesign(df, weights="d", strata="stratum",
...                    cluster="psu", nest=True)
>>> cal = des.calibrate(margins={"sex": {"F": 6000, "M": 5000}})
>>> round(float(cal.weights.sum()))
11000
>>> bool(cal.mean("y").std_error.iloc[0] < des.mean("y").std_error.iloc[0])
True

PoststratResult dataclass

Bases: ResultProtocolMixin

Result of :func:poststratify.

Attributes:

Name Type Description
estimate float

Population-weighted average of the cell predictions (posterior mean when draws are available).

sd float

Posterior standard deviation; nan for point predictions.

lower, upper float

Central posterior interval.

table DataFrame

The estimate overall (and by group with by=): estimate, median, sd, mad_sd, lower, upper, population.

draws DataFrame or None

Posterior draws of each row of table, one column per row.

cells DataFrame

The poststratification table with each cell's predicted mean.

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> cells = pd.DataFrame({"g": ["a", "b"], "N": [300, 700]})
>>> out = sp.poststratify([0.2, 0.6], cells, count="N")
>>> round(out.estimate, 2)
0.48

ReplicateDesign

A survey design described by replicate weights.

Build it with :func:sp.svrepdesign (replicate weights shipped with the data, e.g. ACS, MEPS, NHANES-style files) or :func:sp.as_svrepdesign (jackknife / BRR weights built from a :class:SurveyDesign).

Attributes:

Name Type Description
data DataFrame
weights (ndarray, shape(n))

Full-sample weights.

repweights (ndarray, shape(n, R))

Replicate analysis weights (sampling weights included).

type str

R's name of the replicate type.

scale float
rscales (ndarray, shape(R))
mse bool

Centre the squared deviations at the full-sample estimate (True) or at the mean of the replicates (False).

df float

Design degrees of freedom.

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> df = pd.DataFrame({"psu": np.repeat(np.arange(10), 20),
...                    "w": rng.uniform(1, 3, 200),
...                    "y": rng.normal(size=200)})
>>> rep = sp.as_svrepdesign(sp.svydesign(df, weights="w", cluster="psu"))
>>> rep.n_replicates
10
>>> round(float(rep.mean("y").estimate["y"]), 6) == round(
...     float(np.average(df["y"], weights=df["w"])), 6)
True

n_replicates property

n_replicates: int

Number of replicate-weight columns.

all_weights

all_weights() -> ndarray

(n, 1 + R): the full-sample weights, then the replicates.

variance

variance(theta_full: Any, theta_reps: Any) -> ndarray

Replicate covariance of a statistic.

Parameters:

Name Type Description Default
theta_full (p,) array
required
theta_reps (R, p) array

The statistic recomputed with each replicate's weights.

required

info

info() -> Dict[str, Any]

Summary recorded in a result's model_info['survey_design'].

mean

mean(variables: Union[str, List[str]], **kwargs: Any) -> Any

Replicate-weight :func:sp.svymean.

total

total(variables: Union[str, List[str]], **kwargs: Any) -> Any

Replicate-weight :func:sp.svytotal.

glm

glm(formula: str, family: str = 'gaussian', **kwargs: Any) -> Any

Replicate-weight :func:sp.svyglm.

linear_calibration

linear_calibration(data: DataFrame, totals: Dict[str, float], weight: Optional[str] = None) -> CalibrationResult

Deville-Särndal (1992) linear calibration.

Find calibrated weights w_i = g_i * d_i minimising the chi-squared distance Σ (w_i - d_i)² / d_i = Σ d_i (g_i - 1)² subject to Σ w_i x_{ik} = T_k for each auxiliary variable k with known total T_k. Closed form g = 1 + X (X' D X)^{-1} (T - X' d); identical to R survey::calibrate(design, ~ 0 + x1 + ..., population, calfun = "linear") (unbounded). No intercept is added: include a column of ones with total N for one (a dummy column for a category count).

Returns weights only; for calibration-aware standard errors use design.calibrate(totals=...) on a :class:SurveyDesign.

Parameters:

Name Type Description Default
totals dict

{column_name: population_total} for continuous auxiliary variables.

required
weight str

Design weight column. If None, uses equal weights.

None

Examples:

>>> import statspai as sp
>>> import numpy as np
>>> import pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> df = pd.DataFrame({
...     "income": rng.normal(50.0, 10.0, size=n),
...     "age": rng.normal(40.0, 12.0, size=n),
... })
>>> totals = {"income": df["income"].sum() * 1.05,
...           "age": df["age"].sum() * 0.98}
>>> res = sp.linear_calibration(df, totals=totals)
>>> cal_total = float((res.calibrated_weights * df["income"]).sum())
>>> bool(abs(cal_total - totals["income"]) < 1e-4)
True

rake

rake(data: DataFrame, margins: Dict[str, Dict], weight: Optional[str] = None, max_iter: int = 100, tol: float = 1e-10) -> CalibrationResult

Raking (iterative proportional fitting).

Starting from the design weights, cycles through the margins and rescales the weights within each category so its weighted share equals the target, until every margin holds. The fixed point is the raking-ratio (multiplicative-distance) calibration: R survey::rake run to convergence and survey::calibrate(calfun = "raking") give the same weights.

Parameters:

Name Type Description Default
data DataFrame
required
margins dict

{column_name: {category: target}}. Targets may be proportions or population counts; each margin is normalised to shares. Every category observed in data[column_name] must have a target and every target category must occur in the data (R rake also refuses both).

required
weight str

Existing design weight column. If None, starts with equal weights.

None
max_iter int

Maximum number of full sweeps over the margins.

100
tol float

Convergence: after a sweep, every weighted category share is within tol (relative) of its target. Scale-free, unlike the 1.28.0 criterion (see Notes).

1e-10

Returns:

Type Description
CalibrationResult

calibrated_weights sum to 1 (multiply by the population size to get R weights(rake(...)) when the margins are counts).

Notes

Up to 1.28.0 convergence was declared when the largest absolute change in the sum-to-one weights fell below tol = 1e-6. Those weights are O(1/n), so for large samples the loop stopped after one or two sweeps with margins still off (4e-4 relative at n = 100 000).

The calibrated weights are only weights: passing them to sp.svydesign treats them as fixed, which is not R's calibration-adjusted linearisation variance (residuals of y on the calibration variables). For standard errors that account for the calibration use design.calibrate(margins=...) on a :class:SurveyDesign (test_survey_calibrated_design_parity.py).

Examples:

>>> import statspai as sp
>>> import numpy as np
>>> import pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> df = pd.DataFrame({
...     "sex": rng.choice(["M", "F"], size=n),
...     "age_group": rng.choice(["18-34", "35-54", "55+"], size=n),
... })
>>> margins = {
...     "sex": {"M": 0.49, "F": 0.51},
...     "age_group": {"18-34": 0.30, "35-54": 0.40, "55+": 0.30},
... }
>>> res = sp.rake(df, margins=margins)
>>> bool(res.converged)
True

svydesign

svydesign(data: DataFrame, weights: Union[str, ndarray], strata: Optional[str] = None, cluster: Optional[str] = None, fpc: Optional[str] = None, nest: bool = False, lonely_psu: Optional[str] = None) -> SurveyDesign

Create a survey design object — functional interface.

Parameters are identical to :class:SurveyDesign.

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 300
>>> df = pd.DataFrame({
...     "region": rng.integers(0, 3, size=n),
...     "psu_id": rng.integers(0, 30, size=n),
...     "pw": rng.uniform(0.5, 2.0, size=n),
...     "income": rng.normal(50, 10, size=n),
...     "age": rng.normal(40, 12, size=n),
... })
>>> design = sp.svydesign(data=df, weights='pw', strata='region',
...                       cluster='psu_id', nest=True)
>>> type(design).__name__
'SurveyDesign'
>>> m = design.mean('income')
>>> g = design.glm('income ~ age')

svyglm

svyglm(formula: str, design: 'SurveyDesign', family: str = 'gaussian', alpha: float = 0.05, dof: str = 'design', subpop: Any = None, subpop_df: str = 'stata') -> SurveyResult

Survey-weighted generalised linear model.

Fits WLS (for gaussian family) or weighted IRLS (for binomial/poisson) and computes design-corrected standard errors via the sandwich estimator.

Parameters:

Name Type Description Default
formula str

"y ~ x1 + x2" style formula.

required
design SurveyDesign or ReplicateDesign

The design from :func:sp.svydesign (sandwich over strata / PSUs) or :func:sp.svrepdesign / :func:sp.as_svrepdesign (the model is refitted with every replicate's weights, as R svyglm on a svrepdesign; df are the design's, the rank of the replicate weights minus one).

required
family str

"gaussian", "binomial" (logistic), or "poisson". Canonical links; the sandwich is A^{-1} B A^{-1} with A = X' diag(w V(mu)) X and B the design covariance of the score totals w (y - mu) x (the linearisation sandwich of a design-weighted estimating equation).

'gaussian'
alpha float
0.05
dof ('design', 'residual')

Degrees of freedom for t statistics, p-values and CIs. "design" (default): #PSU - #strata, as Stata svy:. "residual": #PSU - #strata + 1 - #coefficients, as R summary.svyglm / confint.svyglm.

"design"
subpop str or bool array

Domain (subpopulation) estimation, as Stata svy, subpop() and R subset(design, ...): the estimate uses the domain rows, the variance the whole design (scores are zero outside the domain), so strata and PSUs without domain members still count. Filtering the data first would drop them and understate the variance. Rows with a missing formula variable are treated the same way (outside the domain), as R svyglm's na.action does.

None
subpop_df ('stata', 'r')

Design df for a domain: strata without domain members are dropped; Stata then counts every PSU of the remaining strata, R degf(subset(...)) only PSUs with a domain member.

"stata"

Returns:

Type Description
SurveyResult with regression coefficient estimates.

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> df = pd.DataFrame({
...     "income": rng.normal(50, 10, n),
...     "age": rng.integers(20, 65, n),
...     "region": rng.integers(0, 4, n),   # strata
...     "psu_id": rng.integers(0, 20, n),  # clusters
...     "pw": rng.uniform(1.0, 3.0, n),    # sampling weights
... })
>>> design = sp.svydesign(data=df, weights="pw", strata="region",
...                       cluster="psu_id", nest=True)
>>> res = sp.svyglm("income ~ age", design)
>>> list(res.estimate.index)
['Intercept', 'age']

svymean

svymean(variables: Union[str, List[str]], design: 'SurveyDesign', alpha: float = 0.05, deff: str = 'wor', subpop: Any = None, subpop_df: str = 'stata') -> SurveyResult

Survey-weighted mean with design-corrected standard errors.

Uses Taylor-series linearisation; matches R survey::svymean output.

Parameters:

Name Type Description Default
variables str or list of str

Column name(s) in the design's data.

required
design SurveyDesign or ReplicateDesign

The design from :func:sp.svydesign (linearisation) or :func:sp.svrepdesign / :func:sp.as_svrepdesign (replicate weights: the statistic is recomputed with every replicate's weights, as R svymean on a svrepdesign; DEFF is then not computed).

required
alpha float

CIs use the t distribution with the design df (#PSU - #strata), as Stata svy: and R confint(..., df = degf(design)); R's confint.svystat default is the normal quantile.

0.05
deff ('wor', 'replace')

Denominator of the design effect: SRS without replacement with N = sum of weights (R deff=TRUE; Stata estat effects when an fpc is declared) or with replacement (R deff="replace"; Stata estat effects without fpc).

"wor"
subpop str or bool array

Domain (subpopulation) estimation, as Stata svy, subpop() and R subset(design, ...): the estimate uses the domain rows, the variance the whole design (scores are zero outside the domain), so strata and PSUs without domain members still count. Filtering the data first would drop them and understate the variance. DEFF is not computed for domains (NaN).

None
subpop_df ('stata', 'r')

Design df for a domain: strata without domain members are dropped; Stata then counts every PSU of the remaining strata, R degf(subset(...)) only PSUs with a domain member.

"stata"

Returns:

Type Description
SurveyResult

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> df = pd.DataFrame({
...     "income": rng.normal(50, 10, n),
...     "region": rng.integers(0, 4, n),   # strata
...     "psu_id": rng.integers(0, 20, n),  # clusters
...     "pw": rng.uniform(1.0, 3.0, n),    # sampling weights
... })
>>> design = sp.svydesign(data=df, weights="pw", strata="region",
...                       cluster="psu_id", nest=True)
>>> res = sp.svymean("income", design)
>>> list(res.estimate.index)
['income']

svytotal

svytotal(variables: Union[str, List[str]], design: 'SurveyDesign', alpha: float = 0.05, deff: str = 'wor', subpop: Any = None, subpop_df: str = 'stata') -> SurveyResult

Survey-weighted total with design-corrected standard errors.

Parameters:

Name Type Description Default
variables str or list of str
required
design SurveyDesign or ReplicateDesign

The design from :func:sp.svydesign (linearisation) or :func:sp.svrepdesign / :func:sp.as_svrepdesign (replicate weights: the statistic is recomputed with every replicate's weights, as R svymean on a svrepdesign; DEFF is then not computed).

required
alpha float

CIs use the t distribution with the design df (#PSU - #strata), as Stata svy: and R confint(..., df = degf(design)); R's confint.svystat default is the normal quantile.

0.05
deff ('wor', 'replace')

Denominator of the design effect: SRS without replacement with N = sum of weights (R deff=TRUE; Stata estat effects when an fpc is declared) or with replacement (R deff="replace"; Stata estat effects without fpc).

"wor"
subpop str or bool array

Domain (subpopulation) estimation, as Stata svy, subpop() and R subset(design, ...): the estimate uses the domain rows, the variance the whole design (scores are zero outside the domain), so strata and PSUs without domain members still count. Filtering the data first would drop them and understate the variance. DEFF is not computed for domains (NaN).

None
subpop_df ('stata', 'r')

Design df for a domain: strata without domain members are dropped; Stata then counts every PSU of the remaining strata, R degf(subset(...)) only PSUs with a domain member.

"stata"

Returns:

Type Description
SurveyResult

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> df = pd.DataFrame({
...     "income": rng.normal(50, 10, n),
...     "region": rng.integers(0, 4, n),   # strata
...     "psu_id": rng.integers(0, 20, n),  # clusters
...     "pw": rng.uniform(1.0, 3.0, n),    # sampling weights
... })
>>> design = sp.svydesign(data=df, weights="pw", strata="region",
...                       cluster="psu_id", nest=True)
>>> res = sp.svytotal("income", design)
>>> list(res.estimate.index)
['income']

poststratify

poststratify(fit: Any, cells: DataFrame, count: str = 'N', by: Optional[Union[str, Sequence[str]]] = None, level: float = 0.95) -> PoststratResult

Average model predictions over the cells of a known population.

A sample that over- or under-represents some groups gives a biased mean. Fit a regression of the outcome on the variables that define the groups, predict the mean of every population cell, and weight the predictions by the population cell counts: sum_j N_j theta_j / sum_j N_j. With a Bayesian fit the same average is taken draw by draw, so the estimate carries the uncertainty of the regression.

Parameters:

Name Type Description Default
fit fitted model, array of predictions, or matrix of draws

A model with posterior_epred(data) (sp.bayes_regress): full posterior. A classical model with predict(data): point estimate only. Or the predictions themselves, one per cell, or draws by cells.

required
cells DataFrame

The poststratification table: one row per population cell with the columns the model's formula needs and the cell count.

required
count str

Column of cells holding the population count (or share) of each cell.

'N'
by str or list of str

Also report the estimate within the levels of these columns of cells (subpopulations, e.g. states).

None
level float

Mass of the posterior interval.

0.95

Returns:

Type Description
PoststratResult

Raises:

Type Description
MethodIncompatibility

(a ValueError.) The count column is absent, not numeric, negative, missing or all zero; a cell has no finite prediction; the model cannot predict a cell (for instance a factor level it was not fitted on).

Notes

The posterior describes uncertainty in the cell means given the model. It takes the population counts as known and assumes that, within a cell, respondents resemble the people they stand for; when the counts are estimates or nonresponse within cells is selective, the true uncertainty is larger.

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> poll = pd.DataFrame({"pid": rng.choice(["R", "D", "I"], 600,
...                                        p=[0.5, 0.3, 0.2])})
>>> share = poll["pid"].map({"R": 0.8, "D": 0.1, "I": 0.4})
>>> poll["vote"] = (rng.uniform(size=600) < share).astype(int)
>>> fit = sp.bayes_regress("vote ~ C(pid)", poll, model="logit",
...                        draws=4000, burnin=1000, seed=1)
>>> cells = pd.DataFrame({"pid": ["R", "D", "I"], "N": [0.33, 0.36, 0.31]})
>>> out = sp.poststratify(fit, cells, count="N")
>>> bool(0.35 < out.estimate < 0.50)
True
References

gelman2020regression

as_svrepdesign

as_svrepdesign(design: Any, type: str = 'auto', rho: float = 0.0, mse: Optional[bool] = None) -> ReplicateDesign

Replicate weights from a strata / PSU design (R as.svrepdesign).

Parameters:

Name Type Description Default
design SurveyDesign

Built with :func:sp.svydesign; not calibrated.

required
type ('auto', 'JK1', 'JKn', 'BRR', 'Fay')

'auto': JKn with strata, JK1 without.

  • JK1 (unstratified): one replicate per PSU, the PSU deleted and the others scaled by R/(R-1); scale = (R-1)/R (1-f).
  • JKn: one replicate per PSU, deleting it and scaling the rest of its stratum by n_h/(n_h-1); rscales = (n_h-1)/n_h (1-f_h). Every stratum needs two or more PSUs.
  • BRR / Fay: exactly two PSUs per stratum; half-samples from the columns of a Sylvester Hadamard matrix (order the smallest power of two above the number of strata), weights 2 / 0 (BRR) or 2 - rho / rho (Fay). The half-samples are fully balanced but not R's: for linear statistics the variance is the same; for others it can differ at the order of the replication noise. To reproduce R exactly, export R's replicate weights and use :func:sp.svrepdesign.

Bootstrap weights are random and are not built here: pass weights produced elsewhere to :func:sp.svrepdesign with type='bootstrap'.

'auto'
rho float

Fay coefficient for type='Fay'.

0
mse bool

As :func:sp.svrepdesign.

None

Returns:

Type Description
ReplicateDesign

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(1)
>>> df = pd.DataFrame({"h": np.repeat(np.arange(4), 30),
...                    "psu": np.repeat(np.arange(12), 10),
...                    "w": rng.uniform(1, 2, 120),
...                    "y": rng.normal(size=120)})
>>> des = sp.svydesign(df, weights="w", strata="h", cluster="psu")
>>> sp.as_svrepdesign(des, type="JKn").n_replicates
12

svrepdesign

svrepdesign(data: DataFrame, repweights: Any, weights: Union[str, ndarray, None] = None, type: str = 'JK1', rho: Optional[float] = None, scale: Optional[float] = None, rscales: Optional[Sequence[float]] = None, mse: Optional[bool] = None, combined_weights: bool = True, df: Optional[float] = None) -> ReplicateDesign

Declare a survey design by its replicate weights.

R survey::svrepdesign / Stata svyset [pw=w], brrweight(...), jkrweight(...), bsrweight(...), sdrweight(...).

Parameters:

Name Type Description Default
data DataFrame

Survey microdata.

required
repweights list of str, str or (n, R) array

The replicate-weight columns, a regular expression matching their names ('^repwt'), or the matrix itself.

required
weights str or array

Full-sample sampling weights. Without it they are the mean of the replicate weights (with a warning).

None
type ('BRR', 'Fay', 'JK1', 'JKn', 'JK2', 'bootstrap', 'ACS', 'successive-difference', 'other')

Replicate method; sets scale / rscales / mse as R svrepdesign does: BRR 1/R; Fay 1/(R (1 - rho)^2); JK1 (R - 1)/R unless given; JKn 1 with rscales required; JK2 1; bootstrap 1/(R - 1); ACS / successive-difference 4/R with mse=True; other 1.

'BRR'
rho float

Fay coefficient (type='Fay').

None
scale optional

Overall and per-replicate multipliers of the squared deviations (ignored, with a warning, for the types that fix them).

None
rscales optional

Overall and per-replicate multipliers of the squared deviations (ignored, with a warning, for the types that fix them).

None
mse bool

True: deviations from the full-sample estimate (Stata mse); False: from the mean of the replicates. Default False, True for ACS / successive difference.

None
combined_weights bool

The replicate columns already include the sampling weights (the usual case). False: they are multipliers of weights.

True
df float

Design degrees of freedom; default the numerical rank of the replicate weights minus one (R degf).

None

Returns:

Type Description
ReplicateDesign

Usable wherever design= is accepted by an estimator with a weights= argument (the statistic is recomputed with every replicate's weights), and by :func:sp.svymean, :func:sp.svytotal, :func:sp.svyglm.

Examples:

>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> df = pd.DataFrame({"y": rng.normal(size=100), "w": 1.0})
>>> for r in range(10):
...     df[f"rw{r}"] = np.where(np.arange(100) // 10 == r, 0.0, 10 / 9)
>>> rep = sp.svrepdesign(df, repweights="^rw", weights="w", type="JK1")
>>> rep.n_replicates, rep.df
(10, 9.0)