OpTop goodness-of-fit indices table across a grid of LDA models
Source:R/discrepancies.R
optop_index_table.RdComputes one or more OpTop goodness-of-fit indices as computed by optop_index(),
for a list of fitted topic models and returns a tidy table by number of topics \(K\).
Arguments
- models
A non-empty
listof fittedtopicmodels::LDAmodels (VEM or Gibbs), typically a grid over different \(K\); elements may be loader functions returning a fit, materialized one at a time.- dtm
A counts document-term matrix (documents x terms). Use
Matrix::dgCMatrixor aquanteda::dfmthat represents raw counts (not weighted), or anoptop_corpus()of count shards.- metrics
Character vector selecting which indices to compute. Any subset of
c("se", "chisq", "deviance"). Default:c("se","chisq","deviance").- c
Positive scalar used inside
optop_make_partition()to set the per-document rare threshold \(\tau_j = c / L_j\). Default:1, the value the paper adopts and recommends when the deviance index is primary; usec = 5for Pearson-primary work and report sensitivity toc.- macro
Logical; if
TRUE, also compute macro indices (equal-weight averages of per-document indices). Default:FALSE.- partition
Optional precomputed result from
optop_make_partition(). IfNULL, it is computed internally frommodelsanddtm.- baseline
Optional precomputed result from
optop_make_baseline(). IfNULL, it is computed internally fromdtm.- level
Character; aggregation level for the indices.
"document"(default) returns document-level micro/macro indices."word"returns word-level Micro-Word and Macro-Word indices.- block_size
Integer or
NULL; number of vocabulary terms to process at once during word-level computation. Smaller values use less memory but may be slower. IfNULL(default), automatically chosen based on corpus size to target ~1.5 GB memory usage. Only used whenlevel = "word".- n_threads
Integer; number of OpenMP threads used by the compiled index engine and, when
partitionis computed internally, by the partition kernel (default1L). Results are identical for any value.- min_null
Nonnegative scalar; the null-discrepancy floor of the document-level aggregation, applied uniformly across metrics and models (the baseline discrepancy does not depend on \(K\), so the same documents are excluded at every grid point). The default
NULLresolves to the partition threshold constantc;min_null = 0restores the pre-0.14.1 strict positivity rule. See the Null-discrepancy floor paragraph of optop_index.
Value
When level = "document", a data.frame with one row per model and columns:
K: number of topics in the model.R2_SE,R2_chisq,R2_dev: micro indices for the selected metrics.R2_SE_macro,R2_chisq_macro,R2_dev_macro: macro indices whenmacro = TRUE.
When level = "word", a data.frame with columns:
K: number of topics in the model.R2_SE_micro_word,R2_chisq_micro_word,R2_dev_micro_word: Micro-Word indices.R2_SE_macro_word,R2_chisq_macro_word,R2_dev_macro_word: Macro-Word indices.
Missing columns are omitted when the corresponding metric is not requested.
Details
The function wraps optop_index_se(), optop_index_chisq(), and
optop_index_deviance(). It reuses a harmonized support (via
optop_make_partition()) and a fixed baseline (via
optop_make_baseline()) so results are directly comparable across \(K\).
Performance tips
To avoid recomputation, precompute
partition <- optop_make_partition(...)andbaseline <- optop_make_baseline(...)once and pass them in.
References
Lewis, C. M. and Grossetti, F. (2026). Goodness-of-fit indices and diagnostics for topic models. Working paper.
Lewis, C. M. and Grossetti, F. (2022). A statistical approach for optimal topic model identification. Journal of Machine Learning Research, 23(58), 1–20. https://jmlr.org/papers/v23/19-297.html
Examples
# a small grid over a simulated corpus with known truth at K = 3
rdirich <- function(n, k) {
g <- matrix(stats::rgamma(n * k, shape = 1), n, k)
g / rowSums(g)
}
set.seed(1)
theta <- rdirich(40, 3)
rownames(theta) <- sprintf("d%02d", 1:40)
phi <- rdirich(3, 60)
colnames(phi) <- sprintf("w%02d", 1:60)
counts <- sim_dfm(theta, phi, doc_length = 400, seed = 2)
models <- lapply(c(2, 3, 4), function(k) {
if (k == 3) return(optop_model(theta, phi))
th <- rdirich(40, k); rownames(th) <- rownames(theta)
ph <- rdirich(k, 60); colnames(ph) <- colnames(phi)
optop_model(th, ph)
})
# 1) quick run: partition and baseline computed internally
tab <- optop_index_table(models, counts, metrics = c("se", "deviance"),
macro = TRUE)
tab
#> <optop_index_table>: discrepancy indices over the topic grid
#> K R2_SE R2_SE_macro R2_dev R2_dev_macro
#> <int> <num> <num> <num> <num>
#> 1: 2 -2.8761308 -3.6018261 -2.830166 -3.1341924
#> 2: 3 0.5690441 0.5049576 0.447822 0.4073347
#> 3: 4 -1.7714541 -2.1553309 -2.006407 -2.1979370
# 2) faster repeated runs: precompute and reuse partition/baseline
part <- optop_make_partition(models, counts, c = 1)
base <- optop_make_baseline(counts)
tab2 <- optop_index_table(models, counts,
metrics = c("se", "chisq", "deviance"),
partition = part, baseline = base)