API conventions: the shared vocabulary¶
topica has more than twenty models. They feel like one library only if the same
concept wears the same name everywhere: the iteration count is always iters,
the seed is always seed, a covariate design matrix is always reachable as
covariates=. This page records that vocabulary so a new model (the most recent
being GDMR) matches by construction rather than by memory.
The test is the source of truth
A hand-maintained style guide drifts. The authority is
tests/test_naming_conventions.py, which introspects every model's
__init__/fit signature and fails when a model breaks a rule. This page is
the explanation; the test is the contract. When the two disagree, the
test wins and this page is wrong. It pairs with the structural
estimator contract (which methods/attributes a model
must expose) and the implementer's playbook.
Naming rules¶
| Concept | Canonical | Not |
|---|---|---|
| Iteration count | iters |
iterations, n_iter, max_iter, epochs |
| Secondary iteration count | <thing>_iters (var_iters, lbfgs_iters) |
— |
| RNG seed | seed=42 |
random_state, random_seed |
| Counts | num_* (num_topics, num_samples, num_threads, num_theta_draws) |
n_*, n |
| Tolerance | convergence_tol (and em_tol for EM/variational models) |
tol |
| Periodic cadence | *_interval (optimize_interval, sample_interval, progress_interval) |
— |
| Human labels for a matrix/index arg | <thing>_names (feature_names, prevalence_names, label_names) |
— |
| Dirichlet / regression priors | alpha, beta, prior_variance |
— |
Structural rules¶
num_topicsis the first positional argument of the constructor; everything else in__init__is keyword-only (after*).seed=42is always present. The exceptions are principled and recorded in the test: models that discover K (HDP,BERTopic,Top2Vec), models whose leading required input is something else (KeyATMkeywords,SeededLDAseed words,LabeledLDAlabels), and the two-levelPA(num_super,num_sub).fit(self, data, <side-input>, *, ...)—datais first; the model's supervision/covariate input is a positional argument immediately after it, with its<thing>_namesas the first keyword-only argument.- The Gibbs-sampler family shares a fixed keyword block, in this order:
iters, num_samples, sample_interval, progress, progress_interval, keep_theta_draws, num_theta_draws, convergence_tol, check_every. - Every model exposes
model.settings— its constructor configuration as a JSON-serialisable dict, keyword-named to match__init__(issue #400). It reports the effective values in force (e.g.num_threadsafter itsmax(1)floor;sampler/initas the canonical public string, not an internal flag), and omits data/guidance inputs (keywords,seed_words, covariate arrays, embeddings, LLM backends).tests/test_model_settings.pyderives the expected keys from each constructor signature, so a new parameter that is not surfaced insettingsfails the suite — there is no second inventory to maintain. The analysis manifest (record_fit) reads this surface directly.
Threads and stopping rules¶
Reproducibility is the default. Gibbs samplers use num_threads=1 by default:
this is the exact, reference-comparable path. Passing num_threads > 1 is an
explicit opt-in to an approximate parallel sampler; the result remains
reproducible for the same seed and thread count, but it is not expected to equal
the single-threaded fit. Variational and matrix-factorization models may use all
available cores by default when their parallel reductions remain bit-exact.
Likewise, convergence_tol=0.0 is the default for Gibbs samplers. It means
"run the requested number of sweeps," not "the chain has converged." A positive
tolerance is only a pragmatic early-stop heuristic on the log-likelihood trace;
it does not establish MCMC mixing. Use stop_reason(model) to report whether
the heuristic stopped a fit, and reserve MCMC-native diagnostics for retained
draws or multiple chains.
The fit side-input vocabulary¶
The second positional argument names what the model conditions on. One concept, one name:
| Concept | Name | Models |
|---|---|---|
| Document covariate design matrix | covariates (universal alias); native features (DMR/GDMR), prevalence (STM/STS) |
DMR, GDMR, STM, STS, KeyATM |
| Content covariate (group → words) | content |
STM |
| Time index | times |
DTM |
| Document labels | labels |
LabeledLDA |
| Document groups | groups |
SAGE |
| Supervised response | y |
SupervisedLDA |
| Document embeddings | doc_embeddings |
BERTopic, FASTopic, Top2Vec |
| Word embeddings | word_embeddings |
ETM |
The alias policy¶
topica is a faithful drop-in for several reference packages (R stm/keyATM,
MALLET, tomotopy), and migrating users arrive with the reference package's
vocabulary in their fingers. The rule:
- The topica canonical name is the default and what the docs use.
- A model may keep a native primary that matches its reference implementation
where that is the explicit goal —
STM.fit(data, prevalence=...)mirrors Rstm, and that fidelity is intentional, not drift. covariates=is the one cross-model alias that must work on every covariate model, so code written against one transfers to the others. The test enforces this.- Reference-package aliases are welcome as additive keyword aliases to ease
switching (for example
GDMR.fitacceptsmetadata=for users coming from tomotopy'sGDMRModel). Resolve the aliases to one value and raise if more than one is supplied; never let two spellings silently disagree.
Reference: the covariate family¶
These signatures are the template. A new covariate model should look like one of them, adding only its own model-specific knobs.
DMR.__init__(num_topics, *, beta=0.01, optimize_interval=50, burn_in=200,
seed=42, prior_variance=1.0, lbfgs_iters=20, sampler='sparse')
DMR.fit(data, features=None, *, feature_names=None, iters=1000,
num_samples=5, sample_interval=25, progress=None, progress_interval=50,
keep_theta_draws=True, num_theta_draws=25, convergence_tol=0.0,
check_every=10, covariates=None)
STM.fit(data, prevalence=None, *, prevalence_names=None, content=None,
content_names=None, iters=500, convergence_tol=1e-05, ..., covariates=None)
LabeledLDA.fit(data, labels, *, label_names=None, iters=1000, ...)
SupervisedLDA.fit(data, y, *, iters=25, var_iters=15, ...)
Worked example: GDMR (outer shape shared, inner computation unique)¶
GDMR (generalized DMR) is the live test of these rules. It is not "DMR with
continuous features passed raw" — that would just be DMR. The g-DMR contribution
(Lee & Song 2020) is a Legendre-polynomial basis over continuous metadata plus a
decay prior that shrinks higher-order terms, giving a smooth topic-distribution
function. So GDMR follows the rules where they apply and takes the documented
"computation can be unique" carve-out where the model genuinely differs:
- Outer shape matches DMR.
num_topicsfirst positional; keyword-only rest;seed=42.fit(data, features=None, *, iters=1000, ...)with the same keyword block. - Cross-model alias honored.
featuresis the native primary;covariates=works (universal alias);metadata=works (tomotopy migration alias). - Model-specific params are namespaced, not forced into DMR's vocabulary.
degrees,metadata_rangedescribe the basis;sigma,sigma0,decayare the structured prior (DMR's singleprior_variancecannot express a per-degree decay, so reusing that name would mislead). New methodstdf/tdf_linspaceread the fitted surface.
The principle: share the skeleton, keep the organs. A reader who knows DMR can drive GDMR; a reader who knows g-DMR finds the parameters the literature names.
Resolved naming decisions¶
These were the candidate inconsistencies (#155). Each is now decided, so the
KNOWN_DRIFT list in tests/test_naming_conventions.py is empty.
-
LLM-based evaluation is a namespace,
topica.llm.*, not flatllm_*. The diagnostics that call an external model (topica.llm.coherence,topica.llm.intrusion,topica.llm.select_k, and the futureoutlier/repetitiveness/diversity/alignment) are grouped undertopica.llmrather than spread as top-levelllm_*functions. This is a deliberate carve-out from the flat-function rule: the suite is a coherent, growing family that shares one property the rest of the library does not —llm-boundednon-determinism — and the namespace signals that at the call site.topica.llm.backendis the same constructor as the top-leveltopica.llm_backend(kept flat because it is also the bring-your-own-model adapter forTopicGPTandlabel_topics, not only an eval metric). -
Temporal index —
times. Canonical across models (DTM's positional arg). KeyATM now acceptstimes=and keepstimestamps=as an alias. Atest_temporal_models_accept_timescheck enforcestimeson every temporal model; new temporal models must use it and not introduce a third name. check_everyis kept for the convergence-check cadence. The*_intervalpattern names how often we do something (sample, optimize, report);check_everynames how often we test convergence, a deliberately distinct concept, so it is not renamed tocheck_interval.- Gaussian-prior naming follows each model's lineage.
DMRusesprior_variance(a variance, matching its MALLET-family lineage);GDMRusessigma/sigma0(std-devs, matching the g-DMR literature) and additionally needs two scales plusdecay, which a singleprior_variancecannot express. These are different parameterizations of different priors, so they keep different names. labels(LabeledLDA) vsgroups(SAGE) are different things. LabeledLDA'slabelsrestrict which topics a document may use (a per-document topic set); SAGE'sgroupsselect a content group that reshapes the topic-word distribution. Different roles, so different names — the right outcome under the one-concept-one-name rule.