STS: reference-fidelity fitting profiles¶
STS is topica's Structural Topic and Sentiment-Discourse model (Chen & Mankad
2024), which extends STM with a per-document, per-topic continuous
sentiment-discourse latent. topica implements the authors' core likelihood,
gradient, and Hessian. This page documents how topica's fitting defaults relate
to the two reference implementations and how to select a reference-fidelity
configuration.
Two references, one model¶
There are two R implementations, and they differ in their fitting defaults:
- The paper replication code (Chen & Mankad 2024, the Management Science
supplement) defaults to
estimation = "lasso"and applies no damping to the topic-word coefficients. - The CRAN
stspackage (1.4) defaults tokappaEstimation = "adjusted"and damps each topic-word update against the previous one.
Both share the same initialization: an STM anchor-word spectral fit
(max.em.its = 0) supplies the initial per-document eta, the topic-block prior
covariance (invsigma), and the initial topic-word matrix, with the sentiment
block given prior variance 20.
What topica exposes¶
The topica default is topica-native, not a reference target: a fast
ridge-penalized Poisson κ estimator (kappa_estimation="ridge") with topica's
own spectral initialization. It is deterministic and supported, and it is the
right choice when you want a fast, stable fit and are not trying to reproduce a
specific R run.
To reproduce an R run, select a reference profile:
import topica
model = topica.STS(num_topics=20, seed=1)
# CRAN sts (kappaEstimation = "adjusted"):
model.fit(docs, sentiment_seed=stars, prevalence=X, reference="cran")
# Chen & Mankad 2024 replication code (estimation = "lasso"):
model.fit(docs, sentiment_seed=stars, prevalence=X, reference="paper")
Both profiles use the reference-style initialization (STM-derived spectral eta,
sentiment-block prior variance 20). They differ exactly as the two references do:
reference |
κ estimator | κ aggregation | κ damping |
|---|---|---|---|
"none" (default) |
as kappa_estimation (ridge) |
— | none |
"paper" |
"lasso" |
plain group mean of α^(s) |
none |
"cran" |
"adjusted" |
φ-mass-weighted group mean of α^(s) |
half-step (κ_new + κ_old)/2 |
The κ estimators are also available standalone (kappa_estimation="lasso" or
"adjusted") without the profile's initialization and damping.
The "adjusted" estimator¶
CRAN's "adjusted" differs from "lasso" only in how the aggregated sentiment
design column is formed. "lasso" uses the plain group mean of α^(s);
"adjusted" uses a φ-mass-weighted mean, weighting each document's α^(s)_{d,t}
by its expected topic-t token mass:
This reproduces the weighted_alpha <- alpha_s * softmax(log phiD) reweighting in
CRAN opt.kappa.R (a softmax over log topic mass is a mass-proportional weight).
A note on the CRAN opt.kappa.R inner loop
CRAN's "adjusted" branch wraps the reweighting in a while (kk <=
max_inner_iter) loop with max_inner_iter <- 10, but the loop never
reassigns coef inside itself, so delta is identically zero and it breaks
on the first pass. The effective computation is a single reweighting from
coef = 0 followed by one L1/AIC Poisson fit. topica reproduces the
effective behavior — a single φ-mass-weighted aggregation — which is what
every published CRAN sts result actually computes.
Evidence¶
The κ estimator is an L1-penalized Poisson regression over a λ path with
AIC-selected penalty, the same kernel CRAN opt.kappa.R delegates to
glmnet::glmnet(..., family="poisson", alpha=1).
parity/sts_kappa_glmnet.py
validates topica's native solver head-to-head with R glmnet on the actual STS κ
design (block-diagonal topic dummies plus sentiment slopes). On that design the
two agree essentially bit-for-bit (aligned cosine > 0.999), with the occasional
case where AIC selection lands on an adjacent λ. topica's solver is an
independent coordinate-descent implementation, so this is a "tracks glmnet
closely" result, not a bit-identical one. The script shells out to Rscript with
glmnet and skips cleanly when they are unavailable.
The φ-mass-weighted aggregation and the half-step damping are exact ports of the
CRAN arithmetic, checked by Rust unit tests. The reference initialization is a
documented statistically-equivalent substitute for STM's spectral eta /
invsigma, not a byte-identical STM port.
End-to-end vs the R sts package¶
parity/sts_r_package_compare.py
fits the live R sts package and topica's STS (reference="cran") on the
same poliblog corpus — the one that ships with stm (data(poliblog5k)), so the
comparison needs no external data — and reports the aligned topic-word cosine at
mean sentiment. Because the two use independent initializations, this is a
statistical check (same topics up to alignment), like the STM/CTM R parity, not a
bit-identical one. A companion script,
parity/sts_r_compare.py,
compares instead against the authors' frozen published fit and needs the
replication package on disk. Both skip cleanly when Rscript or the R packages are
unavailable.
That live comparison is also frozen into a committed gold so it gates CI
without an R toolchain.
parity/sts_cran_gold.py
records R sts's topic-word distribution at mean sentiment (the adjusted profile,
two stmSeeds for a self-consistency floor) plus the exact tokenized corpus on a
fixed 300-doc poliblog subsample. tests/test_sts_cran_gold.py refits topica on
that frozen corpus and applies two gates. First an absolute bar: reference="cran"
topic-word cosine vs R clears R's two-seed floor minus a margin. R's adjusted fit is
near-identical across seeds (self cosine ~0.998), so the bar lands at ~0.80 — an
externally calibrated cross-implementation threshold (the same floor the live script
uses), which a shuffled-β negative control falls well below. On its own that bar only
catches gross drift, so the test adds a relative gate: reference="cran" must beat
the topica-native ridge default (reference="none") against the same R gold by a
small margin. Both fits are pure topica and drift together across platforms, so the
gap is stable and is what makes the gate specific to the adjusted estimator the gold
is named for. No Rscript is touched at test time.
Scope and honest limits¶
What is validated and committed is: the κ-solver kernel (against R glmnet), the
exact aggregation and damping arithmetic (Rust unit tests), an end-to-end
topic-recovery comparison against the live R sts package on the shipped
poliblog corpus, and a committed offline gold of that comparison that gates CI
without R. The reference profiles are R-aligned configurations whose
numerical kernel is validated and whose topics track R sts; they are not a
claim of byte-for-byte reproduction of a specific R fit, since the two engines
initialize independently and the reference init is a documented
statistically-equivalent substitute for STM's spectral eta / invsigma.