Introduction to predictive margins with svymargins package

Predictive margins (PM)

Usually the distribution of confounding variables like age differ in the groups we wish to compare, thus an adjustment is needed. In a linear regression model the regression coefficient might reveal the adjusted difference of the groups directly. However, in generalized linear models (GLM) like in logistic regression models the parameters represent (log) odds ratios (OR), but an easier interpretation can be based on risk ratios (RR) or risk differences (RD), in which case the adjusted comparisons cannot be based on a single regression parameter any more.

In predictive margins (Graubard and Korn 1999, Biometrics), some covariates are given a fixed value for all observations. For example,

  1. A regression analysis is performed, for example with three covariates: age (continuous), sex (factor) and education (factor).
  2. The covariate sex is given value "M" (men) for all individuals.
  3. Using the parameter estimates of the regression model and the covariate values, the predicted value is calculated for each individual.
  4. The (weighted) mean of the individual predictive values is calculated (“predictive margin”).
  5. Steps 2-4 are repeated, but using different values for the selected covariate, in this case by setting sex to value "F" (women).

The values of other covariates are retained at their observed values, thus the (weighted) means are adjusted means, and we can compare, in this example men and women, assuming that the age and education distributions were the same (as in the full data).

Other software

The SUDAAN software implemented the PREDMARG statement in their statistical analysis procedures, accounting for the survey sampling design and weights.
In the R statistical software, the survey package implements the same functionality via the svypredmean function.
The limitations in these approaches were: (i) It was not possible to choose the covariates to be fixed freely. E.g. if categorical age and sex were main effects in the regression model, it was not possible to define the margins for all combinations of the age and sex categories. The researcher had to include the interaction term in the regression model as well. (ii) It was not possible to restrict the predictive margins to a subset of the full dataset. For example, if the analysis was based on repeated cross-sectional surveys, each representing the population in different years, then the predictive margins represented the average of the populations at different years. A more reasonable solution would be to restrict the predictive margins to the latest year, so that the adjusted means (or prevalences) would represent the population at that year. (iii) The R package survey function svypredmean does not support multinomial logistic regression models, which is commonly used in analysing nominal outcomes.

In Stata, the procedure margins solved these problems.

There is a need for a flexible solution also for R users, and this package aims to provide a solution to the limitations (i)–(iii).

Load R packages

library(survey)
library(svymargins)
library(dplyr)
library(svyVGAM) # for multinomial logistic regression models

Linear model

Generate simulated data

Generate the outcome variable y from normal distribution with three covariates: age (continuous), sex (factor) and education (factor).

n <- 1000
# Generate data:
set.seed(1234)
d <- data.frame(sex=factor(sample(c("M", "F"), n, replace=TRUE)),
                education=factor(sample(c("low", "middle", "high"), n, replace=TRUE)))
d <- d |> mutate(age=runif(n, 0, 40) + as.numeric(education) * 20,
                 y=rnorm(n, sd=5) + as.numeric(education) + 0.05 * age)
summary(d)
#>  sex      education        age              y          
#>  F:486   high  :329   Min.   :20.10   Min.   :-11.170  
#>  M:514   low   :338   1st Qu.:46.09   1st Qu.:  1.400  
#>          middle:333   Median :59.41   Median :  5.082  
#>                       Mean   :60.32   Mean   :  4.952  
#>                       3rd Qu.:75.46   3rd Qu.:  8.495  
#>                       Max.   :99.90   Max.   : 20.009

Analysis with Stata

An additional variable one with constant value 1 for all observations was added to the dataframe d.

. svyset [pw=one]

Sampling weights: one
             VCE: linearized
     Single unit: missing
        Strata 1: <one>
 Sampling unit 1: <observations>
           FPC 1: <zero>
. svy: reg y i.education age i.sex
(running regress on estimation sample)

Survey: Linear regression

Number of strata =     1                              Number of obs   =  1,000
Number of PSUs   = 1,000                              Population size =  1,000
                                                      Design df       =    999
                                                      F(4, 996)       =  40.25
                                                      Prob > F        = 0.0000
                                                      R-squared       = 0.1355

------------------------------------------------------------------------------
             |             Linearized
           y | Coefficient  std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
   education |
        low  |    1.19902   .4801791     2.50   0.013     .2567451    2.141296
     middle  |   2.226229   .6605355     3.37   0.001     .9300326    3.522425
             |
         age |   .0576193   .0136955     4.21   0.000      .030744    .0844946
             |
         sex |
          M  |   .0579228   .3125908     0.19   0.853    -.5554872    .6713327
       _cons |   .3005586   .6491026     0.46   0.643    -.9732024     1.57432
