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
Prerequisites
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.pyExport 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
nltk.download('vader_lexicon') line from Prerequisites once, outside the app.pd.to_datetime(col, format="mixed", errors="coerce").text.strip() guard already skips it.Key Concepts
What to Try Next
df.corr())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.