API conventions: the shared vocabulary¶
topica has more than 40 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 newly added model 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=13 |
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=13is 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. Models steered by user keywords (seed words, keywords, anchor words) have their own shared contract: see keyword & seed parameters.- 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. top_words(n, topic=None)returns bare word strings by default:list[list[str]]for all topics, orlist[str]for onetopic=, so", ".join(model.top_words(n, topic=t))works directly. Passweights=Truefor the(word, prob)pairs instead —list[list[tuple[str, float]]]/list[tuple[str, float]], the topic-word weights in descending order. Both shapes are uniform across every model, including the variants that take extra arguments (SAGE'sgroup=,DTM'stime,HLDA'snode,InfoCTM'slang=);weightsis keyword-only everywhere (issues #742, #752).convergedis a property;coherence(...)andbound()are methods. On a fitted model, readmodel.convergedwith no parentheses — it is a bool attribute, somodel.converged()raisesTypeError: 'bool' object is not callable. In contrastmodel.coherence()andmodel.bound()are called. (Corpusdocuments the same property-vs-method split for its own accessors.)convergedreports early stopping, not "the fit is good": it isTrueonly when a positiveconvergence_tolwas actually hit, so with the default (convergence_tol=0, fulliters) it is alwaysFalse. Readmodel.early_stopped— the same value under the name that says what it means — when the ambiguity would mislead;convergedis kept as an alias. For a plain-English summary of why a fit stopped, calltopica.stop_reason(model)(issue #755).-
coherence(...)'s first positional argument isn, the top-word count — not the corpus. It defaults to the training corpus, so baremodel.coherence()works; pass a reference corpus with the keywordtexts=for the windowed measures. This flips the convention of the module-leveltopica.evaluate.coherence( topics, texts, ...), whose first argument is the topics — somodel.coherence(corpus.documents())misfires. Passing texts positionally now raises a directiveTypeErrornaming thetexts=keyword rather than an opaque "cannot be interpreted as an integer" (issues #752, #755). -
Helper functions live in workflow namespaces; models and corpus ingress stay at the top level (issue #757). The taught surface is
topica.<stage>:select(choosing K),inspect(reading topics),evaluate(validation),effects(covariate effects),data/design(corpus and design-matrix prep), andcompare/provenance/embeddings. Write new docs, docstrings, examples, and error messages in the namespaced form (topica.select.search_k,topica.inspect.topic_table,topica.effects.estimate_effect). Model constructors (topica.LDA) and corpus ingress (topica.Corpus,topica.from_dataframe,topica.tokenize) stay at the root — nouns at the top level, verbs in namespaces. Every flat helper name still resolves as a legacy compatibility alias (topica.search_kis the same object astopica.select.search_k), but it is no longer part of the advertised surface; a new helper should be exported from the namespace that owns it, not added flat.
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=13, 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=13.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 its own namespace,
topica.llm.*, notllm_*helpers. 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 namedllm_*. Beyond the general namespace organization (rule 8 in Structural rules), the LLM suite has its own reason to be set apart: it is a coherent, growing family that shares one property the rest of the library does not —llm-boundednon-determinism — and the dedicated namespace signals that at the call site.topica.llm.backendis the same constructor astopica.llm_backend(the bring-your-own-model adapter thatTopicGPTandlabel_topicsalso use, so it stays reachable at the root too). -
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.