Compute the goodness-of-fit indices \(R^2_D(K)\) of Lewis and Grossetti
(2026) for one fitted topic model: the proportional reduction in
discrepancy achieved by the \(K\)-topic model relative to the no-topics
baseline in which every document follows the corpus word distribution,
$$R^2_D(K) = 1 - D(K) / D(\mathrm{null}),$$
for the squared-error, Pearson chi-square, and deviance discrepancy
families. Document-level indices are evaluated on the harmonized support
of optop_make_partition(); word-level indices are evaluated on the
unbinned vocabulary.
Usage
optop_index_se(
model,
dtm,
partition,
baseline,
macro = FALSE,
level = c("document", "word"),
block_size = NULL,
n_threads = 1L,
min_null = NULL
)
optop_index_chisq(
model,
dtm,
partition,
baseline,
macro = FALSE,
level = c("document", "word"),
block_size = NULL,
n_threads = 1L,
min_null = NULL
)
optop_index_deviance(
model,
dtm,
partition,
baseline,
macro = FALSE,
level = c("document", "word"),
block_size = NULL,
n_threads = 1L,
min_null = NULL
)Arguments
- model
A single fitted
topicmodels::LDAmodel (VEM or Gibbs), or a loader function returning one (materialized on demand).- 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 for corpora past the size of a single matrix (shards stream one at a time; per-document results are bit-identical to an unsharded run).- partition
Result from
optop_make_partition(), computed on a DTM that is aligned to the model vocabulary (same features and order).- baseline
Result from
optop_make_baseline(), computed on the same aligned counts DTM used forpartition.- macro
Logical; if
TRUE, also compute the Macro index (equal-weight average of the per-document indices). Default:FALSE.- level
Character; aggregation level for the index.
"document"(default) computes document-level indices aggregated across words."word"computes word-level indices aggregated across documents.- 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 (default
1L). Results are identical for any value; only wall time changes. The package website article on very large corpora covers thread and BLAS configuration at scale.- min_null
Nonnegative scalar; the null-discrepancy floor of the document-level aggregation. Documents with baseline discrepancy \(D_j(\mathrm{null}) < \mathrm{min\_null}\) are excluded from the Micro and Macro aggregations and carry an
NAdocument-level index. The defaultNULLresolves to the partition threshold constantc;min_null = 0restores the pre-0.14.1 strict positivity rule. See the Null-discrepancy floor paragraph in Details.
Value
When level = "document", a list with:
r2: scalar Micro index over \(J_+\).r2_macro: scalar Macro index ifmacro = TRUE, otherwiseNULL.r2_doc: numeric vector (length J) with per-document indices;NAfor documents with zero baseline discrepancy.d_model,d_null: numeric vectors (length J) with the per-document fitted and baseline discrepancies.n_null_excluded,null_excluded_share: number and share of documents excluded by the null-discrepancy floor.K: number of topics inmodel.metric: one of"se","chisq","deviance".
When level = "word", a list with:
r2_word: named numeric vector (length W) of per-word \(R^2_{D,w}(K)\);NAfor words with zero baseline discrepancy.r2_micro_word: scalar Micro-Word index (discrepancy-weighted average).r2_macro_word: scalar Macro-Word index (unweighted average over words with positive baseline discrepancy).d_model,d_null: numeric vectors (length W) with the per-word fitted and baseline discrepancies.K: number of topics inmodel.metric: one of"se","chisq","deviance".
Details
Harmonized support. For each document, rare words (as determined by
optop_make_partition(), whose union includes the no-topics baseline)
are collapsed into a single "min" bin. Document-level indices are then
evaluated on the union of the non-rare terms and the min bin, so that
changes in the index
across \(K\) reflect changes in model fit, not changes in binning.
Because the harmonized set is a union over the whole model grid, the
reported value at any fixed \(K\) depends on the pair (grid, c):
compare indices across studies only under a common grid and c.
Interpretation. The indices are descriptive effect sizes anchored by the null baseline (lower reference) and the saturated model (upper reference), not tests of correct specification. The deviance index is bounded above by one; the Pearson and squared-error indices are not confined to \([0, 1]\), and negative values are informative: the fitted model performs worse than the no-topics baseline under that discrepancy. In-sample values computed on the estimation corpus summarize descriptive fit and should not be the sole basis for choosing \(K\).
Pearson min-bin rule. For the chi-square family the collapsed min-bin
of a document enters only when
\(\min(\min_K E^K_{j,\min}, B_{j,\min}) \ge c\),
decided once for the whole grid by optop_make_partition() and applied
to the fitted and the null discrepancy simultaneously; excluded shares
are reported.
Alignment requirements. The following must share the same vocabulary and column
order: the dtm passed to the index, the DTM used for partition and baseline, and the
model's term-topic matrix. If they differ, align with optop_align_dtm_to_models() and
recompute partition and baseline.
Counts only. SE/chi-square/deviance indices are defined for multinomial counts.
Do not pass weighted matrices (e.g., proportions from quanteda::dfm_weight(scheme = "prop")
or tf-idf). If your workflow uses proportions elsewhere, reconstruct counts before calling.
Document-level aggregation (level = "document"). The Micro index
pools discrepancies before the ratio and is therefore a
discrepancy-weighted average of the document-level fits, with weights
proportional to the baseline discrepancy \(D_j(\mathrm{null})\);
the Macro index is their unweighted average. Both aggregate over the
non-degenerate documents
\(J_+ = \{j : D_j(\mathrm{null}) \ge \mathrm{min\_null}\}\)
only, and excluded documents carry an NA document-level index. The
Micro-Macro gap is itself diagnostic: it equals the covariance between
document-level fit and baseline discrepancy (normalized by the mean
baseline discrepancy), so a positive gap indicates that fit concentrates
in high-discrepancy (often long or atypical) documents. The returned
d_model and d_null vectors support the recommended scatter of
document-level fit against baseline discrepancy or length.
Null-discrepancy floor. At partition resolution
\(\tau_j = c/L_j\) a document whose support is entirely
swept into the collapsed min-bin has observed equal to baseline counts by
construction, so its baseline discrepancy is zero in every family at
once, and a document whose word distribution sits close to the corpus
baseline has \(D_j(\mathrm{null})\) arbitrarily near zero.
In both regimes the document-level index
\(1 - D_j(K)/D_j(\mathrm{null})\) is unbounded,
and the Macro index, an unweighted mean, can be destroyed by a handful of
such documents, while the Micro index is far less exposed because it
weights documents by \(D_j(\mathrm{null})\) itself. The
aggregation therefore conditions on
\(D_j(\mathrm{null}) \ge \mathrm{min\_null}\),
with default equal to the partition constant c: the estimand becomes
the average fit among documents whose baseline discrepancy is at least
the resolution of the evaluation support. Tiny baseline discrepancies
signal collapsed supports rather than sampling noise: under the null, a
document's binned baseline discrepancy is approximately chi-square with
(number of effective bins - 1) degrees of freedom, so values orders of
magnitude below one arise only when the partition has degenerated.
Exclusions are reported once per call and returned as
n_null_excluded; report sensitivity to min_null alongside c when
the excluded share is non-negligible.
Word-level aggregation (level = "word"). Word-level indices are
computed on the unbinned vocabulary and are descriptive diagnostics: they
reveal which words the topic structure captures well and which it
mispredicts. The word-level deviance is the Poisson unit deviance
\(2\sum_j [N_{jw}\log(N_{jw}/E_{jw}) - (N_{jw}-E_{jw})]\),
which is nonnegative term by term, so the word deviance index never
exceeds one; for the corpus baseline the linear correction vanishes
identically. Micro-Word weights words by their baseline discrepancy;
Macro-Word averages the word-level indices over the words with positive
baseline discrepancy (degenerate words carry NA). Restrict attention to
words with adequate support when interpreting single-word values.
Functions
optop_index_se(): Squared-error index \(R^2_{SE}\).optop_index_chisq(): Pearson chi-square index \(R^2_{chisq}\), subject to the min-bin inclusion rule ofoptop_make_partition().optop_index_deviance(): Deviance index \(R^2_{dev}\), the default likelihood-based summary: the grouped multinomial deviance at document level and the Poisson unit deviance at word level. Bounded above by one; under exact maximum likelihood and nesting the in-sample document-level index is also nonnegative up to binning effects, a guarantee that does not extend to approximate inference.
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
Agresti, A. (1996). An Introduction to Categorical Data Analysis. Wiley, New York.
Examples
# a corpus with known truth: the true model against a random competitor
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)
truth <- optop_model(theta, phi)
rand <- local({
th <- rdirich(40, 3); rownames(th) <- rownames(theta)
ph <- rdirich(3, 60); colnames(ph) <- colnames(phi)
optop_model(th, ph)
})
# the harmonized support is shared by the whole grid
partition <- optop_make_partition(list(truth, rand), counts, c = 1)
baseline <- optop_make_baseline(counts)
res_truth <- optop_index_deviance(truth, counts, partition, baseline,
macro = TRUE)
res_rand <- optop_index_deviance(rand, counts, partition, baseline,
macro = TRUE)
res_truth
#> <optop_index>: deviance discrepancy index, K = 3
#> Micro R^2: 0.4410 | Macro R^2: 0.3996
#> J+ = 40 of 40 documents
c(truth = res_truth$r2, random = res_rand$r2)
#> truth random
#> 0.4409784 -2.4478475
# the Pearson and squared-error variants share the interface
res_x2 <- optop_index_chisq(truth, counts, partition, baseline)
#> ℹ Pearson min bin excluded for 2 documents (5.0% of the corpus; mean excluded observed mass 0.0000)
res_se <- optop_index_se(truth, counts, partition, baseline)
# word-level diagnostic: which words does the model capture best?
word <- optop_index_deviance(truth, counts, partition, baseline,
level = "word")
head(sort(word$r2_word, decreasing = TRUE), 3)
#> w25 w57 w19
#> 0.8486706 0.7648520 0.7521713