Advanced Data Visualization


INS-605: Data Analysis

Lecturer: Dr. Sothea HAS

1 Intro & Motivation

1.1 Introduction

  • In Data Visualization of Data Analysis I, we learned:
    • How to inspect a single variable individually
    • Uncover relationship between multiple variables
    • Detect trend/evolution of variables…
  • All these are to extract knowledge from the data only.
  • We now learn additional tools to build a presentable Dashboard.

1.2 Dashboarding process

  • General view of dashboarding:
          ┌──────────────────────┐
          │  Business objective  │
          └──────────┬───────────┘
                     ▼
                 ┌───────┐
                 │  KPIs │
                 └───┬───┘
                     ▼
   ┌────────────────────────────────────┐
   │ Understanding and prepare the data │
   └─────────────────┬──────────────────┘
                     ▼
┌──────────────────────────────────────────┐
│ Explore the data according to key points │
└────────────────────┬─────────────────────┘
                     ▼
      ┌─────────────────────────────┐
      │ Validate → test → iterate ↩ │
      └─────────────────────────────┘
  • Key: Business objective drives everything a dashboard will cover.
  • Business objective: Why does this dashboard exist? Audiences?
  • Key Point Indicators: Measurable points supporting the objective. Ex:
    • Number of Products
    • Number of Ratings
    • Average Rating
    • % of High Ratings…
  • Data wrangling: Prepare the data.
  • Explore the data: Analyze toward the KPIs & select visual designs.
  • Validate \(\to\) test/feedback \(\to\) revise \(\hookleftarrow\).

Data wrangling is very important as in business, beautiful dashboard built on wrong info is worst than no dashboard at all.

1.2 Dashboarding process (cont.)

       ┌────────────────────────────┐
       │ Boost platform performance │
       └─────────────┬──────────────┘
                     ▼
     ┌───────────────────────────────┐
     │ Customer/Product/Rating Trend │
     └───────────────┬───────────────┘
                     ▼
             ┌────────────────┐
             │ Data Wrangling │
             └───────┬────────┘
                     ▼
 ┌─────────────────────────────────────────┐
 │ Explore the data & select graph designs │
 └───────────────────┬─────────────────────┘
                     ▼
      ┌─────────────────────────────┐
      │ Validate → test → iterate ↩ │
      └─────────────────────────────┘
  • Target audience: Marketing, sales and product managers.
  • Q1: What 3-4 KPIs would you highlight on the dashboard?

2 Overall view

2.1 Overview dashboard

Data Wrangling

  • Quick column normalization:
# Normalize cols
data.columns = (
    data.columns
      .str.strip()
      .str.lower()
)
# Map new names:
rename_map = {}
possible_names = {
    "userid": "user_id",
    "productid": "product_id",
    "producttype": "product_type",
    "rating": "rating",
    "timestamp": "timestamp",
}
for col in data.columns:
    if col in possible_names:
        rename_map[col] = possible_names[col]
data = data.rename(columns=rename_map)
  • We shall ensure data types.
  • Drop/impute missing values.
  • Add: date, year, month for analysis.
data["rating"] = pd.to_numeric(
    data["rating"], 
    errors="coerce")

data["timestamp"] = pd.to_numeric(
    data["timestamp"],
    errors="coerce"
)

# UNIX timestamps -> date time
data["date"] = pd.to_datetime(
    data["timestamp"],
    unit="s",
    errors="coerce"
)
data["year"] = data["date"].dt.year
data["month"] = data["date"].dt\
    .to_period("M").astype(str)

# Drop NA
data = data.dropna()

print(data.shape)
(1348246, 9)

Overall: KPIs

  • Four KPIs are
    • Toal reviews
    • Number of customers
    • Number of products
    • Average rating
Code
kpi = pd.DataFrame({
    'tot_rev': [len(data)],
    'num_cus': [data["user_id"].nunique()],
    'num_prod': [data["product_id"].nunique()],
    'avg_rating': [data["rating"].mean()]},
    index=['KPI'])
kpi.T
KPI
tot_rev 1.348246e+06
num_cus 8.837530e+05
num_prod 2.383800e+04
avg_rating 4.145732e+00
  • We build a function make_kpi().
import plotly.graph_objects as go