------------------------------------------------------------------------------
. margins sex education, grand vce(unconditional)

Predictive margins

Number of strata =     1                               Number of obs   = 1,000
Number of PSUs   = 1,000                               Population size = 1,000
                                                       Design df       =   999

Expression: Linear prediction, predict()

------------------------------------------------------------------------------
             |             Linearized
             |     Margin   std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
         sex |
          F  |   4.922536   .2305945    21.35   0.000     4.470031    5.375041
          M  |   4.980459   .2281417    21.83   0.000     4.532767    5.428151
             |
   education |
       high  |   3.805706   .3909045     9.74   0.000     3.038617    4.572794
        low  |   5.004726   .2794313    17.91   0.000     4.456386    5.553066
     middle  |   6.031934   .3733541    16.16   0.000     5.299286    6.764582
             |
       _cons |   4.952309   .1678844    29.50   0.000     4.622862    5.281755
------------------------------------------------------------------------------
. margins education, at(age=(40 50 60 70)) vce(unconditional)

Predictive margins

Number of strata =     1                               Number of obs   = 1,000
Number of PSUs   = 1,000                               Population size = 1,000
                                                       Design df       =   999

Expression: Linear prediction, predict()
1._at: age = 40
2._at: age = 50
3._at: age = 60
4._at: age = 70

------------------------------------------------------------------------------
             |             Linearized
             |     Margin   std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
         _at#|
   education |
     1#high  |   2.635102   .2769989     9.51   0.000     2.091535    3.178668
      1#low  |   3.834122    .396328     9.67   0.000     3.056391    4.611853
   1#middle  |    4.86133   .6045408     8.04   0.000     3.675015    6.047646
     2#high  |   3.211294   .3065697    10.47   0.000       2.6097    3.812889
      2#low  |   4.410315   .3139029    14.05   0.000      3.79433    5.026299
   2#middle  |   5.437523   .4837841    11.24   0.000     4.488173    6.386873
     3#high  |   3.787487   .3856877     9.82   0.000     3.030636    4.544338
      3#low  |   4.986507    .278402    17.91   0.000     4.440188    5.532827
   3#middle  |   6.013716   .3743501    16.06   0.000     5.279113    6.748318
     4#high  |    4.36368   .4909568     8.89   0.000     3.400255    5.327104
      4#low  |     5.5627    .306584    18.14   0.000     4.961078    6.164322
   4#middle  |   6.589908   .2893828    22.77   0.000      6.02204    7.157776
------------------------------------------------------------------------------

Regression analysis

For simplicity, we use independent sampling design with weights equal to unity. The linear regression model is a main effects model.

# Create survey design:
my.svy <- svydesign(~ 1, weights=~ 1, data=d)
# Run regression analysis:
res <- svyglm(y ~ education + age + sex, design=my.svy)
summary(res)
#> 
#> Call:
#> svyglm(formula = y ~ education + age + sex, design = my.svy)
#> 
#> Survey design:
#> svydesign(~1, weights = ~1, data = d)
#> 
#> Coefficients:
#>                 Estimate Std. Error t value Pr(>|t|)    
#> (Intercept)      0.30056    0.64910   0.463  0.64344    
#> educationlow     1.19902    0.48018   2.497  0.01268 *  
#> educationmiddle  2.22623    0.66054   3.370  0.00078 ***
#> age              0.05762    0.01370   4.207 2.82e-05 ***
#> sexM             0.05792    0.31259   0.185  0.85303    
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> (Dispersion parameter for gaussian family taken to be 24.3666)
#> 
#> Number of Fisher Scoring iterations: 2

Predictive margins

Define the desired margins

In this package, the margins are defined as a (names) list. Each element produces a set of predictive margins, for example, the value of the covariate sex is given value “M” (men) for all individuals.

  • The package automatically searches for all observed levels of factor variables, so it is sufficient to give only the names of the covariates: list("sex").
  • For numeric covariates, the desired covariate values must be given: e.g. list(age=seq(40,70,10)).

The null margin is an empty list, and in this case all covariates retain their observed values. Two (or more) covariates can be set at the same time by including them in one entry of the list, for example, list("education", age=seq(40,70,10)) calculates the predictive margins for all combinations of education and age.

# Define margins as a named list:
target.l <- list(null=list(),
                 sex=list("sex"),
                 educ=list("education"),
                 age=list(age=seq(40,70,10)),
                 educ_age=list("education", age=seq(40,70,10)))

Calculate the predictive margins

After the regression analysis has been conducted and desired margins defined, the predictive margins can be calculated with the svymargins function:

