diff --git a/.github/.gitattributes b/.github/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.github/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/homework/m1_numpy.py b/homework/m1_numpy.py index 5ff41ab..91e59a2 100644 --- a/homework/m1_numpy.py +++ b/homework/m1_numpy.py @@ -15,23 +15,25 @@ # ============================================================ # 🟢 送分題(每題 10 分,共 30 分) # ============================================================ - def green_mean(): """建立 [10, 20, 30, 40, 50],回傳所有元素的平均值 (float)""" # TODO: 你的程式碼 - pass + arr = np.array([10, 20, 30, 40, 50]) + return float(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] # ============================================================ @@ -42,7 +44,7 @@ def green_filter(): def yellow_expensive_count(prices): """回傳單價 > 1000 的商品數量 (int)""" # TODO: 你的程式碼 - pass + return int(np.sum(prices > 1000)) def yellow_top3_stock_indices(stocks): @@ -51,7 +53,7 @@ def yellow_top3_stock_indices(stocks): 提示:np.argsort """ # TODO: 你的程式碼 - pass + return np.argsort(stocks)[::-1][:3] def yellow_restock_cost(prices, stocks): @@ -60,7 +62,8 @@ def yellow_restock_cost(prices, stocks): 提示:布林遮罩 + .sum() """ # TODO: 你的程式碼 - pass + mask = prices < 500 + return float(np.sum(prices[mask] * 50)) # ============================================================ @@ -77,4 +80,10 @@ def red_double11_prices(prices, stocks): 提示:np.where 可以巢狀使用 """ # TODO: 你的程式碼 - pass + return np.where( + stocks >= 100, prices * 0.7, + np.where( + stocks >= 20, prices * 0.9, + prices + ) + ) diff --git a/homework/m2_pandas_cleaning.py b/homework/m2_pandas_cleaning.py index fa36bff..a4a2a2b 100644 --- a/homework/m2_pandas_cleaning.py +++ b/homework/m2_pandas_cleaning.py @@ -20,16 +20,16 @@ def green_read_csv(): 提示:pd.read_csv() """ # TODO: 你的程式碼 - pass + return pd.read_csv('datasets/ecommerce/orders_raw.csv') def green_shape(df): """ 回傳 DataFrame 的 (列數, 欄數) tuple 提示:df.shape - """ + """ # TODO: 你的程式碼 - pass + return df.shape def green_dtypes(df): @@ -38,7 +38,7 @@ def green_dtypes(df): 提示:df.dtypes """ # TODO: 你的程式碼 - pass + return df.dtypes # ============================================================ @@ -52,7 +52,9 @@ def yellow_clean_columns(df): 提示:df.columns.str.strip().str.lower() """ # TODO: 你的程式碼 - pass + result = df.copy() + result.columns = result.columns.str.strip().str.lower() + return result def yellow_clean_amount(df): @@ -63,7 +65,14 @@ def yellow_clean_amount(df): 提示:.str.replace() + .astype(float) """ # TODO: 你的程式碼 - pass + result = df.copy() + result['amount'] = ( + result['amount'] + .str.replace('$', '', regex=False) + .str.replace(',', '', regex=False) + .astype(float) + ) + return result def yellow_drop_duplicates(df): @@ -72,7 +81,7 @@ def yellow_drop_duplicates(df): 提示:df.drop_duplicates() """ # TODO: 你的程式碼 - pass + return df.drop_duplicates() # ============================================================ @@ -93,4 +102,21 @@ def red_clean_orders(path): 提示: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() + + return df diff --git a/homework/m3_pandas_advanced.py b/homework/m3_pandas_advanced.py index 68410e8..81233ba 100644 --- a/homework/m3_pandas_advanced.py +++ b/homework/m3_pandas_advanced.py @@ -24,19 +24,25 @@ def green_load_and_merge(): 提示: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 len(df) def green_column_list(df): """回傳 DataFrame 的所有欄位名稱 (list)""" # TODO: 你的程式碼 - pass + return df.columns.tolist() # ============================================================ @@ -50,7 +56,7 @@ def yellow_top_category(df): 提示:groupby('category')['amount'].sum() """ # TODO: 你的程式碼 - pass + return df.groupby('category')['amount'].sum().idxmax() def yellow_gold_vip_stats(df): @@ -60,7 +66,8 @@ def yellow_gold_vip_stats(df): 提示:df[df['vip_level'] == 'Gold'] """ # TODO: 你的程式碼 - pass + gold = df[df['vip_level'] == 'Gold'] + return (int(len(gold)), float(gold['amount'].sum())) def yellow_region_avg_amount(df): @@ -70,7 +77,7 @@ def yellow_region_avg_amount(df): 提示:groupby('region')['amount'].mean() """ # TODO: 你的程式碼 - pass + return df.groupby('region')['amount'].mean() # ============================================================ @@ -94,4 +101,22 @@ def red_rfm_top5(df): 提示:groupby('customer_id').agg(...) """ # TODO: 你的程式碼 - pass + df['order_date'] = pd.to_datetime(df['order_date']) + + rfm = ( + df.groupby('customer_id') + .agg( + R=('order_date', 'max'), + F=('order_date', 'count'), + M=('amount', 'sum'), + customer_name=('customer_name', 'first') + ) + .reset_index() + ) + + return ( + rfm[['customer_id', 'customer_name', 'R', 'F', 'M']] + .sort_values('M', ascending=False) + .head(5) + .reset_index(drop=True) + ) \ No newline at end of file diff --git a/homework/m4_timeseries.py b/homework/m4_timeseries.py index 047af6a..7fe52fc 100644 --- a/homework/m4_timeseries.py +++ b/homework/m4_timeseries.py @@ -27,7 +27,8 @@ def green_avg_by_month(): 提示:df['order_date'].dt.month """ # TODO: 你的程式碼 - pass + df = _load_data() + return df.groupby(df['order_date'].dt.month)['amount'].mean() def green_top3_dates(): @@ -37,7 +38,9 @@ def green_top3_dates(): 提示:value_counts().head(3) """ # TODO: 你的程式碼 - pass + df = _load_data() + return df['order_date'].dt.date.value_counts().head(3) + def green_date_range(): @@ -46,7 +49,8 @@ def green_date_range(): 格式為 pandas Timestamp """ # TODO: 你的程式碼 - pass + df = _load_data() + return (df['order_date'].min(), df['order_date'].max()) # ============================================================ @@ -60,7 +64,8 @@ def yellow_monthly_revenue(): 提示: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): @@ -71,7 +76,7 @@ def yellow_rolling_avg(monthly_revenue): 提示:.rolling(window=3).mean() """ # TODO: 你的程式碼 - pass + return monthly_revenue.rolling(window=3).mean() def yellow_category_median(df): @@ -81,7 +86,11 @@ def yellow_category_median(df): 提示:groupby + median + sort_values """ # TODO: 你的程式碼 - pass + return ( + df.groupby('category')['amount'] + .median() + .sort_values(ascending=False) + ) # ============================================================ @@ -101,4 +110,19 @@ def red_monthly_report(): 提示: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() * 100 + + return monthly diff --git a/homework/m5_visualization.py b/homework/m5_visualization.py index 7e7335d..af41f87 100644 --- a/homework/m5_visualization.py +++ b/homework/m5_visualization.py @@ -29,7 +29,15 @@ def green_bar_category(): 提示:sns.countplot 或 value_counts().plot.bar() """ # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots(figsize=(8, 5)) + sns.countplot(data=df, x='category', + order=df['category'].value_counts().index, ax=ax) + ax.set_title('Orders by Category') + ax.set_xlabel('Category') + ax.set_ylabel('Order Count') + plt.tight_layout() + return fig def green_hist_amount(): @@ -39,7 +47,14 @@ def green_hist_amount(): 提示:sns.histplot(bins=20) 或 plt.hist() """ # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots(figsize=(8, 5)) + sns.histplot(data=df, x='amount', bins=20, ax=ax) + ax.set_title('Order Amount Distribution') + ax.set_xlabel('Amount') + ax.set_ylabel('Frequency') + plt.tight_layout() + return fig def green_set_labels(): @@ -51,7 +66,14 @@ def green_set_labels(): 回傳 matplotlib Figure 物件 """ # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots(figsize=(8, 5)) + df['category'].value_counts().plot.bar(ax=ax) + ax.set_title('Orders by Category') + ax.set_xlabel('Category') + ax.set_ylabel('Order Count') + plt.tight_layout() + return fig # ============================================================ @@ -68,7 +90,22 @@ def yellow_line_region_trend(): 提示:分別 groupby 再 plot,或用 sns.lineplot(hue='region') """ # TODO: 你的程式碼 - pass + df = _load_data() + df['month'] = df['order_date'].dt.to_period('M') + filtered = df[df['region'].isin(['North', 'South'])] + monthly = ( + filtered.groupby(['month', 'region'])['amount'] + .sum() + .reset_index() + ) + monthly['month'] = monthly['month'].dt.to_timestamp() + fig, ax = plt.subplots(figsize=(10, 5)) + sns.lineplot(data=monthly, x='month', y='amount', hue='region', ax=ax) + ax.set_title('Monthly Revenue: North vs South') + ax.set_xlabel('Month') + ax.set_ylabel('Revenue') + plt.tight_layout() + return fig def yellow_box_vip(): @@ -78,7 +115,14 @@ def yellow_box_vip(): 提示:sns.boxplot(x='vip_level', y='amount', data=df) """ # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots(figsize=(8, 5)) + sns.boxplot(data=df, x='vip_level', y='amount', ax=ax) + ax.set_title('Order Amount by VIP Level') + ax.set_xlabel('VIP Level') + ax.set_ylabel('Amount') + plt.tight_layout() + return fig def yellow_scatter_price_amount(): @@ -88,7 +132,14 @@ def yellow_scatter_price_amount(): 提示:plt.scatter() 或 sns.scatterplot() """ # TODO: 你的程式碼 - pass + df = _load_data() + fig, ax = plt.subplots(figsize=(8, 5)) + sns.scatterplot(data=df, x='unit_price', y='amount', alpha=0.5, ax=ax) + ax.set_title('Unit Price vs Order Amount') + ax.set_xlabel('Unit Price') + ax.set_ylabel('Amount') + plt.tight_layout() + return fig # ============================================================ @@ -107,4 +158,45 @@ def red_category_dashboard(category="Electronics"): 提示: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') + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + fig.suptitle(f'{category} Dashboard', fontsize=16, fontweight='bold') + + # 左上:月營收趨勢 + monthly_rev = cat_df.groupby('month')['amount'].sum() + monthly_rev.index = monthly_rev.index.to_timestamp() + axes[0, 0].plot(monthly_rev.index, monthly_rev.values, marker='o') + axes[0, 0].set_title('Monthly Revenue Trend') + axes[0, 0].set_xlabel('Month') + axes[0, 0].set_ylabel('Revenue') + + # 右上:各地區營收 + region_rev = cat_df.groupby('region')['amount'].sum().sort_values(ascending=False) + axes[0, 1].bar(region_rev.index, region_rev.values) + axes[0, 1].set_title('Revenue by Region') + axes[0, 1].set_xlabel('Region') + axes[0, 1].set_ylabel('Revenue') + + # 左下:Top 5 商品水平長條圖 + top5 = ( + cat_df.groupby('product_name')['amount'] + .sum() + .sort_values(ascending=False) + .head(5) + ) + axes[1, 0].barh(top5.index[::-1], top5.values[::-1]) + axes[1, 0].set_title('Top 5 Products by Revenue') + axes[1, 0].set_xlabel('Revenue') + axes[1, 0].set_ylabel('Product') + + # 右下:訂單金額分佈 + axes[1, 1].hist(cat_df['amount'], bins=20, edgecolor='white') + axes[1, 1].set_title('Order Amount Distribution') + axes[1, 1].set_xlabel('Amount') + axes[1, 1].set_ylabel('Frequency') + + plt.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..f2b25e8 100644 --- a/homework/m6_plotly_capstone.py +++ b/homework/m6_plotly_capstone.py @@ -13,6 +13,9 @@ import plotly.graph_objects as go from plotly.subplots import make_subplots +def _load_enriched(): + return pd.read_csv("datasets/ecommerce/orders_enriched.csv", + parse_dates=["order_date"]) # ============================================================ # 🟢 送分題(每題 10 分,共 30 分) @@ -26,8 +29,20 @@ def green_plotly_bar(): 提示:px.bar() """ # TODO: 你的程式碼 - pass - + df = _load_enriched() + category_rev = ( + df.groupby('category')['amount'] + .sum() + .reset_index() + .sort_values('amount', ascending=False) + ) + fig = px.bar( + category_rev, + x='category', y='amount', + title='Total Revenue by Category', + labels={'amount': 'Revenue', 'category': 'Category'} + ) + return fig def green_plotly_line(): """ @@ -37,7 +52,17 @@ def green_plotly_line(): 提示:先 groupby 月份算總營收,再 px.line() """ # TODO: 你的程式碼 - pass + df = _load_enriched() + df['month'] = df['order_date'].dt.to_period('M').dt.to_timestamp() + monthly = df.groupby('month')['amount'].sum().reset_index() + fig = px.line( + monthly, + x='month', y='amount', + title='Monthly Revenue Trend', + labels={'month': 'Month', 'amount': 'Revenue'}, + markers=True + ) + return fig def green_plotly_pie(): @@ -48,7 +73,15 @@ def green_plotly_pie(): 提示:px.pie() """ # TODO: 你的程式碼 - pass + df = _load_enriched() + vip_counts = df['vip_level'].value_counts().reset_index() + vip_counts.columns = ['vip_level', 'count'] + fig = px.pie( + vip_counts, + names='vip_level', values='count', + title='Order Share by VIP Level' + ) + return fig # ============================================================ @@ -63,7 +96,27 @@ 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'] + .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 = 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): @@ -77,7 +130,12 @@ def yellow_kpi_summary(df): } """ # 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): @@ -91,7 +149,16 @@ 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='Unit Price vs Order Amount', + labels={'unit_price': 'Unit Price', 'amount': 'Order Amount'}, + opacity=0.6 + ) + return fig # ============================================================ @@ -105,14 +172,91 @@ def red_dashboard(): 流程: 1. 清理 orders_raw.csv + 合併三張表 2. 建立 2×2 subplot dashboard(用 plotly make_subplots): - - 左上:月營收趨勢 (line) - - 右上:Top 10 商品營收 (bar) - - 左下:各地區營收 (bar) - - 右下:類別營收佔比 (pie/donut) + - 左上:月營收趨勢 (line) + - 右上:Top 10 商品營收 (bar) + - 左下:各地區營收 (bar) + - 右下:類別營收佔比 (pie/donut) 3. 設定整體標題 回傳 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['month'] = df['order_date'].dt.to_period('M').dt.to_timestamp() + monthly = df.groupby('month')['amount'].sum().reset_index() + + top10 = ( + df.groupby('product_name')['amount'] + .sum() + .sort_values(ascending=False) + .head(10) + .reset_index() + ) + + region_rev = ( + df.groupby('region')['amount'] + .sum() + .sort_values(ascending=False) + .reset_index() + ) + + cat_rev = df.groupby('category')['amount'].sum().reset_index() + + # 建立 2×2 subplots + 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": "xy"}, {"type": "xy"}], + [{"type": "xy"}, {"type": "domain"}] + ] + ) + + # 左上:折線圖 + fig.add_trace( + go.Scatter(x=monthly['month'], y=monthly['amount'], + mode='lines+markers', name='Monthly Revenue'), + row=1, col=1 + ) + + # 右上:水平長條圖 Top 10 + fig.add_trace( + go.Bar(x=top10['amount'], y=top10['product_name'], + orientation='h', name='Product Revenue'), + row=1, col=2 + ) + + # 左下:地區營收長條圖 + fig.add_trace( + go.Bar(x=region_rev['region'], y=region_rev['amount'], + name='Region Revenue'), + row=2, col=1 + ) + + # 右下:類別佔比甜甜圈圖 + fig.add_trace( + go.Pie(labels=cat_rev['category'], values=cat_rev['amount'], + hole=0.4, name='Category Share'), + row=2, col=2 + ) + + fig.update_layout( + title_text='E-Commerce Interactive Dashboard', + title_font_size=20, + height=800, + showlegend=False + ) + + return fig