final-project-2 / eda.py
fernandobriann's picture
Upload eda.py
d41c27a verified
Raw
History Blame Contribute Delete
21.2 kB
# eda.py
import streamlit as st
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.ticker as mticker
import seaborn as sns
from math import pi
# =========================================================
# LOAD DATA
# =========================================================
@st.cache_data
def load_data():
df = pd.read_csv("df_clean.csv", index_col=0)
return df
# =========================================================
# DATASET INFO (used by Home page)
# =========================================================
def get_dataset_info():
df = load_data()
return {
"total_phones": len(df),
"avg_price": int(df["price"].mean()),
}
# =========================================================
# CHART HELPERS
# =========================================================
DARK_BG = "#0a0f1e"
CARD_BG = "#111827"
TEXT_CLR = "#e2e8f0"
GRID_CLR = "#1e293b"
ACCENT = "#6366f1"
ACCENT2 = "#8b5cf6"
# Warna per cluster β€” konsisten di seluruh EDA
CLUSTER_PALETTE = {
"Budget": "#6366f1",
"Camera-focused": "#f472b6",
"Gaming / Performance": "#34d399",
"Flagship": "#fbbf24",
}
def _base_style():
plt.rcParams.update({
"figure.facecolor": DARK_BG,
"axes.facecolor": CARD_BG,
"axes.edgecolor": GRID_CLR,
"axes.labelcolor": TEXT_CLR,
"xtick.color": TEXT_CLR,
"ytick.color": TEXT_CLR,
"text.color": TEXT_CLR,
"grid.color": GRID_CLR,
"grid.linestyle": "--",
"grid.alpha": 0.5,
"font.size": 11,
"axes.titlesize": 13,
"axes.titleweight": "bold",
})
def _savefig(fig):
fig.tight_layout()
st.pyplot(fig)
plt.close(fig)
def _cluster_colors(labels):
"""Return list of colors matching cluster label order."""
return [CLUSTER_PALETTE.get(l, ACCENT) for l in labels]
# =========================================================
# RENDER EDA
# =========================================================
def render_eda():
df = load_data()
_base_style()
# Buat kolom nama cluster jika belum ada
cluster_col = None
for c in ["cluster_label", "segment", "cluster_name"]:
if c in df.columns:
cluster_col = c
break
# Fallback: mapping dari cluster_kmeans int ke nama
CLUSTER_LABELS = {
0: "Budget",
1: "Camera-focused",
2: "Gaming / Performance",
3: "Flagship",
}
if cluster_col is None and "cluster_kmeans" in df.columns:
df["_cluster_name"] = df["cluster_kmeans"].map(CLUSTER_LABELS)
cluster_col = "_cluster_name"
# --------------------------------------------------
# 1. Dataset Overview
# --------------------------------------------------
st.markdown("## πŸ“‹ Dataset Overview")
c1, c2, c3, c4 = st.columns(4)
c1.metric("Total Phones", len(df))
c2.metric("Brands", df["brand"].nunique() if "brand" in df.columns else "β€”")
c3.metric("Avg Price (USD)", f"${int(df['price'].mean())}")
c4.metric("Features", df.shape[1])
st.markdown("### Sample Data")
st.dataframe(df.head(10), use_container_width=True, hide_index=False)
# --------------------------------------------------
# 2. CLUSTER ANALYSIS β€” Ringkasan per Segmen
# --------------------------------------------------
st.markdown("---")
st.markdown("## πŸ—‚οΈ Smartphone Market Segments")
st.markdown(
"Dataset dibagi menjadi **4 segmen** menggunakan K-Means Clustering "
"berdasarkan spesifikasi dan harga smartphone."
)
if cluster_col and cluster_col in df.columns:
# --- 2a. Donut chart distribusi jumlah HP per cluster ---
cluster_counts = df[cluster_col].value_counts()
labels_ordered = [l for l in CLUSTER_PALETTE if l in cluster_counts.index]
counts_ordered = [cluster_counts[l] for l in labels_ordered]
colors_ordered = [CLUSTER_PALETTE[l] for l in labels_ordered]
fig, ax = plt.subplots(figsize=(7, 5))
wedges, texts, autotexts = ax.pie(
counts_ordered,
labels=labels_ordered,
colors=colors_ordered,
autopct="%1.1f%%",
startangle=140,
pctdistance=0.78,
wedgeprops=dict(width=0.55, edgecolor=DARK_BG, linewidth=2),
)
for t in texts:
t.set_color(TEXT_CLR)
t.set_fontsize(10)
for at in autotexts:
at.set_color(DARK_BG)
at.set_fontweight("bold")
at.set_fontsize(9)
ax.set_title("Distribution of Smartphones by Segment", pad=16)
_savefig(fig)
# --- 2b. Tabel ringkasan stat per cluster ---
st.markdown("### πŸ“Š Cluster Summary Statistics")
agg = (
df.groupby(cluster_col)
.agg(
Count=("price", "count"),
Avg_Price=("price", "mean"),
Avg_RAM=("ram", "mean"),
Avg_Battery=("battery_capacity", "mean"),
Avg_Camera=("main_camera_mp", "mean"),
)
.round(1)
.rename(columns={
"Count": "# Phones",
"Avg_Price": "Avg Price ($)",
"Avg_RAM": "Avg RAM (GB)",
"Avg_Battery": "Avg Battery (mAh)",
"Avg_Camera": "Avg Camera (MP)",
})
)
# Reorder biar urut sesuai palette
agg = agg.reindex([l for l in CLUSTER_PALETTE if l in agg.index])
st.dataframe(
agg.style.format({
"Avg Price ($)": "${:.0f}",
"Avg RAM (GB)": "{:.1f}",
"Avg Battery (mAh)":"{:.0f}",
"Avg Camera (MP)": "{:.1f}",
}).background_gradient(cmap="Blues", subset=["Avg Price ($)"]),
use_container_width=True,
)
# --- 2c. Bar chart avg price per cluster ---
st.markdown("### πŸ’° Average Price per Segment")
seg_price = (
df.groupby(cluster_col)["price"]
.mean()
.reindex([l for l in CLUSTER_PALETTE if l in df[cluster_col].unique()])
)
fig, ax = plt.subplots(figsize=(9, 4))
bars = ax.bar(
seg_price.index,
seg_price.values,
color=_cluster_colors(seg_price.index),
edgecolor=DARK_BG,
linewidth=0.5,
width=0.55,
)
ax.bar_label(
bars,
labels=[f"${int(v):,}" for v in seg_price.values],
padding=5,
color=TEXT_CLR,
fontsize=10,
fontweight="bold",
)
ax.set_ylabel("Average Price (USD)")
ax.set_title("Average Smartphone Price per Market Segment")
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"${int(x):,}"))
ax.yaxis.grid(True)
ax.tick_params(axis="x", rotation=10)
_savefig(fig)
# --- 2d. Multi-bar: avg RAM / Battery / Camera per cluster ---
st.markdown("### πŸ”¬ Specification Comparison per Segment")
spec_data = (
df.groupby(cluster_col)
.agg(
RAM=("ram", "mean"),
Battery=("battery_capacity", "mean"),
Camera=("main_camera_mp", "mean"),
)
.reindex([l for l in CLUSTER_PALETTE if l in df[cluster_col].unique()])
)
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
specs = [
("RAM", "Avg RAM (GB)", "#22d3ee"),
("Battery", "Avg Battery (mAh)", "#34d399"),
("Camera", "Avg Main Camera (MP)", "#f472b6"),
]
for ax, (col, ylabel, color) in zip(axes, specs):
bars = ax.bar(
spec_data.index,
spec_data[col],
color=_cluster_colors(spec_data.index),
edgecolor=DARK_BG,
linewidth=0.5,
width=0.55,
)
ax.bar_label(bars, fmt="%.1f", padding=3, color=TEXT_CLR, fontsize=9)
ax.set_title(ylabel)
ax.yaxis.grid(True)
ax.tick_params(axis="x", rotation=15, labelsize=8)
_savefig(fig)
# --- 2e. Radar chart perbandingan profil setiap cluster ---
st.markdown("### πŸ•ΈοΈ Cluster Profile Radar Chart")
st.caption(
"Nilai dinormalisasi 0–1 agar bisa dibandingkan antar fitur."
)
radar_cols = ["price", "ram", "battery_capacity", "main_camera_mp"]
radar_labels = ["Price", "RAM", "Battery", "Camera"]
radar_df = (
df.groupby(cluster_col)[radar_cols]
.mean()
.reindex([l for l in CLUSTER_PALETTE if l in df[cluster_col].unique()])
)
# Normalisasi 0–1
radar_norm = (radar_df - radar_df.min()) / (radar_df.max() - radar_df.min())
N = len(radar_labels)
angles = [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]
fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True))
ax.set_facecolor(CARD_BG)
for seg in radar_norm.index:
values = radar_norm.loc[seg].tolist()
values += values[:1]
color = CLUSTER_PALETTE.get(seg, ACCENT)
ax.plot(angles, values, linewidth=2, color=color, label=seg)
ax.fill(angles, values, alpha=0.12, color=color)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(radar_labels, color=TEXT_CLR, fontsize=11)
ax.set_yticks([0.25, 0.5, 0.75, 1.0])
ax.set_yticklabels(["0.25", "0.5", "0.75", "1.0"], color=GRID_CLR, fontsize=7)
ax.grid(color=GRID_CLR, linestyle="--", linewidth=0.6)
ax.spines["polar"].set_color(GRID_CLR)
ax.set_title("Cluster Profile Comparison", pad=20, color=TEXT_CLR)
legend = ax.legend(
loc="upper right",
bbox_to_anchor=(1.35, 1.15),
framealpha=0,
labelcolor=TEXT_CLR,
fontsize=9,
)
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** Flagship cluster dominates in Price and Camera. "
"Gaming / Performance leads in RAM. Battery cluster surprisingly "
"doesn't have the highest battery on average β€” it selects phones "
"with the best battery-to-price efficiency. Budget cluster scores "
"lowest across all dimensions, as expected."
)
# --- 2f. Price distribution per cluster (violin) ---
st.markdown("### 🎻 Price Distribution per Segment")
segs = [l for l in CLUSTER_PALETTE if l in df[cluster_col].unique()]
data_per_seg = [df[df[cluster_col] == s]["price"].dropna().values for s in segs]
colors_violin = [CLUSTER_PALETTE[s] for s in segs]
fig, ax = plt.subplots(figsize=(11, 5))
parts = ax.violinplot(
data_per_seg,
positions=range(len(segs)),
showmedians=True,
showextrema=True,
)
for i, (pc, color) in enumerate(zip(parts["bodies"], colors_violin)):
pc.set_facecolor(color)
pc.set_alpha(0.6)
pc.set_edgecolor(TEXT_CLR)
for part_name in ["cmedians", "cmins", "cmaxes", "cbars"]:
parts[part_name].set_color(TEXT_CLR)
parts[part_name].set_linewidth(1.2)
ax.set_xticks(range(len(segs)))
ax.set_xticklabels(segs, rotation=10, fontsize=10)
ax.set_ylabel("Price (USD)")
ax.set_title("Price Distribution per Market Segment")
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"${int(x):,}"))
ax.yaxis.grid(True)
# Legend patch
patches = [
mpatches.Patch(color=CLUSTER_PALETTE[s], label=s, alpha=0.7)
for s in segs
]
ax.legend(handles=patches, framealpha=0, labelcolor=TEXT_CLR, fontsize=9)
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** Flagship segment shows the widest price range and highest "
"median price. Budget segment is tightly clustered at the lower end. "
"Gaming and Camera segments overlap in the mid-range, reflecting that "
"good cameras and strong performance are both available at similar price points."
)
# --------------------------------------------------
# 3. Price Distribution (Overall)
# --------------------------------------------------
st.markdown("---")
st.markdown("## πŸ’° Overall Price Distribution")
fig, ax = plt.subplots(figsize=(10, 4))
ax.hist(df["price"], bins=40, color=ACCENT, edgecolor=DARK_BG, linewidth=0.5)
ax.set_title("Distribution of Smartphone Prices")
ax.set_xlabel("Price (USD)")
ax.set_ylabel("Frequency")
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"${int(x):,}"))
ax.yaxis.grid(True)
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** Most smartphones are concentrated between $150–$600. "
"The distribution is right-skewed, meaning premium flagship devices "
"are relatively less common. The market is dominated by budget and mid-range devices."
)
# --------------------------------------------------
# 4. Top Brands
# --------------------------------------------------
if "brand" in df.columns:
st.markdown("---")
st.markdown("## πŸ† Top Smartphone Brands")
top_brands = df["brand"].value_counts().head(10)
fig, ax = plt.subplots(figsize=(10, 5))
bars = ax.bar(
top_brands.index,
top_brands.values,
color=[ACCENT if i == 0 else ACCENT2 for i in range(len(top_brands))],
edgecolor=DARK_BG,
linewidth=0.5,
)
ax.bar_label(bars, padding=3, color=TEXT_CLR, fontsize=10)
ax.set_title("Top 10 Smartphone Brands by Count")
ax.set_xlabel("Brand")
ax.set_ylabel("Count")
ax.tick_params(axis="x", rotation=30)
ax.yaxis.grid(True)
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** Oppo appears most frequently in the dataset, followed by "
"Apple and Vivo. Chinese smartphone brands dominate the dataset composition."
)
# --------------------------------------------------
# 5. RAM Distribution
# --------------------------------------------------
if "ram" in df.columns:
st.markdown("---")
st.markdown("## πŸ–₯️ RAM Distribution")
fig, ax = plt.subplots(figsize=(9, 4))
ax.hist(df["ram"], bins=20, color="#22d3ee", edgecolor=DARK_BG, linewidth=0.5)
ax.set_title("RAM Distribution")
ax.set_xlabel("RAM (GB)")
ax.set_ylabel("Frequency")
ax.yaxis.grid(True)
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** 8 GB RAM is the most common configuration. "
"Modern smartphones increasingly standardize around 6–12 GB RAM."
)
# --------------------------------------------------
# 6. Battery Distribution
# --------------------------------------------------
if "battery_capacity" in df.columns:
st.markdown("---")
st.markdown("## πŸ”‹ Battery Capacity Distribution")
fig, ax = plt.subplots(figsize=(10, 4))
ax.hist(df["battery_capacity"], bins=30, color="#34d399", edgecolor=DARK_BG, linewidth=0.5)
ax.set_title("Battery Capacity Distribution")
ax.set_xlabel("Battery Capacity (mAh)")
ax.set_ylabel("Frequency")
ax.yaxis.grid(True)
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** Most smartphones cluster around 5000 mAh. "
"Large-capacity batteries have become standard across many categories."
)
# --------------------------------------------------
# 7. Camera Distribution
# --------------------------------------------------
if "main_camera_mp" in df.columns:
st.markdown("---")
st.markdown("## πŸ“· Camera (Main MP) Distribution")
fig, ax = plt.subplots(figsize=(10, 4))
ax.hist(df["main_camera_mp"], bins=30, color="#f472b6", edgecolor=DARK_BG, linewidth=0.5)
ax.set_title("Main Camera MP Distribution")
ax.set_xlabel("Main Camera (MP)")
ax.set_ylabel("Frequency")
ax.yaxis.grid(True)
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** 50 MP is the dominant camera configuration. "
"Higher resolution sensors (108 MP, 200 MP) exist but are less common."
)
# --------------------------------------------------
# 8. Correlation Heatmap
# --------------------------------------------------
st.markdown("---")
st.markdown("## πŸ”— Correlation Heatmap")
numeric_cols = df.select_dtypes(include=np.number)
corr = numeric_cols.corr()
fig, ax = plt.subplots(figsize=(12, 7))
sns.heatmap(
corr,
annot=True,
fmt=".2f",
cmap="Blues",
ax=ax,
linewidths=0.4,
linecolor=DARK_BG,
cbar_kws={"shrink": 0.8},
)
ax.set_title("Correlation Heatmap")
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** Price has the strongest positive correlation with CPU tier and RAM. "
"Battery capacity has weak correlation with price β€” large batteries are available "
"even in affordable devices."
)
# --------------------------------------------------
# 9. Average Price by Brand (Top 10)
# --------------------------------------------------
if "brand" in df.columns:
st.markdown("---")
st.markdown("## πŸ’΅ Average Price by Brand (Top 10)")
avg_brand_price = (
df.groupby("brand")["price"]
.mean()
.sort_values(ascending=False)
.head(10)
)
fig, ax = plt.subplots(figsize=(12, 5))
colors = [ACCENT if i == 0 else ACCENT2 for i in range(len(avg_brand_price))]
bars = ax.bar(
avg_brand_price.index,
avg_brand_price.values,
color=colors,
edgecolor=DARK_BG,
linewidth=0.5,
)
ax.bar_label(
bars,
labels=[f"${int(v):,}" for v in avg_brand_price.values],
padding=4, color=TEXT_CLR, fontsize=9,
)
ax.set_title("Top 10 Brands by Average Smartphone Price")
ax.set_xlabel("Brand")
ax.set_ylabel("Average Price (USD)")
ax.tick_params(axis="x", rotation=35)
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"${int(x):,}"))
ax.yaxis.grid(True)
_savefig(fig)
st.info(
"πŸ’‘ **Insight:** Sony and Apple have the highest average prices. "
"Oppo and Vivo focus more on mid-range and affordable markets."
)
# --------------------------------------------------
# 10. Most Expensive & Most Affordable
# --------------------------------------------------
st.markdown("---")
c1, c2 = st.columns(2)
cols_show = [c for c in ["brand", "model", "price"] if c in df.columns]
with c1:
st.markdown("## πŸ₯‡ Top 10 Most Expensive")
expensive = df.sort_values("price", ascending=False).head(10)[cols_show]
st.dataframe(expensive, use_container_width=True, hide_index=True)
with c2:
st.markdown("## πŸ’Έ Top 10 Most Affordable")
budget_phones = df.sort_values("price", ascending=True).head(10)[cols_show]
st.dataframe(budget_phones, use_container_width=True, hide_index=True)
st.info(
"πŸ’‘ **Insight:** Sony Xperia and Apple dominate the premium end. "
"Tecno and Infinix lead the budget segment with competitive specs at entry-level prices."
)
# --------------------------------------------------
# 11. Key Findings
# --------------------------------------------------
st.markdown("---")
st.markdown("## πŸ”‘ Key Findings")
st.markdown("""
- The smartphone market is dominated by **budget and mid-range devices**.
- **8 GB RAM**, **5000 mAh batteries**, and **50 MP cameras** are the most common configurations.
- **CPU tier** shows the strongest relationship with smartphone pricing.
- **Battery capacity** has relatively weak correlation with price.
- High-resolution cameras are increasingly available in **affordable smartphones**.
- **Chinese brands** dominate the dataset composition.
- **Sony** and **Apple** maintain significantly higher average prices.
- The **4 market segments** (Budget, Camera-focused, Gaming, Flagship) each have distinct spec profiles.
""")
st.success(
"πŸ“Œ **Conclusion:** These findings support a recommendation system that matches "
"users with smartphones based on technical specifications and budget preferences, "
"guided by cluster-based market segmentation."
)