# Calculate predictive margins:
svymargins(res, groupfactor=target.l)
#>                      mean     SE
#> null               4.9523 0.1679
#> sex,F              4.9225 0.2321
#> sex,M              4.9805 0.2267
#> educ,high          3.8057 0.3904
#> educ,low           5.0047 0.2806
#> educ,middle        6.0319 0.3729
#> age,40             3.7817 0.3216
#> age,50             4.3579 0.2135
#> age,60             4.9341 0.1588
#> age,70             5.5103 0.2059
#> educ_age,high,40   2.6351 0.2770
#> educ_age,high,50   3.2113 0.3066
#> educ_age,high,60   3.7875 0.3857
#> educ_age,high,70   4.3637 0.4910
#> educ_age,low,40    3.8341 0.3963
#> educ_age,low,50    4.4103 0.3139
#> educ_age,low,60    4.9865 0.2784
#> educ_age,low,70    5.5627 0.3066
#> educ_age,middle,40 4.8613 0.6045
#> educ_age,middle,50 5.4375 0.4838
#> educ_age,middle,60 6.0137 0.3743
#> educ_age,middle,70 6.5899 0.2894
# Get the output table containing the covariate information from "groups" attribute:
attr(svymargins(res, groupfactor=target.l), "groups")
#>                    group_id  sex education age     mean        SE
#> null                   null <NA>      <NA>  NA 4.952309 0.1678844
#> sex,F                   sex    F      <NA>  NA 4.922536 0.2320666
#> sex,M                   sex    M      <NA>  NA 4.980459 0.2267259
#> educ,high              educ <NA>      high  NA 3.805706 0.3904234
#> educ,low               educ <NA>       low  NA 5.004726 0.2806190
#> educ,middle            educ <NA>    middle  NA 6.031934 0.3729471
#> age,40                  age <NA>      <NA>  40 3.781705 0.3216441
#> age,50                  age <NA>      <NA>  50 4.357897 0.2134977
#> age,60                  age <NA>      <NA>  60 4.934090 0.1588113
#> age,70                  age <NA>      <NA>  70 5.510283 0.2058501
#> educ_age,high,40   educ_age <NA>      high  40 2.635102 0.2770388
#> educ_age,high,50   educ_age <NA>      high  50 3.211294 0.3066048
#> educ_age,high,60   educ_age <NA>      high  60 3.787487 0.3857149
#> educ_age,high,70   educ_age <NA>      high  70 4.363680 0.4909774
#> educ_age,low,40    educ_age <NA>       low  40 3.834122 0.3963062
#> educ_age,low,50    educ_age <NA>       low  50 4.410315 0.3138745
#> educ_age,low,60    educ_age <NA>       low  60 4.986507 0.2783688
#> educ_age,low,70    educ_age <NA>       low  70 5.562700 0.3065529
#> educ_age,middle,40 educ_age <NA>    middle  40 4.861330 0.6045403
#> educ_age,middle,50 educ_age <NA>    middle  50 5.437523 0.4837828
#> educ_age,middle,60 educ_age <NA>    middle  60 6.013716 0.3743476
#> educ_age,middle,70 educ_age <NA>    middle  70 6.589908 0.2893785

Confidence intervals

