From f866c3f69386aa3e2994fcd1f7c5922975af8f22 Mon Sep 17 00:00:00 2001 From: TerryHC Date: Sun, 3 May 2026 14:55:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BD=9C=E6=A5=AD=E7=B9=B3?= =?UTF-8?q?=E4=BA=A4=20-=20=E9=BB=83=E6=A8=BA=E8=A3=95=20-=20AIPE08?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homework/m1_numpy.py | 28 ++--- homework/m2_pandas_cleaning.py | 57 +++++++--- homework/m3_pandas_advanced.py | 76 ++++++++++--- homework/m4_timeseries.py | 77 ++++++++++--- homework/m5_visualization.py | 201 ++++++++++++++++++++++++++++++--- homework/m6_plotly_capstone.py | 178 ++++++++++++++++++++++++++--- 6 files changed, 529 insertions(+), 88 deletions(-) diff --git a/homework/m1_numpy.py b/homework/m1_numpy.py index 5ff41ab..6e9b4ea 100644 --- a/homework/m1_numpy.py +++ b/homework/m1_numpy.py @@ -18,20 +18,20 @@ def green_mean(): """建立 [10, 20, 30, 40, 50],回傳所有元素的平均值 (float)""" - # TODO: 你的程式碼 - pass + arr = np.array([10, 20, 30, 40, 50]) + return arr.mean() def green_double(): """建立 [10, 20, 30, 40, 50],回傳所有元素乘以 2 的 ndarray""" - # TODO: 你的程式碼 - pass + arr = np.array([10, 20, 30, 40, 50]) + return arr * 2 def green_filter(): """建立 [10, 20, 30, 40, 50],回傳大於 25 的元素 (ndarray)""" - # TODO: 你的程式碼 - pass + arr = np.array([10, 20, 30, 40, 50]) + return arr[arr > 25] # ============================================================ @@ -41,8 +41,7 @@ def green_filter(): def yellow_expensive_count(prices): """回傳單價 > 1000 的商品數量 (int)""" - # TODO: 你的程式碼 - pass + return (prices > 1000).sum() def yellow_top3_stock_indices(stocks): @@ -50,8 +49,7 @@ def yellow_top3_stock_indices(stocks): 回傳庫存最多的前 3 個商品的索引位置 (ndarray, 由大到小排) 提示:np.argsort """ - # TODO: 你的程式碼 - pass + return np.argsort(stocks)[-3:][::-1] def yellow_restock_cost(prices, stocks): @@ -59,8 +57,7 @@ def yellow_restock_cost(prices, stocks): 單價 < 500 的商品,每種各進貨 50 個,回傳總花費 (float/int) 提示:布林遮罩 + .sum() """ - # TODO: 你的程式碼 - pass + return (prices[prices < 500] * 50).sum() # ============================================================ @@ -76,5 +73,8 @@ def red_double11_prices(prices, stocks): 回傳每個商品的雙 11 售價 (ndarray) 提示:np.where 可以巢狀使用 """ - # TODO: 你的程式碼 - pass + return np.where( + stocks >= 100, + prices * 0.7, + np.where(stocks >= 20, prices * 0.9, prices) + ) \ No newline at end of file diff --git a/homework/m2_pandas_cleaning.py b/homework/m2_pandas_cleaning.py index fa36bff..cf1aeec 100644 --- a/homework/m2_pandas_cleaning.py +++ b/homework/m2_pandas_cleaning.py @@ -8,7 +8,7 @@ 資料路徑:datasets/ecommerce/orders_raw.csv """ import pandas as pd - +DATA = '../datasets/ecommerce/orders_raw.csv' # ============================================================ # 🟢 送分題(每題 10 分,共 30 分) @@ -19,17 +19,15 @@ def green_read_csv(): 讀取 orders_raw.csv,回傳原始 DataFrame(不做任何清理) 提示:pd.read_csv() """ - # TODO: 你的程式碼 - pass - + return pd.read_csv('datasets/ecommerce/orders_raw.csv') def green_shape(df): """ 回傳 DataFrame 的 (列數, 欄數) tuple 提示:df.shape """ - # TODO: 你的程式碼 - pass + print('原始形狀:', df.shape) + return df.shape def green_dtypes(df): @@ -37,8 +35,7 @@ def green_dtypes(df): 回傳 DataFrame 的欄位型別 (Series) 提示:df.dtypes """ - # TODO: 你的程式碼 - pass + return df.dtypes # ============================================================ @@ -51,8 +48,8 @@ def yellow_clean_columns(df): 回傳清理後的 DataFrame(不要修改原始 df) 提示:df.columns.str.strip().str.lower() """ - # TODO: 你的程式碼 - pass + df.columns = df.columns.str.strip().str.lower() + return df def yellow_clean_amount(df): @@ -62,8 +59,14 @@ def yellow_clean_amount(df): 回傳清理後的 DataFrame(不要修改原始 df) 提示:.str.replace() + .astype(float) """ - # TODO: 你的程式碼 - pass + df['amount'] = ( + df['amount'] + .astype(str) + .str.replace('$', '', regex=False) + .str.replace(',', '', regex=False) + .astype(float) + ) + return df def yellow_drop_duplicates(df): @@ -71,8 +74,7 @@ def yellow_drop_duplicates(df): 移除完全重複的列,回傳去重後的 DataFrame 提示:df.drop_duplicates() """ - # TODO: 你的程式碼 - pass + return df.drop_duplicates() # ============================================================ @@ -92,5 +94,28 @@ def red_clean_orders(path): 回傳:清理後的 DataFrame 提示:pd.to_datetime(errors='coerce') """ - # TODO: 你的程式碼 - pass + # 1. + df = pd.read_csv(path) + + # 2. + df.columns = df.columns.str.strip().str.lower() + + # 3. + df['amount'] = ( + df['amount'] + .astype(str) + .str.replace('$', '', regex=False) + .str.replace(',', '', regex=False) + .astype(float) + ) + + # 4. + df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce') + + # 5. + df = df.dropna(subset=['order_date']).dropna(subset=['amount']) + + # 6. + df = df.drop_duplicates() + + return df diff --git a/homework/m3_pandas_advanced.py b/homework/m3_pandas_advanced.py index 68410e8..5641b24 100644 --- a/homework/m3_pandas_advanced.py +++ b/homework/m3_pandas_advanced.py @@ -23,20 +23,25 @@ def green_load_and_merge(): - 再 LEFT JOIN products.csv ON product_id 提示:pd.merge(how='left') """ - # TODO: 你的程式碼 - pass + DATA = 'datasets/ecommerce' + orders = pd.read_csv(f'{DATA}/orders_clean.csv', parse_dates=['order_date']) + customers = pd.read_csv(f'{DATA}/customers.csv') + products = pd.read_csv(f'{DATA}/products.csv') + df = pd.merge(orders, customers, on='customer_id', how='left') + df = pd.merge(df, products, on='product_id', how='left') + + return df def green_row_count(df): """回傳 DataFrame 的列數 (int)""" - # TODO: 你的程式碼 - pass + # PS:df.shape[1]欄位數 + return (df.shape[0]) def green_column_list(df): """回傳 DataFrame 的所有欄位名稱 (list)""" - # TODO: 你的程式碼 - pass + return list(df.columns) # ============================================================ @@ -49,8 +54,12 @@ def yellow_top_category(df): 回傳該類別名稱 (str) 提示:groupby('category')['amount'].sum() """ - # TODO: 你的程式碼 - pass + category_rev = ( + df.groupby('category')['amount'] + .sum() + .sort_values(ascending=False) + ) + return category_rev.idxmax() def yellow_gold_vip_stats(df): @@ -59,8 +68,10 @@ def yellow_gold_vip_stats(df): 回傳 tuple: (訂單數 int, 總金額 float) 提示:df[df['vip_level'] == 'Gold'] """ - # TODO: 你的程式碼 - pass + gold = df[df['vip_level'] == 'Gold'] + gold_stat = gold['amount'].agg(['count', 'sum']) + + return (int(gold_stat['count']), float(gold_stat['sum'])) def yellow_region_avg_amount(df): @@ -69,8 +80,14 @@ def yellow_region_avg_amount(df): 回傳 Series(index=region, values=平均金額) 提示:groupby('region')['amount'].mean() """ - # TODO: 你的程式碼 - pass + region_mean = ( + df.groupby('region')['amount'] + .mean() + .round(2) + .sort_values(ascending=False) + ) + + return region_mean # ============================================================ @@ -93,5 +110,36 @@ def red_rfm_top5(df): 提示:groupby('customer_id').agg(...) """ - # TODO: 你的程式碼 - pass + + rfm = ( + df.groupby('customer_id') + .agg( + R=('order_date', 'max'), + F=('order_id', 'count'), + M=('amount', 'sum'), + ) + .reset_index() + ) + + rfm = ( + df.groupby('customer_id') + .agg( + R=('order_date', 'max'), + F=('order_id', 'count'), + M=('amount', 'sum'), + ) + .reset_index() + ) + + names = df[['customer_id', 'customer_name']].drop_duplicates() + + rfm = pd.merge(rfm, names, on='customer_id', how='left') + + top5 = ( + rfm[['customer_id', 'customer_name', 'R', 'F', 'M']] + .sort_values('M', ascending=False) + .head(5) + .reset_index(drop=True) + ) + + return top5 \ No newline at end of file diff --git a/homework/m4_timeseries.py b/homework/m4_timeseries.py index 047af6a..8f4d8b6 100644 --- a/homework/m4_timeseries.py +++ b/homework/m4_timeseries.py @@ -13,6 +13,11 @@ def _load_data(): """輔助函式:讀取並解析日期""" df = pd.read_csv("datasets/ecommerce/orders_enriched.csv", parse_dates=["order_date"]) + df['year'] = df['order_date'].dt.year + df['month'] = df['order_date'].dt.month + df['weekday'] = df['order_date'].dt.day_name() + df['year_mon'] = df['order_date'].dt.to_period('M') + return df @@ -26,8 +31,11 @@ def green_avg_by_month(): 回傳 Series(index=月份 1~12, values=平均金額) 提示:df['order_date'].dt.month """ - # TODO: 你的程式碼 - pass + + df = _load_data() + avg_by_month = df.groupby('month')['amount'].mean().round(1) + + return avg_by_month def green_top3_dates(): @@ -36,8 +44,10 @@ def green_top3_dates(): 回傳 Series(index=日期, values=訂單數, 由多到少排序) 提示:value_counts().head(3) """ - # TODO: 你的程式碼 - pass + df = _load_data() + top3_days = df['order_date'].dt.date.value_counts().head(3) + + return top3_days def green_date_range(): @@ -45,8 +55,8 @@ def green_date_range(): 回傳資料的日期範圍 tuple: (最早日期, 最晚日期) 格式為 pandas Timestamp """ - # TODO: 你的程式碼 - pass + df = _load_data() + return (df['order_date'].min(), df['order_date'].max()) # ============================================================ @@ -59,8 +69,14 @@ def yellow_monthly_revenue(): 回傳 Series(index=月底日期 period, values=總營收) 提示:set_index('order_date').resample('ME')['amount'].sum() """ - # TODO: 你的程式碼 - pass + df = _load_data() + monthly_revenue = ( + df.set_index('order_date') + .resample('ME')['amount'] + .sum() + ) + + return monthly_revenue def yellow_rolling_avg(monthly_revenue): @@ -70,8 +86,8 @@ def yellow_rolling_avg(monthly_revenue): 回傳 Series(同樣 index,values=移動平均,前 2 筆可為 NaN) 提示:.rolling(window=3).mean() """ - # TODO: 你的程式碼 - pass + return monthly_revenue.rolling(window=3).mean().round(1) + def yellow_category_median(df): @@ -80,8 +96,14 @@ def yellow_category_median(df): 回傳 Series(index=category, values=中位數) 提示:groupby + median + sort_values """ - # TODO: 你的程式碼 - pass + median_by_cat = ( + df.groupby('category')['amount'] + .median() + .sort_values(ascending=False) + .round(1) + ) + + return median_by_cat # ============================================================ @@ -100,5 +122,32 @@ def red_monthly_report(): index 為月份 (period 或 datetime) 提示:resample + agg + pct_change """ - # TODO: 你的程式碼 - pass + df = _load_data() + monthly_report = ( + df.groupby('year_mon') + .agg( + order_count=('order_id', 'count'), + revenue=('amount', 'sum'), + active_customers=('customer_id', 'nunique'), + ) + .sort_index() + ) + + monthly_report['avg_order_value'] = ( + monthly_report['revenue'] / monthly_report['order_count'] + ).round(1) + + monthly_report['revenue_growth'] = ( + monthly_report['revenue'].pct_change() * 100 + ).round(2) + + return monthly_report + + + + + + + + + diff --git a/homework/m5_visualization.py b/homework/m5_visualization.py index 7e7335d..d2a6554 100644 --- a/homework/m5_visualization.py +++ b/homework/m5_visualization.py @@ -5,7 +5,9 @@ 資料路徑:datasets/ecommerce/orders_enriched.csv """ + import matplotlib + matplotlib.use("Agg") # 無 GUI 環境也能跑 import matplotlib.pyplot as plt import pandas as pd @@ -14,22 +16,45 @@ def _load_data(): """輔助函式:讀取資料""" - return pd.read_csv("datasets/ecommerce/orders_enriched.csv", - parse_dates=["order_date"]) + + return pd.read_csv( + "datasets/ecommerce/orders_enriched.csv", parse_dates=["order_date"] + ) # ============================================================ # 🟢 送分題(每題 10 分,共 30 分) # ============================================================ + def green_bar_category(): """ 畫出每個商品類別 (category) 的訂單數長條圖 回傳 matplotlib Figure 物件 提示:sns.countplot 或 value_counts().plot.bar() """ - # TODO: 你的程式碼 - pass + df = _load_data() + cat_counts = df["category"].value_counts().reset_index() + cat_counts.columns = ["category", "order_count"] + + plt.figure(figsize=(8, 4)) + sns.barplot( + data=cat_counts, + x="category", + y="order_count", + hue="category", + palette="viridis", + legend=False, + ) + plt.title("Order Count by Category", fontweight="bold") + plt.xlabel("Category") + plt.ylabel("Order Count") + + for i, v in enumerate(cat_counts["order_count"]): + plt.text(i, v, f"{v:,}", ha="center", va="bottom", fontsize=10) + + plt.tight_layout() + return plt.gcf() def green_hist_amount(): @@ -38,8 +63,16 @@ def green_hist_amount(): 回傳 matplotlib Figure 物件 提示:sns.histplot(bins=20) 或 plt.hist() """ - # TODO: 你的程式碼 - pass + + df = _load_data() + plt.figure(figsize=(9, 4)) + sns.histplot(data=df, x="amount", bins=30, kde=True, color="steelblue") + plt.title("Order Amount Distribution", fontweight="bold") + plt.xlabel("Amount (NT$)") + plt.ylabel("Frequency") + plt.tight_layout() + + return plt.gcf() def green_set_labels(): @@ -50,14 +83,29 @@ def green_set_labels(): - Y 軸標籤 (ylabel) 回傳 matplotlib Figure 物件 """ - # TODO: 你的程式碼 - pass + df = _load_data() + cat_counts = df["category"].value_counts().reset_index() + cat_counts.columns = ["category", "order_count"] + plt.figure(figsize=(8, 4)) + sns.barplot( + data=cat_counts, + x="category", + y="order_count", + hue="category", + palette="viridis", + legend=False, + ) + plt.title("Order Count by Category", fontweight="bold") + plt.xlabel("Category") + plt.ylabel("Order Count") + return plt.gcf() # ============================================================ # 🟡 核心題(每題 15 分,共 45 分) # ============================================================ + def yellow_line_region_trend(): """ 畫折線圖:比較 North 和 South 兩個地區的月營收趨勢 @@ -67,9 +115,29 @@ def yellow_line_region_trend(): 回傳 matplotlib Figure 物件 提示:分別 groupby 再 plot,或用 sns.lineplot(hue='region') """ - # TODO: 你的程式碼 - pass + df = _load_data() + df["month"] = df["order_date"].dt.to_period("M").astype(str) + ns_df = df[df["region"].isin(["North", "South"])] + monthly_ns = ns_df.groupby(["month", "region"])["amount"].sum().reset_index() + + plt.figure(figsize=(10, 4)) + sns.lineplot( + data=monthly_ns, + x="month", + y="amount", + hue="region", + marker="o", + linewidth=2, + ) + plt.title("Monthly Revenue: North vs South", fontweight="bold") + plt.xlabel("Month") + plt.ylabel("Revenue (NT$)") + plt.xticks(rotation=45) + plt.legend(title="Region") + plt.tight_layout() + + return plt.gcf() def yellow_box_vip(): """ @@ -77,8 +145,21 @@ def yellow_box_vip(): 回傳 matplotlib Figure 物件 提示:sns.boxplot(x='vip_level', y='amount', data=df) """ - # TODO: 你的程式碼 - pass + df = _load_data() + plt.figure(figsize=(9, 5)) + sns.boxplot( + data=df, + x="vip_level", + y="amount", + hue="vip_level", + palette="Set3", + legend=False, + ) + plt.title("Order Amount Distribution by VIP Level", fontweight="bold") + plt.xlabel("VIP Level") + plt.ylabel("Amount (NT$)") + plt.tight_layout() + return plt.gcf() def yellow_scatter_price_amount(): @@ -87,14 +168,24 @@ def yellow_scatter_price_amount(): 回傳 matplotlib Figure 物件 提示:plt.scatter() 或 sns.scatterplot() """ - # TODO: 你的程式碼 - pass + df = _load_data() + plt.figure(figsize=(8, 5)) + + sns.scatterplot(data=df, x="unit_price", y="amount", alpha=0.5) + + plt.title("Unit Price vs Order Amount") + plt.xlabel("Unit Price") + plt.ylabel("Amount") + + plt.tight_layout() + return plt.gcf() # ============================================================ # 🔴 挑戰題(25 分) # ============================================================ + def red_category_dashboard(category="Electronics"): """ 針對指定類別,畫 2×2 的 subplot dashboard: @@ -106,5 +197,83 @@ def red_category_dashboard(category="Electronics"): 回傳 matplotlib Figure 物件 提示:fig, axes = plt.subplots(2, 2, figsize=(14, 10)) """ - # TODO: 你的程式碼 - pass + df = _load_data() + df["month"] = df["order_date"].dt.to_period("M").astype(str) + elec = df[df["category"] == category].copy() + # 預先準備四張圖需要的資料 + elec_monthly = elec.groupby("month")["amount"].sum().reset_index() + elec_region = ( + elec.groupby("region")["amount"].sum().sort_values(ascending=False).reset_index() + ) + elec_top5 = ( + elec.groupby("product_name")["amount"] + .sum() + .sort_values(ascending=False) + .head(5) + .reset_index() + ) + + # 2x2 儀表板 + fig, axes = plt.subplots(2, 2, figsize=(14, 9)) + fig.suptitle("Electronics Category Dashboard", fontsize=16, fontweight="bold") + + # (0,0) 月度趨勢 + sns.lineplot( + data=elec_monthly, + x="month", + y="amount", + marker="o", + linewidth=2, + color="#1f77b4", + ax=axes[0, 0], + ) + axes[0, 0].set_title("Monthly Revenue Trend") + axes[0, 0].set_xlabel("Month") + axes[0, 0].set_ylabel("Revenue (NT$)") + axes[0, 0].tick_params(axis="x", rotation=45) + + # (0,1) 地區排名 + sns.barplot( + data=elec_region, + x="region", + y="amount", + hue="region", + palette="viridis", + legend=False, + ax=axes[0, 1], + ) + axes[0, 1].set_title("Revenue by Region") + axes[0, 1].set_xlabel("Region") + axes[0, 1].set_ylabel("Revenue (NT$)") + for i, v in enumerate(elec_region["amount"]): + axes[0, 1].text(i, v, f"{v:,.0f}", ha="center", va="bottom", fontsize=9) + + # (1,0) 商品 Top 5 + sns.barplot( + data=elec_top5, + y="product_name", + x="amount", + hue="product_name", + palette="magma", + legend=False, + ax=axes[1, 0], + ) + axes[1, 0].set_title("Top 5 Products") + axes[1, 0].set_xlabel("Revenue (NT$)") + axes[1, 0].set_ylabel("Product") + + # (1,1) 金額分布 + sns.histplot( + data=elec, + x="amount", + bins=25, + kde=True, + color="#d62728", + ax=axes[1, 1], + ) + axes[1, 1].set_title("Amount Distribution") + axes[1, 1].set_xlabel("Amount (NT$)") + axes[1, 1].set_ylabel("Frequency") + + plt.tight_layout() + return plt.gcf() diff --git a/homework/m6_plotly_capstone.py b/homework/m6_plotly_capstone.py index 0e2c32a..4d8ad9c 100644 --- a/homework/m6_plotly_capstone.py +++ b/homework/m6_plotly_capstone.py @@ -25,8 +25,16 @@ def green_plotly_bar(): 回傳 plotly Figure 物件 提示:px.bar() """ - # TODO: 你的程式碼 - pass + df = pd.read_csv("datasets/ecommerce/orders_enriched.csv") + + fig = px.bar( + df.groupby("category", as_index=False)["amount"].sum(), + x="category", + y="amount", + title="Revenue by Category", + ) + + return fig def green_plotly_line(): @@ -36,8 +44,19 @@ def green_plotly_line(): 回傳 plotly Figure 物件 提示:先 groupby 月份算總營收,再 px.line() """ - # TODO: 你的程式碼 - pass + df = pd.read_csv( + "datasets/ecommerce/orders_enriched.csv", parse_dates=["order_date"] + ) + + df["year_mon"] = df["order_date"].dt.to_period("M").astype(str) + + monthly = df.groupby("year_mon", as_index=False)["amount"].sum() + + fig = px.line( + monthly, x="year_mon", y="amount", markers=True, title="Monthly Revenue Trend" + ) + + return fig def green_plotly_pie(): @@ -47,8 +66,14 @@ def green_plotly_pie(): 回傳 plotly Figure 物件 提示:px.pie() """ - # TODO: 你的程式碼 - pass + df = pd.read_csv("datasets/ecommerce/orders_enriched.csv") + + vip = df["vip_level"].value_counts().reset_index() + vip.columns = ["vip_level", "count"] + + fig = px.pie(vip, names="vip_level", values="count", title="VIP Distribution") + + return fig # ============================================================ @@ -62,8 +87,34 @@ def yellow_clean_and_merge(raw_path, customers_path, products_path): 2. 合併 customers.csv 和 products.csv 回傳:合併後的 DataFrame """ - # TODO: 你的程式碼 - pass + df = pd.read_csv(raw_path) + + # clean columns + df.columns = df.columns.str.strip().str.lower() + + # clean amount + df["amount"] = ( + df["amount"] + .astype(str) + .str.replace("$", "", regex=False) + .str.replace(",", "", regex=False) + .astype(float) + ) + + # clean date + df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce") + + # drop bad rows + df = df.dropna(subset=["amount", "order_date"]) + df = df.drop_duplicates() + + customers = pd.read_csv(customers_path) + products = pd.read_csv(products_path) + + df = df.merge(customers, on="customer_id", how="left") + df = df.merge(products, on="product_id", how="left") + + return df def yellow_kpi_summary(df): @@ -76,8 +127,12 @@ def yellow_kpi_summary(df): "avg_order_value": float, # 平均客單價 } """ - # TODO: 你的程式碼 - pass + return { + "total_revenue": float(df["amount"].sum()), + "order_count": int(len(df)), + "active_customers": int(df["customer_id"].nunique()), + "avg_order_value": float(df["amount"].mean()), + } def yellow_plotly_scatter(df): @@ -90,8 +145,16 @@ def yellow_plotly_scatter(df): 回傳 plotly Figure 物件 提示:px.scatter(hover_data=['product_name']) """ - # TODO: 你的程式碼 - pass + fig = px.scatter( + df, + x="unit_price", + y="amount", + color="category", + hover_data=["product_name"], + title="Price vs Amount", + ) + + return fig # ============================================================ @@ -114,5 +177,92 @@ def red_dashboard(): 回傳 plotly Figure 物件 提示:from plotly.subplots import make_subplots """ - # TODO: 你的程式碼 - pass + df = yellow_clean_and_merge( + "datasets/ecommerce/orders_raw.csv", + "datasets/ecommerce/customers.csv", + "datasets/ecommerce/products.csv", + ) + + + df["order_date"] = pd.to_datetime(df["order_date"]) + df["month"] = df["order_date"].dt.to_period("M").astype(str) + + # ===== 聚合資料 ===== + monthly = df.groupby("month", as_index=False)["amount"].sum() + + top_prod = ( + df.groupby("product_name", as_index=False)["amount"] + .sum() + .sort_values("amount", ascending=False) + .head(10) + ) + + region = df.groupby("region", as_index=False)["amount"].sum() + + cat = df.groupby("category", as_index=False)["amount"].sum() + + # ===== subplot ===== + fig = make_subplots( + rows=2, + cols=2, + specs=[ + [{"type": "xy"}, {"type": "xy"}], + [{"type": "xy"}, {"type": "domain"}], + ], + subplot_titles=( + "Monthly Revenue", + "Top Products", + "Region Revenue", + "Category Share", + ), + ) + + fig.add_trace( + go.Scatter( + x=monthly["month"], + y=monthly["amount"], + mode="lines+markers", + name="Monthly Revenue", + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Bar( + x=top_prod["product_name"], + y=top_prod["amount"], + name="Top Products", + ), + row=1, + col=2, + ) + + fig.add_trace( + go.Bar( + x=region["region"], + y=region["amount"], + name="Region Revenue", + ), + row=2, + col=1, + ) + + fig.add_trace( + go.Pie( + labels=cat["category"], + values=cat["amount"], + hole=0.4, + name="Category Share", + ), + row=2, + col=2, + ) + + fig.update_layout( + title="E-commerce Revenue Dashboard", + height=750, + showlegend=False, + ) + + return fig