def make_kpi(value, title, color, value_format=",d"):
    fig = go.Figure()   # initalize `go` object

    fig.add_trace(      # add trace as `indicator`
        go.Indicator(
            mode="number",
            value=value, # `value` argument is used.
            title={
                "text": title,
                "font": {
                    "size": 15,
                    "color": color
                }
            },
            number={
                "valueformat": value_format,
                "font": {
                    "size": 28,
                    "color": "#F5F5F5" # white text
                }
            }
        )
    )
    fig.update_layout(
        paper_bgcolor="#454545", # bk color
        plot_bgcolor="#454545",  # bk color
        margin=dict(l=10, r=10, t=90, b=10),
        height=140
    )

    return fig

Overall: KPIs (cont.)

  • Apply on Total reviews:
Code
# Apply to 'Total reviews'
kpi_reviews = make_kpi(
    kpi["tot_rev"][0],
    "Total Reviews",
    "#4CC9F0"
)
kpi_reviews\
    .update_layout(
        width=450,
        height=175
    ).show()
  • On Total customers:
  • We build a function make_kpi().
import plotly.graph_objects as go

def make_kpi(value, title, color, value_format=",d"):
    fig = go.Figure()   # initalize `go` object

    fig.add_trace(      # add trace as `indicator`
        go.Indicator(
            mode="number",
            value=value, # `value` argument is used.
            title={
                "text": title,
                "font": {
                    "size": 15,
                    "color": color
                }
            },
            number={
                "valueformat": value_format,
                "font": {
                    "size": 28,
                    "color": "#F5F5F5" # white text
                }
            }
        )
    )
    fig.update_layout(
        paper_bgcolor="#454545", # bk color
        plot_bgcolor="#454545",  # bk color
        margin=dict(l=10, r=10, t=90, b=10),
        height=140
    )

    return fig

Graphs

  • Reveiws by product types.
Code
## category summary
category_summary = (
    data.groupby("product_type")
      .agg(
          reviews=("rating", "size"),
          avg_rating=("rating", "mean"),
          customers=("user_id", "nunique"),
          products=("product_id", "nunique")
      )
      .reset_index()
      .sort_values("reviews", ascending=False)
)
# select top 15 products
top_categories = category_summary.head(15)
# Initialize `go` object
fig_product = go.Figure()
# Create bar chart
fig_product.add_trace(
    go.Bar(
        x=top_categories["reviews"][::-1],
        y=top_categories["product_type"][::-1],
        orientation="h",
        marker_color="#4CC9F0"
    )
)
fig_product.update_yaxes(
    showgrid=False
)
# Set the graph property
fig_product.update_layout(
    width=450,
    height=430,
    title="Reviews by Product Type",
    paper_bgcolor="#454545",
    plot_bgcolor="#454545",
    font=dict(color="#F5F5F5"),
    showlegend=False
).show()
  • Rating & Reveiw Trend:
Code
# Review activity
fig_rating = go.Figure()
fig_rating.add_trace(
    go.Bar(
        x=rating_summary["rating"],
        y=rating_summary["reviews"],
        text=rating_summary["reviews"],
        texttemplate="%{text:,}",
        textposition="outside",
        marker_color="#F4A261"
    )
)
fig_rating.update_xaxes(
    showgrid=False
)
# Set high enough box to show all numbers
max_reviews = rating_summary["reviews"].max()
fig_rating.update_yaxes(
    range=[0, max_reviews * 1.15]
)
fig_rating.update_layout(
    height=180,
    width=450,
    title="Rating Distribution",
    paper_bgcolor="#454545",
    plot_bgcolor="#454545",
    font=dict(color="#F5F5F5"),
    showlegend=False
).show()
Code
# Review activity
fig_activity = go.Figure()
fig_activity.add_trace(
    go.Scatter(
        x=monthly["date"],
        y=monthly["reviews"],
        mode="lines+markers",
        line=dict(
            color="#72D572",
            width=3
        ),
        marker=dict(size=6)
    )
)
fig_activity.update_xaxes(
    showgrid=False
)
fig_activity.update_layout(
    height=180,
    width=450,
    title="Review Activity Over Time",
    paper_bgcolor="#454545",
    plot_bgcolor="#454545",
    font=dict(color="#F5F5F5"),
    showlegend=False
)

