Tech With Galvan
Back to Articles
AI10/6/2025 • 4 min read

Review Sentiment Dashboard using Python and Streamlit

Analyze customer reviews at scale with Python and Streamlit — VADER scoring, sentiment trends, word clouds per mood, and worst-review triage.

Galvan
Galvan

Founder & Creator

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

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 the nltk.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.