select_max_dist()
selects items using the entire item-exposure history. It treats
the item pool as a weighted graph and administers the item farthest, in
shortest-path distance, from the items a respondent has already seen.
This balances exposure control against measurement efficiency.
The item pool is a weighted graph: nodes are items, and edge weights
derive from the co-exposure matrix adj_mat (entry \((i, j)\) is the number of respondents who
have seen both items). The Floyd–Warshall algorithm
(Rfast::floyd()) turns the edge-weight matrix \(W\) into an all-pairs shortest-path
distance matrix \(D\). For each
respondent, the distance of a candidate item to the set of administered
items is the minimum distance to any of them, and the farthest candidate
is administered (ties broken by maximum information).
The edge-weight function maps co-exposure counts to graph weights, and the choice shapes behavior. All of these are bundled and unchanged:
edge_weight_inverse(adj_mat, alpha = 1) # 1 / (adj_mat + alpha)
edge_weight_negative_log(adj_mat, alpha = 1) # -log(adj_mat + alpha)
edge_weight_linear(adj_mat, max_co_responses = NULL) # adj_mat / max(adj_mat)
edge_weight_power(adj_mat, beta = 0.5, alpha = 1) # (adj_mat + alpha)^beta
edge_weight_exponential(adj_mat, lambda = 0.1) # exp(-lambda*(adj_mat+alpha))beta < 1 dampens and
beta > 1 amplifies the effect of high co-response
counts.select_max_dist() follows the item selection contract
(vignette("item-selection")): it works on the matrix
administration state and returns an updated admin. After
the distance matrix is computed, the per-item distances are obtained
with Rfast::colMins() rather than a row-wise data-frame
operation:
select_max_dist <- function(pers, item, R, admin, adj_mat = NULL, n_candidates = 1) {
if (!any(admin != 0)) {
admin[, seq_len(min(5, ncol(admin)))] <- 1L # seed five items
return(admin)
}
dist_mat <- Rfast::floyd(1 / adj_mat) # all-pairs shortest paths
info <- { # 2PL information matrix
lin <- sweep(outer(pers$theta, item$b, "-"), 2, item$a, "*")
P <- stats::plogis(lin); sweep(P * (1 - P), 2, item$a^2, "*")
}
for (i in which(rowSums(admin == 0) > 0)) {
administered <- which(admin[i, ] != 0)
candidates <- which(admin[i, ] == 0)
sub <- dist_mat[administered, candidates, drop = FALSE]
cand_dist <- if (length(administered) == 1L) sub[1, ] else Rfast::colMins(sub, value = TRUE)
pool <- candidates[cand_dist >= max(cand_dist)] # farthest items
admin[i, pool[which.max(info[i, pool])]] <- 1L # tie-break by information
}
admin
}select_max_dist_enhanced() is identical except that the
edge weights come from a user-supplied edge_weight_fun
applied to adj_mat before Rfast::floyd().
A small runnable example with the default inverse weights:
sim <- meow(
select_fun = select_max_dist,
update_fun = update_theta_mle,
data_loader = data_simple_1pl,
data_args = list(N_persons = 50, N_items = 30),
select_args = list(n_candidates = 3),
fix = "item"
)
nrow(sim$results)
#> [1] 26Swap in a different edge-weight function through
select_max_dist_enhanced():
# Power transformation with beta = 0.3
meow(
select_fun = select_max_dist_enhanced,
update_fun = update_theta_mle,
data_loader = data_simple_1pl,
data_args = list(N_persons = 100, N_items = 50),
select_args = list(
n_candidates = 3,
edge_weight_fun = edge_weight_power,
edge_weight_args = list(beta = 0.3, alpha = 1)
),
fix = "item"
)| Strategy | Goal | Trade-off |
|---|---|---|
| Inverse (default) | spread exposure across dissimilar items | may over-expose clusters |
| Linear | keep item clusters / topic areas together | can reduce efficiency |
| Power | tune sensitivity to co-response counts | requires choosing beta |
| Exponential | strong exposure control | can reduce efficiency |
Rfast::floyd() is \(O(n^3)\) in the number of items and is run
each iteration, so network selection is more expensive than
select_max_info().n_candidates (1–5) to trade exposure
control against measurement efficiency, and compare against simpler
selectors as a baseline.