Lộ trình học Python cho người muốn làm Data Analyst
Data Analyst là công việc trả lời câu hỏi kinh doanh bằng dữ liệu: doanh thu tháng này giảm vì sao? Nhóm khách hàng nào đáng đầu tư nhất? Chiến dịch nào thực sự hiệu quả?
Python là công cụ quan trọng — nhưng nói thẳng: Python chỉ chiếm khoảng 40% công việc. Phần còn lại là SQL, tư duy phân tích, hiểu nghiệp vụ, và giao tiếp với người ra quyết định.
Bài viết này là lộ trình 20 tuần, trong đó phần quan trọng nhất là giai đoạn 5 — thứ mà hầu hết khoá học bỏ qua.
🎯 Data Analyst thực sự làm gì?
| Hoạt động | Tỷ lệ thời gian | Ghi chú |
|---|---|---|
| Viết SQL lấy dữ liệu | 30% | Kỹ năng #1, không phải Python |
| Làm sạch và kiểm tra dữ liệu | 20% | Phần "không ai thấy" nhưng tốn thời gian nhất |
| Phân tích, tìm insight | 20% | Phần thú vị nhất |
| Trực quan hoá và làm báo cáo | 20% | Dashboard, slide, báo cáo định kỳ |
| Họp, trao đổi với stakeholder | 10% | Quan trọng hơn bạn tưởng |
💡 Sự thật cần biết trước: công việc Data Analyst có rất nhiều phần lặp lại và nhàm chán. Người thành công là người tự động hoá phần nhàm chán để dành thời gian cho phân tích thật.
🗓 Lộ trình 20 tuần
Tuần 1–4 Python nền tảng + pandas
Tuần 5–8 SQL (quan trọng nhất!)
Tuần 9–10 Làm sạch dữ liệu thực chiến
Tuần 11–12 Trực quan hoá với matplotlib, seaborn, plotly
Tuần 13–14 Thống kê ứng dụng cho phân tích
Tuần 15–16 Excel/Sheets nâng cao + kết nối dữ liệu
Tuần 17 Tự động hoá báo cáo
Tuần 18 Dashboard với Streamlit / Power BI / Tableau
Tuần 19 Dự án portfolio
Tuần 20 Chuẩn bị phỏng vấn
🐍 Tuần 1–2: Python nền tảng
Đừng học hết Python. Với Data Analyst, bạn cần chính xác những phần sau:
# 1. Cấu trúc dữ liệu — dùng mỗi ngày
khach_hang = {"ten": "Minh", "tuoi": 28, "thanh_pho": "Hà Nội"}
danh_sach_don = [120000, 350000, 89000]
# 2. List/dict comprehension — để biến đổi dữ liệu gọn gàng
don_lon = [d for d in danh_sach_don if d > 100000]
tong_theo_thanh_pho = {tp: 0 for tp in {"Hà Nội", "TP.HCM"}}
# 3. Vòng lặp + điều kiện — nhưng HẠN CHẾ dùng, pandas thay thế
for d in danh_sach_don:
if d > 200000:
print(f"Đơn lớn: {d:,} đ")
# 4. Hàm — tái sử dụng logic
def phan_loai_khach(so_don: int) -> str:
if so_don >= 20:
return "VIP"
if so_don >= 5:
return "Thân thiết"
return "Mới"
# 5. Xử lý file
import json
from pathlib import Path
du_lieu = json.loads(Path("du-lieu.json").read_text(encoding="utf-8"))
Path("ket-qua.json").write_text(
json.dumps(du_lieu, ensure_ascii=False, indent=2), encoding="utf-8"
)
# 6. Ngoại lệ — dữ liệu thật luôn bẩn
def chuyen_so(gt):
try:
return float(str(gt).replace(",", "").replace("đ", "").strip())
except (ValueError, AttributeError):
return None
Chủ đề KHÔNG cần học ở giai đoạn này: decorator, async, OOP phức tạp, metaclass, đa luồng. Bạn sẽ học chúng khi thật sự cần.
🐼 Tuần 3–4: pandas — công cụ chính
pip install pandas openpyxl
import pandas as pd
# --- Đọc dữ liệu ---
df = pd.read_csv("don-hang.csv", encoding="utf-8", parse_dates=["ngay_dat"])
df_excel = pd.read_excel("bao-cao.xlsx", sheet_name="Sheet1", skiprows=3)
df_sql = pd.read_sql("SELECT * FROM don_hang", conn)
# --- Khám phá ban đầu (LUÔN làm 4 bước này) ---
print(df.shape) # (số dòng, số cột)
print(df.dtypes) # kiểu dữ liệu từng cột
print(df.head(10)) # xem 10 dòng đầu
print(df.describe()) # thống kê mô tả
print(df.isna().sum()) # đếm giá trị thiếu mỗi cột
print(df.duplicated().sum()) # đếm dòng trùng
Bảng thao tác pandas phải thành thạo:
| Việc cần làm | Cú pháp |
|---|---|
| Chọn cột | df[["ten", "gia"]] |
| Lọc dòng | df[df["gia"] > 100000] |
| Lọc nhiều điều kiện | df[(df.gia > 1e5) & (df.thanh_pho == "Hà Nội")] |
| Lọc theo danh sách | df[df.thanh_pho.isin(["Hà Nội", "Đà Nẵng"])] |
| Sắp xếp | df.sort_values("gia", ascending=False) |
| Tạo cột mới | df["gia_vnd"] = df["gia_usd"] * 25400 |
| Đổi tên cột | df.rename(columns={"ten_kh": "khach_hang"}) |
| Nhóm và tổng hợp | df.groupby("thanh_pho")["gia"].sum() |
| Nhóm nhiều hàm | df.groupby("thanh_pho").agg(so_don=("id","count"), dt=("gia","sum")) |
| Pivot | df.pivot_table(index="thang", columns="thanh_pho", values="gia", aggfunc="sum") |
| Nối dọc | pd.concat([df1, df2], ignore_index=True) |
| Nối ngang | df1.merge(df2, on="khach_hang_id", how="left") |
| Ngày tháng | df["ngay_dat"].dt.to_period("M"), .dt.dayofweek |
| Chuỗi | df["ten"].str.strip().str.title() |
| Áp dụng hàm | df["loai"] = df["so_don"].apply(phan_loai_khach) |
Ví dụ phân tích thực tế — doanh thu theo tháng và thành phố:
import pandas as pd
df = pd.read_csv("don-hang.csv", parse_dates=["ngay_dat"])
# Làm sạch nhanh
df = df.dropna(subset=["khach_hang_id", "gia"])
df = df[df["gia"] > 0]
df = df.drop_duplicates(subset=["ma_don"])
# Tạo cột thời gian
df["thang"] = df["ngay_dat"].dt.to_period("M").astype(str)
# Báo cáo tổng hợp
bao_cao = (
df.groupby(["thang", "thanh_pho"])
.agg(
so_don=("ma_don", "nunique"),
doanh_thu=("gia", "sum"),
gia_tb=("gia", "mean"),
)
.reset_index()
)
# Tính tăng trưởng so với tháng trước
bao_cao["tang_truong_pct"] = (
bao_cao.sort_values("thang")
.groupby("thanh_pho")["doanh_thu"]
.pct_change()
.mul(100)
.round(2)
)
print(bao_cao.to_string(index=False))
# Xuất kết quả
bao_cao.to_excel("bao-cao-doanh-thu.xlsx", index=False)
bao_cao.to_csv("bao-cao-doanh-thu.csv", index=False, encoding="utf-8-sig")
💡
encoding="utf-8-sig"khi xuất CSV cho Excel — nếu không, tiếng Việt sẽ hiển thị lỗi font.
Ba lỗi pandas người mới luôn mắc:
# ❌ SettingWithCopyWarning — thao tác trên bản sao
df[df.gia > 100]["gia"] = 0
# ✅ Dùng .loc
df.loc[df.gia > 100, "gia"] = 0
# ❌ Vòng lặp qua từng dòng — cực chậm với dữ liệu lớn
for i, row in df.iterrows():
df.loc[i, "gia_vnd"] = row["gia"] * 25400
# ✅ Vector hoá — nhanh hơn hàng trăm lần
df["gia_vnd"] = df["gia"] * 25400
# ❌ Đọc cả file Excel 500MB vào bộ nhớ
df = pd.read_excel("khong-lo.xlsx")
# ✅ Đọc từng phần
for chunk in pd.read_csv("khong-lo.csv", chunksize=100_000):
xu_ly(chunk)
🗄️ Tuần 5–8: SQL — kỹ năng quan trọng nhất
Đây là giai đoạn quan trọng nhất của lộ trình. Data Analyst dành nhiều thời gian viết SQL hơn Python.
Cấu trúc truy vấn đầy đủ
SELECT kh.thanh_pho,
COUNT(DISTINCT dh.id) AS so_don,
SUM(dh.tong_tien) AS doanh_thu,
AVG(dh.tong_tien) AS gia_tri_tb,
ROUND(SUM(dh.tong_tien) FILTER (WHERE dh.trang_thai = 'hoan_thanh')
/ NULLIF(SUM(dh.tong_tien), 0) * 100, 2) AS ty_le_hoan_thanh
FROM don_hang dh
JOIN khach_hang kh ON kh.id = dh.khach_hang_id
LEFT JOIN san_pham sp ON sp.id = dh.san_pham_id
WHERE dh.ngay_dat >= '2026-01-01'
AND dh.trang_thai <> 'da_huy'
GROUP BY kh.thanh_pho
HAVING SUM(dh.tong_tien) > 100000000
ORDER BY doanh_thu DESC
LIMIT 20;
Thứ tự thực thi (quan trọng để hiểu lỗi):
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
Vì WHERE chạy trước GROUP BY, bạn không thể dùng alias hay hàm tổng hợp trong WHERE — phải dùng HAVING.
Các kỹ thuật SQL phải biết
1. JOIN — hiểu rõ từng loại:
-- INNER JOIN: chỉ lấy dòng khớp ở cả hai bảng
SELECT kh.ten, dh.tong_tien
FROM khach_hang kh
INNER JOIN don_hang dh ON dh.khach_hang_id = kh.id;
-- LEFT JOIN: giữ TẤT CẢ khách hàng, kể cả người chưa mua
SELECT kh.ten, COUNT(dh.id) AS so_don
FROM khach_hang kh
LEFT JOIN don_hang dh ON dh.khach_hang_id = kh.id
GROUP BY kh.id, kh.ten; -- khách chưa mua → so_don = 0
⚠️ Bẫy kinh điển: đặt điều kiện của bảng phải vào
WHEREthay vìONsẽ biến LEFT JOIN thành INNER JOIN:-- ❌ Sai: khách không có đơn trong 2026 sẽ bị loại hoàn toàn
LEFT JOIN don_hang dh ON dh.khach_hang_id = kh.id
WHERE dh.ngay_dat >= '2026-01-01'
-- ✅ Đúng: điều kiện nằm trong ON
LEFT JOIN don_hang dh ON dh.khach_hang_id = kh.id AND dh.ngay_dat >= '2026-01-01'
2. Window function — kỹ năng phân biệt junior và mid:
-- Xếp hạng khách hàng theo doanh thu trong từng thành phố
SELECT
kh.ten,
kh.thanh_pho,
SUM(dh.tong_tien) AS doanh_thu,
RANK() OVER (PARTITION BY kh.thanh_pho ORDER BY SUM(dh.tong_tien) DESC) AS xep_hang,
SUM(SUM(dh.tong_tien)) OVER (PARTITION BY kh.thanh_pho) AS tong_thanh_pho,
ROUND(SUM(dh.tong_tien) * 100.0
/ SUM(SUM(dh.tong_tien)) OVER (PARTITION BY kh.thanh_pho), 2) AS phan_tram
FROM khach_hang kh
JOIN don_hang dh ON dh.khach_hang_id = kh.id
GROUP BY kh.id, kh.ten, kh.thanh_pho
ORDER BY kh.thanh_pho, xep_hang;
3. CTE — viết truy vấn phức tạp cho dễ đọc:
WITH doanh_thu_thang AS (
SELECT DATE_TRUNC('month', ngay_dat) AS thang,
SUM(tong_tien) AS doanh_thu
FROM don_hang
WHERE trang_thai = 'hoan_thanh'
GROUP BY 1
),
so_sanh AS (
SELECT thang,
doanh_thu,
LAG(doanh_thu) OVER (ORDER BY thang) AS thang_truoc
FROM doanh_thu_thang
)
SELECT thang,
doanh_thu,
thang_truoc,
ROUND((doanh_thu - thang_truoc) * 100.0 / NULLIF(thang_truoc, 0), 2) AS tang_truong_pct
FROM so_sanh
ORDER BY thang;
4. CASE WHEN — phân nhóm có điều kiện:
SELECT
CASE
WHEN tong_tien >= 5000000 THEN 'Rất cao'
WHEN tong_tien >= 1000000 THEN 'Cao'
WHEN tong_tien >= 300000 THEN 'Trung bình'
ELSE 'Thấp'
END AS nhom_gia_tri,
COUNT(*) AS so_don
FROM don_hang
GROUP BY 1
ORDER BY 2 DESC;
5. Hiệu năng — hiểu INDEX:
EXPLAIN ANALYZE
SELECT * FROM don_hang WHERE khach_hang_id = 12345 AND ngay_dat >= '2026-01-01';
-- Nếu thấy "Seq Scan" trên bảng lớn → thiếu index
CREATE INDEX idx_don_hang_kh_ngay ON don_hang(khach_hang_id, ngay_dat);
-- Sau khi tạo index, chạy lại EXPLAIN → thấy "Index Scan"
Kết nối Python với database
import pandas as pd
from sqlalchemy import create_engine, text
engine = create_engine("postgresql+psycopg://user:pass@localhost:5432/analytics")
# Đọc dữ liệu — LUÔN dùng tham số hoá
truy_van = text("""
SELECT kh.thanh_pho, SUM(dh.tong_tien) AS doanh_thu
FROM don_hang dh
JOIN khach_hang kh ON kh.id = dh.khach_hang_id
WHERE dh.ngay_dat >= :tu_ngay
GROUP BY kh.thanh_pho
ORDER BY doanh_thu DESC
""")
df = pd.read_sql(truy_van, engine, params={"tu_ngay": "2026-01-01"})
print(df.head())
🧹 Tuần 9–10: Làm sạch dữ liệu thực chiến
Đây là phần chiếm nhiều thời gian nhất trong công việc thật. Dữ liệu thật luôn bẩn.
import pandas as pd
import numpy as np
df = pd.read_csv("du-lieu-tho.csv")
# --- 1. Kiểm tra tổng quan ---
print(df.info())
print(df.isna().mean().sort_values(ascending=False)) # tỷ lệ thiếu mỗi cột
# --- 2. Chuẩn hoá tên cột ---
df.columns = (
df.columns.str.strip()
.str.lower()
.str.replace(r"[^\w]+", "_", regex=True)
)
# --- 3. Xử lý giá trị thiếu ---
# Cột số: điền median (an toàn hơn mean khi có outlier)
df["gia"] = df["gia"].fillna(df["gia"].median())
# Cột phân loại: điền nhãn rõ ràng, KHÔNG điền mode
df["thanh_pho"] = df["thanh_pho"].fillna("Không xác định")
# Cột quan trọng thiếu nhiều: bỏ luôn
df = df.dropna(subset=["khach_hang_id"])
# --- 4. Xử lý outlier bằng IQR ---
Q1, Q3 = df["gia"].quantile([0.25, 0.75])
IQR = Q3 - Q1
gioi_han_duoi, gioi_han_tren = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
df["gia"] = df["gia"].clip(gioi_han_duoi, gioi_han_tren)
# --- 5. Chuẩn hoá chuỗi ---
df["email"] = df["email"].str.strip().str.lower()
df["ten"] = df["ten"].str.strip().str.title()
# --- 6. Chuẩn hoá ngày tháng ---
df["ngay_dat"] = pd.to_datetime(df["ngay_dat"], errors="coerce", dayfirst=True)
df = df.dropna(subset=["ngay_dat"]) # bỏ dòng ngày không parse được
# --- 7. Loại trùng ---
df = df.drop_duplicates(subset=["ma_don"], keep="last")
# --- 8. Kiểm tra tính hợp lý ---
assert (df["gia"] >= 0).all(), "Có giá âm!"
assert df["ngay_dat"].max() <= pd.Timestamp.now(), "Có ngày trong tương lai!"
print(f"✅ Sau làm sạch: {df.shape[0]:,} dòng × {df.shape[1]} cột")
⚠️ Nguyên tắc đạo đức quan trọng: mọi quyết định làm sạch dữ liệu phải được ghi lại và báo cáo. Nếu bạn bỏ 15% dữ liệu, người đọc báo cáo phải biết điều đó. Âm thầm xoá dữ liệu để có kết quả đẹp hơn là gian lận phân tích.
📊 Tuần 11–12: Trực quan hoá dữ liệu
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams["figure.figsize"] = (12, 6)
plt.rcParams["font.size"] = 11
sns.set_theme(style="whitegrid")
# --- Biểu đồ cột ---
theo_thanh_pho = df.groupby("thanh_pho")["gia"].sum().sort_values()
ax = theo_thanh_pho.plot(kind="barh", color="#2563eb")
ax.set_title("Doanh thu theo thành phố", fontsize=14, fontweight="bold")
ax.set_xlabel("Doanh thu (VNĐ)")
ax.set_ylabel("")
ax.bar_label(ax.containers[0], fmt="%.0f", padding=3)
plt.tight_layout()
plt.savefig("doanh-thu-thanh-pho.png", dpi=150)
plt.show()
Chọn biểu đồ đúng — quan trọng hơn code:
| Loại biểu đồ | Dùng khi | KHÔNG dùng khi |
|---|---|---|
| Cột | So sánh giữa các nhóm | Nhóm > 12 loại |
| Cột ngang | Tên nhóm dài | — |
| Đường | Xu hướng theo thời gian | Dữ liệu không có thứ tự |
| Tròn | Tối đa 4–5 phần | So sánh chính xác giữa các phần |
| Histogram | Phân phối một biến số | So sánh nhóm |
| Box plot | So sánh phân phối nhiều nhóm | Người xem không quen thống kê |
| Scatter | Tương quan hai biến | Biến phân loại |
| Heatmap | Ma trận quan hệ | Bảng nhỏ |
Bốn quy tắc trực quan hoá chuyên nghiệp:
- Một thông điệp mỗi biểu đồ. Nếu bạn cần 2 câu để giải thích, hãy tách ra.
- Tiêu đề là kết luận, không phải mô tả. ❌ "Doanh thu theo thành phố" → ✅ "TP.HCM chiếm 45% doanh thu".
- Bỏ mọi mực thừa. Xoá gridline thừa, khung, 3D, đổ bóng.
- Bắt đầu trục tung từ 0. Với biểu đồ cột, cắt trục là đánh lừa người xem (thường là vô tình).
📈 Tuần 13–14: Thống kê ứng dụng
Bạn không cần toán nặng, nhưng cần những khái niệm sau để không kết luận sai:
import numpy as np
from scipy import stats
# --- 1. Phân biệt mean, median, mode ---
# Lương: 5 người, 1 người lương 500tr → mean bị kéo lệch
luong = [8, 9, 10, 11, 500]
print(f"Mean: {np.mean(luong):.1f} (bị outlier kéo lệch)")
print(f"Median: {np.median(luong):.1f} (đại diện tốt hơn)")
# --- 2. Độ lệch chuẩn — đo mức biến động ---
print(f"Độ lệch chuẩn: {np.std(luong, ddof=1):.2f}")
# --- 3. Kiểm định giả thuyết: hai nhóm có khác nhau thật không? ---
nhom_a = [120, 135, 128, 142, 119, 131] # thiết kế cũ
nhom_b = [145, 152, 138, 160, 149, 155] # thiết kế mới
t_stat, p_value = stats.ttest_ind(nhom_a, nhom_b)
print(f"p-value = {p_value:.4f}")
if p_value < 0.05:
print("✅ Khác biệt có ý nghĩa thống kê")
else:
print("⚠️ Chưa đủ bằng chứng để kết luận khác biệt")
# --- 4. Tương quan ---
r, p = stats.pearsonr(df["chi_phi_marketing"], df["doanh_thu"])
print(f"Hệ số tương quan r = {r:.3f}, p = {p:.4f}")
⚠️ Ba sai lầm thống kê nguy hiểm nhất:
- Tương quan ≠ nhân quả. Kem bán chạy và tai nạn đuối nước cùng tăng vào mùa hè — nhưng kem không gây đuối nước.
- p-hacking. Thử 20 cách chia nhóm rồi báo cáo cách có p < 0.05 là gian lận.
- Survivorship bias. Chỉ phân tích khách còn hoạt động → kết luận "tỷ lệ rời bỏ thấp".
🤖 Tuần 17: Tự động hoá báo cáo — kỹ năng được trả lương cao nhất
Đây là phần tạo giá trị lớn nhất cho bạn. Bất kỳ báo cáo nào bạn làm thủ công hàng tuần đều có thể tự động hoá.
"""Báo cáo tuần tự động: lấy dữ liệu → tính toán → xuất file → gửi email."""
import smtplib
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from pathlib import Path
from datetime import datetime, timedelta
import pandas as pd
from sqlalchemy import create_engine, text
def lay_du_lieu(tu_ngay, den_ngay) -> pd.DataFrame:
engine = create_engine("postgresql+psycopg://user:pass@localhost/analytics")
truy_van = text("""
SELECT dh.ngay_dat, kh.thanh_pho, dh.tong_tien, dh.trang_thai
FROM don_hang dh
JOIN khach_hang kh ON kh.id = dh.khach_hang_id
WHERE dh.ngay_dat BETWEEN :tu AND :den
""")
return pd.read_sql(truy_van, engine, params={"tu": tu_ngay, "den": den_ngay})
def tinh_bao_cao(df: pd.DataFrame) -> pd.DataFrame:
df = df[df["trang_thai"] == "hoan_thanh"].copy()
df["tuan"] = df["ngay_dat"].dt.to_period("W").astype(str)
return (
df.groupby(["tuan", "thanh_pho"])
.agg(so_don=("tong_tien", "count"),
doanh_thu=("tong_tien", "sum"),
don_tb=("tong_tien", "mean"))
.round(0)
.reset_index()
)
def xuat_file(bao_cao: pd.DataFrame, tp: Path) -> Path:
with pd.ExcelWriter(tp, engine="openpyxl") as writer:
bao_cao.to_excel(writer, sheet_name="Chi tiết", index=False)
tong_hop = (bao_cao.groupby("thanh_pho")["doanh_thu"].sum()
.sort_values(ascending=False).reset_index())
tong_hop.to_excel(writer, sheet_name="Tổng hợp", index=False)
# Tự điều chỉnh độ rộng cột
for sheet in writer.sheets.values():
for cot in sheet.columns:
do_dai = max(len(str(o.value or "")) for o in cot)
sheet.column_dimensions[cot[0].column_letter].width = min(do_dai + 4, 40)
return tp
def gui_email(tp: Path, nguoi_nhan: list[str], tom_tat_html: str) -> None:
msg = MIMEMultipart("mixed")
msg["Subject"] = f"Báo cáo doanh thu tuần — {datetime.now():%d/%m/%Y}"
msg["From"] = "bao-cao@congty.com"
msg["To"] = ", ".join(nguoi_nhan)
msg.attach(MIMEText(tom_tat_html, "html", "utf-8"))
with open(tp, "rb") as f:
dinh_kem = MIMEApplication(f.read(), Name=tp.name)
dinh_kem["Content-Disposition"] = f'attachment; filename="{tp.name}"'
msg.attach(dinh_kem)
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
server.login("bao-cao@congty.com", "mat-khau-ung-dung")
server.send_message(msg)
def chay_bao_cao_tuan():
den_ngay = datetime.now().date()
tu_ngay = den_ngay - timedelta(days=7)
df = lay_du_lieu(tu_ngay, den_ngay)
if df.empty:
print("⚠️ Không có dữ liệu trong kỳ, bỏ qua.")
return
bao_cao = tinh_bao_cao(df)
tp = xuat_file(bao_cao, Path(f"bao-cao-{den_ngay:%Y-%m-%d}.xlsx"))
tong = bao_cao["doanh_thu"].sum()
so_don = bao_cao["so_don"].sum()
tom_tat = f"""
<h2>Báo cáo tuần {tu_ngay:%d/%m} – {den_ngay:%d/%m/%Y}</h2>
<ul>
<li>Tổng doanh thu: <b>{tong:,.0f} đ</b></li>
<li>Số đơn hoàn thành: <b>{so_don:,}</b></li>
<li>Giá trị đơn trung bình: <b>{tong / so_don:,.0f} đ</b></li>
</ul>
<p>Chi tiết xem trong file đính kèm.</p>
"""
gui_email(tp, ["sep@congty.com", "team@congty.com"], tom_tat)
print(f"✅ Đã gửi báo cáo: {tp.name}")
if __name__ == "__main__":
chay_bao_cao_tuan()
Đặt lịch chạy tự động:
| Hệ điều hành | Cách đặt lịch |
|---|---|
| Windows | Task Scheduler |
| macOS / Linux | cron: 0 8 * * 1 python /path/bao_cao.py |
| Cloud | GitHub Actions schedule: |
| Nâng cao | Airflow, Prefect |
💡 Lời khuyên sự nghiệp: tự động hoá một báo cáo thủ công mất 4 giờ/tuần thành 5 phút là dự án portfolio mạnh nhất bạn có thể làm. Nó chứng minh bạn tạo ra giá trị đo lường được — điều mà mọi nhà tuyển dụng muốn thấy.
🎨 Tuần 18: Dashboard
Ba lựa chọn phổ biến trên thị trường Việt Nam:
| Công cụ | Chi phí | Khi nào dùng |
|---|---|---|
| Power BI | Miễn phí bản desktop | Phổ biến nhất ở doanh nghiệp VN |
| Tableau | Có phí (đắt) | Doanh nghiệp lớn, quốc tế |
| Streamlit | Miễn phí, mã nguồn mở | Team kỹ thuật, cần tuỳ biến |
| Looker Studio | Miễn phí | Báo cáo marketing, Google Ads |
Kết hợp Python + Power BI là combo rất mạnh: dùng Python để làm sạch và tính toán, Power BI để hiển thị và chia sẻ.
# streamlit_dashboard.py — dashboard nhanh bằng Python
import streamlit as st
import pandas as pd
import plotly.express as px
st.set_page_config(page_title="Dashboard Bán hàng", layout="wide")
st.title("📊 Dashboard Bán hàng")
@st.cache_data(ttl=600)
def tai_du_lieu():
return pd.read_csv("du-lieu-da-sach.csv", parse_dates=["ngay_dat"])
df = tai_du_lieu()
with st.sidebar:
st.header("Bộ lọc")
thanh_pho = st.multiselect("Thành phố", sorted(df["thanh_pho"].unique()),
default=sorted(df["thanh_pho"].unique()))
khoang = st.date_input("Khoảng thời gian",
value=(df["ngay_dat"].min(), df["ngay_dat"].max()))
df_loc = df[df["thanh_pho"].isin(thanh_pho)]
df_loc = df_loc[(df_loc["ngay_dat"] >= pd.Timestamp(khoang[0]))
& (df_loc["ngay_dat"] <= pd.Timestamp(khoang[1]))]
c1, c2, c3 = st.columns(3)
c1.metric("Doanh thu", f"{df_loc['tong_tien'].sum():,.0f} đ")
c2.metric("Số đơn", f"{len(df_loc):,}")
c3.metric("Đơn trung bình", f"{df_loc['tong_tien'].mean():,.0f} đ")
st.plotly_chart(
px.line(df_loc.groupby("ngay_dat")["tong_tien"].sum().reset_index(),
x="ngay_dat", y="tong_tien", title="Doanh thu theo ngày"),
use_container_width=True,
)
st.plotly_chart(
px.bar(df_loc.groupby("thanh_pho")["tong_tien"].sum().sort_values().reset_index(),
x="tong_tien", y="thanh_pho", orientation="h", title="Doanh thu theo thành phố"),
use_container_width=True,
)
💼 Tuần 19: Dự án portfolio
Công thức một dự án Data Analyst tốt:
Câu hỏi kinh doanh rõ ràng + Dữ liệu thật + Phân tích có phương pháp
+ Trực quan hoá rõ ràng + Khuyến nghị hành động cụ thể
Ba dự án nên làm:
| Dự án | Kỹ năng thể hiện |
|---|---|
| Phân tích hành vi khách hàng (RFM segmentation) | SQL nâng cao, pandas, phân nhóm, khuyến nghị |
| Dashboard doanh thu tự động | Trực quan hoá, Streamlit/Power BI, tự động hoá |
| Phân tích A/B test | Thống kê, kiểm định giả thuyết, viết kết luận |
Cách trình bày dự án trên GitHub/portfolio:
## 1. Bối cảnh & câu hỏi
Công ty X có 50.000 khách hàng nhưng không biết ai đáng đầu tư giữ chân.
## 2. Dữ liệu
- Nguồn: ... (link)
- Kích thước: 1,2 triệu dòng giao dịch, 2024–2026
- Hạn chế: thiếu dữ liệu kênh marketing trong Q1
## 3. Phương pháp
- Làm sạch: loại 3,2% dòng thiếu customer_id, xử lý 812 outlier bằng IQR
- Phân nhóm RFM: chia 5 nhóm theo quintile
## 4. Kết quả