marg <- svymargins(res, groupfactor=target.l)
cbind(marg, confint(marg))
#>                        marg    2.5 %   97.5 %
#> null               4.952309 4.623261 5.281356
#> sex,F              4.922536 4.467694 5.377379
#> sex,M              4.980459 4.536084 5.424834
#> educ,high          3.805706 3.040490 4.570921
#> educ,low           5.004726 4.454723 5.554729
#> educ,middle        6.031934 5.300971 6.762897
#> age,40             3.781705 3.151294 4.412116
#> age,50             4.357897 3.939449 4.776345
#> age,60             4.934090 4.622825 5.245354
#> age,70             5.510283 5.106824 5.913741
#> educ_age,high,40   2.635102 2.092115 3.178088
#> educ_age,high,50   3.211294 2.610360 3.812229
#> educ_age,high,60   3.787487 3.031500 4.543474
#> educ_age,high,70   4.363680 3.401381 5.325978
#> educ_age,low,40    3.834122 3.057376 4.610868
#> educ_age,low,50    4.410315 3.795132 5.025497
#> educ_age,low,60    4.986507 4.440914 5.532100
#> educ_age,low,70    5.562700 4.961867 6.163533
#> educ_age,middle,40 4.861330 3.676453 6.046207
#> educ_age,middle,50 5.437523 4.489326 6.385720
#> educ_age,middle,60 6.013716 5.280008 6.747423
#> educ_age,middle,70 6.589908 6.022737 7.157080
marg <- svymargins(res, groupfactor=target.l)
cbind(attr(marg, "groups"), confint(marg))
#>                    group_id  sex education age     mean        SE    2.5 %
#> null                   null <NA>      <NA>  NA 4.952309 0.1678844 4.623261
#> sex,F                   sex    F      <NA>  NA 4.922536 0.2320666 4.467694
#> sex,M                   sex    M      <NA>  NA 4.980459 0.2267259 4.536084
#> educ,high              educ <NA>      high  NA 3.805706 0.3904234 3.040490
#> educ,low               educ <NA>       low  NA 5.004726 0.2806190 4.454723
#> educ,middle            educ <NA>    middle  NA 6.031934 0.3729471 5.300971
#> age,40                  age <NA>      <NA>  40 3.781705 0.3216441 3.151294
#> age,50                  age <NA>      <NA>  50 4.357897 0.2134977 3.939449
#> age,60                  age <NA>      <NA>  60 4.934090 0.1588113 4.622825
#> age,70                  age <NA>      <NA>  70 5.510283 0.2058501 5.106824
#> educ_age,high,40   educ_age <NA>      high  40 2.635102 0.2770388 2.092115
#> educ_age,high,50   educ_age <NA>      high  50 3.211294 0.3066048 2.610360
#> educ_age,high,60   educ_age <NA>      high  60 3.787487 0.3857149 3.031500
#> educ_age,high,70   educ_age <NA>      high  70 4.363680 0.4909774 3.401381
#> educ_age,low,40    educ_age <NA>       low  40 3.834122 0.3963062 3.057376
#> educ_age,low,50    educ_age <NA>       low  50 4.410315 0.3138745 3.795132
#> educ_age,low,60    educ_age <NA>       low  60 4.986507 0.2783688 4.440914
#> educ_age,low,70    educ_age <NA>       low  70 5.562700 0.3065529 4.961867
#> educ_age,middle,40 educ_age <NA>    middle  40 4.861330 0.6045403 3.676453
#> educ_age,middle,50 educ_age <NA>    middle  50 5.437523 0.4837828 4.489326
#> educ_age,middle,60 educ_age <NA>    middle  60 6.013716 0.3743476 5.280008
#> educ_age,middle,70 educ_age <NA>    middle  70 6.589908 0.2893785 6.022737
#>                      97.5 %
#> null               5.281356
#> sex,F              5.377379
#> sex,M              5.424834
#> educ,high          4.570921
#> educ,low           5.554729
#> educ,middle        6.762897
#> age,40             4.412116
#> age,50             4.776345
#> age,60             5.245354
#> age,70             5.913741
#> educ_age,high,40   3.178088
#> educ_age,high,50   3.812229
#> educ_age,high,60   4.543474
#> educ_age,high,70   5.325978
#> educ_age,low,40    4.610868
#> educ_age,low,50    5.025497
#> educ_age,low,60    5.532100
#> educ_age,low,70    6.163533
#> educ_age,middle,40 6.046207
#> educ_age,middle,50 6.385720
#> educ_age,middle,60 6.747423
#> educ_age,middle,70 7.157080

Note that for binary outcomes (values 0 or 1), the asymm_ci function can be used to obtain asymmetric confidence intervals, which stay within 0 and 1. For example, confint(marg) |> asymm_ci().

Contrasts using the survey package

marg <- svymargins(res, groupfactor=target.l)

# Difference of adjusted means between high education and population:
svycontrast(marg, quote(`educ,high` - `null`))
#>            nlcon     SE
#> contrast -1.1466 0.3559

# Difference of adjusted means between high and low education:
svycontrast(marg, quote(`educ,high` - `educ,low`))
#>           nlcon     SE
#> contrast -1.199 0.4802
# ... and confidence interval:
svycontrast(marg, quote(`educ,high` - `educ,low`)) |> confint()
#>              2.5 %     97.5 %
#> contrast -2.140154 -0.2578867

# Ratio of adjusted means between high and low education:
svycontrast(marg, quote(`educ,high` / `educ,low`))
#>            nlcon     SE
#> contrast 0.76042 0.0888

Multinomial logistic regression model with svyVGAM package

The simulated dataset has a three-category outcome variable y with levels A, B and C. The first level A is used as the reference outcome level.

n <- 10000
# Generate data:
set.seed(1234)
d <- data.frame(sex=factor(sample(c("M", "F"), n, replace=TRUE)),
                education=factor(sample(c("low", "middle", "high"), n, replace=TRUE)))
