Skip to contents

Computes 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\).

Usage

optop_index_table(
  models,
  dtm,
  metrics = c("se", "chisq", "deviance"),
  c = 1,
  macro = FALSE,
  partition = NULL,
  baseline = NULL,
  level = c("document", "word"),
  block_size = NULL,
  n_threads = 1L,
  min_null = NULL
)

Arguments

models

A non-empty list of fitted topicmodels::LDA models (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::dgCMatrix or a quanteda::dfm that represents raw counts (not weighted), or an optop_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; use c = 5 for Pearson-primary work and report sensitivity to c.

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(). If NULL, it is computed internally from models and dtm.

baseline

Optional precomputed result from optop_make_baseline(). If NULL, it is computed internally from dtm.

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. If NULL (default), automatically chosen based on corpus size to target ~1.5 GB memory usage. Only used when level = "word".

n_threads

Integer; number of OpenMP threads used by the compiled index engine and, when partition is computed internally, by the partition kernel (default 1L). 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 NULL resolves to the partition threshold constant c; min_null = 0 restores 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 when macro = 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(...) and baseline <- 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)