spifa() expects data in a specific shape: one row per
respondent, and the binary item responses stored as a single
matrix-valued column (nobs x nitems) rather than
one column per item. This is the same convention base R uses for
multivariate lm(). This vignette builds a small dataset
acceptable by spifa().
#> Linking to GEOS 3.12.1, GDAL 3.8.4, PROJ 9.4.0; sf_use_s2() is TRUE
Simulating a raw questionnaire
Five binary items, a household wealth covariate, and GPS
coordinates, for 20 households:
set.seed(1)
nobs <- 20
nitems <- 5
raw <- data.frame(
id = seq_len(nobs),
wealth = rnorm(nobs),
lon = runif(nobs, -71.70, -71.68),
lat = runif(nobs, -7.06, -7.04),
item1 = rbinom(nobs, 1, 0.1) == 1,
item2 = rbinom(nobs, 1, 0.2) == 1,
item3 = rbinom(nobs, 1, 0.3) == 1,
item4 = rbinom(nobs, 1, 0.4) == 1,
item5 = rbinom(nobs, 1, 0.5) == 1
)
raw#> id wealth lon lat item1 item2 item3 item4 item5
#> 1 1 -0.62645381 -71.68358 -7.041742 FALSE FALSE TRUE TRUE FALSE
#> 2 2 0.18364332 -71.68706 -7.054128 FALSE FALSE FALSE TRUE TRUE
#> 3 3 -0.83562861 -71.68434 -7.050819 FALSE FALSE FALSE FALSE FALSE
#> 4 4 1.59528080 -71.68894 -7.053352 FALSE TRUE FALSE FALSE TRUE
#> 5 5 0.32950777 -71.68941 -7.046983 FALSE FALSE TRUE TRUE TRUE
#> 6 6 -0.82046838 -71.68421 -7.054840 FALSE FALSE FALSE FALSE FALSE
#> 7 7 0.48742905 -71.69953 -7.050429 FALSE FALSE FALSE FALSE FALSE
#> 8 8 0.73832471 -71.69046 -7.044674 FALSE FALSE FALSE TRUE FALSE
#> 9 9 0.57578135 -71.68535 -7.058315 FALSE TRUE FALSE FALSE TRUE
#> 10 10 -0.30538839 -71.68615 -7.042494 FALSE FALSE FALSE TRUE FALSE
#> 11 11 1.51178117 -71.69045 -7.053219 FALSE TRUE FALSE TRUE TRUE
#> 12 12 0.38984324 -71.68278 -7.043211 FALSE FALSE FALSE FALSE TRUE
#> 13 13 -0.62124058 -71.69124 -7.053066 FALSE FALSE FALSE FALSE TRUE
#> 14 14 -2.21469989 -71.69510 -7.053325 FALSE FALSE FALSE FALSE FALSE
#> 15 15 1.12493092 -71.69859 -7.050473 FALSE FALSE TRUE FALSE FALSE
#> 16 16 -0.04493361 -71.69801 -7.042156 FALSE FALSE FALSE FALSE TRUE
#> 17 17 -0.01619026 -71.69367 -7.042713 FALSE FALSE FALSE FALSE TRUE
#> 18 18 0.94383621 -71.68963 -7.052200 FALSE FALSE FALSE FALSE TRUE
#> 19 19 0.82122120 -71.68676 -7.044454 FALSE FALSE TRUE FALSE TRUE
#> 20 20 0.59390132 -71.69186 -7.040788 FALSE FALSE FALSE FALSE TRUE
Data for spifa()
We extract the item as a matrix, and then provide it to a new
data.frame to later convert to sf:
# get items as a matrix
items <- subset(raw, select = grep("^item", names(raw))) |>
as.matrix() |>
unname()
# create data.frame and geo-reference it
data <- data.frame(items = I(items), wealth = raw$wealth, x = raw$lon, y = raw$lat) |>
st_as_sf(coords = c("x", "y"), crs = 4326)
data#> Simple feature collection with 20 features and 2 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: -71.69953 ymin: -7.058315 xmax: -71.68278 ymax: -7.040788
#> Geodetic CRS: WGS 84
#> First 10 features:
#> items.1 items.2 items.3 items.4 items.5 wealth geometry
#> 1 FALSE FALSE TRUE TRUE FALSE -0.6264538 POINT (-71.68358 -7.041742)
#> 2 FALSE FALSE FALSE TRUE TRUE 0.1836433 POINT (-71.68706 -7.054128)
#> 3 FALSE FALSE FALSE FALSE FALSE -0.8356286 POINT (-71.68434 -7.050819)
#> 4 FALSE TRUE FALSE FALSE TRUE 1.5952808 POINT (-71.68894 -7.053352)
#> 5 FALSE FALSE TRUE TRUE TRUE 0.3295078 POINT (-71.68941 -7.046983)
#> 6 FALSE FALSE FALSE FALSE FALSE -0.8204684 POINT (-71.68421 -7.05484)
#> 7 FALSE FALSE FALSE FALSE FALSE 0.4874291 POINT (-71.69953 -7.050429)
#> 8 FALSE FALSE FALSE TRUE FALSE 0.7383247 POINT (-71.69046 -7.044674)
#> 9 FALSE TRUE FALSE FALSE TRUE 0.5757814 POINT (-71.68535 -7.058315)
#> 10 FALSE FALSE FALSE TRUE FALSE -0.3053884 POINT (-71.68615 -7.042494)
Notice that we can easily access the response items:
data$items#> [,1] [,2] [,3] [,4] [,5]
#> [1,] FALSE FALSE TRUE TRUE FALSE
#> [2,] FALSE FALSE FALSE TRUE TRUE
#> [3,] FALSE FALSE FALSE FALSE FALSE
#> [4,] FALSE TRUE FALSE FALSE TRUE
#> [5,] FALSE FALSE TRUE TRUE TRUE
#> [6,] FALSE FALSE FALSE FALSE FALSE
#> [7,] FALSE FALSE FALSE FALSE FALSE
#> [8,] FALSE FALSE FALSE TRUE FALSE
#> [9,] FALSE TRUE FALSE FALSE TRUE
#> [10,] FALSE FALSE FALSE TRUE FALSE
#> [11,] FALSE TRUE FALSE TRUE TRUE
#> [12,] FALSE FALSE FALSE FALSE TRUE
#> [13,] FALSE FALSE FALSE FALSE TRUE
#> [14,] FALSE FALSE FALSE FALSE FALSE
#> [15,] FALSE FALSE TRUE FALSE FALSE
#> [16,] FALSE FALSE FALSE FALSE TRUE
#> [17,] FALSE FALSE FALSE FALSE TRUE
#> [18,] FALSE FALSE FALSE FALSE TRUE
#> [19,] FALSE FALSE TRUE FALSE TRUE
#> [20,] FALSE FALSE FALSE FALSE TRUE
This dataset can directly be used to fit a model:
spifa(items ~ wealth, data = data, nfactors = 2, niter = 5)#> Item factor analysis model: spifa_pred
#> Formula: items ~ wealth
#> Dimensions: 20 respondents, 5 items, 2 latent factors, 2 spatial processes
#> MCMC: 1 chain, iter = 5, thin = 1, samples = 5
#>
#> Item model parameters:
#> mean median sd q10 q90 ess_bulk rhat
#> c[1] -1.478 -1.471 0.24 -1.71 -1.241 NA 0.9
#> c[2] -0.298 -0.353 0.20 -0.44 -0.091 NA 1.9
#> c[3] -0.651 -0.783 0.20 -0.79 -0.422 NA 0.9
#> c[4] -0.734 -0.705 0.30 -1.02 -0.488 NA 0.9
#> c[5] -0.039 -0.044 0.17 -0.19 0.111 NA 0.9
#> A[1,1] 0.470 0.500 0.22 0.25 0.663 NA 1.9
#> A[2,1] 0.770 0.753 0.16 0.63 0.917 NA 1.9
#> A[3,1] 0.846 0.907 0.35 0.47 1.179 NA 0.9
#> A[4,1] -0.031 -0.173 0.63 -0.55 0.625 NA 1.9
#> A[5,1] 0.441 0.370 0.56 -0.11 1.010 NA 1.9
#> A[2,2] 1.176 1.282 0.34 0.80 1.480 NA 1.9
#> A[3,2] 0.035 -0.024 0.20 -0.15 0.252 NA 0.9
#> A[4,2] -0.394 -0.343 0.38 -0.76 -0.070 NA 1.9
#> A[5,2] -0.161 -0.254 0.27 -0.35 0.111 NA 1.9
#>
#> Factor model parameters:
#> mean median sd q10 q90 ess_bulk rhat
#> B[1,1] 0.1078 0.1789 0.33165 -0.250 0.402 NA 1.9
#> B[1,2] 0.0023 0.1551 0.35755 -0.384 0.321 NA 1.9
#> T[1,1] 0.4868 0.4828 0.01278 0.477 0.500 NA 0.9
#> T[2,2] 0.6929 0.6908 0.00903 0.684 0.702 NA 1.9
#> phi[1] 0.0471 0.0477 0.00097 0.046 0.048 NA 1.9
#> phi[2] 0.0536 0.0539 0.00062 0.053 0.054 NA 0.9
#> Corr[2,1] -0.0021 0.0023 0.02035 -0.023 0.018 NA 1.9
#>
#> ess_bulk is the bulk effective sample size; rhat is the potential
#> scale reduction factor on split chains (Rhat = 1 at convergence).
#> Use summary() for the full set of statistics (incl. ess_tail).
Alternatively, you could also create the sf data first
and then assign the items:
data <- data.frame(wealth = raw$wealth, x = raw$lon, y = raw$lat) |>
st_as_sf(coords = c("x", "y"), crs = 4326)
data$items <- items
data#> Simple feature collection with 20 features and 2 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: -71.69953 ymin: -7.058315 xmax: -71.68278 ymax: -7.040788
#> Geodetic CRS: WGS 84
#> First 10 features:
#> wealth geometry items.1 items.2 items.3 items.4 items.5
#> 1 -0.6264538 POINT (-71.68358 -7.041742) FALSE FALSE TRUE TRUE FALSE
#> 2 0.1836433 POINT (-71.68706 -7.054128) FALSE FALSE FALSE TRUE TRUE
#> 3 -0.8356286 POINT (-71.68434 -7.050819) FALSE FALSE FALSE FALSE FALSE
#> 4 1.5952808 POINT (-71.68894 -7.053352) FALSE TRUE FALSE FALSE TRUE
#> 5 0.3295078 POINT (-71.68941 -7.046983) FALSE FALSE TRUE TRUE TRUE
#> 6 -0.8204684 POINT (-71.68421 -7.05484) FALSE FALSE FALSE FALSE FALSE
#> 7 0.4874291 POINT (-71.69953 -7.050429) FALSE FALSE FALSE FALSE FALSE
#> 8 0.7383247 POINT (-71.69046 -7.044674) FALSE FALSE FALSE TRUE FALSE
#> 9 0.5757814 POINT (-71.68535 -7.058315) FALSE TRUE FALSE FALSE TRUE
#> 10 -0.3053884 POINT (-71.68615 -7.042494) FALSE FALSE FALSE TRUE FALSE
spifa(items ~ wealth, data = data, nfactors = 2, niter = 5)#> Item factor analysis model: spifa_pred
#> Formula: items ~ wealth
#> Dimensions: 20 respondents, 5 items, 2 latent factors, 2 spatial processes
#> MCMC: 1 chain, iter = 5, thin = 1, samples = 5
#>
#> Item model parameters:
#> mean median sd q10 q90 ess_bulk rhat
#> c[1] -0.521 -0.620 1.15 -1.596 0.6837 NA 1.9
#> c[2] -0.395 -0.422 0.22 -0.586 -0.1786 NA 1.9
#> c[3] -0.680 -0.644 0.64 -1.319 -0.0379 NA 1.9
#> c[4] -0.269 -0.381 0.26 -0.506 0.0105 NA 0.9
#> c[5] 0.343 0.310 0.14 0.240 0.4781 NA 0.9
#> A[1,1] 0.719 0.623 0.26 0.570 0.9666 NA 1.9
#> A[2,1] 0.733 0.877 0.24 0.464 0.9076 NA 1.9
#> A[3,1] -0.418 -0.393 0.43 -0.853 0.0078 NA 1.9
#> A[4,1] 0.072 0.084 0.13 -0.060 0.2057 NA 1.9
#> A[5,1] 0.539 0.674 0.46 0.039 0.9192 NA 1.9
#> A[2,2] 0.513 0.558 0.14 0.365 0.6409 NA 0.9
#> A[3,2] -0.640 -0.627 0.30 -0.939 -0.3380 NA 1.9
#> A[4,2] 0.338 0.604 0.59 -0.306 0.8515 NA 1.9
#> A[5,2] -0.347 -0.385 0.23 -0.562 -0.1122 NA 1.9
#>
#> Factor model parameters:
#> mean median sd q10 q90 ess_bulk rhat
#> B[1,1] 0.0339 0.0249 0.36675 -0.3279 0.345 NA 0.9
#> B[1,2] 0.0378 0.0188 0.19827 -0.1308 0.222 NA 1.9
#> T[1,1] 0.5518 0.5554 0.01123 0.5399 0.560 NA 0.9
#> T[2,2] 0.5037 0.5025 0.00435 0.5008 0.508 NA 0.9
#> phi[1] 0.0487 0.0487 0.00079 0.0480 0.049 NA 1.9
#> phi[2] 0.0512 0.0511 0.00082 0.0505 0.052 NA 1.9
#> Corr[2,1] 0.0061 0.0043 0.01438 -0.0058 0.021 NA 1.9
#>
#> ess_bulk is the bulk effective sample size; rhat is the potential
#> scale reduction factor on split chains (Rhat = 1 at convergence).
#> Use summary() for the full set of statistics (incl. ess_tail).
Missing values
Missing item responses
Individual missing item responses (NA) don’t need to be
dropped or imputed – spifa() handles them natively,
treating a missing response as an unobserved auxiliary variable to be
sampled along with everything else, rather than requiring complete
rows:
data_na <- data
data_na$items[1:5, 1] <- NA
fit_na <- spifa(items ~ 1, data = data_na, nfactors = 2, ngp = 0, niter = 5)
fit_na#> Item factor analysis model: eifa
#> Formula: items ~ 1
#> Dimensions: 20 respondents, 5 items, 2 latent factors, 0 spatial processes
#> MCMC: 1 chain, iter = 5, thin = 1, samples = 5
#>
#> Item model parameters:
#> mean median sd q10 q90 ess_bulk rhat
#> c[1] -0.76 -0.759 0.41 -1.1637 -0.35 NA 1.9
#> c[2] -1.09 -1.114 0.34 -1.4281 -0.76 NA 1.9
#> c[3] -0.11 -0.083 0.29 -0.3795 0.18 NA 1.9
#> c[4] -0.51 -0.431 0.29 -0.8142 -0.24 NA 0.9
#> c[5] 0.35 0.348 0.10 0.2492 0.45 NA 1.9
#> A[1,1] 0.45 0.577 0.25 0.1766 0.67 NA 0.9
#> A[2,1] -0.37 -0.461 0.25 -0.5951 -0.11 NA 1.9
#> A[3,1] 0.25 0.284 0.27 -0.0021 0.48 NA 1.9
#> A[4,1] 0.59 0.670 0.24 0.3540 0.74 NA 1.9
#> A[5,1] 0.23 0.194 0.24 0.0111 0.47 NA 1.9
#> A[2,2] 1.49 1.534 0.30 1.2155 1.72 NA 0.9
#> A[3,2] 0.15 0.217 0.20 -0.0657 0.34 NA 0.9
#> A[4,2] 0.28 0.413 0.38 -0.1132 0.54 NA 0.9
#> A[5,2] 0.30 0.239 0.16 0.1605 0.47 NA 1.9
#>
#> ess_bulk is the bulk effective sample size; rhat is the potential
#> scale reduction factor on split chains (Rhat = 1 at convergence).
#> Use summary() for the full set of statistics (incl. ess_tail).
As you can see in Dimensions, the missing responses are
not removed but rather modelled.
Missing predictor values
Missing predictor values behave differently, and are worth
calling out: a NA in a predictor column drops that
respondent from the whole fit:
data_na <- data
data_na$wealth[1:5] <- NA
fit_na <- spifa(items ~ wealth, data = data_na, nfactors = 2, ngp = 0, niter = 5)
fit_na#> Item factor analysis model: cifa_pred
#> Formula: items ~ wealth
#> Dimensions: 15 respondents, 5 items, 2 latent factors, 0 spatial processes
#> MCMC: 1 chain, iter = 5, thin = 1, samples = 5
#>
#> Item model parameters:
#> mean median sd q10 q90 ess_bulk rhat
#> c[1] -0.286 -0.472 0.48 -0.71 0.249 NA 1.9
#> c[2] -0.724 -0.606 0.69 -1.39 -0.081 NA 1.9
#> c[3] -1.120 -1.276 0.40 -1.46 -0.673 NA 1.9
#> c[4] -0.614 -0.572 0.22 -0.84 -0.436 NA 1.9
#> c[5] 0.214 0.299 0.14 0.06 0.311 NA 1.9
#> A[1,1] 0.437 0.412 0.63 -0.19 1.082 NA 1.9
#> A[2,1] -0.266 -0.233 0.18 -0.46 -0.111 NA 1.9
#> A[3,1] 0.148 -0.040 0.38 -0.17 0.571 NA 1.9
#> A[4,1] 0.099 0.151 0.24 -0.13 0.276 NA 1.9
#> A[5,1] -0.601 -0.679 0.35 -0.90 -0.242 NA 1.9
#> A[2,2] 0.547 0.559 0.31 0.25 0.815 NA 1.9
#> A[3,2] -0.070 -0.022 0.14 -0.22 0.048 NA 0.9
#> A[4,2] -0.224 -0.257 0.23 -0.44 0.019 NA 0.9
#> A[5,2] 0.046 -0.070 0.27 -0.19 0.338 NA 1.9
#>
#> Factor model parameters:
#> mean median sd q10 q90 ess_bulk rhat
#> B[1,1] -0.495 -0.45 0.329 -0.8420 -0.19 NA 1.9
#> B[1,2] -0.780 -0.91 0.247 -0.9809 -0.50 NA 0.9
#> Corr[2,1] 0.016 0.01 0.022 -0.0031 0.04 NA 0.9
#>
#> ess_bulk is the bulk effective sample size; rhat is the potential
#> scale reduction factor on split chains (Rhat = 1 at convergence).
#> Use summary() for the full set of statistics (incl. ess_tail).
As you can see in Dimensions, the 5 respondents with a
missing wealth value are dropped – 15 respondents remain,
unlike the item-response case above.