d <- d |>
  mutate(age=runif(n, 0, 40) + as.numeric(education) * 20,
         pr1=1,
         pr2=exp(-1 + 0.5 * (as.numeric(education)-1) + 0.02 * age),
         pr3=exp(1 + -0.5 * (as.numeric(education)-1) + 0.02 * age)) |> 
  mutate(across(matches("pr[0-9]"), ~ .x / (pr1 + pr2 + pr3))) |> 
  rowwise() |>
  mutate(y=which(rmultinom(1, 1, c(pr1, pr2, pr3))[,1] == 1)) |> 
  ungroup() |>
  mutate(y=factor(y, levels=1:3, labels=LETTERS[1:3]))
summary(d)
#>  sex       education         age             pr1               pr2         
#>  F:5037   high  :3324   Min.   :20.00   Min.   :0.06339   Min.   :0.09793  
#>  M:4963   low   :3358   1st Qu.:44.89   1st Qu.:0.09323   1st Qu.:0.10647  
#>           middle:3318   Median :60.06   Median :0.11134   Median :0.23733  
#>                         Mean   :59.89   Mean   :0.11447   Mean   :0.26431  
#>                         3rd Qu.:74.83   3rd Qu.:0.13388   3rd Qu.:0.44491  
#>                         Max.   :99.99   Max.   :0.17843   Max.   :0.46831  
#>       pr3         y       
#>  Min.   :0.4346   A:1134  
#>  1st Qu.:0.4619   B:2610  
#>  Median :0.6452   C:6256  
#>  Mean   :0.6212           
#>  3rd Qu.:0.7477           
#>  Max.   :0.8025
# Create survey design:
my.svy <- svydesign(~ 1, weights=~ 1, data=d)
# Run regression analysis:
res <- svy_vglm(y ~ education + age + sex,
                family=multinomial(refLevel=1), design=my.svy)
summary(res)
#> svy_vglm.survey.design(y ~ education + age + sex, family = multinomial(refLevel = 1), 
#>     design = my.svy)
#> Independent Sampling design (with replacement)
#> svydesign(~1, weights = ~1, data = d)
#>                         Coef         SE       z         p
#> (Intercept):1     -1.1176191  0.1444215 -7.7386 1.005e-14
#> (Intercept):2      0.9156547  0.1213080  7.5482 4.414e-14
#> educationlow:1     0.4637916  0.1139856  4.0689 4.724e-05
#> educationlow:2    -0.4635464  0.0952348 -4.8674 1.131e-06
#> educationmiddle:1  0.7548945  0.1553385  4.8597 1.176e-06
#> educationmiddle:2 -1.0799278  0.1369905 -7.8832 3.190e-15
#> age:1              0.0234133  0.0031483  7.4369 1.031e-13
#> age:2              0.0219073  0.0028058  7.8078 5.822e-15
#> sexM:1             0.0434120  0.0725970  0.5980    0.5498
#> sexM:2             0.0126961  0.0647859  0.1960    0.8446

Predictive margins

Here the user needs to specify, for which outcome level (category) the adjusted prevalences are calculated using the y.lev argument.

# Define margins as a named list:
target.l <- list(null=list(),
                 educ=list("education"),
                 age=list(age=seq(40,70,10)),
                 educ_age=list("education", age=seq(40,70,10)))

# Calculate predictive margins
# for the 1st outcome level "A":
svymargins(res, groupfactor=target.l, y.lev=1)
#>                        mean     SE
#> null               0.113400 0.0032
#> educ,high          0.091826 0.0055
#> educ,low           0.118254 0.0057
#> educ,middle        0.150369 0.0109
#> age,40             0.164884 0.0088
#> age,50             0.136518 0.0051
#> age,60             0.112343 0.0033
#> age,70             0.091969 0.0035
#> educ_age,high,40   0.126719 0.0058
#> educ_age,high,50   0.104216 0.0058
#> educ_age,high,60   0.085317 0.0063
#> educ_age,high,70   0.069577 0.0067
#> educ_age,low,40    0.162486 0.0100
#> educ_age,low,50    0.134365 0.0068
#> educ_age,low,60    0.110465 0.0054
#> educ_age,low,70    0.090369 0.0052
#> educ_age,middle,40 0.205569 0.0193
#> educ_age,middle,50 0.171078 0.0135
#> educ_age,middle,60 0.141337 0.0092
#> educ_age,middle,70 0.116037 0.0064

