From a92cdac1f470f6ec6349930fc0c08dc985a1e5f6 Mon Sep 17 00:00:00 2001 From: Django Date: Sun, 10 May 2026 13:21:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BD=9C=E6=A5=AD-=E9=84=AD?= =?UTF-8?q?=E5=85=B8-AIPE03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homework/m1_numpy.py | 34 ++++++-- homework/m2_pandas_cleaning.py | 50 +++++++++-- homework/m3_pandas_advanced.py | 42 +++++++-- homework/m4_timeseries.py | 54 ++++++++++-- homework/m5_visualization.py | 140 ++++++++++++++++++++++++++++-- homework/m6_plotly_capstone.py | 151 +++++++++++++++++++++++++++++++-- 6 files changed, 428 insertions(+), 43 deletions(-) diff --git a/homework/m1_numpy.py b/homework/m1_numpy.py index 5ff41ab..ff7ee20 100644 --- a/homework/m1_numpy.py +++ b/homework/m1_numpy.py @@ -19,19 +19,22 @@ def green_mean(): """建立 [10, 20, 30, 40, 50],回傳所有元素的平均值 (float)""" # TODO: 你的程式碼 - pass + np_mean = np.array([10, 20, 30, 40, 50]) + return float(np.mean(np_mean)) def green_double(): """建立 [10, 20, 30, 40, 50],回傳所有元素乘以 2 的 ndarray""" # TODO: 你的程式碼 - pass + np_double = np.array([10, 20, 30, 40, 50]) * 2 + return np_double def green_filter(): """建立 [10, 20, 30, 40, 50],回傳大於 25 的元素 (ndarray)""" # TODO: 你的程式碼 - pass + np_filter = np.array([10, 20, 30, 40, 50]) + return np_filter[np_filter > 25] # ============================================================ @@ -42,7 +45,10 @@ def green_filter(): def yellow_expensive_count(prices): """回傳單價 > 1000 的商品數量 (int)""" # TODO: 你的程式碼 - pass + mask_expensive = prices > 1000 + num_expensive = mask_expensive.sum() + return num_expensive + def yellow_top3_stock_indices(stocks): @@ -51,7 +57,9 @@ def yellow_top3_stock_indices(stocks): 提示:np.argsort """ # TODO: 你的程式碼 - pass + sorted_stock = np.argsort(stocks) + top3_stock = sorted_stock[-3:][::-1] + return top3_stock def yellow_restock_cost(prices, stocks): @@ -60,7 +68,10 @@ def yellow_restock_cost(prices, stocks): 提示:布林遮罩 + .sum() """ # TODO: 你的程式碼 - pass + mask_cheap = prices < 500 + cheap_prices = prices[mask_cheap] + total_cost = round((cheap_prices * 50).sum(), 0) + return total_cost # ============================================================ @@ -77,4 +88,13 @@ def red_double11_prices(prices, stocks): 提示:np.where 可以巢狀使用 """ # TODO: 你的程式碼 - pass + final_price = np.where( + stocks >= 100, + prices * 0.7, + np.where( + stocks >= 20, + prices * 0.9, + prices + ) + ) + return final_price diff --git a/homework/m2_pandas_cleaning.py b/homework/m2_pandas_cleaning.py index fa36bff..40d1c1c 100644 --- a/homework/m2_pandas_cleaning.py +++ b/homework/m2_pandas_cleaning.py @@ -20,7 +20,8 @@ def green_read_csv(): 提示:pd.read_csv() """ # TODO: 你的程式碼 - pass + df_raw = pd.read_csv() + return df_raw def green_shape(df): @@ -29,7 +30,9 @@ def green_shape(df): 提示:df.shape """ # TODO: 你的程式碼 - pass + df_shape = df.shape + return df_shape + def green_dtypes(df): @@ -38,7 +41,8 @@ def green_dtypes(df): 提示:df.dtypes """ # TODO: 你的程式碼 - pass + df_dtypes = df.dtypes + return df_dtypes # ============================================================ @@ -52,7 +56,9 @@ def yellow_clean_columns(df): 提示:df.columns.str.strip().str.lower() """ # TODO: 你的程式碼 - pass + df_clean = df.copy() + df_clean.columns = df_clean.columns.str.strip().str.lower() + return df_clean def yellow_clean_amount(df): @@ -63,7 +69,16 @@ def yellow_clean_amount(df): 提示:.str.replace() + .astype(float) """ # TODO: 你的程式碼 - pass + df_clean = df.copy() + df_clean['amount'] = ( + df_clean['amount'] + .astype(str) + .str.replace('$', '', regex = False) + .str.replace(',', '', regex = False) + .astype(float) + ) + return df_clean + def yellow_drop_duplicates(df): @@ -72,7 +87,8 @@ def yellow_drop_duplicates(df): 提示:df.drop_duplicates() """ # TODO: 你的程式碼 - pass + df_duplicates = df.drop_duplicates() + return df_duplicates # ============================================================ @@ -93,4 +109,24 @@ def red_clean_orders(path): 提示:pd.to_datetime(errors='coerce') """ # TODO: 你的程式碼 - pass + df = pd.read_csv(path) + df_before = len(df) + + df_columns = df.columns.str.strip().str.lower() + + 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() + df_after = len(df) + + print(f"清理之前{df_before},清理之後{df_after}") + print('-----------------------------') + return df diff --git a/homework/m3_pandas_advanced.py b/homework/m3_pandas_advanced.py index 68410e8..cce46ae 100644 --- a/homework/m3_pandas_advanced.py +++ b/homework/m3_pandas_advanced.py @@ -24,19 +24,24 @@ def green_load_and_merge(): 提示:pd.merge(how='left') """ # TODO: 你的程式碼 - pass + df = ( + orders + .merge(customers, on='customer_id', how='left') + .merge(products, on='product_id', how='left') + ) + return df def green_row_count(df): """回傳 DataFrame 的列數 (int)""" # TODO: 你的程式碼 - pass + return len(df) def green_column_list(df): """回傳 DataFrame 的所有欄位名稱 (list)""" # TODO: 你的程式碼 - pass + return list(df.columns) # ============================================================ @@ -50,8 +55,8 @@ def yellow_top_category(df): 提示:groupby('category')['amount'].sum() """ # TODO: 你的程式碼 - pass - + df_category = df.groupby('category')['amount'].sum() + return df_category.idxmax() def yellow_gold_vip_stats(df): """ @@ -60,7 +65,10 @@ def yellow_gold_vip_stats(df): 提示:df[df['vip_level'] == 'Gold'] """ # TODO: 你的程式碼 - pass + gold = df[df['vip_level'] == 'Gold'] + gold_count = len(gold) + gold_total = gold['amount'].sum() + return (gold_count, gold_total) def yellow_region_avg_amount(df): @@ -70,7 +78,13 @@ def yellow_region_avg_amount(df): 提示:groupby('region')['amount'].mean() """ # TODO: 你的程式碼 - pass + region_mean = ( + df.groupby('region')['amount'] + .mean() + .round(2) + .sort_values(ascending=False) + ) + return region_mean # ============================================================ @@ -94,4 +108,16 @@ def red_rfm_top5(df): 提示:groupby('customer_id').agg(...) """ # TODO: 你的程式碼 - pass + rfm = ( + df.groupby(['customer_id', 'customer_name']) + .agg( + r = ('order_date', 'max'), + f = ('order_id', 'count'), + m = ('amount', 'sum'), + ) + .reset_index() + .sort_values('m', ascending=False) + .head(5) + ) + + return rfm diff --git a/homework/m4_timeseries.py b/homework/m4_timeseries.py index 047af6a..a64bd72 100644 --- a/homework/m4_timeseries.py +++ b/homework/m4_timeseries.py @@ -27,7 +27,12 @@ def green_avg_by_month(): 提示:df['order_date'].dt.month """ # TODO: 你的程式碼 - pass + avg_month = ( + df.groupby(df['order_date'].dt.month)['amount'] + .mean() + .round(2) + ) + return avg_month def green_top3_dates(): @@ -37,7 +42,9 @@ def green_top3_dates(): 提示:value_counts().head(3) """ # TODO: 你的程式碼 - pass + top3_dates = df['order_date'].dt.date.value_counts().head(3) + return top3_dates + def green_date_range(): @@ -46,7 +53,9 @@ def green_date_range(): 格式為 pandas Timestamp """ # TODO: 你的程式碼 - pass + earliest = df['order_date'].min() + latest = df['order_date'].max() + return (earliest, latest) # ============================================================ @@ -60,7 +69,10 @@ def yellow_monthly_revenue(): 提示:set_index('order_date').resample('ME')['amount'].sum() """ # TODO: 你的程式碼 - pass + monthly_revenue = ( + df.set_index('order_date').resample('ME')['amount'].sum() + ) + return monthly_revenue def yellow_rolling_avg(monthly_revenue): @@ -71,7 +83,8 @@ def yellow_rolling_avg(monthly_revenue): 提示:.rolling(window=3).mean() """ # TODO: 你的程式碼 - pass + rolling_avg = monthly_revenue.rolling(window=3).mean() + return rolling_avg def yellow_category_median(df): @@ -81,7 +94,13 @@ def yellow_category_median(df): 提示:groupby + median + sort_values """ # TODO: 你的程式碼 - pass + category_median = ( + df.groupby('category')['amount'] + .mean() + .round(1) + .sort_values(ascending=False) + ) + return category_median # ============================================================ @@ -101,4 +120,25 @@ def red_monthly_report(): 提示:resample + agg + pct_change """ # TODO: 你的程式碼 - pass + df = _load_data() + df = df.set_index('order_date') + + monthly_report = ( + df.resample('ME') + .agg( + 總訂單數 = ('order_id', 'count'), + 總營收 = ('amount', 'sum'), + 活躍客戶數 = ('customer_id', 'nunique'), + ) + .sort_index() + ) + + monthly_report['客單價'] = ( + monthly_report['總營收']/monthly_report['總訂單數'] + ).round(1) + + monthly_report['月營收成長率'] = ( + monthly_report['總營收'].pct_change() * 100 + ).round(2) + + return monthly_report diff --git a/homework/m5_visualization.py b/homework/m5_visualization.py index 7e7335d..41475e7 100644 --- a/homework/m5_visualization.py +++ b/homework/m5_visualization.py @@ -29,7 +29,21 @@ def green_bar_category(): 提示:sns.countplot 或 value_counts().plot.bar() """ # TODO: 你的程式碼 - pass + cate_count = df['category'].value_counts().reset_index() + cate_count.columns = ['category', 'order_count'] + + fig, ax = plt.subplots(figsize=(8,4)) + sns.barplot( + data = cate_count, + x = 'category', + y = 'order_count', + hue = 'category', + palette = 'viridis', + legend = False, + ax = ax, + ) + plt.tight_layout() + return plt def green_hist_amount(): @@ -39,7 +53,17 @@ def green_hist_amount(): 提示:sns.histplot(bins=20) 或 plt.hist() """ # TODO: 你的程式碼 - pass + fig, ax = plt.subplots(figsize = (8,4)) + sns.histplot( + data = df, + x = 'amount', + bins = 20, + ax = ax, + ) + ax.set_xlabel('Amount(NT$)') + ax.set_ylabel('Frequency') + plt.tight_layout() + return fig def green_set_labels(): @@ -51,7 +75,26 @@ def green_set_labels(): 回傳 matplotlib Figure 物件 """ # TODO: 你的程式碼 - pass + cate_count = df['category'].value_counts().reset_index() + cate_count.columns = ['category', 'order_count'] + + fig, ax = plt.subplots(figsize=(8,4)) + sns.barplot( + data = cate_count, + x = 'category', + y = 'order_count', + hue = 'category', + palette = 'viridis', + legend = False, + ax = ax, + ) + ax.set_title('Order Count by Category', fontweight='bold') + ax.set_xlabel('Category') + ax.set_ylabel('Order Count') + + plt.tight_layout() + return fig + # ============================================================ @@ -68,7 +111,32 @@ def yellow_line_region_trend(): 提示:分別 groupby 再 plot,或用 sns.lineplot(hue='region') """ # TODO: 你的程式碼 - pass + ns_df = df[df['region'].isin(['North', 'South'])].copy() + ns_df['month'] = ns_df['order_date'].dt.to_period('M') + + monthly_ns = ( + ns_df.groupby(['month', 'region'])['amount'] + .sum() + .reset_index() + ) + monthly_ns['month'] = monthly_ns['month'].astype(str) + + fig, ax = plt.subplots(figsize = (10, 4)) + sns.lineplot( + data = monthly_ns, + hue = 'region', + x = 'month', + y = 'amount', + marker = 'o', + linewidth = 2, + ) + ax.set_title('Monthly Revenue: North vs South', fontweight='bold') + ax.set_xlabel('Month') + ax.set_ylabel('Revenue(NT$)') + plt.xticks(rotation = 45) + ax.legend(title = 'Region') + plt.tight_layout + return fig def yellow_box_vip(): @@ -78,7 +146,21 @@ def yellow_box_vip(): 提示:sns.boxplot(x='vip_level', y='amount', data=df) """ # TODO: 你的程式碼 - pass + fig, ax = plt.subplots(figsize=(9, 5)) + sns.boxplot( + data = df, + x = 'vip_level', + y = 'amount', + hue = 'vip_level', + palette = 'Set3', + legend = False, + ax = ax + ) + ax.set_title('Order Amount Distribution by VIP Level', fontweight='bold') + ax.set_xlabel('VIP Level') + ax.set_ylabel('Amount(NT$)') + plt.tight_layout() + return fig def yellow_scatter_price_amount(): @@ -88,7 +170,20 @@ def yellow_scatter_price_amount(): 提示:plt.scatter() 或 sns.scatterplot() """ # TODO: 你的程式碼 - pass + fig, ax = plt.subplots(figsize=(8, 4)) + sns.scatterplot( + data = df, + x = 'unit_price', + y = 'amount', + alpha = 0.5, + ax = ax + ) + ax.set_title('Unit Price vs Amount') + ax.set_xlabel('Unit Price(NT$)') + ax.set_ylabel('Amount(NT$)') + plt.tight_layout() + return fig + # ============================================================ @@ -107,4 +202,35 @@ def red_category_dashboard(category="Electronics"): 提示:fig, axes = plt.subplots(2, 2, figsize=(14, 10)) """ # TODO: 你的程式碼 - pass + 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'{category} Dashboard', fontsize=16, fontweight='bold') + + monthly = cat_df.groupby('month')['amount'].sum().reset_index() + sns.lineplot(data=monthly, x='month', y='amount', marker='o', ax=axes[0, 0]) + axes[0, 0].set_title('Monthly Revenue Trend') + axes[0, 0].set_xlabel('Month') + axes[0, 0].set_ylabel('Revenue') + axes[0, 0].tick_params(axis='x', rotation=45) + + region = cat_df.groupby('region')['amount'].sum().reset_index() + sns.barplot(data=region, x='region', y='amount', palette='viridis', ax=axes[0, 1]) + axes[0, 1].set_title('Revenue by Region') + axes[0, 1].set_xlabel('Region') + axes[0, 1].set_ylabel('Revenue') + + top5 = cat_df.groupby('product_name')['amount'].sum().nlargest(5).reset_index() + sns.barplot(data=top5, x='amount', y='product_name', palette='viridis', ax=axes[1, 0]) + axes[1, 0].set_title('Top 5 Products') + axes[1, 0].set_xlabel('Revenue') + axes[1, 0].set_ylabel('Product') + + sns.histplot(data=cat_df, x='amount', bins=20, ax=axes[1, 1]) + axes[1, 1].set_title('Amount Distribution') + axes[1, 1].set_xlabel('Amount') + axes[1, 1].set_ylabel('Frequency') + + plt.tight_layout() + return fig diff --git a/homework/m6_plotly_capstone.py b/homework/m6_plotly_capstone.py index 0e2c32a..b727f27 100644 --- a/homework/m6_plotly_capstone.py +++ b/homework/m6_plotly_capstone.py @@ -26,7 +26,23 @@ def green_plotly_bar(): 提示:px.bar() """ # TODO: 你的程式碼 - pass + category_revenue = ( + df_enriched.groupby('category')['amount'] + .sum() + .reset_index() + .sort_values('amount', ascending = False) + ) + + fig = px.bar( + data_frame = category_revenue, + x = 'category', + y = 'amount', + color = 'category', + title = 'Total Revenue by Category', + labels = {'amount' : 'Revenue(NT$)', 'category' : 'Categoty'} + ) + return fig + def green_plotly_line(): @@ -37,7 +53,23 @@ def green_plotly_line(): 提示:先 groupby 月份算總營收,再 px.line() """ # TODO: 你的程式碼 - pass + monthly = ( + df_enriched.groupby(df_enriched['order_date'].dt.to_period('M'))['amount'] + .sum() + .reset_index() + ) + monthly['order_date'] = monthly['order_date'].astype(str) + + fig = px.line( + data_frame = monthly, + x = 'order_date', + y = 'amount', + markers = True, + title = 'Monthly Revenue Trend', + labels = {'order_date' : 'Month', 'amount' : 'Revenue(NT$)'}, + ) + return fig + def green_plotly_pie(): @@ -48,7 +80,20 @@ def green_plotly_pie(): 提示:px.pie() """ # TODO: 你的程式碼 - pass + vip_count = ( + df_enriched['vip_level'].value_counts() + .reset_index() + ) + vip_count.columns = ['vip_level', 'order_count'] + + fig = px.pie ( + data_frame = vip_count, + names = 'vip_level', + values = 'order_count', + title = 'VIP Level Share', + hole = 0.4, + ) + return fig # ============================================================ @@ -63,7 +108,28 @@ def yellow_clean_and_merge(raw_path, customers_path, products_path): 回傳:合併後的 DataFrame """ # TODO: 你的程式碼 - pass + df = pd.read_csv(raw_path) + customers = pd.read_csv(customers_path) + products = pd.read_csv(products_path) + + df.columns = df.columns.str.strip().str.lower() + 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() + + df = ( + df + .merge(customers, on = 'customer_id', how = 'left') + .merge(products, on = 'product_id', how = 'left') + ) + return df def yellow_kpi_summary(df): @@ -77,7 +143,17 @@ def yellow_kpi_summary(df): } """ # TODO: 你的程式碼 - pass + total_revenue = round(df['amount'].sum(), 2) + order_count = len(df) + active_customers = df['customer_id'].nunique() + avg_order_value = round(total_revenue / order_count, 2) + + return { + 'total_revenue': total_revenue, + 'order_count': order_count, + 'active_customers': active_customers, + 'avg_order_value': avg_order_value + } def yellow_plotly_scatter(df): @@ -91,7 +167,15 @@ def yellow_plotly_scatter(df): 提示:px.scatter(hover_data=['product_name']) """ # TODO: 你的程式碼 - pass + fig = px.scatter ( + df, + x='unit_price', + y='amount', + color='category', + hover_data='product_name', + title='product_unit_price v.s. order_unit_price' + ) + return fig # ============================================================ @@ -115,4 +199,57 @@ def red_dashboard(): 提示: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' + ) + + monthly = ( + df.groupby(df['order_date'].dt.to_period('M'))['amount'] + .sum().reset_index() + ) + monthly['order_date'] = monthly['order_date'].astype(str) + + top10 = ( + df.groupby('product_name')['amount'] + .sum().nlargest(10).reset_index() + .sort_values('amount') + ) + + region = ( + df.groupby('region')['amount'] + .sum().reset_index() + .sort_values('amount', ascending = False) + ) + + category = ( + df.groupby('category')['amount'] + .sum().reset_index() + ) + + fig = make_subplots( + rows = 2, cols = 2, + subplot_titles=('Monthly Revenue Trend', + 'Top 10 Products', + 'Revenue by Region', + 'Category Share'), + specs=[[{'type': 'xy'}, {'type': 'xy'}], + [{'type': 'xy'}, {'type': 'domain'}]], + ) + + fig.add_trace(go.Scatter(x=monthly['order_date'], y=monthly['amount'], + mode='lines+markers', name='Monthly'), row=1, col=1) + fig.add_trace(go.Bar(x=top10['product_name'], y=top10['amount'], + name='Top Products'), row=1, col=2) + fig.add_trace(go.Bar(x=region['region'], y=region['amount'], + name='Region'), row=2, col=1) + fig.add_trace(go.Pie(labels=category['category'], values=category['amount'], + name='Category', hole=0.4), row=2, col=2) + + fig.update_layout( + title_text='E-Commerce Sales Dashboard — 2025【解答版】', + height=750, showlegend=False, + ) + + return fig