AIOctober 06, 20254 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

  • Python 3.8+ — from python.org.
  • Dependencies:
  • code
    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:

    code
    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

    code
    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.