Assemble everything

  • We assemble everythin using make_subplots():
# Assemble everything with make_subplots
from plotly.subplots import make_subplots
fig_overall = make_subplots(
    rows=2,     # number of rows
    cols=12,    # number of cols but break into 4 and 3
    specs=[
        [
            {"type": "indicator", "colspan": 3}, None, None,
            {"type": "indicator", "colspan": 3}, None, None,
            {"type": "indicator", "colspan": 3}, None, None,
            {"type": "indicator", "colspan": 3}, None, None
        ],
        [
            {"type": "bar", "colspan": 4}, None, None, None,
            {"type": "bar", "colspan": 4}, None, None, None,
            {"type": "scatter", "colspan": 4}, None, None, None
        ]
    ],          # Define different cols between 1st and 2nd row.
    subplot_titles=[
        "Total Reviews",
        "Total Customers",
        "Total Products",
        "Average Rating",
        "Reviews by Product Type",
        "Rating Distribution",
        "Review Activity Over Time"
    ],          # Define subplots' names
    vertical_spacing=0.05,
    horizontal_spacing=0.05
)
  • Think about it as an empty canvas to be painted on.
  • KPI can be added as trace to the empty canvas.
# Add KPI traces
fig_overall.add_trace(
    kpi_reviews.data[0],
    row=1, col=1
)
fig_overall.add_trace(
    kpi_customers.data[0],
    row=1, col=4
)
fig_overall.add_trace(
    kpi_products.data[0],
    row=1, col=7
)
fig_overall.add_trace(
    kpi_rating.data[0],
    row=1, col=10
)
  • The rests and their properties are added the same way:
Code
# Add product:
fig_overall.add_trace(
    fig_product.data[0],
    row=2, col=1
)

##-------> Product property
fig_overall.update_xaxes(
    showgrid=False,
    row=2, col=1
)
fig_overall.update_yaxes(
    showgrid=False,
    row=2, col=1
)

# Add Rating:
fig_overall.add_trace(
    fig_rating.data[0],
    row=2, col=5
)
##-------> Rating graph properties
max_reviews = rating_summary["reviews"].max()
fig_overall.update_yaxes(
    range=[0, max_reviews * 1.15],
    row=2,
    col=5
)
fig_overall.update_xaxes(
    showgrid=False,
    color=MUTED_TEXT,
    tickfont=dict(color=MUTED_TEXT),
    row=2,
    col=5
)

# Add Review Trend:
fig_overall.add_trace(
    fig_activity.data[0],
    row=2, col=9
)
##-------> Review activity
fig_overall.update_xaxes(
    showgrid=False,
    color=MUTED_TEXT,
    tickfont=dict(color=MUTED_TEXT),
    row=2,
    col=9
)

Assemble everything (cont.)

  • Put the boxes around KPIs:
# Define the four KPI card positions
kpi_boxes = [
    (0.010, 0.215, KPI_COLORS[0]),
    (0.275, 0.470, KPI_COLORS[1]),
    (0.535, 0.730, KPI_COLORS[2]),
    (0.795, 1.000, KPI_COLORS[3])
]

for x0, x1, color in kpi_boxes:
    fig_overall.add_shape(
        type="rect",
        xref="paper",
        yref="paper",
        x0=x0,
        x1=x1,
        y0=0.58,
        y1=0.98,

        fillcolor=CARD_COLOR,
        line=dict(
            color=color,
            width=2
        ),
        layer="below"
    )
  • Final full layout touch.
# Full layout
fig_overall = fig_overall.update_layout(
    title=dict(
        text="Amazon Product Reviews — Overall View",
        font=dict(
            size=24,
            color=TEXT_COLOR
        ),
        x=0.02,
        xanchor="left"
    ),
    height=530,
    width=970,
    paper_bgcolor=BG_COLOR,
    plot_bgcolor=BG_COLOR,
    showlegend=False,
    margin=dict(
        l=60, r=40, t=90, b=50
    ),
    font=dict(
        family="Arial",
        color=TEXT_COLOR
    )
)
  • Process: init. go objects \(\to\) make_subplots().
  • One can also do: init. make_subplots \(\leftarrow\) go.

Final result

fig_overall.show()

