Introduction
One review tells you a story; a hundred reviews tell you a trend. This app scores a whole CSV of customer reviews with VADER, then turns the scores into decisions: sentiment distribution, trends over time, the exact words driving negative feedback, and a triage queue of the angriest reviews that deserve a response today.
It scales the single-text analysis from the sentiment analyzer tutorial to datasets, using the aggregation patterns of the Pandas dashboard — one app, two prior lessons, combined.
Features
- Bulk scoring — a whole CSV of reviews in one pass.
- Sentiment mix — positive/negative/neutral donut with counts.
- Trend line — average sentiment over time (if a date column exists).
- Word clouds per mood — what happy vs angry customers actually say.
- Triage queue — the 10 most negative reviews, ranked.
Prerequisites
- Python 3.8+ — from python.org.
- Dependencies:
pip install streamlit pandas nltk wordcloud matplotlib
python -c "import nltk; nltk.download('vader_lexicon')"
Step 1: Create the Script
Save as review_dashboard.py:
import streamlit as st
import pandas as pd
import nltk
from nltk.sentiment import SentimentIntensityAnalyzer
from wordcloud import WordCloud, STOPWORDS
import matplotlib.pyplot as plt
st.set_page_config(page_title="Review Sentiment Dashboard", page_icon="⭐", layout="wide")
st.title("⭐ Review Sentiment Dashboard")
uploaded = st.file_uploader("Upload reviews CSV (needs a 'review' text column)", type=["csv"])
if uploaded:
df = pd.read_csv(uploaded)
text_col = st.selectbox("Review column", [c for c in df.columns if df[c].dtype == "object"])
date_col = st.selectbox("Date column (optional)", ["None"] + [c for c in df.columns])
@st.cache_data
def score_reviews(texts):
sia = SentimentIntensityAnalyzer()
return [sia.polarity_scores(t)["compound"] for t in texts]
df["score"] = score_reviews(df[text_col].astype(str).tolist())
def bucket(s):
return "Positive" if s >= 0.05 else "Negative" if s <= -0.05 else "Neutral"
df["mood"] = df["score"].map(bucket)
c1, c2, c3, c4 = st.columns(4)
c1.metric("Reviews", len(df))
c2.metric("Positive", f"{(df.mood == 'Positive').mean():.0%}")
c3.metric("Negative", f"{(df.mood == 'Negative').mean():.0%}")
c4.metric("Avg score", f"{df['score'].mean():.2f}")
left, right = st.columns(2)
with left:
st.bar_chart(df["mood"].value_counts())
st.caption("Sentiment mix")
with right:
if date_col != "None":
df[date_col] = pd.to_datetime(df[date_col], errors="coerce")
trend = df.dropna(subset=[date_col]).set_index(date_col)["score"].resample("W").mean()
st.line_chart(trend)
st.caption("Average sentiment per week")
else:
st.info("Add a date column to see trends.")
st.subheader("What each mood talks about")
stops = STOPWORDS | {"br", "the", "and"}
wc_cols = st.columns(2)
for ax_col, mood in zip(wc_cols, ["Positive", "Negative"]):
text = " ".join(df.loc[df["mood"] == mood, text_col].astype(str))
if text.strip():
wc = WordCloud(width=600, height=300, background_color="white", stopwords=stops).generate(text)
fig, ax = plt.subplots(figsize=(6, 3))
ax.imshow(wc)
ax.axis("off")
ax_col.pyplot(fig)
st.subheader("🚨 Triage — respond to these first")
worst = df.nsmallest(10, "score")[[text_col, "score"]]
st.dataframe(worst, hide_index=True, use_container_width=True)
Step 2: Run the App
streamlit run review_dashboard.py
Export reviews from any platform (or generate a sample CSV with mixed fake reviews) and explore — the triage queue is where the value hides.
How It Works
The pipeline is score → bucket → aggregate. VADER’s compound score (−1 to +1) reduces each review to a number; the ±0.05 thresholds bucket it into three moods; Pandas does the rest. Scoring runs inside @st.cache_data because VADER is fast per-review but not per-1000-reviews-per-rerun — caching makes filter changes instant, the same discipline as the stock dashboard’s price cache.
The word clouds per mood are the insight engine: positive reviews cluster around what you do well, negative around what’s broken — the same generation logic as the word cloud tutorial, pointed at a slice of the data.
The triage queue is nsmallest(10, "score") — one method call that answers “which customers are angriest right now?” Dashboards earn their keep when they end in an action, and this table is the action.
Common Errors & Fixes
Resource vader_lexicon not found— run thenltk.download('vader_lexicon')line from Prerequisites once, outside the app.- All reviews score neutral — you’re scoring the column name or empty strings; check the column selector and drop nulls before scoring.
- Date parsing produces NaT — mixed formats; try
pd.to_datetime(col, format="mixed", errors="coerce"). - Word cloud blank for a mood — no reviews in that bucket (a good problem); the
text.strip()guard already skips it.
Key Concepts
- Score → bucket → aggregate — the standard sentiment-at-scale pipeline.
- Cache the expensive step — scoring once beats scoring every rerun.
- Slice-and-compare visuals — clouds per mood reveal vocabulary differences.
- Actionable endpoints — dashboards should end in a queue, not just charts.
What to Try Next
- Add star-rating correlation — do VADER scores agree with 1–5 stars? (
df.corr()) - Add keyword drill-down — filter reviews containing a word, re-render everything.
- Detect complaint topics with TF-IDF top terms per negative review, like the spam detector.
- Export the triage queue to Excel with the report generator.
FAQ
Why VADER instead of a transformer model?
Speed and simplicity: thousands of reviews per second, zero GPU, no downloads beyond a lexicon. For product-review English, its accuracy is competitive — and the spam detector shows where to upgrade when it isn’t.
My reviews aren’t in English — will it work?
VADER is English-only. Translate first (the translator app pattern) or switch to a multilingual model.
What’s a healthy sentiment mix?
Depends on the platform — but a sudden drop in weekly average sentiment (the trend line) is the real signal, not the absolute level.