--- title: "Network-Based Item Selection" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Network-Based Item Selection} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(meow) ``` `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. # Mathematical foundation 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). # Edge weight strategies The edge-weight function maps co-exposure counts to graph weights, and the choice shapes behavior. All of these are bundled and unchanged: ```{r, eval = FALSE} 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)) ``` * **Inverse / negative log / exponential**: more co-responses give *smaller* weights, so frequently co-administered items are "closer" and the algorithm spreads exposure across dissimilar items. * **Linear**: more co-responses give *larger* weights, inverting that logic. * **Power**: `beta < 1` dampens and `beta > 1` amplifies the effect of high co-response counts. # Implementation `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: ```{r, eval = FALSE} 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()`. # Using different edge weight strategies A small runnable example with the default inverse weights: ```{r} 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) ``` Swap in a different edge-weight function through `select_max_dist_enhanced()`: ```{r, eval = FALSE} # 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" ) ``` # Choosing a strategy | 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 | # Considerations * `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()`. * Experiment with `n_candidates` (1--5) to trade exposure control against measurement efficiency, and compare against simpler selectors as a baseline.