3 Customer veiw

3.1 Customer view

  • What aspects of customers would you consider given the following information:
['user_id', 'product_id', 'product_type', 'rating', 'date', 'year', 'month']

3.1 Customer view

  • Customer information we can compute:
    • How enganged are they?
    • What do they buy/review?
    • How do their ratings behave?
    • How active they are?…
  • We compute those aspects:
customer_summary = (
    data.groupby("user_id")
      .agg(
          reviews=("rating", "size"),
          products=("product_id", "nunique"),
          product_types=("product_type", "nunique"),
          avg_rating=("rating", "mean"),
          first_review=("date", "min"),
          last_review=("date", "max")
      )
      .reset_index()
)
customer_summary["active_days"] = (
    customer_summary["last_review"]
    - customer_summary["first_review"]
).dt.days

customer_summary["is_repeat"] = (
    customer_summary["reviews"] > 1
)
  • This’s the core table for customer view.

Customer KPIs

  • Number of customers.
  • Average number of reviews.
  • Average rating per customer.
  • % of repeat customers.
total_customers = (
    customer_summary["user_id"].nunique()
)
avg_reviews_customer = (
    customer_summary["reviews"].mean()
)
repeat_customer_rate = (
    customer_summary["is_repeat"].mean() * 100
)
avg_customer_rating = (
    customer_summary["avg_rating"].mean()
)
  • These are four indicators for our cumstomer dashboard.
  • We start from make_subplots and eventually add more elements.
  • What are those elements?
    1. Highly engaged customers.
    2. Highly satisfied customers.
    3. Satisfaction vs engagement.
  • What graph would you use for each?

Customer KPIs

  • Number of customers.
  • Average number of reviews.
  • Average rating per customer.
  • % of repeat customers.
total_customers = (
    customer_summary["user_id"].nunique()
)
avg_reviews_customer = (
    customer_summary["reviews"].mean()
)
repeat_customer_rate = (
    customer_summary["is_repeat"].mean() * 100
)
avg_customer_rating = (
    customer_summary["avg_rating"].mean()
)
  • These are four indicators for our cumstomer dashboard.
  • We start from make_subplots and eventually add more elements.
  • Initialize fig_customer of make_subplots object.
fig_customer = make_subplots(
    rows=2,
    cols=12,
    specs=[
        # Row 1: 4 KPI
        [
            {"type": "indicator", "colspan": 3}, None, None,
            {"type": "indicator", "colspan": 3}, None, None,
            {"type": "indicator", "colspan": 3}, None, None,
            {"type": "indicator", "colspan": 3}, None, None
        ],
        # Row 2: 2 charts
        [
            {"type": "bar", "colspan": 4}, None, None, None,
            {"type": "xy", "colspan": 4}, None, None, None,
            {"type": "bar", "colspan": 4}, None, None, None
        ],
    ],
    subplot_titles=[
        "Total Customers", "Avg Review", "Repeat Rate", "Avg Rating",
        "Customer Reviews",
        "Customer Engagement vs Satisfaction",
        "Customers Satisfaction"
    ],
    vertical_spacing=0.05,
    horizontal_spacing=0.08
)

Customer KPIs (cont.)

  • We then add all the KPIs:
# Add number of customers
fig_customer = fig_customer.add_trace(
    go.Indicator(
        mode="number",
        value=total_customers, # alr computed
        title=dict(
            text="Total Customers",
            font=dict(
                size=15,
                color=KPI_COLORS[0]
            )
        ),
        number=dict(
            valueformat=",d",
            font=dict(
                size=28,
                color=TEXT_COLOR
            )
        )
    ),
    row=1,
    col=1
)
  • The rests are added the same way.
