lpmecThis tutorial demonstrates how to use the lpmec package
for measurement error correction in regression models using latent
variable estimation. The package implements bootstrapped analyses to
account for measurement error in observed indicators and provides
corrected regression coefficients.
First install the required dependencies and the lpmec
package:
Simulate data with a latent predictor and observed binary indicators:
set.seed(123)
n <- 1000 # Number of observations
d <- 10 # Number of observable indicators
# Generate latent variable and observed outcomes
x_true <- rnorm(n)
Yobs <- 0.4 * x_true + rnorm(n, sd = 0.35)
# Generate binary indicators of latent variable
ObservablesMat <- sapply(1:d, function(j) {
p <- pnorm(0.5 * x_true + rnorm(n, sd = 0.5))
rbinom(n, 1, p)
})Use lpmec to estimate corrected coefficients:
library(lpmec)
# Run analysis with bootstrap uncertainty estimates
results <- lpmec(
Y = Yobs,
observables = as.data.frame(ObservablesMat),
n_boot = 10, # Use 0 to disable bootstrap resampling
n_partition = 5, # Reduced for demonstration
estimation_method = "em"
)## Warning: n_boot < 199 gives coarse bootstrap confidence intervals; increase
## n_boot for interval estimation.
## {booti_ 1 of 11} -- {parti_ 1 of 5}
## {booti_ 1 of 11} -- {parti_ 2 of 5}
## {booti_ 1 of 11} -- {parti_ 3 of 5}
## {booti_ 1 of 11} -- {parti_ 4 of 5}
## {booti_ 1 of 11} -- {parti_ 5 of 5}
## {booti_ 2 of 11} -- {parti_ 1 of 5}
## {booti_ 2 of 11} -- {parti_ 2 of 5}
## {booti_ 2 of 11} -- {parti_ 3 of 5}
## {booti_ 2 of 11} -- {parti_ 4 of 5}
## {booti_ 2 of 11} -- {parti_ 5 of 5}
## {booti_ 3 of 11} -- {parti_ 1 of 5}
## {booti_ 3 of 11} -- {parti_ 2 of 5}
## {booti_ 3 of 11} -- {parti_ 3 of 5}
## {booti_ 3 of 11} -- {parti_ 4 of 5}
## {booti_ 3 of 11} -- {parti_ 5 of 5}
## {booti_ 4 of 11} -- {parti_ 1 of 5}
## {booti_ 4 of 11} -- {parti_ 2 of 5}
## {booti_ 4 of 11} -- {parti_ 3 of 5}
## {booti_ 4 of 11} -- {parti_ 4 of 5}
## {booti_ 4 of 11} -- {parti_ 5 of 5}
## {booti_ 5 of 11} -- {parti_ 1 of 5}
## {booti_ 5 of 11} -- {parti_ 2 of 5}
## {booti_ 5 of 11} -- {parti_ 3 of 5}
## {booti_ 5 of 11} -- {parti_ 4 of 5}
## {booti_ 5 of 11} -- {parti_ 5 of 5}
## {booti_ 6 of 11} -- {parti_ 1 of 5}
## {booti_ 6 of 11} -- {parti_ 2 of 5}
## {booti_ 6 of 11} -- {parti_ 3 of 5}
## {booti_ 6 of 11} -- {parti_ 4 of 5}
## {booti_ 6 of 11} -- {parti_ 5 of 5}
## {booti_ 7 of 11} -- {parti_ 1 of 5}
## {booti_ 7 of 11} -- {parti_ 2 of 5}
## {booti_ 7 of 11} -- {parti_ 3 of 5}
## {booti_ 7 of 11} -- {parti_ 4 of 5}
## {booti_ 7 of 11} -- {parti_ 5 of 5}
## {booti_ 8 of 11} -- {parti_ 1 of 5}
## {booti_ 8 of 11} -- {parti_ 2 of 5}
## {booti_ 8 of 11} -- {parti_ 3 of 5}
## {booti_ 8 of 11} -- {parti_ 4 of 5}
## {booti_ 8 of 11} -- {parti_ 5 of 5}
## {booti_ 9 of 11} -- {parti_ 1 of 5}
## {booti_ 9 of 11} -- {parti_ 2 of 5}
## {booti_ 9 of 11} -- {parti_ 3 of 5}
## {booti_ 9 of 11} -- {parti_ 4 of 5}
## {booti_ 9 of 11} -- {parti_ 5 of 5}
## {booti_ 10 of 11} -- {parti_ 1 of 5}
## {booti_ 10 of 11} -- {parti_ 2 of 5}
## {booti_ 10 of 11} -- {parti_ 3 of 5}
## {booti_ 10 of 11} -- {parti_ 4 of 5}
## {booti_ 10 of 11} -- {parti_ 5 of 5}
## {booti_ 11 of 11} -- {parti_ 1 of 5}
## {booti_ 11 of 11} -- {parti_ 2 of 5}
## {booti_ 11 of 11} -- {parti_ 3 of 5}
## {booti_ 11 of 11} -- {parti_ 4 of 5}
## {booti_ 11 of 11} -- {parti_ 5 of 5}
Compare naive and corrected estimates:
## Latent Predictor Measurement Error Correction (LPMEC) Model Results
## -------------------------------------------------------------------
## Resampling: n_out_of_n, m = 1000, CI = percentile
## Uncorrected Coefficient (OLS): 0.301 (SE: 0.022)
## Corrected Coefficient: 0.425 (SE: 0.027)
## Bayesian OLS (Outer): NA (SE: NA)
## Use summary() for detailed results.
## Latent Predictor Measurement Error Correction (LPMEC) Model Summary
## ====================================================================
## Resampling: n_out_of_n, m = 1000 (m/n = 1.000), CI = percentile, replace = TRUE
## Bootstrap success rate: 1.000
## Estimate SE CI_Lower CI_Upper
## OLS 0.3008490 0.02179329 0.2608019 0.3227255
## IV 0.7411981 0.05831891 0.6598075 0.8314390
## Corrected IV 0.4250383 0.02723953 0.3916324 0.4690671
## Corrected OLS 0.4250383 0.02723953 0.3916324 0.4690671
## Bayesian OLS (Outer) NA NA NA NA
## Bayesian OLS (Inner) NA NA NA NA
You can visualize the relationship between split-half estimates:
The default partition aggregation is the finite-partition median. Because the median is nonsmooth, the formal route for uncertainty is subsampling or m-out-of-n bootstrap with root-scaled intervals:
results_med <- lpmec(
Y = Yobs,
observables = as.data.frame(ObservablesMat),
n_boot = 499L,
n_partition = 10L,
estimation_method = "em",
partition_aggregation = "median",
bootstrap_method = "subsampling",
boot_m_rule = "power",
boot_m_exponent = 0.70,
boot_ci_type = "root",
seed = 123
)This path holds the same realized partition set fixed across the
original sample and all subsamples, reruns the full latent-score and
correction pipeline inside each subsample, and rescales the empirical
root statistic from m to the original n rate.
Inspect boot_m, boot_m_ratio,
valid_partitions_by_boot,
bootstrap_success_rate, and sensitivity across a grid of
boot_m values in applied work.
The package supports multiple estimation approaches:
# Bayesian MCMC estimation with the default pscl backend
if(FALSE){
mcmc_results <- lpmec(
Y = Yobs,
observables = as.data.frame(ObservablesMat),
estimation_method = "mcmc"
)
# To use the optional NumPyro backend, first run build_backend()
# or provide your own Python environment with JAX and NumPyro.
numpyro_results <- lpmec(
Y = Yobs,
observables = as.data.frame(ObservablesMat),
estimation_method = "mcmc",
mcmc_control = list(backend = "numpyro"),
conda_env = "lpmec"
)
}Fixed-effects and differencing transforms strip the persistent part
of a latent signal but not its transient measurement error, so the naive
panel coefficient is attenuated far more than a pooled correction would
suggest. lpmec_panel() estimates the design-specific
reliability of each measure, corrects the naive OLS coefficient
(b * sqrt(rho_pooled) / rho_design), and reports a
design-robust split-IV alternative (which multiplies by
sqrt(rho_pooled) instead of dividing – IV multiplies, OLS
divides). Simulate a small panel with two noisy half scores of one
latent measure:
set.seed(1234)
n_units <- 40
n_periods <- 10
unit <- rep(seq_len(n_units), times = n_periods)
time <- rep(seq_len(n_periods), each = n_units)
# Latent predictor with a persistent unit component; Y has unit fixed effects
latent_panel <- rnorm(n_units)[unit] + rnorm(n_units * n_periods, sd = 0.7)
half1 <- latent_panel + rnorm(n_units * n_periods, sd = 0.5)
half2 <- latent_panel + rnorm(n_units * n_periods, sd = 0.5)
Y_panel <- 0.4 * latent_panel + rnorm(n_units)[unit] +
rnorm(n_units * n_periods)
panel_results <- lpmec_panel(
Y = Y_panel, unit = unit, time = time,
split_scores = list(measure1 = cbind(half1, half2)),
design = "within",
n_boot = 3, # Increase in applied work
seed = 42
)The summary shows the naive within-unit OLS estimate next to the corrected OLS coefficient (with its reliability-variant interval) and the corrected split-IV estimate, with cluster-bootstrap standard errors:
## Panel LPMEC Results
## -------------------
## Design: within | Units: 40 | Observations: 400
## Measures: measure1
## Bootstrap replications: 3 | Partitions: 1
## Use summary() for coefficient details.
## Panel LPMEC Summary
## ===================
## Design: within | Units: 40 | Observations: 400 | Bootstrap replications: 3
## OLS OLS_SE Corrected_OLS Corrected_OLS_SE Corrected_OLS_Lower
## measure1 0.4533262 0.1650578 0.5312052 0.1993716 0.5312052
## Corrected_OLS_Upper Corrected_IV Corrected_IV_SE Split_Correlation
## measure1 0.5312052 0.5312754 0.1993871 0.8454672
## Rho_Split Triad First_Stage_F
## measure1 0.9162636 NA 357.2075
The half scores used for the reliability estimates can be inspected visually:
With three or more measures of the same latent trait,
lpmec_reliability_bounds() brackets each measure’s
reliability between the within-measure split correlation (an upper
candidate when halves share method error) and the cross-measure triad
estimate rho*_m = r_ml * r_mk / r_lk (a lower candidate
under cross-measure error dependence), reporting the identification
interval [rho_lo, rho_hi]:
set.seed(2025)
n_rb <- 400
latent_rb <- rnorm(n_rb)
rb_half1 <- latent_rb + rnorm(n_rb, sd = 0.6)
rb_half2 <- latent_rb + rnorm(n_rb, sd = 0.6)
score2 <- latent_rb + rnorm(n_rb, sd = 0.7)
score3 <- latent_rb + rnorm(n_rb, sd = 0.7)
bounds <- lpmec_reliability_bounds(
split_scores = list(measure1 = cbind(rb_half1, rb_half2)),
scores = list(measure2 = score2, measure3 = score3)
)
print(bounds)## LPMEC Reliability Sensitivity Diagnostics
## -----------------------------------------
## Measures: measure2, measure3, measure1
## Designs: pooled
## design measure source split_correlation split_n rho_split triad rho_lo
## 1 pooled measure2 scores NA 0 NA 0.680 0.680
## 2 pooled measure3 scores NA 0 NA 0.632 0.632
## 3 pooled measure1 split_scores 0.747 400 0.855 0.865 0.855
## rho_hi
## 1 0.680
## 2 0.632
## 3 0.865
Passing unit, time, and
designs = c("pooled", "within") reports how much each
measure’s reliability deteriorates under the design transform actually
used for estimation.
When a randomized treatment’s effect is moderated by a latent trait
measured with error, the naive interaction coefficient from
Y ~ treatment * x converges to
b_TX * sqrt(rho) for a standardized moderator score of
reliability rho. lpmec_moderator() estimates
the Spearman-Brown as-used reliability of the score entered in the
regression and divides the naive interaction by its square root.
Simulate an experiment with a 4-item moderator battery:
set.seed(321)
n_mod <- 400
X_mod <- rnorm(n_mod)
treatment <- rbinom(n_mod, 1, 0.5)
Y_mod <- 0.2 * treatment + 0.2 * X_mod +
0.3 * treatment * X_mod + rnorm(n_mod)
items <- sapply(c(1.2, 1.0, 0.8, 1.1), function(discrimination) {
rbinom(n_mod, 1, pnorm(discrimination * X_mod - 0.2))
})
colnames(items) <- paste0("item", 1:4)
moderator_results <- lpmec_moderator(
Y = Y_mod,
treatment = treatment,
observables = items,
estimation_method = "averaging",
n_boot = 5, # Increase in applied work
n_partition = 2, # Reduced for demonstration
seed = 99
)The corrected interaction is reported with bootstrap intervals that
propagate the uncertainty in the reliability estimate itself, and
rho_score appears as a headline quantity:
## Latent-Moderator LPMEC Results
## ------------------------------
## Naive Interaction: 0.160 (SE: 0.098)
## Corrected Interaction: 0.198 (SE: 0.131)
## Score Reliability (rho_score): 0.658 (SE: 0.046)
## Use summary() for detailed results.
## Latent-Moderator LPMEC Summary
## ==============================
## Resampling: 5 bootstrap replication(s), 2 partition(s), SB factor = 2
## Estimate SE CI_Lower CI_Upper
## Treatment 0.1672999 0.01676742 0.19530064 0.2370210
## Moderator 0.1571119 0.06564976 0.10704098 0.2712724
## Interaction 0.1601962 0.09811269 0.03407685 0.2539701
## Corrected Moderator 0.1937018 0.08115870 0.13259837 0.3325540
## Corrected Interaction 0.1975043 0.13056407 0.04232470 0.3366831
## rho_half 0.4911531 0.04864081 0.39859902 0.5082435
## rho_score 0.6584122 0.04600930 0.56993795 0.6739057
The lpmec package provides robust measurement error
correction through:
lpmec_panel)lpmec_reliability_bounds)lpmec_moderator)Refer to package documentation for advanced configuration options.