- Nhóm "Champions": 8% khách hàng, đóng góp 42% doanh thu
- Nhóm "At risk": 15% khách hàng, đã ngừng mua 90+ ngày
## 5. Khuyến nghị
1. Chiến dịch win-back cho nhóm At Risk: dự kiến thu hồi 12% (≈ 340 triệu)
2. Chương trình VIP cho Champions: giữ chân nhóm đóng góp 42% doanh thu
## 6. Hạn chế
- Chưa tính được chi phí acquisition nên chưa tính được LTV đầy đủ
Phần 5 và 6 là phần nhà tuyển dụng đọc kỹ nhất — 95% ứng viên bỏ qua chúng.
🎤 Tuần 20: Chuẩn bị phỏng vấn
Cấu trúc phỏng vấn Data Analyst điển hình:
| Vòng | Nội dung | Chuẩn bị gì |
|---|---|---|
| 1 | Sàng lọc với HR | Kể câu chuyện nghề nghiệp 2 phút |
| 2 | SQL test (thường live coding) | Luyện 50 bài SQL |
| 3 | Python/pandas test | groupby, merge, làm sạch dữ liệu |
| 4 | Case study nghiệp vụ | "Doanh thu giảm 20%, bạn kiểm tra gì?" |
| 5 | Văn hoá & hành vi | Câu hỏi STAR |
Câu hỏi SQL hay được hỏi:
- Viết truy vấn tìm khách hàng có trên 5 đơn trong 30 ngày qua.
- Tính doanh thu tích luỹ theo tháng (running total).
- Tìm khách hàng chưa từng mua trong 90 ngày.
- Tính tỷ lệ giữ chân khách hàng theo cohort.
- Tìm sản phẩm bán chạy thứ 2 trong mỗi danh mục.
- Viết truy vấn phát hiện dòng trùng lặp.
Khung trả lời case study — dùng cấu trúc này:
1. LÀM RÕ: "Doanh thu giảm so với kỳ nào? Giảm bao nhiêu %? Ở khu vực nào?"
2. CHIA NHỎ: tách theo thời gian, khu vực, sản phẩm, kênh, nhóm khách hàng
3. LOẠI TRỪ: vấn đề dữ liệu (tracking lỗi, mất data) trước khi kết luận về nghiệp vụ
4. GIẢ THUYẾT: nêu 2-3 giả thuyết khả dĩ nhất
5. KIỂM CHỨNG: truy vấn nào để xác nhận/phủ định từng giả thuyết
6. HÀNH ĐỘNG: đề xuất cụ thể, có số liệu dự kiến
💡 Điểm khác biệt lớn nhất: ứng viên trung bình trả lời ngay. Ứng viên giỏi hỏi lại để làm rõ trước.
📐 Checklist một phân tích đáng tin
Trước khi gửi bất kỳ báo cáo nào, hãy tự kiểm tra qua danh sách này.
Về dữ liệu
- Nguồn dữ liệu có đáng tin không? Ai tạo ra, cập nhật khi nào, có độ trễ không?
- Khoảng thời gian có phù hợp? So sánh tháng 2 (28 ngày) với tháng 3 (31 ngày) mà không chuẩn hoá là sai.
- Có dòng trùng không? Kiểm tra bằng khóa nghiệp vụ, không phải toàn bộ dòng.
- Giá trị thiếu chiếm bao nhiêu %? Nếu > 20% ở cột quan trọng, kết luận phải kèm cảnh báo.
- Có dữ liệu ngoài khoảng hợp lý không? Tuổi âm, ngày trong tương lai, giá bằng 0.
- Đã ghi lại mọi quyết định làm sạch chưa?
# Đưa việc kiểm tra vào code — chạy được, không phải tự nhớ
from dataclasses import dataclass
@dataclass
class KetQuaKiemTra:
ten: str
dat: bool
chi_tiet: str
def kiem_tra_chat_luong(df, cot_so: list[str], cot_khoa: str) -> list[KetQuaKiemTra]:
kq = []
trung = df.duplicated(subset=[cot_khoa]).sum()
kq.append(KetQuaKiemTra("Không trùng khoá", trung == 0, f"{trung} dòng trùng"))
thieu = df.isna().mean().max()
kq.append(KetQuaKiemTra("Tỷ lệ thiếu < 20%", thieu < 0.2, f"cao nhất {thieu:.1%}"))
am = (df[cot_so] < 0).any().any()
kq.append(KetQuaKiemTra("Không có giá trị âm bất thường", not am, ""))
kq.append(KetQuaKiemTra("Có dữ liệu", len(df) > 0, f"{len(df):,} dòng"))
return kq
for k in kiem_tra_chat_luong(df, ["doanh_thu"], "ma_don"):
print(f"{'✅' if k.dat else '❌'} {k.ten} — {k.chi_tiet}")
Về phương pháp
- Đã loại trừ vấn đề dữ liệu trước khi kết luận về nghiệp vụ chưa? Doanh thu giảm 50% thường là lỗi tracking, không phải mất khách.
- Có so sánh với kỳ tương đương không? Cùng kỳ năm trước, cùng số ngày.
- Có phân rã theo chiều không? Tổng không đổi nhưng cơ cấu thay đổi là thông tin quan trọng.
- Có kiểm tra tương quan nhân quả không? Có biến gây nhiễu nào không?
- Cỡ mẫu có đủ không? Kết luận từ 12 dòng dữ liệu là không đáng tin.
- Đã kiểm tra giả thuyết thay thế chưa? Nếu kết quả đúng, còn cách giải thích nào khác?
Về trình bày
- Tiêu đề là kết luận hay mô tả? Viết "Doanh thu giảm 12% do nhóm khách hàng cũ" tốt hơn "Biểu đồ doanh thu".
- Trục tung có bắt đầu từ 0 không? Với biểu đồ cột, cắt trục là đánh lừa người xem.
- Có nêu rõ hạn chế của phân tích không? Không có phân tích nào hoàn hảo.
- Có khuyến nghị cụ thể kèm số liệu dự kiến không? "Tăng 15% ngân sách cho nhóm A, dự kiến thu về 420 triệu" mạnh hơn "nên đầu tư thêm".
- Người đọc có hiểu trong 30 giây đầu không? Nếu không, hãy viết lại phần mở đầu.
Về tái lập
- Người khác chạy lại được code của bạn không?
- Có file dữ liệu hoặc truy vấn SQL kèm theo không?
- Có ghim phiên bản thư viện không?
pip freeze > requirements.txt
💡 Phép thử cuối cùng: đưa báo cáo cho một đồng nghiệp không tham gia phân tích và hỏi họ ba câu: (1) Bạn rút ra kết luận gì? (2) Bạn có tin không, vì sao? (3) Bạn sẽ làm gì tiếp theo? Nếu họ không trả lời được, báo cáo cần viết lại — không phải vì họ kém, mà vì báo cáo chưa rõ.
⚠️ 6 sai lầm khiến bạn mãi không được nhận
- Học Python mà bỏ SQL. SQL là kỹ năng #1 của Data Analyst. Bỏ qua nghĩa là trượt vòng phỏng vấn kỹ thuật.
- Chỉ làm dự án trên dữ liệu Titanic/Iris. Ai cũng làm. Hãy dùng dữ liệu thật, có câu hỏi kinh doanh thật.
- Không ghi lại quyết định làm sạch dữ liệu. Kết quả không thể tái lập là kết quả vô giá trị.
- Kết luận nhân quả từ tương quan. Sai lầm nghiêm trọng nhất.
- Báo cáo chỉ có số, không có khuyến nghị. Sếp không cần số — sếp cần biết nên làm gì.
- Không hỏi lại khi yêu cầu mơ hồ. "Cho tôi báo cáo doanh thu" là yêu cầu thiếu 5 thông tin.
❓ Câu hỏi thường gặp
Cần bằng đại học không? Không bắt buộc, nhưng phổ biến. Ngành Kinh tế, Tài chính, QTKD đều vào được nếu portfolio tốt. Kỹ năng quan trọng hơn bằng cấp.
Python hay Excel quan trọng hơn? Excel vẫn rất cần — nhiều doanh nghiệp Việt Nam vẫn làm việc chính trên Excel. Nhưng Python là thứ giúp bạn tự động hoá và xử lý dữ liệu lớn mà Excel bó tay. Học cả hai.
Data Analyst có bị AI thay thế không? Phần viết code lấy dữ liệu và làm biểu đồ cơ bản đang bị tự động hoá. Phần hiểu nghiệp vụ, đặt câu hỏi đúng, và truyền đạt insight thì không. Điều này có nghĩa: kỹ năng nghiệp vụ ngày càng quan trọng.
Bao lâu để đi làm? Nếu đã có nền tảng nghiệp vụ (kế toán, marketing, vận hành): 4–6 tháng. Nếu bắt đầu từ đầu: 8–12 tháng.
Nên học Power BI hay Tableau? Power BI ở Việt Nam phổ biến hơn và miễn phí bản desktop. Học Power BI trước; Tableau dễ học thêm sau.
🎯 Tóm lại
4 tuần → Python + pandas
4 tuần → SQL (đầu tư nhiều nhất vào đây)
2 tuần → Làm sạch dữ liệu
2 tuần → Trực quan hoá
2 tuần → Thống kê ứng dụng
2 tuần → Excel/Sheets nâng cao
1 tuần → Tự động hoá báo cáo ← tạo giá trị lớn nhất
1 tuần → Dashboard
1 tuần → Dự án portfolio
1 tuần → Phỏng vấn
Điều quyết định thành công không phải là bạn biết bao nhiêu thư viện, mà là bạn có thể trả lời một câu hỏi kinh doanh bằng dữ liệu và đề xuất hành động cụ thể hay không.