Appearance
🛠 Workshop — Xây Dashboard Tương Tác
Import cleaned CSV → tạo data model (star schema) → viết DAX measures (Revenue, Growth %, AOV) → xây dashboard tương tác (KPI cards + trend + breakdown + filter) → publish & share. Output: file .pbix/.twbx + link dashboard!
🎯 Mục tiêu workshop
Sau khi hoàn thành workshop này, bạn sẽ:
- Import data vào Power BI Desktop từ CSV files
- Transform data với Power Query — rename, change types, clean
- Tạo data model — star schema, relationships, DIM_DATE
- Viết DAX measures — Total Revenue, AOV, Growth %, Revenue Share %
- Xây dashboard tương tác — KPI cards, line trend, bar breakdown, slicers, cross-filter
- Publish & share — Power BI Service hoặc export .pbix
🧰 Yêu cầu
| Yêu cầu | Chi tiết |
|---|---|
| Kiến thức | Đã hoàn thành Buổi 10 (Visualization) + Buổi 11 lý thuyết (Power BI/Tableau) |
| Công cụ | Power BI Desktop (free, Windows) HOẶC Tableau Public (free, Win/Mac) |
| Download | Power BI: powerbi.microsoft.com/desktop |
| Input | 3 CSV files (tạo bằng Python bên dưới) |
| Thời gian | 90–120 phút |
💡 Naming convention
Đặt tên file: HoTen_Buoi11_Dashboard.pbix (Power BI) hoặc HoTen_Buoi11_Dashboard.twbx (Tableau)
📦 Dataset: E-commerce Sales Data
Tạo dataset bằng Python
Chạy đoạn Python sau trong Jupyter Notebook để tạo 3 CSV files — đây sẽ là input cho Power BI/Tableau:
python
# Cell 1: Tạo 3 CSV files cho Power BI / Tableau workshop
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
np.random.seed(42)
# ═══════════════════════════════════════════
# TABLE 1: FACT_SALES — 10,000 transactions
# ═══════════════════════════════════════════
n_orders = 10000
start_date = datetime(2024, 1, 1)
# Generate dates with seasonality
dates = []
for i in range(n_orders):
day_offset = np.random.randint(0, 730) # 2 years
d = start_date + timedelta(days=day_offset)
dates.append(d)
categories = ["Electronics", "Fashion", "Food & Beverage", "Beauty", "Sports"]
cat_weights = [0.25, 0.22, 0.28, 0.15, 0.10]
regions = ["TP.HCM", "Hà Nội", "Đà Nẵng", "Cần Thơ", "Hải Phòng"]
region_weights = [0.35, 0.30, 0.15, 0.12, 0.08]
channels = ["Online", "Offline"]
channel_weights = [0.45, 0.55]
fact_sales = []
for i in range(n_orders):
cat = np.random.choice(categories, p=cat_weights)
# Base price per category
base_price = {
"Electronics": 3500000,
"Fashion": 850000,
"Food & Beverage": 250000,
"Beauty": 620000,
"Sports": 1200000
}[cat]
quantity = np.random.randint(1, 6)
revenue = round(base_price * quantity * np.random.uniform(0.7, 1.3))
cost = round(revenue * np.random.uniform(0.55, 0.75))
profit = revenue - cost
fact_sales.append({
"order_id": f"ORD-{i+1:05d}",
"order_date": dates[i].strftime("%Y-%m-%d"),
"product_id": f"P{np.random.randint(1, 51):03d}",
"customer_id": f"C{np.random.randint(1, 2001):04d}",
"category": cat,
"region": np.random.choice(regions, p=region_weights),
"channel": np.random.choice(channels, p=channel_weights),
"quantity": quantity,
"revenue": revenue,
"cost": cost,
"profit": profit,
})
df_sales = pd.DataFrame(fact_sales)
# ═══════════════════════════════════════════
# TABLE 2: DIM_PRODUCT — 50 products
# ═══════════════════════════════════════════
products = []
product_names = {
"Electronics": ["Laptop", "Phone", "Tablet", "Headphones", "Smartwatch",
"Camera", "Speaker", "Monitor", "Keyboard", "Mouse"],
"Fashion": ["T-Shirt", "Jeans", "Dress", "Jacket", "Sneakers",
"Handbag", "Sunglasses", "Watch", "Belt", "Scarf"],
"Food & Beverage": ["Coffee", "Tea", "Snacks", "Juice", "Chocolate",
"Rice", "Noodles", "Sauce", "Milk", "Cookies"],
"Beauty": ["Skincare Set", "Lipstick", "Perfume", "Shampoo", "Sunscreen",
"Face Mask", "Serum", "Foundation", "Moisturizer", "Eye Cream"],
"Sports": ["Running Shoes", "Yoga Mat", "Dumbbells", "Tennis Racket", "Swimming Goggles",
"Basketball", "Football", "Gym Bag", "Water Bottle", "Fitness Tracker"],
}
pid = 1
for cat, names in product_names.items():
for name in names:
products.append({
"product_id": f"P{pid:03d}",
"product_name": name,
"category": cat,
"sub_category": f"{cat} - {name.split()[0]}",
"unit_price": round(np.random.uniform(100000, 5000000)),
})
pid += 1
df_products = pd.DataFrame(products)
# ═══════════════════════════════════════════
# TABLE 3: DIM_CUSTOMER — 2,000 customers
# ═══════════════════════════════════════════
segments = ["Consumer", "Corporate", "Home Office"]
seg_weights = [0.50, 0.30, 0.20]
customers = []
for i in range(2000):
customers.append({
"customer_id": f"C{i+1:04d}",
"customer_name": f"Customer_{i+1}",
"segment": np.random.choice(segments, p=seg_weights),
"region": np.random.choice(regions, p=region_weights),
"join_date": (start_date + timedelta(days=np.random.randint(0, 1095))).strftime("%Y-%m-%d"),
})
df_customers = pd.DataFrame(customers)
# ═══════════════════════════════════════════
# SAVE TO CSV
# ═══════════════════════════════════════════
df_sales.to_csv("fact_sales.csv", index=False, encoding="utf-8-sig")
df_products.to_csv("dim_product.csv", index=False, encoding="utf-8-sig")
df_customers.to_csv("dim_customer.csv", index=False, encoding="utf-8-sig")
print(f"✅ fact_sales.csv: {len(df_sales):,} rows × {len(df_sales.columns)} columns")
print(f"✅ dim_product.csv: {len(df_products):,} rows × {len(df_products.columns)} columns")
print(f"✅ dim_customer.csv: {len(df_customers):,} rows × {len(df_customers.columns)} columns")
print(f"\n📊 Tổng quan FACT_SALES:")
print(f" Revenue range: {df_sales['revenue'].min():,.0f} – {df_sales['revenue'].max():,.0f} VND")
print(f" Date range: {df_sales['order_date'].min()} → {df_sales['order_date'].max()}")
print(f" Categories: {df_sales['category'].nunique()}")
print(f" Regions: {df_sales['region'].nunique()}")Phần 1: Import Data & Power Query
Import 3 CSV files vào Power BI, clean & transform trong Power Query
Bước 1.1: Import CSV files
Power BI Desktop:
━━━━━━━━━━━━━━━━
1. Mở Power BI Desktop
2. Home → Get Data → Text/CSV
3. Chọn file "fact_sales.csv" → Load
4. Lặp lại cho "dim_product.csv" và "dim_customer.csv"
5. Kết quả: 3 tables trong Fields pane (bên phải)⚠️ Nếu dùng Tableau
Tableau Desktop / Public:
━━━━━━━━━━━━━━━━━━━━━━━
1. Mở Tableau → Connect → Text File
2. Chọn "fact_sales.csv" → drag vào canvas
3. Click "Add" → chọn "dim_product.csv"
4. Click "Add" → chọn "dim_customer.csv"
5. Tạo joins: fact_sales.product_id = dim_product.product_id
fact_sales.customer_id = dim_customer.customer_idBước 1.2: Power Query Transformations
Power BI → Home → Transform Data (mở Power Query Editor):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Bảng fact_sales:
1. Kiểm tra Column Quality: View → Column Quality ✓
2. Change type "order_date" → Date (nếu chưa đúng)
3. Change type "revenue", "cost", "profit" → Whole Number
4. Change type "quantity" → Whole Number
5. Rename: "order_date" → "Order Date", "revenue" → "Revenue"
(Right-click column → Rename)
Bảng dim_product:
1. Change type "unit_price" → Whole Number
2. Rename columns theo PascalCase: "product_name" → "Product Name"
Bảng dim_customer:
1. Change type "join_date" → Date
2. Rename columns theo PascalCase
→ Close & Apply (Home → Close & Apply)Bước 1.3: Tạo DIM_DATE (Date Table)
Trong Power BI, tạo Date table bằng DAX — bắt buộc cho time intelligence functions:
dax
// Tạo bảng DIM_DATE — vào Modeling → New Table
DIM_DATE =
ADDCOLUMNS(
CALENDAR(DATE(2024, 1, 1), DATE(2025, 12, 31)),
"Year", YEAR([Date]),
"Quarter", "Q" & FORMAT([Date], "Q"),
"Month Number", MONTH([Date]),
"Month Name", FORMAT([Date], "MMMM"),
"Month Short", FORMAT([Date], "MMM"),
"Year-Month", FORMAT([Date], "YYYY-MM"),
"Day of Week", FORMAT([Date], "dddd"),
"Week Number", WEEKNUM([Date]),
"Is Weekend", IF(WEEKDAY([Date], 2) >= 6, TRUE(), FALSE())
)Sau khi tạo: Right-click DIM_DATE → Mark as Date Table → chọn column Date.
Phần 2: Data Model — Relationships
Tạo star schema: FACT_SALES ← DIM_PRODUCT, DIM_CUSTOMER, DIM_DATE
Bước 2.1: Tạo Relationships
Power BI → Model view (icon bên trái):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Kéo fact_sales[product_id] → dim_product[product_id]
→ Relationship: Many-to-One, Single direction ✓
2. Kéo fact_sales[customer_id] → dim_customer[customer_id]
→ Relationship: Many-to-One, Single direction ✓
3. Kéo fact_sales[Order Date] → DIM_DATE[Date]
→ Relationship: Many-to-One, Single direction ✓
Kết quả: Star schema với 1 fact + 3 dimensionsBước 2.2: Verify Model
Kiểm tra trong Model View:
━━━━━━━━━━━━━━━━━━━━━━━━
✅ 3 relationships: FACT → DIM_PRODUCT, DIM_CUSTOMER, DIM_DATE
✅ Tất cả Many-to-One (* ─── 1)
✅ Tất cả Single direction (mũi tên 1 chiều: DIM → FACT)
✅ Không có bi-directional
✅ DIM_DATE marked as Date TableBước 2.3: Hide Technical Columns
Trong Model view hoặc Data view:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Right-click → Hide in Report View:
✅ fact_sales[product_id] — key chỉ dùng cho relationship
✅ fact_sales[customer_id] — key chỉ dùng cho relationship
✅ fact_sales[order_id] — ID, không cần trong report
✅ dim_product[product_id] — key
✅ dim_customer[customer_id] — keyPhần 3: DAX Measures
Viết 8 DAX measures cho dashboard: Revenue, Orders, AOV, Customers, Growth, Profit Margin, Share %
Bước 3.1: Basic Measures
Tạo measures bằng: Home → New Measure, hoặc right-click table → New Measure:
dax
// ═══════════════════════════════════════════
// MEASURE GROUP 1: BASIC AGGREGATIONS
// ═══════════════════════════════════════════
// M1: Total Revenue
Total Revenue = SUM(fact_sales[Revenue])
// M2: Total Orders
Total Orders = COUNTROWS(fact_sales)
// M3: Total Profit
Total Profit = SUM(fact_sales[profit])
// M4: Total Customers (unique)
Total Customers = DISTINCTCOUNT(fact_sales[customer_id])
// M5: Average Order Value (AOV)
AOV = DIVIDE([Total Revenue], [Total Orders], 0)Bước 3.2: Advanced Measures — Growth & Share
dax
// ═══════════════════════════════════════════
// MEASURE GROUP 2: GROWTH & SHARE
// ═══════════════════════════════════════════
// M6: Revenue Previous Year (cần DIM_DATE đúng)
Revenue PY =
CALCULATE(
[Total Revenue],
SAMEPERIODLASTYEAR(DIM_DATE[Date])
)
// M7: Year-over-Year Growth %
YoY Growth % =
VAR CurrentRev = [Total Revenue]
VAR PrevRev = [Revenue PY]
RETURN
IF(
PrevRev = 0,
BLANK(),
DIVIDE(CurrentRev - PrevRev, PrevRev, 0)
)
// M8: Profit Margin %
Profit Margin % = DIVIDE([Total Profit], [Total Revenue], 0)
// M9: Revenue Share % (dùng ALL để tính tổng không bị filter)
Revenue Share % =
DIVIDE(
[Total Revenue],
CALCULATE([Total Revenue], ALL(fact_sales[category])),
0
)💡 Verify measures
Sau khi tạo measures, kéo vào 1 Table visual để verify:
- Rows:
dim_product[category] - Values:
[Total Revenue],[Revenue Share %] - Kiểm tra: Revenue Share % tổng = 100%
- Thêm slicer Region → click TP.HCM → Revenue Share % phải thay đổi (dynamic!)
Phần 4: Dashboard — Xây Visuals
Tạo dashboard tương tác: KPI cards + line trend + bar breakdown + table + slicers
Bước 4.1: Page Layout
Report view → Format → Page size:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Width: 1280
Height: 720
Background: White (#FFFFFF)
Layout plan (Z-pattern):
┌────────────────────────────────────────────────┐
│ 🔽 Slicer: Region 🔽 Slicer: Year-Month │
├──────────┬──────────┬──────────┬───────────────│
│ Revenue │ Orders │ AOV │ YoY Growth │
│ KPI Card │ KPI Card │ KPI Card │ KPI Card │
├──────────┴──────────┴──────────┴───────────────│
│ 📈 Revenue Trend (Line) │ 📊 Revenue by │
│ Monthly, 24 months │ Category (Bar) │
│ (Hero Chart — 50% width) │ (30% width) │
├────────────────────────────┴───────────────────│
│ 📋 Top 10 Products (Table) │ 🍩 Channel │
│ Product, Revenue, Qty, Margin │ Split (Donut) │
└────────────────────────────────────────────────┘Bước 4.2: KPI Cards (Row 1)
Tạo 4 KPI Cards:
━━━━━━━━━━━━━━━━
1. Visualizations → Card visual
2. Kéo [Total Revenue] vào Fields
3. Format → Data label → Display units: Millions, Decimal: 1
4. Format → Category label: "Total Revenue"
5. Lặp lại cho: [Total Orders], [AOV], [YoY Growth %]
Format YoY Growth %:
→ Display as: Percentage
→ Conditional formatting: Green if > 0, Red if < 0
(Format → Data label → fx → Rules)Bước 4.3: Revenue Trend — Line Chart (Hero)
Revenue Trend Line Chart:
━━━━━━━━━━━━━━━━━━━━━━━━
1. Visualizations → Line Chart
2. X-axis: DIM_DATE[Year-Month]
3. Y-axis: [Total Revenue]
4. Sort: X-axis ascending (chronological)
Customize:
→ Data colors: #1976D2 (blue) — single line
→ Data labels: ON — show values
→ Title: "Monthly Revenue Trend (2024–2025)"
→ Y-axis: Format → Display units: Millions
→ Markers: ON
→ Size: ~50% page width, ~30% heightBước 4.4: Revenue by Category — Bar Chart
Category Bar Chart:
━━━━━━━━━━━━━━━━━━
1. Visualizations → Clustered Bar Chart (horizontal)
2. Y-axis: dim_product[category]
3. X-axis: [Total Revenue]
4. Sort: [Total Revenue] descending
Customize:
→ Data colors: Use theme colors
→ Data labels: ON — show values in Millions
→ Title: "Revenue by Category"
→ Size: ~30% page widthBước 4.5: Top Products Table
Top Products Table:
━━━━━━━━━━━━━━━━━━
1. Visualizations → Table
2. Columns: dim_product[Product Name], [Total Revenue],
[Total Orders], [Profit Margin %]
3. Sort: [Total Revenue] descending
4. TopN filter: Top 10 by [Total Revenue]
Customize:
→ Conditional formatting on Revenue: Data bars (blue)
→ Conditional formatting on Margin: Color scale (red → green)
→ Style: Alternating rowsBước 4.6: Channel Split — Donut Chart
Channel Donut:
━━━━━━━━━━━━━━
1. Visualizations → Donut Chart
2. Legend: fact_sales[channel]
3. Values: [Total Revenue]
Customize:
→ Colors: Online=#1976D2 (blue), Offline=#FF9800 (orange)
→ Detail labels: Category + Percentage
→ Title: "Online vs Offline"Bước 4.7: Slicers — Region & Date
Slicer 1: Region
━━━━━━━━━━━━━━━━
1. Visualizations → Slicer
2. Field: dim_customer[region]
3. Format → Slicer settings → Style: Dropdown
4. Position: Top-left
Slicer 2: Year-Month
━━━━━━━━━━━━━━━━━━━
1. Visualizations → Slicer
2. Field: DIM_DATE[Year-Month]
3. Format → Slicer settings → Style: Between (range slider)
4. Position: Top-rightBước 4.8: Cross-filter & Interactions
Test Cross-filter:
━━━━━━━━━━━━━━━━━━
1. Click "Electronics" trên bar chart
→ Kiểm tra: KPI cards filter? ✅ Line chart filter? ✅ Table filter? ✅
2. Click "TP.HCM" trên slicer
→ Kiểm tra: All visuals update for HCM? ✅
3. Edit Interactions (optional):
→ Click bar chart → Format → Edit Interactions
→ Chọn filter/highlight/none cho từng visual khácPhần 5: Dashboard Polish
Title, theme, mobile layout
Bước 5.1: Dashboard Title & Footer
Title:
━━━━━━
1. Insert → Text box
2. Text: "📊 E-Commerce Sales Dashboard — FY2024-2025"
3. Font: Segoe UI, 18pt, Bold
4. Position: Top center
Footer:
━━━━━━━
1. Insert → Text box
2. Text: "Data refreshed: [date] | Source: E-commerce Database | Built with Power BI"
3. Font: 8pt, Gray, Italic
4. Position: BottomBước 5.2: Theme
Theme:
━━━━━━
1. View → Themes → Browse for themes
2. Hoặc: View → Themes → Customize current theme
→ Colors: Primary=#1976D2, Secondary=#FF9800
→ Font: Segoe UI
→ Background: White
3. Apply → tất cả visuals tự updateBước 5.3: Mobile Layout
Mobile Layout:
━━━━━━━━━━━━━━
1. View → Mobile Layout
2. Kéo visuals vào phone canvas:
→ Row 1: Slicer Region (dropdown)
→ Row 2-3: 4 KPI Cards (stack vertical)
→ Row 4-5: Revenue Trend (line chart)
→ Row 6-7: Category Breakdown (bar)
→ Row 8: Channel Split (donut)
3. Không kéo Table vào mobile (quá chi tiết cho phone)Phần 6: Publish & Share
Bước 6.1: Save file
File → Save As:
━━━━━━━━━━━━━━
Filename: HoTen_Buoi11_Dashboard.pbix
Location: Thư mục workshopBước 6.2: Publish (nếu có Power BI Pro)
Publish:
━━━━━━━━
1. Home → Publish
2. Chọn Workspace (My Workspace hoặc team workspace)
3. Đợi upload → "Success"
4. Click link → mở trên Power BI Service (browser)
5. Share: Share → nhập email → chọn quyền View/EditBước 6.3: Export (nếu không có Pro license)
Export Options:
━━━━━━━━━━━━━━
File → Export → PDF → all pages
File → Export → PowerPoint
Hoặc: Share file .pbix trực tiếp qua email/drive💡 Tableau alternative
Nếu dùng Tableau Public:
- File → Save to Tableau Public
- Login account → Upload
- Share link từ Tableau Public profile
- Hoặc: File → Export as Packaged Workbook (.twbx)
📋 Deliverable
Sau khi hoàn thành workshop, nộp:
| # | File | Mô tả |
|---|---|---|
| 1 | HoTen_Buoi11_Dashboard.pbix | Power BI file hoàn chỉnh (hoặc .twbx cho Tableau) |
| 2 | fact_sales.csv | Dataset chính — 10,000 transactions |
| 3 | dim_product.csv | Dimension table — 50 products |
| 4 | dim_customer.csv | Dimension table — 2,000 customers |
| 5 | Link dashboard (optional) | URL Power BI Service hoặc Tableau Public |
| 6 | Screenshot dashboard | PNG screenshot cho archive |
💡 Checklist trước khi nộp
- [ ] 3 CSV files đã tạo thành công
- [ ] Data model — 3 relationships, star schema, DIM_DATE created
- [ ] 8+ DAX measures — Revenue, Orders, AOV, Customers, Growth, Profit Margin, Share %, Revenue PY
- [ ] Dashboard có: 4 KPI cards + Line trend + Bar breakdown + Table + Donut + 2 Slicers
- [ ] Cross-filter hoạt động — click visual A → visual B update
- [ ] Slicer hoạt động — filter Region, filter Date range
- [ ] Mobile layout đã tạo
- [ ] Title + Footer có trên dashboard
- [ ] Theme consistent — 1 color palette, readable font
📊 Rubric — Thang điểm
| Tiêu chí | Điểm | Mô tả |
|---|---|---|
| Data Import & Power Query (Phần 1) | 10 | 3 CSV imported, data types correct, columns renamed |
| DIM_DATE Table (Phần 1) | 5 | Date table created with DAX, marked as date table |
| Data Model (Phần 2) | 15 | Star schema, 3 relationships, Many-to-One, single direction, hidden keys |
| Basic DAX Measures (Phần 3) | 15 | M1-M5: Revenue, Orders, Profit, Customers, AOV — correct & verified |
| Advanced DAX Measures (Phần 3) | 15 | M6-M9: Revenue PY, YoY Growth %, Profit Margin %, Revenue Share % |
| Dashboard Visuals (Phần 4) | 20 | 4 KPI cards + Line + Bar + Table + Donut + 2 Slicers — Z-pattern layout |
| Interactivity (Phần 4) | 10 | Cross-filter working, slicers filter all visuals, data labels |
| Polish & Mobile (Phần 5) | 5 | Title, footer, theme, mobile layout created |
| Publish / Export (Phần 6) | 5 | .pbix/.twbx file saved, screenshot, optional publish link |
| Tổng | 100 |
⚠️ Lưu ý quan trọng
- Star schema bắt buộc — không dùng flat table (1 bảng to). Phải có FACT + DIM separate.
- DAX measures bắt buộc — không dùng drag-and-drop default aggregation. Phải viết measure rõ ràng.
- DIM_DATE bắt buộc — phải tạo date table cho time intelligence (SAMEPERIODLASTYEAR).
- Cross-filter phải hoạt động — click 1 visual → visual khác phải phản ứng.
- Mobile layout phải có — mất 5 điểm nếu thiếu.
- Mỗi visual phải có title mô tả insight — không chỉ label.
- Dashboard tổng cộng ≤ 8 visuals (không tính slicers) — nếu nhiều hơn sẽ bị trừ điểm visual overload.