# Get the output table containing the covariate information from "groups" attribute
# for the 2nd outcome level "B":
attr(svymargins(res, groupfactor=target.l, y.lev="B"), "groups")
#>                    y group_id education age      mean          SE
#> null               B     null      <NA>  NA 0.2610000 0.004392512
#> educ,high          B     educ      high  NA 0.1154941 0.007253262
#> educ,low           B     educ       low  NA 0.2373699 0.007335719
#> educ,middle        B     educ    middle  NA 0.4055084 0.013056277
#> age,40             B      age      <NA>  40 0.2336034 0.009937456
#> age,50             B      age      <NA>  50 0.2447548 0.007030187
#> age,60             B      age      <NA>  60 0.2547999 0.004806895
#> age,70             B      age      <NA>  70 0.2638098 0.004808076
#> educ_age,high,40   B educ_age      high  40 0.1080408 0.005393824
#> educ_age,high,50   B educ_age      high  50 0.1122954 0.005937567
#> educ_age,high,60   B educ_age      high  60 0.1161833 0.007150073
#> educ_age,high,70   B educ_age      high  70 0.1197449 0.008849008
#> educ_age,low,40    B educ_age       low  40 0.2202772 0.010047307
#> educ_age,low,50    B educ_age       low  50 0.2302085 0.008103387
#> educ_age,low,60    B educ_age       low  60 0.2391883 0.007370292
#> educ_age,low,70    B educ_age       low  70 0.2472939 0.008459925
#> educ_age,middle,40 B educ_age    middle  40 0.3728347 0.021566596
#> educ_age,middle,50 B educ_age    middle  50 0.3921307 0.017247027
#> educ_age,middle,60 B educ_age    middle  60 0.4094232 0.013213021
#> educ_age,middle,70 B educ_age    middle  70 0.4248075 0.009972701

Confidence intervals

Symmetric confidence intervals

The usual confint method works also here.

marg <- svymargins(res, groupfactor=target.l, y.lev=1)
cbind(marg, confint(marg))
#>                          marg      2.5 %     97.5 %
#> null               0.11340000 0.10718469 0.11961531
#> educ,high          0.09182557 0.08109644 0.10255470
#> educ,low           0.11825412 0.10698483 0.12952340
#> educ,middle        0.15036914 0.12902514 0.17171315
#> age,40             0.16488358 0.14760961 0.18215755
#> age,50             0.13651763 0.12647416 0.14656109
#> age,60             0.11234293 0.10585138 0.11883449
#> age,70             0.09196903 0.08509722 0.09884083
#> educ_age,high,40   0.12671872 0.11540394 0.13803351
#> educ_age,high,50   0.10421594 0.09290416 0.11552772
#> educ_age,high,60   0.08531684 0.07303613 0.09759756
#> educ_age,high,70   0.06957717 0.05651751 0.08263682
#> educ_age,low,40    0.16248554 0.14285770 0.18211338
#> educ_age,low,50    0.13436518 0.12095138 0.14777898
#> educ_age,low,60    0.11046513 0.09989286 0.12103741
#> educ_age,low,70    0.09036875 0.08009188 0.10064563
#> educ_age,middle,40 0.20556851 0.16764929 0.24348774
#> educ_age,middle,50 0.17107759 0.14455597 0.19759922
#> educ_age,middle,60 0.14133710 0.12336725 0.15930695
#> educ_age,middle,70 0.11603709 0.10357141 0.12850278
marg <- svymargins(res, groupfactor=target.l, y.lev="B")
cbind(attr(marg, "groups"), confint(marg))
#>                    y group_id education age      mean          SE      2.5 %
#> null               B     null      <NA>  NA 0.2610000 0.004392512 0.25239083
#> educ,high          B     educ      high  NA 0.1154941 0.007253262 0.10127793
#> educ,low           B     educ       low  NA 0.2373699 0.007335719 0.22299214
#> educ,middle        B     educ    middle  NA 0.4055084 0.013056277 0.37991854
#> age,40             B      age      <NA>  40 0.2336034 0.009937456 0.21412636
#> age,50             B      age      <NA>  50 0.2447548 0.007030187 0.23097584
#> age,60             B      age      <NA>  60 0.2547999 0.004806895 0.24537852
#> age,70             B      age      <NA>  70 0.2638098 0.004808076 0.25438614
#> educ_age,high,40   B educ_age      high  40 0.1080408 0.005393824 0.09746907
#> educ_age,high,50   B educ_age      high  50 0.1122954 0.005937567 0.10065796
#> educ_age,high,60   B educ_age      high  60 0.1161833 0.007150073 0.10216939
#> educ_age,high,70   B educ_age      high  70 0.1197449 0.008849008 0.10240113
#> educ_age,low,40    B educ_age       low  40 0.2202772 0.010047307 0.20058488
#> educ_age,low,50    B educ_age       low  50 0.2302085 0.008103387 0.21432614
#> educ_age,low,60    B educ_age       low  60 0.2391883 0.007370292 0.22474277
#> educ_age,low,70    B educ_age       low  70 0.2472939 0.008459925 0.23071275
#> educ_age,middle,40 B educ_age    middle  40 0.3728347 0.021566596 0.33056499
#> educ_age,middle,50 B educ_age    middle  50 0.3921307 0.017247027 0.35832717
#> educ_age,middle,60 B educ_age    middle  60 0.4094232 0.013213021 0.38352615
#> educ_age,middle,70 B educ_age    middle  70 0.4248075 0.009972701 0.40526137
#>                       97.5 %
#> null               0.2696092
#> educ,high          0.1297102
#> educ,low           0.2517476
#> educ,middle        0.4310982
#> age,40             0.2530805
#> age,50             0.2585337
#> age,60             0.2642212
#> age,70             0.2732335
#> educ_age,high,40   0.1186125
#> educ_age,high,50   0.1239328
#> educ_age,high,60   0.1301972
#> educ_age,high,70   0.1370886
#> educ_age,low,40    0.2399696
#> educ_age,low,50    0.2460908
#> educ_age,low,60    0.2536338
#> educ_age,low,70    0.2638750
#> educ_age,middle,40 0.4151045
#> educ_age,middle,50 0.4259343
#> educ_age,middle,60 0.4353202
#> educ_age,middle,70 0.4443536