Code
fig_customer = fig_customer.add_trace(
    go.Indicator(
        mode="number",
        value=avg_reviews_customer,
        title=dict(
            text="Avg Reviews / Customer",
            font=dict(
                size=15,
                color=KPI_COLORS[1]
            )
        ),
        number=dict(
            valueformat=".1f",
            font=dict(
                size=28,
                color=TEXT_COLOR
            )
        )
    ),
    row=1,
    col=4
)
fig_customer = fig_customer.add_trace(

    go.Indicator(
        mode="number",
        value=repeat_customer_rate,
        title=dict(
            text="Repeat Customer Rate",
            font=dict(
                size=15,
                color=KPI_COLORS[2]
            )
        ),
        number=dict(
            valueformat=".1f",
            suffix="%",
            font=dict(
                size=28,
                color=TEXT_COLOR
            )
        )
    ),
    row=1,
    col=7
)
fig_customer = fig_customer.add_trace(
    go.Indicator(
        mode="number",
        value=avg_customer_rating,
        title=dict(
            text="Avg Rating",
            font=dict(
                size=15,
                color=KPI_COLORS[3]
            )
        ),
        number=dict(
            valueformat=".2f",
            font=dict(
                size=28,
                color=TEXT_COLOR
            )
        )
    ),
    row=1,
    col=10
)
  • We add boxes to KPIs:
KPI_BOXES = [
    (0.01, 0.185),
    (0.245, 0.485),
    (0.520, 0.750),
    (0.815, 0.998)
]
for (x0, x1), color in zip(
        KPI_BOXES,
        KPI_COLORS):
    fig_customer = fig_customer.add_shape(
        type="rect",
        xref="paper",
        yref="paper",
        x0=x0,
        x1=x1,
        y0=0.58,
        y1=0.98,
        fillcolor=CARD_COLOR,
        line=dict(
            color=color,
            width=2),
        layer="below")
  • Current result can be viewed using fig_customer.show().

Customer charts

  1. Highly engaged customers.
# Sort reviews
high_engagement = customer_summary\
    .nlargest(10, "reviews")\
    .sort_values('reviews',
        ascending=False)
fig_customer = fig_customer.add_trace(
    go.Bar(
        y=high_engagement["reviews"],
        x=high_engagement["user_id"].astype(str),
        orientation="v",
        marker=dict(
            color=KPI_COLORS[0]
        ),
        hovertemplate=(
            "<b>Customer: %{x}</b><br>"
            "Reviews: %{y:,}"
            "<extra></extra>"
        )
    ),
    row=2,
    col=1
)
  • Highly engagement can indicate potential customers or problems (fake reviews).
  1. Highly satisfied customers.
# Sort reviews
satisfied_customers = (
    data.assign(
        high_rating=data['rating'].isin([4,5])
    ).groupby("user_id")
    .agg(
        high_ratings=("high_rating", 'sum'),
        reviews=("rating", "size"),
        avg_rating=("rating", "mean")
    )
    .reset_index()
    .nlargest(10, "high_ratings")
    .sort_values("high_ratings", ascending=False)
)
fig_customer = fig_customer.add_trace(
    go.Bar(
        y=satisfied_customers["high_ratings"],
        x=satisfied_customers["user_id"].astype(str),
        orientation="v",
        marker=dict(
            color=KPI_COLORS[3]),
        hovertemplate=(
            "<b>Customer: %{x}</b><br>"
            "High Rating Count: %{y:,}"
            "<extra></extra>"
        )
    ),
    row=2,
    col=9
)

Customer charts (cont.)

  • Engagement vs satisfaction.
comparison = (
    customer_summary[
        ["user_id", "reviews", "avg_rating"]
    ]
    .merge(
        satisfied_customers[
            ["user_id", "high_ratings"]
        ],
        on="user_id",
        how="left"
    )
)

comparison["high_ratings"] = (
    comparison["high_ratings"]
    .fillna(0)
)
  • Scatter plot is a good graph:
Code
eng_sat = comparison.iloc[:10000]
fig_customer = fig_customer.add_trace(
    go.Scatter(
        x=eng_sat["reviews"],
        y=eng_sat["avg_rating"],
        mode="markers",
        marker=dict(
            size=(
                (eng_sat["avg_rating"] * 10)
                .clip(lower=1)
            ),
            color = KPI_COLORS[2],
            line=dict(
                color="#F5F5F5",
                width=0.5
            ),
            sizemode="area"
        ),
        customdata=np.column_stack([
            eng_sat["user_id"],
            eng_sat["high_ratings"]
        ]),
        hovertemplate=(
            "<b>Customer %{customdata[0]}</b><br>"
            "Reviews: %{x:,}<br>"
            "Avg Rating: %{y:.2f}<br>"
            "High Ratings: %{customdata[1]:,}<br>"
            "<extra></extra>"
        )
    ),
    row=2,
    col=5
)

