diff --git a/.gitignore b/.gitignore index 911bb2d..2169d7a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ *.pyc .pytest_cache/ .pytest_results.json +venv \ No newline at end of file diff --git a/homework/m1_numpy.py b/homework/m1_numpy.py index 5ff41ab..c48b408 100644 --- a/homework/m1_numpy.py +++ b/homework/m1_numpy.py @@ -15,23 +15,26 @@ # ============================================================ # 🟢 送分題(每題 10 分,共 30 分) # ============================================================ - + def green_mean(): """建立 [10, 20, 30, 40, 50],回傳所有元素的平均值 (float)""" - # TODO: 你的程式碼 - pass - + arr = np.array([10, 20, 30, 40, 50]).mean() + print(arr) + return arr def green_double(): """建立 [10, 20, 30, 40, 50],回傳所有元素乘以 2 的 ndarray""" - # TODO: 你的程式碼 - pass + arr = np.array([10, 20, 30, 40, 50]) + arr = arr*2 + return arr + def green_filter(): """建立 [10, 20, 30, 40, 50],回傳大於 25 的元素 (ndarray)""" - # TODO: 你的程式碼 - pass + arr = np.array([10, 20, 30, 40, 50]) + arr = arr[arr>25] + return arr # ============================================================ @@ -39,29 +42,32 @@ def green_filter(): # 以下函式會接收從 products.csv 讀出的 prices, stocks 陣列 # ============================================================ +# ---------- 載入 products.csv ---------- +DATA_PATH = "datasets/ecommerce/products.csv" +prices = np.genfromtxt(DATA_PATH, delimiter=",", skip_header=1, usecols=3) +stocks = np.genfromtxt(DATA_PATH, delimiter=",", skip_header=1, usecols=4) + def yellow_expensive_count(prices): """回傳單價 > 1000 的商品數量 (int)""" - # TODO: 你的程式碼 - pass - + rrrr = len(prices[prices>1000]) + return rrrr def yellow_top3_stock_indices(stocks): """ 回傳庫存最多的前 3 個商品的索引位置 (ndarray, 由大到小排) 提示:np.argsort """ - # TODO: 你的程式碼 - pass - + GGGG = np.argsort(stocks)[::-1][:3] + return GGGG def yellow_restock_cost(prices, stocks): """ 單價 < 500 的商品,每種各進貨 50 個,回傳總花費 (float/int) 提示:布林遮罩 + .sum() """ - # TODO: 你的程式碼 - pass - + rrrr = (prices[prices<500]*50).sum() + + return rrrr # ============================================================ # 🔴 挑戰題(25 分) @@ -76,5 +82,6 @@ def red_double11_prices(prices, stocks): 回傳每個商品的雙 11 售價 (ndarray) 提示:np.where 可以巢狀使用 """ - # TODO: 你的程式碼 - pass + new_prices = np.where(stocks>=100 ,prices * 0.7,np.where((stocks>=20)&(stocks<100),prices*0.9,prices)) + print(new_prices) + return new_prices diff --git a/homework/m2_pandas_cleaning.py b/homework/m2_pandas_cleaning.py index fa36bff..b0c8462 100644 --- a/homework/m2_pandas_cleaning.py +++ b/homework/m2_pandas_cleaning.py @@ -9,7 +9,6 @@ """ import pandas as pd - # ============================================================ # 🟢 送分題(每題 10 分,共 30 分) # ============================================================ @@ -17,10 +16,10 @@ def green_read_csv(): """ 讀取 orders_raw.csv,回傳原始 DataFrame(不做任何清理) - 提示:pd.read_csv() + 提示:pd.read_csv(s) """ - # TODO: 你的程式碼 - pass + df = pd.read_csv('datasets/ecommerce/orders_raw.csv') + return df 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,9 +35,7 @@ def green_dtypes(df): 回傳 DataFrame 的欄位型別 (Series) 提示:df.dtypes """ - # TODO: 你的程式碼 - pass - + return df.dtypes # ============================================================ # 🟡 核心題(每題 15 分,共 45 分) @@ -51,9 +47,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,17 +58,19 @@ def yellow_clean_amount(df): 回傳清理後的 DataFrame(不要修改原始 df) 提示:.str.replace() + .astype(float) """ - # TODO: 你的程式碼 - pass - + new_df = df.copy() + new_df['amount'] = new_df['amount'].str.replace('$','',regex=False).str.replace(',','',regex=False).astype(float) + return new_df def yellow_drop_duplicates(df): """ 移除完全重複的列,回傳去重後的 DataFrame 提示:df.drop_duplicates() """ - # TODO: 你的程式碼 - pass + new_df = df.copy() + new_df = new_df.drop_duplicates(subset=None, keep='first', inplace=False) + return new_df + # ============================================================ @@ -92,5 +90,11 @@ 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() + df['amount'] = df['amount'].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(subset=None, keep='first', inplace=False) + return df + diff --git a/homework/m3_pandas_advanced.py b/homework/m3_pandas_advanced.py index 68410e8..f1e7246 100644 --- a/homework/m3_pandas_advanced.py +++ b/homework/m3_pandas_advanced.py @@ -23,34 +23,56 @@ def green_load_and_merge(): - 再 LEFT JOIN products.csv ON product_id 提示:pd.merge(how='left') """ - # TODO: 你的程式碼 - pass + + path = "datasets/ecommerce/" + + df_orders = pd.read_csv(path + "orders_clean.csv") + df_customers = pd.read_csv(path + "customers.csv") + df_products = pd.read_csv(path + "products.csv") + m1 = pd.merge(df_orders,df_customers,on='customer_id',how = 'left') + m2 = pd.merge(m1,df_products,on='product_id',how = 'left') + + return m2 + + + def green_row_count(df): """回傳 DataFrame 的列數 (int)""" - # TODO: 你的程式碼 - pass - + return len(df) def green_column_list(df): """回傳 DataFrame 的所有欄位名稱 (list)""" - # TODO: 你的程式碼 - pass - + return df.columns.tolist() # ============================================================ # 🟡 核心題(每題 15 分,共 45 分) # ============================================================ +# def yellow_top_category(df): +# """ +# 哪個商品類別 (category) 的總營收最高? +# 回傳該類別名稱 (str) +# 提示:groupby('category')['amount'].sum() +# """ +# # return df.groupby('category')['amount'].sum().idxmax() +# df = df.groupby('category')['amount'].sum().idxmax() +# print(df) +# return df +# print(green_load_and_merge()) + + def yellow_top_category(df): """ 哪個商品類別 (category) 的總營收最高? - 回傳該類別名稱 (str) - 提示:groupby('category')['amount'].sum() """ - # TODO: 你的程式碼 - pass + + category_totals = df.groupby('category')['amount'].sum().idxmax() + + return category_totals + + def yellow_gold_vip_stats(df): @@ -59,8 +81,11 @@ def yellow_gold_vip_stats(df): 回傳 tuple: (訂單數 int, 總金額 float) 提示:df[df['vip_level'] == 'Gold'] """ - # TODO: 你的程式碼 - pass + + gold_df = df[df['vip_level'] == 'Gold'] + order_count = len(gold_df) + total_amount = gold_df['amount'].sum() + return (int(order_count), float(total_amount)) def yellow_region_avg_amount(df): @@ -69,8 +94,8 @@ def yellow_region_avg_amount(df): 回傳 Series(index=region, values=平均金額) 提示:groupby('region')['amount'].mean() """ - # TODO: 你的程式碼 - pass + return df.groupby('region')['amount'].mean() + # ============================================================ @@ -93,5 +118,15 @@ def red_rfm_top5(df): 提示:groupby('customer_id').agg(...) """ - # TODO: 你的程式碼 - pass + + rrr = df.groupby(['customer_id','customer_name']).agg( + {'order_date':'max','order_id':'count','amount':'sum'} + ).reset_index() + rrr.columns = ['customer_id','customer_name','R','F','M'] + result = rrr.sort_values(by='M',ascending=False).head(5) + print(result) + return result + + + + diff --git a/homework/m4_timeseries.py b/homework/m4_timeseries.py index 047af6a..f63682b 100644 --- a/homework/m4_timeseries.py +++ b/homework/m4_timeseries.py @@ -26,9 +26,12 @@ def green_avg_by_month(): 回傳 Series(index=月份 1~12, values=平均金額) 提示:df['order_date'].dt.month """ - # TODO: 你的程式碼 - pass + df = _load_data() + df['order_date'] = pd.to_datetime(df['order_date']) + df = df.groupby(df['order_date'].dt.month)['amount'].mean() + print(df) + return df def green_top3_dates(): """ @@ -36,18 +39,23 @@ def green_top3_dates(): 回傳 Series(index=日期, values=訂單數, 由多到少排序) 提示:value_counts().head(3) """ - # TODO: 你的程式碼 - pass - + df = _load_data() + df['order_date'] = pd.to_datetime(df['order_date']) + print(df) + return df['order_date'].value_counts().head(3) def green_date_range(): """ 回傳資料的日期範圍 tuple: (最早日期, 最晚日期) 格式為 pandas Timestamp """ - # TODO: 你的程式碼 - pass + df = _load_data() + df['order_date'] = pd.to_datetime(df['order_date']) + min_date = df['order_date'].min() + max_date = df['order_date'].max() + return (min_date,max_date) +green_date_range() # ============================================================ # 🟡 核心題(每題 15 分,共 45 分) @@ -59,9 +67,11 @@ def yellow_monthly_revenue(): 回傳 Series(index=月底日期 period, values=總營收) 提示:set_index('order_date').resample('ME')['amount'].sum() """ - # TODO: 你的程式碼 - pass - + df = _load_data() + df['order_date'] = pd.to_datetime(df['order_date']) + df = df.set_index(['order_date']).resample('ME')['amount'].sum() + return df + def yellow_rolling_avg(monthly_revenue): """ @@ -70,8 +80,9 @@ 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 +91,13 @@ def yellow_category_median(df): 回傳 Series(index=category, values=中位數) 提示:groupby + median + sort_values """ - # TODO: 你的程式碼 - pass + df = _load_data() + df = df.groupby('category')['amount'].median().sort_values(ascending=False) + print(df.head()) + return df + + + # ============================================================ @@ -100,5 +116,17 @@ def red_monthly_report(): index 為月份 (period 或 datetime) 提示:resample + agg + pct_change """ - # TODO: 你的程式碼 - pass + df = _load_data() + df['order_date'] = pd.to_datetime(df['order_date']) + + monthly_report = df.set_index('order_date').resample('ME').agg( + order_count=('order_id', 'count'), + revenue=('amount', 'sum'), + active_customers=('customer_id', 'nunique') + ) + + monthly_report['avg_order_value'] = monthly_report['revenue'] / monthly_report['order_count'] + monthly_report['revenue_growth'] = monthly_report['revenue'].pct_change() * 100 + + return monthly_report + \ No newline at end of file diff --git a/homework/m5_visualization.py b/homework/m5_visualization.py index 7e7335d..7bfbef1 100644 --- a/homework/m5_visualization.py +++ b/homework/m5_visualization.py @@ -28,9 +28,10 @@ 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) + return fig def green_hist_amount(): """ @@ -38,8 +39,10 @@ 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) + return fig def green_set_labels(): @@ -50,8 +53,13 @@ def green_set_labels(): - Y 軸標籤 (ylabel) 回傳 matplotlib Figure 物件 """ - # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots() + df['category'].value_counts().plot.bar(ax=ax) + ax.set_title("Order Count by Category") + ax.set_xlabel("Category") + ax.set_ylabel("Count") + return fig # ============================================================ @@ -67,8 +75,19 @@ def yellow_line_region_trend(): 回傳 matplotlib Figure 物件 提示:分別 groupby 再 plot,或用 sns.lineplot(hue='region') """ - # TODO: 你的程式碼 - pass + df = _load_data() + # 建立月份欄位 (如: 2025-01) + df['month'] = df['order_date'].dt.to_period('M').astype(str) + + # 過濾地區並計算月營收 + df_filtered = df[df['region'].isin(['North', 'South'])] + monthly_revenue = df_filtered.groupby(['month', 'region'])['amount'].sum().reset_index() + + fig, ax = plt.subplots(figsize=(10, 6)) + sns.lineplot(x='month', y='amount', hue='region', data=monthly_revenue, ax=ax, marker='o') + plt.xticks(rotation=45) + ax.legend(title='Region') + return fig def yellow_box_vip(): @@ -77,8 +96,10 @@ 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) + return fig def yellow_scatter_price_amount(): @@ -87,8 +108,10 @@ 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) + return fig # ============================================================ @@ -106,5 +129,29 @@ def red_category_dashboard(category="Electronics"): 回傳 matplotlib Figure 物件 提示:fig, axes = plt.subplots(2, 2, figsize=(14, 10)) """ - # TODO: 你的程式碼 - pass + df = _load_data() + cat_df = df[df['category'] == category].copy() + cat_df['month'] = cat_df['order_date'].dt.to_period('M').astype(str) + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + fig.suptitle(f"Dashboard for {category}", fontsize=16) + + # 1. 左上:該類別月營收趨勢 (折線圖) + cat_df.groupby('month')['amount'].sum().plot(ax=axes[0, 0], marker='s') + axes[0, 0].set_title("Monthly Revenue Trend") + + # 2. 右上:該類別各地區營收 (長條圖) + cat_df.groupby('region')['amount'].sum().plot(kind='bar', ax=axes[0, 1]) + axes[0, 1].set_title("Revenue by Region") + + # 3. 左下:該類別 Top 5 商品營收 (水平長條圖) + top5 = cat_df.groupby('product_name')['amount'].sum().sort_values(ascending=False).head(5) + top5.sort_values().plot(kind='barh', ax=axes[1, 0]) + axes[1, 0].set_title("Top 5 Products by Revenue") + + # 4. 右下:該類別訂單金額分佈 (直方圖) + sns.histplot(cat_df['amount'], bins=15, ax=axes[1, 1], kde=True) + axes[1, 1].set_title("Order Amount Distribution") + + plt.tight_layout(rect=[0, 0.03, 1, 0.95]) + return fig \ No newline at end of file diff --git a/homework/m6_plotly_capstone.py b/homework/m6_plotly_capstone.py index 0e2c32a..40d4e82 100644 --- a/homework/m6_plotly_capstone.py +++ b/homework/m6_plotly_capstone.py @@ -25,8 +25,10 @@ def green_plotly_bar(): 回傳 plotly Figure 物件 提示:px.bar() """ - # TODO: 你的程式碼 - pass + df = pd.read_csv("datasets/ecommerce/orders_enriched.csv") + revenue_by_cat = df.groupby('category')['amount'].sum().reset_index() + fig = px.bar(revenue_by_cat, x='category', y='amount', title="Category Revenue") + return fig def green_plotly_line(): @@ -36,8 +38,11 @@ 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').astype(str) + revenue_by_month = df.groupby('month')['amount'].sum().reset_index() + fig = px.line(revenue_by_month, x='month', y='amount', title="Monthly Revenue Trend") + return fig def green_plotly_pie(): @@ -47,8 +52,9 @@ def green_plotly_pie(): 回傳 plotly Figure 物件 提示:px.pie() """ - # TODO: 你的程式碼 - pass + df = pd.read_csv("datasets/ecommerce/orders_enriched.csv") + fig = px.pie(df, names='vip_level', title="Orders by VIP Level") + return fig # ============================================================ @@ -62,8 +68,24 @@ def yellow_clean_and_merge(raw_path, customers_path, products_path): 2. 合併 customers.csv 和 products.csv 回傳:合併後的 DataFrame """ - # TODO: 你的程式碼 - pass + + orders = pd.read_csv(raw_path) + + orders.columns = orders.columns.str.strip().str.lower().str.replace(' ', '_') + + if 'amount' in orders.columns and orders['amount'].dtype == 'object': + orders['amount'] = orders['amount'].str.replace('$', '', regex=False).str.replace(',', '', regex=False).astype(float) + + orders = orders.drop_duplicates().dropna() + orders['order_date'] = pd.to_datetime(orders['order_date']) + + customers = pd.read_csv(customers_path) + products = pd.read_csv(products_path) + + df = orders.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 +98,16 @@ def yellow_kpi_summary(df): "avg_order_value": float, # 平均客單價 } """ - # TODO: 你的程式碼 - pass + total_rev = float(df['amount'].sum()) + order_cnt = int(len(df)) + + kpi_dict = { + "total_revenue": total_rev, + "order_count": order_cnt, + "active_customers": int(df['customer_id'].nunique()), + "avg_order_value": total_rev / order_cnt if order_cnt > 0 else 0.0 + } + return kpi_dict def yellow_plotly_scatter(df): @@ -90,8 +120,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 Amount" + ) + return fig # ============================================================ @@ -114,5 +151,40 @@ def red_dashboard(): 回傳 plotly Figure 物件 提示:from plotly.subplots import make_subplots """ - # TODO: 你的程式碼 - pass + # 1. 執行 ETL 取得乾淨合併的資料 + df = yellow_clean_and_merge( + "datasets/ecommerce/orders_raw.csv", + "datasets/ecommerce/customers.csv", + "datasets/ecommerce/products.csv" + ) + df['month'] = df['order_date'].dt.to_period('M').astype(str) + + # 2. 建立 2x2 子圖架構 + fig = make_subplots( + rows=2, cols=2, + subplot_titles=("Monthly Revenue Trend", "Top 10 Products by Revenue", + "Revenue by Region", "Revenue Share by Category"), + specs=[[{"type": "scatter"}, {"type": "bar"}], + [{"type": "bar"}, {"type": "pie"}]] + ) + + # 左上:月營收趨勢 (line) + m_rev = df.groupby('month')['amount'].sum().reset_index() + fig.add_trace(go.Scatter(x=m_rev['month'], y=m_rev['amount'], mode='lines+markers', name="Trend"), row=1, col=1) + + # 右上:Top 10 商品營收 (bar) + top10 = df.groupby('product_name')['amount'].sum().sort_values(ascending=False).head(10).reset_index() + fig.add_trace(go.Bar(x=top10['product_name'], y=top10['amount'], name="Top 10"), row=1, col=2) + + # 左下:各地區營收 (bar) + reg_rev = df.groupby('region')['amount'].sum().reset_index() + fig.add_trace(go.Bar(x=reg_rev['region'], y=reg_rev['amount'], name="Region"), row=2, col=1) + + # 右下:類別營收佔比 (pie/donut) + cat_rev = df.groupby('category')['amount'].sum().reset_index() + fig.add_trace(go.Pie(labels=cat_rev['category'], values=cat_rev['amount'], name="Category"), row=2, col=2) + + # 3. 設定整體標題與排版 + fig.update_layout(title_text="E-commerce Interactive Dashboard", height=800, showlegend=False) + + return fig \ No newline at end of file