library(kstools)mianalyze()library(kstools)Missing data are common in clinical trials, observational studies, registries, and real-world evidence analyses. A standard approach for handling missing values is multiple imputation (MI), where several complete datasets are generated, each containing plausible replacements for missing observations.
After analysing each imputed dataset, the analyst obtains:
The final step is to combine these results into a single inference. This process is commonly known as pooling or MI analysis and is described by Rubin (1987).
The mianalyze() function in kstools implements Rubin’s pooling rules and supports the Barnard-Rubin (1999) small-sample degrees-of-freedom adjustment, equivalent to the EDF= option in SAS PROC MIANALYZE. The exported alias ks_mianalyze() provides the same interface.
The typical workflow is:
Conceptually:
%%{init: {"theme": "dark", "themeVariables": {"lineColor": "#94a3b8", "primaryTextColor": "#f8fafc"}}}%%
flowchart TD
A([Original Dataset])
B([Multiple Imputation])
D1[Imputed Dataset 1<br/>Q₁, U₁]
D2[Imputed Dataset 2<br/>Q₂, U₂]
D3[Imputed Dataset 3<br/>Q₃, U₃]
Ddots[...]
Dm[Imputed Dataset m<br/>Qₘ, Uₘ]
R{{Rubin's Rules<br/>Pool Estimates and Variances}}
F([Pooled Estimate<br/>SE, CI, p-value])
A --> B
B --> D1
B --> D2
B --> D3
B --> Ddots
B --> Dm
D1 --> R
D2 --> R
D3 --> R
Ddots --> R
Dm --> R
R --> F
classDef source fill:#1e40af,stroke:#93c5fd,stroke-width:2px,color:#f8fafc;
classDef process fill:#c2410c,stroke:#fdba74,stroke-width:2px,color:#f8fafc;
classDef imputed fill:#166534,stroke:#86efac,stroke-width:2px,color:#f8fafc;
classDef pooling fill:#6d28d9,stroke:#c4b5fd,stroke-width:2px,color:#f8fafc;
classDef result fill:#15803d,stroke:#86efac,stroke-width:3px,color:#f8fafc;
class A source;
class B process;
class D1,D2,D3,Dm imputed;
class R pooling;
class F result;
linkStyle default stroke:#94a3b8,stroke-width:1.5px
Suppose Qi is the estimate from imputation i and Ui is its variance. Let m be the number of imputations.
$$\bar{Q} = \frac{1}{m} \sum_{i=1}^{m}Q_i$$
$$\bar{U} = \frac{1}{m} \sum_{i=1}^{m}U_i$$
This quantity reflects uncertainty that would exist even if no data were missing.
$$B = \frac{1}{m-1}\sum_{i=1}^{m}(Q_i-\bar{Q})^2$$
Large values indicate substantial uncertainty due to missing data.
$$T = \bar{U} + \left(1+\frac{1}{m}\right) B$$
The pooled standard error is $SE = \sqrt{T}$.
$$r = \frac{\left(1+\frac{1}{m}\right) B}{\bar{U}}$$
Values near zero indicate little impact from missing data.
For finite Rubin degrees of freedom vm,
$$\hat\lambda = \frac{r + 2/(v_m + 3)}{r + 1}$$
When vm is treated as infinite (large-sample case), this simplifies to λ̂ = r/(r + 1). The quantity is often interpreted as the proportion of uncertainty attributable to missingness.
| λ | Interpretation |
|---|---|
| < 0.05 | Negligible missing information |
| 0.05–0.20 | Mild impact |
| 0.20–0.50 | Moderate impact |
| > 0.50 | Severe impact |
$$RE = \frac{1}{1 + \lambda/m}$$
Values close to 1 indicate that additional imputations would provide little benefit.
$$v_m = (m-1)\left(1+\frac{\bar{U}}{\left(1+m^{-1}\right)B}\right)^2$$
When m is small and missing information is limited, vm can become very large.
Suppose a model parameter is based on only 40 residual degrees of freedom. Rubin’s formula may yield vm = 500, which exceeds the information available in the complete-data analysis and can produce confidence intervals that are unrealistically narrow.
Barnard and Rubin (1999) proposed a small-sample correction. Let v0 be the complete-data degrees of freedom. Define
$$\gamma = \frac{(1 + m^{-1}) B}{T}$$
and
$$\hat{v}_{obs} = (1-\gamma)\, v_0\, \frac{v_0+1}{v_0+3}$$
The adjusted degrees of freedom are
$$v_m^* = \left(\frac{1}{v_m} + \frac{1}{\hat{v}_{obs}}\right)^{-1}$$
This is the method used by SAS PROC MIANALYZE when EDF= is specified, and by mianalyze(edf = ...).
est <- c(0.345, 0.303, 0.298, 0.378)
err <- c(0.056, 0.101, 0.099, 0.034)
out <- mianalyze(est, err)
outVariance Information (4 Imputations)
| Between | Within | Total | DF |
|---|---|---|---|
| 0.001426 | 0.006074 | 0.007856 | 58.2727 |
Missing Data Diagnostics
| Relative Increase in Variance | Fraction Missing Information | Relative Efficiency |
|---|---|---|
| 0.293488 | 0.252131 | 0.940705 |
Parameter Estimates (4 Imputations)
| Estimate | Std Error | LCL | UCL | DF |
|---|---|---|---|---|
| 0.331 | 0.088634 | 0.153597 | 0.508403 | 58.2727 |
Hypothesis Test
| Minimum | Maximum | Theta0 | Statistic | PValue |
|---|---|---|---|---|
| 0.298 | 0.378 | 0 | 3.734455 | 0.00043 |
est <- c(0.345, 0.303, 0.298, 0.378)
err <- c(0.056, 0.101, 0.099, 0.034)
m <- length(est)
Qbar <- mean(est)
Vw <- mean(err^2)
Vb <- var(est)
Vt <- Vw + (1 + 1 / m) * Vb
manual <- c(Estimate = Qbar, Vw = Vw, Vb = Vb, Vt = Vt, SE = sqrt(Vt))
pooled <- c(
Estimate = out$Estimate, Vw = out$Vw, Vb = out$Vb,
Vt = out$Vt, SE = out$StdErr
)
rbind(manual = manual, mianalyze = pooled)
#> Estimate Vw Vb Vt SE
#> manual 0.331 0.0060735 0.001426 0.007856 0.08863408
#> mianalyze 0.331 0.0060735 0.001426 0.007856 0.08863408est <- c(-2.8, -3.2, -2.5, -3.1, -2.9)
err <- c(1.10, 1.15, 1.05, 1.18, 1.12)
mianalyze(est, err)Variance Information (5 Imputations)
| Between | Within | Total | DF |
|---|---|---|---|
| 0.075 | 1.25636 | 1.34636 | 895.1532 |
Missing Data Diagnostics
| Relative Increase in Variance | Fraction Missing Information | Relative Efficiency |
|---|---|---|
| 0.071636 | 0.068925 | 0.986402 |
Parameter Estimates (5 Imputations)
| Estimate | Std Error | LCL | UCL | DF |
|---|---|---|---|---|
| -2.9 | 1.160328 | -5.177279 | -0.622721 | 895.1532 |
Hypothesis Test
| Minimum | Maximum | Theta0 | Statistic | PValue |
|---|---|---|---|---|
| -3.2 | -2.5 | 0 | -2.499294 | 0.012622 |
Interpretation:
Pool results on the log scale, then exponentiate.
log_or <- c(0.18, 0.21, 0.15, 0.19, 0.23)
se_log_or <- c(0.09, 0.10, 0.09, 0.08, 0.11)
res <- mianalyze(log_or, se_log_or)
resVariance Information (5 Imputations)
| Between | Within | Total | DF |
|---|---|---|---|
| 0.00092 | 0.00894 | 0.010044 | 331.0818 |
Missing Data Diagnostics
| Relative Increase in Variance | Fraction Missing Information | Relative Efficiency |
|---|---|---|
| 0.12349 | 0.115245 | 0.97747 |
Parameter Estimates (5 Imputations)
| Estimate | Std Error | LCL | UCL | DF |
|---|---|---|---|---|
| 0.192 | 0.10022 | -0.005148 | 0.389148 | 331.0818 |
Hypothesis Test
| Minimum | Maximum | Theta0 | Statistic | PValue |
|---|---|---|---|---|
| 0.15 | 0.23 | 0 | 1.91579 | 0.056253 |
exp(c(OR = res$Estimate, LCL = res$lcl, UCL = res$ucl))
#> OR LCL UCL
#> 1.2116705 0.9948654 1.4757226Cox models typically return log-hazard ratios.
log_hr <- c(-0.22, -0.17, -0.31, -0.28, -0.24)
se <- c(0.14, 0.13, 0.15, 0.12, 0.13)
res <- mianalyze(log_hr, se)
resVariance Information (5 Imputations)
| Between | Within | Total | DF |
|---|---|---|---|
| 0.00293 | 0.01806 | 0.021576 | 150.6275 |
Missing Data Diagnostics
| Relative Increase in Variance | Fraction Missing Information | Relative Efficiency |
|---|---|---|
| 0.194684 | 0.173856 | 0.966397 |
Parameter Estimates (5 Imputations)
| Estimate | Std Error | LCL | UCL | DF |
|---|---|---|---|---|
| -0.244 | 0.146888 | -0.534226 | 0.046226 | 150.6275 |
Hypothesis Test
| Minimum | Maximum | Theta0 | Statistic | PValue |
|---|---|---|---|---|
| -0.31 | -0.17 | 0 | -1.661133 | 0.098767 |
exp(c(HR = res$Estimate, LCL = res$lcl, UCL = res$ucl))
#> HR LCL UCL
#> 0.7834876 0.5861225 1.0473115Assume the complete-data analysis would have v0 = 40:
est <- c(0.345, 0.303, 0.298, 0.378)
err <- c(0.056, 0.101, 0.099, 0.034)
br <- mianalyze(est, err, edf = 40)
rubin <- mianalyze(est, err)
data.frame(
method = c("Barnard-Rubin (edf=40)", "Rubin large-sample"),
df = c(br$df, rubin$df),
lcl = c(br$lcl, rubin$lcl),
ucl = c(br$ucl, rubin$ucl)
)
#> method df lcl ucl
#> 1 Barnard-Rubin (edf=40) 19.57893 0.1458573 0.5161427
#> 2 Rubin large-sample 58.27270 0.1535973 0.5084027The adjusted degrees of freedom are smaller, producing slightly wider confidence intervals.
mianalyze() is a univariate analogue of SAS PROC MIANALYZE. The formulas for Q̄, Ū, B, T, r, λ̂, RE, vm, and the Barnard-Rubin vm* match those in the SAS/STAT User’s Guide (Chapter The MIANALYZE Procedure).
SAS PROC MIANALYZE |
mianalyze() |
|---|---|
Estimates + standard errors (e.g. STDERR / PARMS) |
est, err |
ALPHA= |
alpha (default 0.05) |
THETA0= |
theta0 (default 0) |
EDF= (Barnard-Rubin) |
edf (default Inf = no adjustment) |
| — | v0 (deprecated alias for edf) |
Note: SAS documents a default of EDF=1 with no adjustment when EDF= is omitted. In kstools, omitting edf (or using edf = Inf) likewise uses Rubin’s large-sample vm without Barnard-Rubin adjustment.
Aligned with SAS univariate MI inference:
edf.Not implemented (multivariate SAS features):
MULT / MULTIVARIATE);TEST linear hypotheses;EDF=30)The Fitness example in SAS/STAT 12.3 Example 58.1 combines univariate means and standard errors across m = 5 imputations with EDF=30 (sample size n = 31). Reproducing Oxygen from the published input table:
oxygen <- mianalyze(
est = c(47.0120, 47.2407, 47.4995, 47.1485, 47.0042),
err = c(0.95984, 0.93540, 1.00766, 0.95439, 0.96528),
edf = 30
)
oxygenVariance Information (5 Imputations)
| Between | Within | Total | DF |
|---|---|---|---|
| 0.04147 | 0.930854 | 0.980619 | 26.29868 |
Missing Data Diagnostics
| Relative Increase in Variance | Fraction Missing Information | Relative Efficiency |
|---|---|---|
| 0.053461 | 0.051968 | 0.989713 |
Parameter Estimates (5 Imputations)
| Estimate | Std Error | LCL | UCL | DF |
|---|---|---|---|---|
| 47.18098 | 0.990262 | 45.14659 | 49.21537 | 26.29868 |
Hypothesis Test
| Minimum | Maximum | Theta0 | Statistic | PValue |
|---|---|---|---|---|
| 47.0042 | 47.4995 | 0 | 47.64495 | 0 |
Comparison with SAS Output 58.1.2 / 58.1.3 (Oxygen). Small differences are expected because the User’s Guide prints input means/SEs with limited precision.
sas_oxygen <- data.frame(
quantity = c(
"Estimate", "StdErr", "Between", "Within", "Total",
"DF", "r", "lambda", "RE", "LCL", "UCL"
),
sas = c(
47.180993, 0.990266, 0.041478, 0.930853, 0.980626,
26.298, 0.053471, 0.051977, 0.989712, 45.1466, 49.2154
),
mianalyze = c(
oxygen$Estimate, oxygen$StdErr, oxygen$Vb, oxygen$Vw, oxygen$Vt,
oxygen$df, oxygen$r, oxygen$lambda, oxygen$re, oxygen$lcl, oxygen$ucl
)
)
sas_oxygen$abs_diff <- abs(sas_oxygen$mianalyze - sas_oxygen$sas)
knitr::kable(sas_oxygen, digits = 6)| quantity | sas | mianalyze | abs_diff |
|---|---|---|---|
| Estimate | 47.180993 | 47.180980 | 0.000013 |
| StdErr | 0.990266 | 0.990262 | 0.000004 |
| Between | 0.041478 | 0.041470 | 0.000008 |
| Within | 0.930853 | 0.930854 | 0.000001 |
| Total | 0.980626 | 0.980619 | 0.000007 |
| DF | 26.298000 | 26.298679 | 0.000679 |
| r | 0.053471 | 0.053461 | 0.000010 |
| lambda | 0.051977 | 0.051968 | 0.000009 |
| RE | 0.989712 | 0.989713 | 0.000001 |
| LCL | 45.146600 | 45.146592 | 0.000008 |
| UCL | 49.215400 | 49.215368 | 0.000032 |
The same pattern holds for RunTime and RunPulse in that example, and for the Getting Started / Example 58.3 regression coefficients when moments are reconstructed from the published Between/Within variances (with and without EDF=28). Automated checks live in tests/testthat/test-ks_mianalyze-sas.R.
SAS tabulates RE = 1/(1 + λ/m). For illustration:
re_grid <- expand.grid(
m = c(3, 5, 10, 20),
lambda = c(0.1, 0.2, 0.3, 0.5, 0.7)
)
re_grid$RE <- with(re_grid, 1 / (1 + lambda / m))
knitr::kable(
xtabs(RE ~ m + lambda, data = re_grid),
digits = 4,
caption = "Relative efficiency (matches SAS Table 58.2)"
)| 0.1 | 0.2 | 0.3 | 0.5 | 0.7 | |
|---|---|---|---|---|---|
| 3 | 0.9677 | 0.9375 | 0.9091 | 0.8571 | 0.8108 |
| 5 | 0.9804 | 0.9615 | 0.9434 | 0.9091 | 0.8772 |
| 10 | 0.9901 | 0.9804 | 0.9709 | 0.9524 | 0.9346 |
| 20 | 0.9950 | 0.9901 | 0.9852 | 0.9756 | 0.9662 |
SAS:
proc mianalyze data=outuni edf=30;
modeleffects Oxygen;
stderr SOxygen;
run;
Equivalent R:
mianalyze(est = oxygen_est, err = oxygen_se, edf = 30)A common workflow with the mice package:
library(mice)
imp <- mice(adsl, m = 20)
fits <- with(
imp,
lm(CHG ~ TRT01A + BASE)
)
est <- sapply(
fits$analyses,
function(x) coef(summary(x))["TRT01AActive", "Estimate"]
)
err <- sapply(
fits$analyses,
function(x) coef(summary(x))["TRT01AActive", "Std. Error"]
)
mianalyze(est, err)| Component | Meaning |
|---|---|
Vw (Vw) |
Within-imputation variance Ū |
Vb (Vb) |
Between-imputation variance B |
Vt (Vt) |
Total variance T |
Large Vb relative to Vw indicates substantial missing-data uncertainty.
| Metric | Meaning |
|---|---|
| r | Relative increase in variance |
| λ | Fraction of missing information |
| RE | Relative efficiency |
| Statistic | Meaning |
|---|---|
| Estimate | Pooled estimate Q̄ |
| StdErr | Pooled standard error $\sqrt{T}$ |
| lcl / ucl | Confidence interval |
| t | Test statistic for H0: θ = θ0 |
| pval | Two-sided p-value |
mianalyze(est = 10, err = 2)Variance Information (1 Imputations)
| Between | Within | Total | DF |
|---|---|---|---|
| 0 | 4 | 4 | Inf |
Missing Data Diagnostics
| Relative Increase in Variance | Fraction Missing Information | Relative Efficiency |
|---|---|---|
| 0 | 0 | 1 |
Parameter Estimates (1 Imputations)
| Estimate | Std Error | LCL | UCL | DF |
|---|---|---|---|---|
| 10 | 2 | 6.080072 | 13.91993 | Inf |
Hypothesis Test
| Minimum | Maximum | Theta0 | Statistic | PValue |
|---|---|---|---|---|
| 10 | 10 | 0 | 5 | 1e-06 |
Here B = 0, T = Ū, and results reduce to ordinary complete-data inference.
mianalyze(est = rep(5, 5), err = rep(1, 5))Variance Information (5 Imputations)
| Between | Within | Total | DF |
|---|---|---|---|
| 0 | 1 | 1 | Inf |
Missing Data Diagnostics
| Relative Increase in Variance | Fraction Missing Information | Relative Efficiency |
|---|---|---|
| 0 | 0 | 1 |
Parameter Estimates (5 Imputations)
| Estimate | Std Error | LCL | UCL | DF |
|---|---|---|---|---|
| 5 | 1 | 3.040036 | 6.959964 | Inf |
Hypothesis Test
| Minimum | Maximum | Theta0 | Statistic | PValue |
|---|---|---|---|---|
| 5 | 5 | 0 | 5 | 1e-06 |
When all imputations produce the same estimate, B = 0 and no additional uncertainty is attributed to missing data.
mianalyze() / ks_mianalyze() pool MI point estimates and standard errors with Rubin’s rules: Q̄, T = Ū + (1 + 1/m)B, t-based intervals and tests.edf applies the Barnard-Rubin (1999) small-sample df adjustment, matching SAS EDF=.PROC MIANALYZE was checked against SAS/STAT 12.3 formulas, Table 58.2, Example 58.1 (EDF=30), and Example 58.3 (EDF=28).edf whenever complete-data residual df are modest so that vm does not exceed the information in the original sample.Barnard, J. and Rubin, D.B. (1999). Small-sample degrees of freedom with multiple imputation. Biometrika, 86, 948–955.
Rubin, D.B. (1987). Multiple Imputation for Nonresponse in Surveys. Wiley.
SAS Institute Inc. (2013). SAS/STAT 12.3 User’s Guide. Chapter 58, The MIANALYZE Procedure. Cary, NC: SAS Institute Inc.
Van Buuren, S. (2018). Flexible Imputation of Missing Data. Second Edition. Chapman & Hall/CRC.