# Mean values
x_mean = eng_sat["reviews"].mean()
y_mean = eng_sat["avg_rating"].mean()

Final Touch

Code
fig_customer.update_xaxes(
    title_text="Customers",
    color=MUTED_TEXT,
    showgrid=False,
    tickangle=-45,
    row=2,
    col=1
)

fig_customer.update_yaxes(
    title_text="Reviews",
    color=MUTED_TEXT,
    gridcolor=GRID_COLOR,
    row=2,
    col=1
)

fig_customer.update_xaxes(
    title_text="Customers",
    color=MUTED_TEXT,
    gridcolor=GRID_COLOR,
    tickangle=-45,
    row=2,
    col=9
)

fig_customer.update_yaxes(
    title_text="High Rating Counts",
    categoryorder="total ascending",
    color=MUTED_TEXT,
    showgrid=False,
    row=2,
    col=9
)

fig_customer.update_xaxes(
    title_text="Engagement",
    color=MUTED_TEXT,
    showgrid=False,
    row=2,
    col=5
)

fig_customer.update_yaxes(
    title_text="Satisfaction",
    color=MUTED_TEXT,
    gridcolor=GRID_COLOR,
    row=2,
    col=5
)
fig_customer\
    .update_layout(
        width=970,
        height=520,
        title=dict(
            text="Amazon Product Reviews — Customer View",
            font=dict(
                size=24,
                color=TEXT_COLOR
            ),
            x=0.02,
            xanchor="left"
        ),
        paper_bgcolor="#454545",
        plot_bgcolor="#454545",
        font=dict(color="#F5F5F5"),
        showlegend=False,
        margin=dict(l=10, r=15, t=90, b=10)
    ).show()

4 Product view

4.1 Product Information

  • Based on the same principle, we set the following KPIs:
    • Total products
    • Percentage of High Rated Products
    • Products Needing Attention
  • Charts information:
    • Top Rated Products
    • Lowest Rated Products
    • Product Rating vs. Review Volume
Code
# Product-level aggregation
product = (
    data.groupby("product_id")
        .agg(
            product_type=("product_type", "nunique"),
            reviews=("user_id", "count"),
            avg_rating=("rating", "mean")
        ).reset_index()
)
# Total product
total_product = product["product_id"].nunique()
# High rated products
high_rated_products = (
    product["avg_rating"].ge(4).mean() * 100
)
# Low rated products: Low rated but high reviews
low_rated_products = product["avg_rating"].lt(3)
median_reviews = product["reviews"].median()
attention_products = (
    low_rated_products &
    product["reviews"].ge(median_reviews)
).mean() * 100

# Charts
top_rated = (
    product
    .sort_values(
        ["avg_rating", "reviews"],
        ascending=[False, False]
    )
    .head(30)
)

low_rated = (
    product
    .sort_values(
        ["avg_rating", "reviews"],
        ascending=[True, False]
    )
    .head(30)
)


product_scatter = product.copy()

Product Dashboard

Code
from plotly.subplots import make_subplots
import plotly.graph_objects as go

fig_product = make_subplots(
    rows=2,
    cols=3,
    specs=[
        [
            {"type": "indicator"},
            {"type": "indicator"},
            {"type": "indicator"},
        ],
        [
            {"type": "bar"},
            {"type": "scatter"},
            {"type": "bar"},
        ],
    ],
    horizontal_spacing=0.06,
    vertical_spacing=0.15,
    subplot_titles=(
        "Total Product",
        "High Rated Products",
        "Products Needing Attention",
        "Top 30 Highest Rated Products",
        "Product Rating vs. Review Volume",
        "Bottom 30 Lowest Rated Products",
    ),
)
# KPI 1
fig_product = fig_product.add_trace(
    go.Indicator(
        mode="number",
        value=total_product,
        title=dict(
            text="Total Product",
            font=dict(
                size=15,
                color=KPI_COLORS[0]
            )
        ),
        number=dict(
            valueformat=",d",
            font=dict(
                size=28,
                color=TEXT_COLOR
            )
        )
    ),
    row=1,
    col=1
)
# KPI 2
fig_product = fig_product.add_trace(
    go.Indicator(
        mode="number",
        value=high_rated_products,
        title=dict(
            text="High Rated Products",
            font=dict(
                size=15,
                color=KPI_COLORS[1]
            )
        ),
        number=dict(
            valueformat=".1f",
            suffix="%",
            font=dict(
                size=28,
                color=TEXT_COLOR
            )
        )
    ),
    row=1,
    col=2
)
# KPI 3
fig_product = fig_product.add_trace(
    go.Indicator(
        mode="number",
        value=attention_products,
        title=dict(
            text="Products Needing Attention",
            font=dict(
                size=15,
                color=KPI_COLORS[2]
            )
        ),
        number=dict(
            valueformat=".1f",
            suffix="%",
            font=dict(
                size=28,
                color=TEXT_COLOR
            )
        )
    ),
    row=1,
    col=3
)

