Latent Variable Measurement Error Correction with lpmec

Introduction

This 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.

Installation

First install the required dependencies and the lpmec package:

# Install lpmec from source (replace with appropriate installation method)
# devtools::install_github("cjerzak/lpmec-software", subdir = "lpmec")

Basic Usage

Data Simulation

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)
})

Running the Analysis

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:

print(results)
## 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.
summary(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:

plot(results)

Inference for Median Partition Aggregation

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.

Advanced Features

Using Different Estimation Methods

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"
)
}

Panel and Fixed-Effects Designs

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:

print(panel_results)
## Panel LPMEC Results
## -------------------
## Design: within | Units: 40 | Observations: 400
## Measures: measure1
## Bootstrap replications: 3 | Partitions: 1
## Use summary() for coefficient details.
summary(panel_results)
## 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:

plot(panel_results)

Reliability Bounds with Multiple Measures

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.

Latent Moderators in Experiments

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:

print(moderator_results)
## 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.
summary(moderator_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

Conclusion

The lpmec package provides robust measurement error correction through:

  • Bootstrapped latent variable estimation
  • Multiple correction methods (OLS, IV, Bayesian)
  • Flexible estimation backends (EM, MCMC)
  • Panel/fixed-effects and differencing-design corrections (lpmec_panel)
  • Reliability bounds from split and triad estimates (lpmec_reliability_bounds)
  • Latent-moderator interaction corrections in experiments (lpmec_moderator)

Refer to package documentation for advanced configuration options.