Asymmetric confidence intervals

Probability scale parameters (i.e. with values between 0 and 1) can be made asymmetric using the asymm_ci helper function. This way the confidence intervals always remain between 0 and 1.

marg <- svymargins(res, groupfactor=target.l, y.lev=1)
cbind(marg, confint(marg) |> asymm_ci())
#>                          marg      2.5 %     97.5 %
#> null               0.11340000 0.10733166 0.11976541
#> educ,high          0.09182557 0.08164510 0.10313290
#> educ,low           0.11825412 0.10744147 0.12999644
#> educ,middle        0.15036914 0.13024726 0.17298140
#> age,40             0.16488358 0.14832734 0.18289098
#> age,50             0.13651763 0.12678156 0.14687560
#> age,60             0.11234293 0.10601334 0.11900013
#> age,70             0.09196903 0.08532408 0.09907543
#> educ_age,high,40   0.12671872 0.11582905 0.13847185
#> educ_age,high,50   0.10421594 0.09343440 0.11608228
#> educ_age,high,60   0.08531684 0.07381073 0.09842600
#> educ_age,high,70   0.06957717 0.05759826 0.08382577
#> educ_age,low,40    0.16248554 0.14379973 0.18308026
#> educ_age,low,50    0.13436518 0.12150779 0.14835333
#> educ_age,low,60    0.11046513 0.10032751 0.12148879
#> educ_age,low,70    0.09036875 0.08060463 0.10118549
#> educ_age,middle,40 0.20556851 0.17022423 0.24607513
#> educ_age,middle,50 0.17107759 0.14616130 0.19925021
#> educ_age,middle,60 0.14133710 0.12430299 0.16027827
#> educ_age,middle,70 0.11603709 0.10414115 0.12909609
marg <- svymargins(res, groupfactor=target.l, y.lev="B")
cbind(attr(marg, "groups"), confint(marg) |> asymm_ci())
#>                    y group_id education age      mean          SE      2.5 %
#> null               B     null      <NA>  NA 0.2610000 0.004392512 0.25248311
#> educ,high          B     educ      high  NA 0.1154941 0.007253262 0.10202061
#> educ,low           B     educ       low  NA 0.2373699 0.007335719 0.22329317
#> educ,middle        B     educ    middle  NA 0.4055084 0.013056277 0.38019620
#> age,40             B      age      <NA>  40 0.2336034 0.009937456 0.21469306
#> age,50             B      age      <NA>  50 0.2447548 0.007030187 0.23123925
#> age,60             B      age      <NA>  60 0.2547999 0.004806895 0.24549366
#> age,70             B      age      <NA>  70 0.2638098 0.004808076 0.25449472
#> educ_age,high,40   B educ_age      high  40 0.1080408 0.005393824 0.09791463
#> educ_age,high,50   B educ_age      high  50 0.1122954 0.005937567 0.10117396
#> educ_age,high,60   B educ_age      high  60 0.1161833 0.007150073 0.10288652
#> educ_age,high,70   B educ_age      high  70 0.1197449 0.008849008 0.10345697
#> educ_age,low,40    B educ_age       low  40 0.2202772 0.010047307 0.20121704
#> educ_age,low,50    B educ_age       low  50 0.2302085 0.008103387 0.21471123
#> educ_age,low,60    B educ_age       low  60 0.2391883 0.007370292 0.22504305
#> educ_age,low,70    B educ_age       low  70 0.2472939 0.008459925 0.23108827
#> educ_age,middle,40 B educ_age    middle  40 0.3728347 0.021566596 0.33162450
#> educ_age,middle,50 B educ_age    middle  50 0.3921307 0.017247027 0.35889134
#> educ_age,middle,60 B educ_age    middle  60 0.4094232 0.013213021 0.38379922
#> educ_age,middle,70 B educ_age    middle  70 0.4248075 0.009972701 0.40538852
#>                       97.5 %
#> null               0.2697005
#> educ,high          0.1304883
#> educ,low           0.2520460
#> educ,middle        0.4313330
#> age,40             0.2536414
#> age,50             0.2587943
#> age,60             0.2643353
#> age,70             0.2733408
#> educ_age,high,40   0.1190759
#> educ_age,high,50   0.1244700
#> educ_age,high,60   0.1309476
#> educ_age,high,70   0.1382018
#> educ_age,low,40    0.2405991
#> educ_age,low,50    0.2464732
#> educ_age,low,60    0.2539313
#> educ_age,low,70    0.2642454
#> educ_age,middle,40 0.4159789
#> educ_age,middle,50 0.4264011
#> educ_age,middle,60 0.4355487
#> educ_age,middle,70 0.4444614

