
┌──────────────────────┐
│ Business objective │
└──────────┬───────────┘
▼
┌───────┐
│ KPIs │
└───┬───┘
▼
┌────────────────────────────────────┐
│ Understanding and prepare the data │
└─────────────────┬──────────────────┘
▼
┌──────────────────────────────────────────┐
│ Explore the data according to key points │
└────────────────────┬─────────────────────┘
▼
┌─────────────────────────────┐
│ Validate → test → iterate ↩ │
└─────────────────────────────┘
Data wrangling is very important as in business, beautiful dashboard built on wrong info is worst than no dashboard at all.
┌────────────────────────────┐
│ Boost platform performance │
└─────────────┬──────────────┘
▼
┌───────────────────────────────┐
│ Customer/Product/Rating Trend │
└───────────────┬───────────────┘
▼
┌────────────────┐
│ Data Wrangling │
└───────┬────────┘
▼
┌─────────────────────────────────────────┐
│ Explore the data & select graph designs │
└───────────────────┬─────────────────────┘
▼
┌─────────────────────────────┐
│ Validate → test → iterate ↩ │
└─────────────────────────────┘
# 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)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)
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 figmake_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## 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()# 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()# 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
)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
)trace to the empty canvas.# 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
)# 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"
)# 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
)
)go objects \(\to\) make_subplots().make_subplots \(\leftarrow\) go.['user_id', 'product_id', 'product_type', 'rating', 'date', 'year', 'month']
make_subplots and eventually add more elements.make_subplots and eventually add more elements.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
)# 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
)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
)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")fig_customer.show().# 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
)# 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
)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()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()# 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()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()# Monthly rating statistics
# -------------------------
monthly_rating = (
data.set_index("date")
.resample("M")
.agg(
reviews=("rating", "size"),
avg_rating=("rating", "mean")
)
.reset_index()
)
# Rating proportions
# ------------------
rating_month = (
data.assign(month=data["date"]
.dt.to_period("M")
.dt.to_timestamp()) # 1st day
.groupby(["month", "rating"])
.size()
.reset_index(name="reviews")
)
rating_month["share"] = (
rating_month
.groupby("month")["reviews"]
.transform(lambda x: x / x.sum()) # normalize
)
# Category trends
# ---------------
category_month = (
data.assign(
month=data["date"]
.dt.to_period("M")
.dt.to_timestamp()
)
.groupby(["month", "product_type"])
.agg(
reviews=("rating", "size"),
avg_rating=("rating", "mean")
)
.reset_index()
)
# Keep the largest categories
top_types = (
data["product_type"]
.value_counts()
.head(5)
.index
)
category_month_top = category_month[
category_month["product_type"].isin(top_types)
]# 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()🥳 Yeahhhh 🥂!!!
Any question? Take a break!
