diff --git a/homework/m1_numpy.py b/homework/m1_numpy.py index 5ff41ab..79c966b 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 int((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..dc1864c 100644 --- a/homework/m2_pandas_cleaning.py +++ b/homework/m2_pandas_cleaning.py @@ -19,8 +19,7 @@ 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): @@ -28,8 +27,7 @@ def green_shape(df): 回傳 DataFrame 的 (列數, 欄數) tuple 提示:df.shape """ - # TODO: 你的程式碼 - pass + 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,9 @@ def yellow_clean_columns(df): 回傳清理後的 DataFrame(不要修改原始 df) 提示:df.columns.str.strip().str.lower() """ - # TODO: 你的程式碼 - pass + new_df = df.copy() + new_df.columns = new_df.columns.str.strip().str.lower() + return new_df def yellow_clean_amount(df): @@ -62,8 +60,15 @@ def yellow_clean_amount(df): 回傳清理後的 DataFrame(不要修改原始 df) 提示:.str.replace() + .astype(float) """ - # TODO: 你的程式碼 - pass + new_df = df.copy() + new_df["amount"] = ( + new_df["amount"] + .astype(str) + .str.replace("$", "", regex=False) + .str.replace(",", "", regex=False) + .astype(float) + ) + return new_df def yellow_drop_duplicates(df): @@ -71,8 +76,7 @@ def yellow_drop_duplicates(df): 移除完全重複的列,回傳去重後的 DataFrame 提示:df.drop_duplicates() """ - # TODO: 你的程式碼 - pass + return df.drop_duplicates().copy() # ============================================================ @@ -92,5 +96,27 @@ def red_clean_orders(path): 回傳:清理後的 DataFrame 提示:pd.to_datetime(errors='coerce') """ - # TODO: 你的程式碼 - pass + df = pd.read_csv(path) + + # 欄位名稱清理 + df.columns = df.columns.str.strip().str.lower() + + # amount 清理 + df["amount"] = ( + df["amount"] + .astype(str) + .str.replace("$", "", regex=False) + .str.replace(",", "", regex=False) + .astype(float) + ) + + # 日期轉換 + df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce") + + # 刪除關鍵欄位缺值 + df = df.dropna(subset=["amount", "order_date"]) + + # 去重 + df = df.drop_duplicates() + + return df.copy() \ No newline at end of file diff --git a/homework/m3_pandas_advanced.py b/homework/m3_pandas_advanced.py index 68410e8..046e8ea 100644 --- a/homework/m3_pandas_advanced.py +++ b/homework/m3_pandas_advanced.py @@ -23,20 +23,23 @@ def green_load_and_merge(): - 再 LEFT JOIN products.csv ON product_id 提示:pd.merge(how='left') """ - # TODO: 你的程式碼 - pass + orders = pd.read_csv("datasets/ecommerce/orders_clean.csv") + customers = pd.read_csv("datasets/ecommerce/customers.csv") + products = pd.read_csv("datasets/ecommerce/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 + return int(len(df)) def green_column_list(df): """回傳 DataFrame 的所有欄位名稱 (list)""" - # TODO: 你的程式碼 - pass + return list(df.columns) # ============================================================ @@ -49,8 +52,8 @@ def yellow_top_category(df): 回傳該類別名稱 (str) 提示:groupby('category')['amount'].sum() """ - # TODO: 你的程式碼 - pass + revenue_by_cat = df.groupby("category")["amount"].sum() + return revenue_by_cat.idxmax() def yellow_gold_vip_stats(df): @@ -59,8 +62,10 @@ def yellow_gold_vip_stats(df): 回傳 tuple: (訂單數 int, 總金額 float) 提示:df[df['vip_level'] == 'Gold'] """ - # TODO: 你的程式碼 - pass + sub = df[df["vip_level"] == "Gold"] + order_count = int(len(sub)) + total_amount = float(sub["amount"].sum()) + return order_count, total_amount def yellow_region_avg_amount(df): @@ -69,8 +74,7 @@ def yellow_region_avg_amount(df): 回傳 Series(index=region, values=平均金額) 提示:groupby('region')['amount'].mean() """ - # TODO: 你的程式碼 - pass + return df.groupby("region")["amount"].mean() # ============================================================ @@ -93,5 +97,30 @@ def red_rfm_top5(df): 提示:groupby('customer_id').agg(...) """ - # TODO: 你的程式碼 - pass + data = df.copy() + + # 確保日期型別可比較 + data["order_date"] = pd.to_datetime(data["order_date"], errors="coerce") + + agg = ( + data.groupby("customer_id") + .agg( + R=("order_date", "max"), + F=("order_date", "count"), + M=("amount", "sum"), + ) + .reset_index() + ) + + # 取每位客戶的名稱(假設同一 customer_id 對應唯一名稱) + names = ( + data[["customer_id", "customer_name"]] + .drop_duplicates(subset=["customer_id"]) + ) + + result = pd.merge(agg, names, on="customer_id", how="left") + + result = result[["customer_id", "customer_name", "R", "F", "M"]] + result = result.sort_values("M", ascending=False).head(5) + + return result.reset_index(drop=True) \ No newline at end of file diff --git a/homework/m4_timeseries.py b/homework/m4_timeseries.py index 047af6a..2e87405 100644 --- a/homework/m4_timeseries.py +++ b/homework/m4_timeseries.py @@ -26,8 +26,8 @@ def green_avg_by_month(): 回傳 Series(index=月份 1~12, values=平均金額) 提示:df['order_date'].dt.month """ - # TODO: 你的程式碼 - pass + df = _load_data() + return df.groupby(df["order_date"].dt.month)["amount"].mean() def green_top3_dates(): @@ -36,8 +36,8 @@ def green_top3_dates(): 回傳 Series(index=日期, values=訂單數, 由多到少排序) 提示:value_counts().head(3) """ - # TODO: 你的程式碼 - pass + df = _load_data() + return df["order_date"].dt.date.value_counts().head(3) def green_date_range(): @@ -45,8 +45,8 @@ def green_date_range(): 回傳資料的日期範圍 tuple: (最早日期, 最晚日期) 格式為 pandas Timestamp """ - # TODO: 你的程式碼 - pass + df = _load_data() + return df["order_date"].min(), df["order_date"].max() # ============================================================ @@ -59,8 +59,8 @@ def yellow_monthly_revenue(): 回傳 Series(index=月底日期 period, values=總營收) 提示:set_index('order_date').resample('ME')['amount'].sum() """ - # TODO: 你的程式碼 - pass + df = _load_data() + return df.set_index("order_date").resample("ME")["amount"].sum() def yellow_rolling_avg(monthly_revenue): @@ -70,8 +70,7 @@ def yellow_rolling_avg(monthly_revenue): 回傳 Series(同樣 index,values=移動平均,前 2 筆可為 NaN) 提示:.rolling(window=3).mean() """ - # TODO: 你的程式碼 - pass + return monthly_revenue.rolling(window=3).mean() def yellow_category_median(df): @@ -80,8 +79,7 @@ def yellow_category_median(df): 回傳 Series(index=category, values=中位數) 提示:groupby + median + sort_values """ - # TODO: 你的程式碼 - pass + return df.groupby("category")["amount"].median().sort_values(ascending=False) # ============================================================ @@ -100,5 +98,19 @@ def red_monthly_report(): index 為月份 (period 或 datetime) 提示:resample + agg + pct_change """ - # TODO: 你的程式碼 - pass + df = _load_data() + + monthly = ( + df.set_index("order_date") + .resample("ME") + .agg( + order_count=("amount", "count"), + revenue=("amount", "sum"), + active_customers=("customer_id", "nunique"), + ) + ) + + monthly["avg_order_value"] = monthly["revenue"] / monthly["order_count"] + monthly["revenue_growth"] = monthly["revenue"].pct_change() + + return monthly \ No newline at end of file diff --git a/homework/m5_visualization.py b/homework/m5_visualization.py index 7e7335d..7e995a3 100644 --- a/homework/m5_visualization.py +++ b/homework/m5_visualization.py @@ -28,8 +28,14 @@ def green_bar_category(): 回傳 matplotlib Figure 物件 提示:sns.countplot 或 value_counts().plot.bar() """ - # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots() + sns.countplot(x="category", data=df, ax=ax) + ax.set_title("Order Count by Category") + ax.set_xlabel("Category") + ax.set_ylabel("Count") + fig.tight_layout() + return fig def green_hist_amount(): @@ -38,8 +44,14 @@ def green_hist_amount(): 回傳 matplotlib Figure 物件 提示:sns.histplot(bins=20) 或 plt.hist() """ - # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots() + sns.histplot(df["amount"], bins=20, ax=ax) + ax.set_title("Distribution of Order Amount") + ax.set_xlabel("Amount") + ax.set_ylabel("Frequency") + fig.tight_layout() + return fig def green_set_labels(): @@ -50,8 +62,13 @@ def green_set_labels(): - Y 軸標籤 (ylabel) 回傳 matplotlib Figure 物件 """ - # TODO: 你的程式碼 - pass + fig, ax = plt.subplots() + ax.bar(["A", "B", "C"], [10, 20, 15]) + ax.set_title("Simple Bar Chart") + ax.set_xlabel("Category") + ax.set_ylabel("Value") + fig.tight_layout() + return fig # ============================================================ @@ -67,8 +84,25 @@ def yellow_line_region_trend(): 回傳 matplotlib Figure 物件 提示:分別 groupby 再 plot,或用 sns.lineplot(hue='region') """ - # TODO: 你的程式碼 - pass + df = _load_data() + + sub = df[df["region"].isin(["North", "South"])].copy() + sub["month"] = sub["order_date"].dt.to_period("M").dt.to_timestamp() + + monthly = ( + sub.groupby(["month", "region"])["amount"] + .sum() + .reset_index() + ) + + fig, ax = plt.subplots() + sns.lineplot(data=monthly, x="month", y="amount", hue="region", ax=ax) + ax.set_title("Monthly Revenue Trend (North vs South)") + ax.set_xlabel("Month") + ax.set_ylabel("Revenue") + ax.legend(title="Region") + fig.tight_layout() + return fig def yellow_box_vip(): @@ -77,8 +111,14 @@ def yellow_box_vip(): 回傳 matplotlib Figure 物件 提示:sns.boxplot(x='vip_level', y='amount', data=df) """ - # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots() + sns.boxplot(x="vip_level", y="amount", data=df, ax=ax) + ax.set_title("Amount Distribution by VIP Level") + ax.set_xlabel("VIP Level") + ax.set_ylabel("Amount") + fig.tight_layout() + return fig def yellow_scatter_price_amount(): @@ -87,8 +127,14 @@ def yellow_scatter_price_amount(): 回傳 matplotlib Figure 物件 提示:plt.scatter() 或 sns.scatterplot() """ - # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots() + sns.scatterplot(x="unit_price", y="amount", data=df, ax=ax) + ax.set_title("Unit Price vs Order Amount") + ax.set_xlabel("Unit Price") + ax.set_ylabel("Amount") + fig.tight_layout() + return fig # ============================================================ @@ -106,5 +152,36 @@ def red_category_dashboard(category="Electronics"): 回傳 matplotlib Figure 物件 提示:fig, axes = plt.subplots(2, 2, figsize=(14, 10)) """ - # TODO: 你的程式碼 - pass + df = _load_data() + sub = df[df["category"] == category].copy() + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + + # 1. 月營收趨勢 + sub["month"] = sub["order_date"].dt.to_period("M").dt.to_timestamp() + monthly = sub.groupby("month")["amount"].sum().reset_index() + sns.lineplot(data=monthly, x="month", y="amount", ax=axes[0, 0]) + axes[0, 0].set_title(f"{category} Monthly Revenue") + + # 2. 各地區營收 + region_rev = sub.groupby("region")["amount"].sum().reset_index() + sns.barplot(data=region_rev, x="region", y="amount", ax=axes[0, 1]) + axes[0, 1].set_title(f"{category} Revenue by Region") + + # 3. Top 5 商品 + top_products = ( + sub.groupby("product_name")["amount"] + .sum() + .sort_values(ascending=False) + .head(5) + .reset_index() + ) + sns.barplot(data=top_products, y="product_name", x="amount", ax=axes[1, 0]) + axes[1, 0].set_title(f"Top 5 Products in {category}") + + # 4. 金額分佈 + sns.histplot(sub["amount"], bins=20, ax=axes[1, 1]) + axes[1, 1].set_title(f"{category} Amount Distribution") + + fig.tight_layout() + return fig \ No newline at end of file diff --git a/homework/m6_plotly_capstone.py b/homework/m6_plotly_capstone.py index 0e2c32a..3d48e6a 100644 --- a/homework/m6_plotly_capstone.py +++ b/homework/m6_plotly_capstone.py @@ -25,8 +25,11 @@ def green_plotly_bar(): 回傳 plotly Figure 物件 提示:px.bar() """ - # TODO: 你的程式碼 - pass + df = pd.read_csv("datasets/ecommerce/orders_enriched.csv", + parse_dates=["order_date"]) + agg = df.groupby("category")["amount"].sum().reset_index() + fig = px.bar(agg, x="category", y="amount", title="Revenue by Category") + return fig def green_plotly_line(): @@ -36,8 +39,12 @@ 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["month"] = df["order_date"].dt.to_period("M").dt.to_timestamp() + agg = df.groupby("month")["amount"].sum().reset_index() + fig = px.line(agg, x="month", y="amount", title="Monthly Revenue Trend") + return fig def green_plotly_pie(): @@ -47,8 +54,12 @@ def green_plotly_pie(): 回傳 plotly Figure 物件 提示:px.pie() """ - # TODO: 你的程式碼 - pass + df = pd.read_csv("datasets/ecommerce/orders_enriched.csv") + agg = df["vip_level"].value_counts().reset_index() + agg.columns = ["vip_level", "count"] + fig = px.pie(agg, names="vip_level", values="count", + title="Order Share by VIP Level") + return fig # ============================================================ @@ -62,8 +73,33 @@ 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) + + # 欄位名稱清理 + df.columns = df.columns.str.strip().str.lower() + + # amount 清理 + df["amount"] = ( + df["amount"] + .astype(str) + .str.replace("$", "", regex=False) + .str.replace(",", "", regex=False) + .astype(float) + ) + + # 日期轉換 + df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce") + + # 移除缺值與重複 + df = df.dropna(subset=["amount", "order_date"]).drop_duplicates() + + customers = pd.read_csv(customers_path) + products = pd.read_csv(products_path) + + df = pd.merge(df, customers, on="customer_id", how="left") + df = pd.merge(df, products, on="product_id", how="left") + + return df def yellow_kpi_summary(df): @@ -76,8 +112,17 @@ def yellow_kpi_summary(df): "avg_order_value": float, # 平均客單價 } """ - # TODO: 你的程式碼 - pass + total_revenue = float(df["amount"].sum()) + order_count = int(len(df)) + active_customers = int(df["customer_id"].nunique()) + avg_order_value = float(total_revenue / order_count) if order_count > 0 else 0.0 + + return { + "total_revenue": total_revenue, + "order_count": order_count, + "active_customers": active_customers, + "avg_order_value": avg_order_value, + } def yellow_plotly_scatter(df): @@ -90,8 +135,15 @@ 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="Unit Price vs Order Amount" + ) + return fig # ============================================================ @@ -114,5 +166,61 @@ 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" + ) + + fig = make_subplots( + rows=2, cols=2, + subplot_titles=( + "Monthly Revenue", + "Top 10 Products", + "Revenue by Region", + "Revenue Share by Category" + ), + specs=[ + [{"type": "xy"}, {"type": "xy"}], + [{"type": "xy"}, {"type": "domain"}], + ] + ) + + # 月營收 + df["month"] = df["order_date"].dt.to_period("M").dt.to_timestamp() + monthly = df.groupby("month")["amount"].sum().reset_index() + fig.add_trace( + go.Scatter(x=monthly["month"], y=monthly["amount"], mode="lines", name="Revenue"), + row=1, col=1 + ) + + # Top 10 商品 + top_products = ( + df.groupby("product_name")["amount"] + .sum() + .sort_values(ascending=False) + .head(10) + .reset_index() + ) + fig.add_trace( + go.Bar(x=top_products["product_name"], y=top_products["amount"], name="Top Products"), + row=1, col=2 + ) + + # 地區營收 + region = df.groupby("region")["amount"].sum().reset_index() + fig.add_trace( + go.Bar(x=region["region"], y=region["amount"], name="Region Revenue"), + row=2, col=1 + ) + + # 類別佔比 + category = df.groupby("category")["amount"].sum().reset_index() + fig.add_trace( + go.Pie(labels=category["category"], values=category["amount"], hole=0.4), + row=2, col=2 + ) + + fig.update_layout(title="E-commerce Dashboard") + + return fig \ No newline at end of file