Contrasts using the survey package

marg <- svymargins(res, groupfactor=target.l, y.lev=2)

# Adjusted risk (or prevalence) difference between high education and population:
svycontrast(marg, quote(`educ,high` - `null`))
#>             nlcon     SE
#> contrast -0.14551 0.0072

# Adjusted risk (or prevalence) difference between high and low education:
svycontrast(marg, quote(`educ,high` - `educ,low`))
#>             nlcon     SE
#> contrast -0.12188 0.0102
# ... and confidence interval:
svycontrast(marg, quote(`educ,high` - `educ,low`)) |> confint()
#>               2.5 %     97.5 %
#> contrast -0.1418093 -0.1019423

# Adjusted risk (or prevalence) ratio between high and low education:
svycontrast(marg, quote(`educ,high` / `educ,low`))
#>            nlcon     SE
#> contrast 0.48656 0.0337

# Population attributable fraction (PAF) of high education:
svycontrast(marg, quote(1 - `educ,high` / `null`)) 
#>            nlcon     SE
#> contrast 0.55749 0.0263

More formal presentation of the variance estimation

Let \(y_i\) denote the outcome and \(X_i\) the vector of covariates of individual \(i=1,\ldots,n\). The corresponding vector of regression parameters of a generalized linear model (GLM) are \(\beta\). The linear predictor is \(X_i\beta\). The inverse link function of the GLM is \(\phi(\cdot)\), which maps the linear predictor into the expected value of the outcome for individual \(i\): \[ \mathbf{E}[Y_i\,|\,X_i,\,\beta]=\phi(X_i\beta). \] The predictive margin is defined as the (weighted) mean of the individual expected values: \[ \text{PM}(X,\,\beta):=\frac{\sum_{i=1}^nw_i\phi(X_i\beta)}{\sum_{i=1}^nw_i}, \] where \(w_i\ge0\) are the (often poststratification or inverse probability) weights. The covariate values can be modified as described above to yield the desired margins.

Let \(\hat{\beta}\) denote the point estimates and \(\hat{V}\) denote the estimated covariance matrix of the regression parameters of the GLM. The point estimate of the PM is calculated by replacing the parameters \(\beta\) with the estimates \(\hat{\beta}\).

The variance estimate is based on the total variance, following the R code in the svypredmeans function of the survey package: \[ \text{Var}\bigl\{\text{PM}(X,\,\hat{\beta})\bigr\} = \text{Var}\left\{\mathbf{E}\bigl[\text{PM}(X,\,\hat{\beta})\,|\,X\bigr]\right\} + \mathbf{E}\left[\text{Var}\bigl\{\text{PM}(X,\,\hat{\beta})\,|\,X\bigr\}\right]. \] The first term can be calculated using the svymeans function, which accounts for the weights and the sampling design in calculating the variance of the expected values of the outcome, which were first calculated using the predict function.

The second term representing the uncertainty in the regression parameter estimates is calculated using the delta method, based on the gradient of \(\nabla\text{PM}=\partial\text{PM}(X_i,\,\beta)/\partial\beta\) (evaluated at the point estimates \(\hat{\beta}\)) and the covariance matrix \(\hat{V}\): \[ \nabla\text{PM}^T|_{\beta=\hat{\beta}}\,\hat{V}\,\nabla\text{PM}|_{\beta=\hat{\beta}}. \]