# Chart 1: Top products
fig_product = fig_product.add_trace(
    go.Bar(
        x=top_rated["product_id"],
        y=top_rated["avg_rating"],
        textposition="outside",
        marker=dict(
            color=KPI_COLORS[0]),
        customdata=top_rated[["reviews"]],
        hovertemplate=(
            "Product: %{x}<br>"
            "Average Rating: %{y:.2f}<br>"
            "Reviews: %{customdata[0]:,}"
            "<extra></extra>"
        ),
    ),
    row=2,
    col=1
)
# Chart 2: Low products
fig_product = fig_product.add_trace(
    go.Scatter(
        x=product["reviews"],
        y=product["avg_rating"],
        mode="markers",
        marker=dict(
            size=8,
            opacity=0.65,
        ),
        customdata=product[["product_type"]],
        hovertemplate=(
            "Product: %{customdata[0]}<br>"
            "Reviews: %{x:,}<br>"
            "Average Rating: %{y:.2f}"
            "<extra></extra>"
        ),
    ),
    row=2,
    col=2
)
# Chart 3: Product rating vs Reviews
fig_product = fig_product.add_trace(
    go.Bar(
        x=low_rated["product_id"],
        y=low_rated["avg_rating"],
        textposition="outside",
        marker=dict(
            color=KPI_COLORS[2]),
        customdata=low_rated[["reviews"]],
        hovertemplate=(
            "Product: %{x}<br>"
            "Average Rating: %{y:.2f}<br>"
            "Reviews: %{customdata[0]:,}"
            "<extra></extra>"
        ),
    ),
    row=2,
    col=3
)
# Final layout
fig_product.update_xaxes(
    title_text="Product",
    tickangle=-45,
    color=MUTED_TEXT,
    gridcolor=GRID_COLOR,
    row=2,
    col=1
)
fig_product.update_yaxes(
    title_text="Average Rating",
    range=[0, 5.2],
    showgrid=False,
    color=MUTED_TEXT,
    gridcolor=GRID_COLOR,
    row=2,
    col=1
)
fig_product.update_xaxes(
    title_text="Reviews",
    color=MUTED_TEXT,
    row=2,
    col=2
)
fig_product.update_yaxes(
    title_text="Average Rating",
    range=[1, 5],
    color=MUTED_TEXT,
    gridcolor=GRID_COLOR,
    row=2,
    col=2
)
fig_product.update_xaxes(
    title_text="Product",
    tickangle=-45,
    color=MUTED_TEXT,
    gridcolor=GRID_COLOR,
    row=2,
    col=3
)
fig_product.update_yaxes(
    title_text="Average Rating",
    range=[0, 2.1],
    color=MUTED_TEXT,
    gridcolor=GRID_COLOR,
    row=2,
    col=3
)

# Box KPIs
KPI_BOXES = [
    (0.001, 0.300),
    (0.360, 0.650),
    (0.700, 0.998)
]

for (x0, x1), color in zip(
        KPI_BOXES,
        KPI_COLORS):

    fig_product = fig_product.add_shape(
        type="rect",
        xref="paper",
        yref="paper",
        x0=x0,
        x1=x1,
        y0=0.58,
        y1=0.98,
        fillcolor=CARD_COLOR,
        line=dict(
            color=color,
            width=2
        ),
        layer="below"
    )
