Skip to content

Sentiment as a covariate: what complaints and praise are about

When the thing you want to explain is valence — what people talk about when they condemn versus praise — a covariate topic model earns its keep. This example fits a DMR (Dirichlet-Multinomial Regression: LDA whose topic prior is a regression on document covariates) with a review's star rating as a single ordinal covariate, and reads off which topics rise and fall along the one-to-five scale, each with a confidence interval. DMR is the lighter-weight sibling of STM when your covariate acts on prevalence alone.

Focus of this example

Covariate LDA (DMR) with an ordinal covariate · keeping negation with SENTIMENT_STOPWORDS · reading complaint vs praise topics with CIs. For a two-covariate STM (group × time) see Congress.

Data: load_reviews — 1,500 Yelp reviews, 300 at each star rating, shipped in the wheel (loads offline).

1. Keep the words that carry sentiment

The default stoplist would delete not, no, and very — which is fatal here, because "not clean" is the opposite of "clean". Pass SENTIMENT_STOPWORDS instead, and enter the rating as a single centered ordinal column so its coefficient reads as a per-star slope.

import numpy as np
import topica

df = topica.datasets.load_reviews()
corpus = topica.from_dataframe(
    df, text_col="text",
    stopwords=topica.data.SENTIMENT_STOPWORDS,   # keeps not / no / very / too
    min_doc_freq=5, max_doc_fraction=0.5,
)
X = (corpus.metadata["stars"].to_numpy(float) - 3.0).reshape(-1, 1)  # centered 1..5
model = topica.DMR(num_topics=12, seed=13).fit(
    corpus.documents(), X, feature_names=["stars"]
)

2. Estimate the star effect on every topic

estimate_effect propagates topic-estimation uncertainty into the slopes. A negative slope is a complaint topic (more prevalent in low-star reviews); a positive slope is praise.

effects = topica.effects.estimate_effect(model.doc_topic, X, feature_names=["stars"],
                                 nsims=60, seed=0)
for eff in effects:
    star = eff.effect_of("stars")     # dict: coef, se, z, ci_low, ci_high, pvalue
Topic (FREX) Slope per star 95% CI
Complaint us, took, said, asked, wrong −0.041 [−0.044, −0.038]
better, nothing, quality, bad −0.019 [−0.022, −0.016]
work, reviews, car, management −0.016 [−0.019, −0.014]
Praise friendly, favorite, highly, amazing, excellent +0.056 [+0.053, +0.058]
beer, sushi, atmosphere, music, family +0.015 [+0.013, +0.018]
store, shop, home, help +0.014 [+0.012, +0.017]

Every interval excludes zero. The split is substantive, not merely tonal: complaint is a process narrative — a first-person account of being ignored or misled (us, took, asked, wrong) — while the strongest praise topic is an evaluative register (amazing, excellent, highly) rather than any particular experience. The steepest complaint topic loses about four points of prevalence per star; the praise register gains almost six.

When DMR instead of STM

Reach for DMR when your covariates shift topic prevalence and you do not need STM's content covariates, topic correlations, or its variational content model — DMR is a faster Gibbs sampler with the same prevalence-regression idea. When the covariate should also change the words within a topic, or you want topic correlations, use STM.

Reproduce

seed=13; raise iters and nsims for a publication run. The single most important choice on this corpus is the stoplist: SENTIMENT_STOPWORDS over the default, so negation survives to carry the signal.