Skip to main content

NLP Text Analytics

A third of the reviews were too thin to model, so the pipeline flags them and keeps them out of the analysis.


What a sentiment score measures

Steam gives players a thumbs-up or thumbs-down button, but the button doesn't say what they liked, what frustrated them, or how that differs by genre. I pulled 2,382 reviews across 12 games, compared VADER sentiment with Steam's recommendation flag, grouped themes with BERTopic, and served the results from SQLite through parameterized queries.

VADER agrees with the recommendation flag on 69.4% of the usable reviews. The other third is the interesting part: it shows the gap between how a review sounds and what the reviewer decided.

This was the April 2026 baseline. In September I ran a second phase on 29 strategy games with a language filter and a transformer sentiment model. Read the market study.

Source Code

The source code is on GitHub. The April study stays reproducible from its own tagged commit.


Dashboard preview

Steam review analysis dashboard

The hosted application may need a moment to wake up. Open the live dashboard in a separate tab. It opens on the September market study, and the sidebar switches to this one.


Why Steam reviews

I wanted a text corpus with a label to check against. Star ratings are ordinal and vary by reviewer. Steam's recommendation flag is a single yes or no from every reviewer, which makes a clean benchmark for a sentiment score.

The Steam Reviews API needs no authentication. I pulled 200 reviews from each of 12 games across four genres: FPS, indie, RPG, and strategy (Stardew Valley had 182). The reviews run from March 2023 to April 2026. The repository includes a 2.3 MB seed database, so the dashboard runs without an API key.


A third of the reviews were too short to model

Of the 2,382 reviews, 823 were under 10 characters or had fewer than 3 tokens after cleaning. Emoji, one-word jokes, ASCII art. That left 1,559 reviews for analysis.

Preprocessing runs in two stages. Structural cleaning comes first: HTML, BBCode, URLs, emails, and encoding damage (mojibake) get stripped. Then spaCy tokenizes and lemmatizes the text, and a domain stopword list removes words like "game," "play," "player," and "steam" that appear in every review.

Reviews that fail the length check stay in the database with a dropped=True flag and a reason, but they never reach sentiment scoring or topic modeling. A rerun sees the flag and processes nothing new. VADER gives a lone "great" a score of 0.66, and BERTopic can't place a two-token review anywhere useful, so letting them through would only add noise.


The 69.4% question

VADER agrees with the recommendation flag on 69.4% of the 1,559 kept reviews. That looks weak until you read the disagreements.

A player writes three paragraphs about bad netcode, slow matchmaking, and server crashes, then clicks thumbs-up because the core loop is fun. VADER reads the text and scores it negative. The flag says positive. Both are right. The flag records a decision ("would I tell a friend to buy this?"), while VADER scores the wording.

Strategy reviews make this worse. Players of Civilization VI, Stellaris, and Total War: WARHAMMER III average 396 to 534 hours of play when they write a review, against 27 to 169 hours for most other titles. Those long reviews mix praise and criticism for paragraphs before reaching a verdict, and a word-level scorer can't follow that.

In September I tested this directly. On the same 1,559 reviews, with nothing tuned on them, RoBERTa-large reached 87.8% balanced accuracy against 70.7% for VADER.

Live dashboard


Topic modeling

BERTopic with sentence-transformer embeddings (all-MiniLM-L6-v2) and UMAP found three clusters in the 1,559 kept reviews. One English catch-all holds 1,513 reviews. The other two hold 29 Polish reviews and 17 Russian ones.

That's a weak result, and the reason is plain. The April pipeline had no language filter, so BERTopic grouped foreign-language reviews by shared vocabulary. At about 130 kept reviews per game, the English reviews don't separate into stable themes.

An earlier fit reported 11 topics. When I refit it in September after fixing the topic labels, the same code produced these three clusters, so the 11-topic result couldn't be reproduced and I replaced it.


Preprocessing decisions

Each review is stored in two forms. cleaned_text keeps capitalization and punctuation because VADER uses them: capital letters amplify sentiment and exclamation marks add intensity. tokens is lowercased, lemmatized, and stripped of stop words for BERTopic. One pipeline, two outputs, and the schema makes clear which one feeds which model.

BBCode removal runs before HTML removal. Steam reviews use BBCode markup ([b], [i], [url=...]) that can contain HTML-like characters. Stripping HTML first splits those tags and leaves fragments behind.

I removed domain stopwords (game, play, player, steam) because they appeared in nearly every review and crowded the topic labels. The word "game" tells you nothing about what makes one cluster different from another.


Architecture

Steam API → data_pull.py → data/raw/*.json → db_setup.py → SQLite
                                                              |
                                              preprocessing.py → sentiment.py → topics.py
                                                              |
                                                        dashboard/app.py

The database is SQLite with five tables:

TableWhat's in it
metadata12 games with app_id, name, and genre
documents2,382 raw reviews with recommendation flag, playtime, timestamps
processed_documentsCleaned text, token arrays (JSON), token counts, drop flags and reasons
nlp_resultsVADER compound/pos/neg/neu scores and topic assignments per review
topicsTopic labels with top words and document counts

Every load uses INSERT OR IGNORE keyed on the review ID, so the pipeline is idempotent: run it twice, get the same database. Failed API pulls are never cached, and raw JSON is kept.

Dashboard queries hit SQLite directly. Every sidebar filter (genre, game, sentiment range, topic, date range) becomes a parameterized SQL WHERE clause. Nothing is loaded into a DataFrame and filtered in memory. A GitHub Actions job with Playwright opens the dashboard every 25 minutes so it stays awake.


What the second phase changed

The April version ended with three open problems. The September market study took on all three:

  • Language. A lingua language filter now drops reviews Steam lists as English that aren't.
  • Scale. 209,223 reviews pulled across 29 strategy games, with a random sample of up to 500 per game.
  • The sentiment model. RoBERTa-large replaced VADER after beating it on held-out reviews, under a rule I wrote before reading the test data.

Quick start

git clone https://github.com/ShameekConyers/nlp_dashboard.git cd nlp_dashboard python3 -m venv .venv .venv/bin/pip install -r requirements.txt .venv/bin/python -m spacy download en_core_web_sm .venv/bin/streamlit run dashboard/app.py

No API key needed. The seed databases ship with every review, preprocessing result, sentiment score, and topic assignment for both studies.

Tools: Python, SQLite, spaCy, NLTK (VADER), BERTopic, sentence-transformers, UMAP, Streamlit, Plotly, GitHub Actions, Playwright, pytest