# Background color
fig_product\
    .update_layout(
        width=970,
        height=520,
        title=dict(
            text="Amazon Product Reviews — Product View",
            font=dict(
                size=24,
                color=TEXT_COLOR
            ),
            x=0.02,
            xanchor="left"
        ),
        paper_bgcolor="#454545",
        plot_bgcolor="#454545",
        font=dict(color="#F5F5F5"),
        showlegend=False,
        margin=dict(l=10, r=15, t=90, b=10)
    ).show()
Code
# Figure
# ------
fig_trend = make_subplots(
    rows=2,
    cols=2,
    subplot_titles=[
        "Review Volume Over Time",
        "Average Rating Over Time",
        "Rating Composition",
        "Category Rating Trends"
    ],
    vertical_spacing=0.12,
    horizontal_spacing=0.05
)
# -------------
# Review volume
fig_trend.add_trace(
    go.Scatter(
        x=monthly_rating["date"],
        y=monthly_rating["reviews"],
        mode="lines",
        fill="tozeroy",
        name='Review evolution',
        hovertemplate=(
            "%{x|%b %Y}<br>"
            "Reviews: %{y:,}<extra></extra>"
        )
    ),
    row=1,
    col=1
)
# Average rating trend
# --------------------
fig_trend.add_trace(
    go.Scatter(
        x=monthly_rating["date"],
        y=monthly_rating["avg_rating"],
        mode="lines+markers",
        name='Avg Rating evolution',
        hovertemplate=(
            "%{x|%b %Y}<br>"
            "Average rating: %{y:.2f}<extra></extra>"
        )
    ),
    row=1,
    col=2
)
# Rating composition
# ------------------
for rating in sorted(rating_month["rating"].unique()):
    temp = rating_month[
        rating_month["rating"] == rating
    ]
    fig_trend.add_trace(
        go.Scatter(
            x=temp["month"],
            y=temp["share"],
            mode="lines",
            stackgroup="one",
            name=f"{rating}★",
            hovertemplate=(
                f"{rating}★<br>"
                "%{x|%b %Y}<br>"
                "Share: %{y:.1%}<extra></extra>"
            )
        ),
        row=2,
        col=1
    )
# Category rating trends
# ----------------------
for product_type in top_types:
    temp = category_month_top[
        category_month_top["product_type"] == product_type
    ]
    fig_trend.add_trace(
        go.Scatter(
            x=temp["month"],
            y=temp["avg_rating"],
            mode="lines",
            name=str(product_type),
            hovertemplate=(
                f"{product_type}<br>"
                "%{x|%b %Y}<br>"
                "Rating: %{y:.2f}<extra></extra>"
            )
        ),
        row=2,
        col=2
    )

fig_trend.update_layout(
    title="Amazon Beauty Products — Rating & Market Trends",
    width=970,
    height=520,
    paper_bgcolor="#454545",
    plot_bgcolor="#454545",
    font=dict(color="#F5F5F5"),
    margin=dict(l=10, r=15, t=90, b=10)
)

fig_trend.update_yaxes(
    tickformat=".0%",
    row=2,
    col=1
)

for i in range(4):
    fig_trend.update_xaxes(
        showgrid=False,
        row=i//2+1,
        col=i%2+1
    )

fig_trend.show()

6 Final Dashboard

6.1 Quarto Tabs

6.2 Dash Framework

  • Dumb all the figures then assemble them back using dash.
import pickle

figures = {
    "overview": fig_overall,
    "product": fig_product,
    "customer": fig_customer,
    "trend": fig_trend
}

with open("dashboard_figures.pkl", "wb") as f:
    pickle.dump(figures, f)
  • Dash is a great tool for dashboard development and deployment.
  • We can then read back the 4 figures and assemble them into a nice dashboard: Amason dashboard.

6.2 Dash Framework

6.3 Summary

  • Business objective is always the main driver of how the dashboard should look like.
  • Define KPIs that highlights the indicators to the main objective.
  • Data Wrangling and Manipulation is another important step to start building the dashboard.
  • Select format and aesthetic of the dashboard.
  • Python & Plotly are tools for CS students.
  • Power BI and Tableau are for business and less technical people.
  • AI Tools such as Claude, Gemini… are also great but you have to know what you are doing.

🥳 Yeahhhh 🥂!!!










Any question? Take a break!