| |
|
|
| 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 |
|
|
| |
| |
| |
|
|
| @st.cache_data |
| def load_data(): |
| df = pd.read_csv("df_clean.csv", index_col=0) |
| return df |
|
|
| |
| |
| |
|
|
| def get_dataset_info(): |
| df = load_data() |
| return { |
| "total_phones": len(df), |
| "avg_price": int(df["price"].mean()), |
| } |
|
|
| |
| |
| |
|
|
| DARK_BG = "#0a0f1e" |
| CARD_BG = "#111827" |
| TEXT_CLR = "#e2e8f0" |
| GRID_CLR = "#1e293b" |
| ACCENT = "#6366f1" |
| ACCENT2 = "#8b5cf6" |
|
|
| |
| 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] |
|
|
| |
| |
| |
|
|
| def render_eda(): |
| df = load_data() |
| _base_style() |
|
|
| |
| cluster_col = None |
| for c in ["cluster_label", "segment", "cluster_name"]: |
| if c in df.columns: |
| cluster_col = c |
| break |
|
|
| |
| 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" |
|
|
| |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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: |
|
|
| |
| 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) |
|
|
| |
| 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)", |
| }) |
| ) |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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()]) |
| ) |
|
|
| |
| 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." |
| ) |
|
|
| |
| 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) |
|
|
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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." |
| ) |
|
|