Chuyển tới nội dung chính

3 bài viết được gắn thẻ "Nghề nghiệp"

Xem tất cả thẻ

Lộ trình học Python cho người học Data Science / AI

· 29 phút để đọc

Data Science và AI là hai lĩnh vực bị hiểu sai nhiều nhất trong ngành công nghệ. Người ngoài nghĩ đây là công việc "dạy máy tính thông minh". Thực tế, 70–80% thời gian là làm sạch dữ liệu và hiểu vấn đề — phần xây model chỉ chiếm phần nhỏ.

Bài viết này là lộ trình 24 tuần, nói thẳng về những gì cần học và những gì không cần.


🎯 Trước khi bắt đầu: ba sự thật cần biết​

1. Toán quan trọng, nhưng không phải toán thi đại học. Bạn cần đại số tuyến tính (ma trận, vector), xác suất thống kê, và giải tích cơ bản (đạo hàm). Bạn không cần giải tích phức, phương trình vi phân, hay hình học vi phân (trừ khi làm research thuần).

2. Data Science ≠ AI Engineer ≠ ML Engineer.

Vị tríCông việc chínhCông cụ
Data AnalystTrả lời câu hỏi kinh doanhSQL, pandas, BI tools
Data ScientistPhân tích, thí nghiệm, xây model dự đoánPython, scikit-learn, thống kê
ML EngineerĐưa model vào sản phẩm, scalePyTorch, Docker, MLOps, cloud
AI ResearchNghiên cứu thuật toán mớiToán nặng, PyTorch, đọc paper
AI Application DevXây app dùng LLM/APIPython, prompt engineering, RAG

Đa số việc làm ở Việt Nam nằm ở Data Analyst → Data Scientist → ML Engineer. Research thuần rất ít và thường yêu cầu bằng tiến sĩ.

3. Thị trường đã thay đổi.

Với sự bùng nổ của LLM, công việc "train model từ đầu" giảm mạnh. Nhu cầu tăng ở:

  • MLOps — đưa model vào production, giám sát, cập nhật.
  • AI application — dùng LLM API, RAG, fine-tuning nhẹ.
  • Data engineering — hạ tầng dữ liệu cho AI.

💡 Lời khuyên thực tế: nếu bạn muốn làm AI, hãy học kỹ thuật phần mềm + dữ liệu thật tốt, rồi thêm AI. Người chỉ biết gọi model.fit() mà không biết deploy, không biết xử lý dữ liệu, sẽ rất khó có việc.


🗓 Lộ trình 24 tuần​

Tuần 1–4     Python cho khoa học dữ liệu: numpy, pandas
Tuần 5–7 Toán nền tảng: đại số tuyến tính, xác suất, giải tích
Tuần 8–10 Phân tích dữ liệu & trực quan hoá
Tuần 11–15 Machine Learning với scikit-learn
Tuần 16–19 Deep Learning với PyTorch
Tuần 20 NLP và LLM cơ bản
Tuần 21 MLOps: đóng gói và triển khai model
Tuần 22–23 Dự án portfolio
Tuần 24 Phỏng vấn và định hướng

🐍 Tuần 1–4: Python cho khoa học dữ liệu​

pip install numpy pandas matplotlib seaborn jupyter
jupyter lab

NumPy — nền tảng của mọi thứ​

import numpy as np

# Mảng và hình dạng
a = np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3)
print(a.shape, a.dtype, a.ndim)

# Vector hoá — nhanh hơn vòng lặp Python hàng chục lần
arr = np.arange(1_000_000)
print((arr * 2 + 1).sum())

# Broadcasting — quy tắc then chốt
diem = np.array([[8, 7, 9], [6, 8, 7]]) # (2, 3) — 2 học sinh, 3 môn
he_so = np.array([1.0, 1.5, 2.0]) # (3,) — tự broadcast
print(diem * he_so) # (2, 3)

# Thống kê dọc theo trục
print(diem.mean(axis=1)) # điểm TB của từng học sinh → [8.0, 7.0]
print(diem.mean(axis=0)) # điểm TB của từng môn → [7.0, 7.5, 8.0]

# Tạo dữ liệu giả để thử nghiệm
rng = np.random.default_rng(42) # seed để tái lập kết quả
du_lieu = rng.normal(loc=100, scale=15, size=(1000, 5))

# Đại số tuyến tính — nền tảng của ML
X = np.array([[1, 2], [3, 4], [5, 6]])
print(X.T) # chuyển vị
print(X @ X.T) # nhân ma trận
print(np.linalg.inv(np.array([[2.0, 1.0], [1.0, 3.0]])))

⚠️ Quy tắc số 1 của NumPy: không bao giờ lặp qua từng phần tử. Mọi thứ đều vector hoá được, và vector hoá nhanh hơn 10–100 lần.

pandas — công cụ làm việc hàng ngày​

import pandas as pd

df = pd.read_csv("du-lieu.csv", parse_dates=["ngay"])

# Luôn khám phá trước khi làm gì khác
print(df.shape, df.dtypes.to_dict())
print(df.isna().mean().sort_values(ascending=False).head(10))
print(df.describe(include="all"))

# Biến đổi
df["doanh_thu"] = df["so_luong"] * df["don_gia"]
df["thang"] = df["ngay"].dt.to_period("M")
df["nhom"] = pd.cut(df["doanh_thu"], bins=[0, 1e6, 5e6, np.inf],
labels=["Thấp", "Trung bình", "Cao"])

# Nhóm
tom_tat = (df.groupby(["thang", "nhom"], observed=True)
.agg(so_don=("id", "count"),
dt=("doanh_thu", "sum"),
tb=("doanh_thu", "mean"))
.reset_index())

# Merge — như JOIN trong SQL
df_full = df.merge(khach_hang, on="khach_hang_id", how="left", validate="many_to_one")

validate= trong merge là tham số ít người biết nhưng cực hữu ích: nó báo lỗi nếu quan hệ bạn giả định không đúng (ví dụ bạn nghĩ many_to_one nhưng thực tế là many_to_many → dữ liệu sẽ nhân lên).


🧮 Tuần 5–7: Toán nền tảng​

Học toán song song với code, không học chay.

Đại số tuyến tính — quan trọng nhất​

Khái niệmÝ nghĩa trong MLỨng dụng
VectorMột điểm dữ liệu (các đặc trưng)Mọi dòng dữ liệu là một vector
Ma trậnToàn bộ tập dữ liệuX shape (n_samples, n_features)
Tích vô hướngĐộ tương đồngCosine similarity, attention
Nhân ma trậnBiến đổi tuyến tínhMọi layer của neural network
Trị riêng / vector riêngHướng chính của dữ liệuPCA, giảm chiều
Chuẩn (norm)Độ lớn vectorRegularization (L1, L2)
import numpy as np

# Tích vô hướng = độ tương đồng
a = np.array([1, 2, 3])
b = np.array([2, 4, 6]) # b = 2a → cùng hướng
cos = (a @ b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(f"Cosine similarity: {cos:.4f}") # = 1.0

# PCA từ đầu — hiểu nó hoạt động thế nào
X = np.random.default_rng(42).normal(size=(200, 5))
X_tam = X - X.mean(axis=0) # 1. chuẩn hoá tâm
cov = np.cov(X_tam, rowvar=False) # 2. ma trận hiệp phương sai
gia_tri_rieng, vector_rieng = np.linalg.eigh(cov)
idx = np.argsort(gia_tri_rieng)[::-1] # 3. sắp xếp giảm dần
X_pca = X_tam @ vector_rieng[:, idx[:2]] # 4. chiếu xuống 2 chiều

print(f"Tỷ lệ phương sai giữ lại: "
f"{gia_tri_rieng[idx[:2]].sum() / gia_tri_rieng.sum():.2%}")

Hiểu PCA ở mức này quan trọng hơn gọi PCA(n_components=2).fit_transform(X) mà không biết nó làm gì.

Xác suất thống kê​

Khái niệmDùng ở đâu
Phân phối xác suấtGiả định của model, kiểm tra dữ liệu
BayesNaive Bayes, suy luận xác suất
Kỳ vọng, phương saiHàm mất mát, đánh giá
Ước lượng hợp lý cực đạiNền tảng của hồi quy logistic
Khoảng tin cậy, p-valueĐánh giá thí nghiệm A/B
Định lý giới hạn trung tâmVì sao bootstrap hoạt động

Giải tích — chỉ cần đạo hàm​

# Đạo hàm = hướng và tốc độ thay đổi → cơ sở của gradient descent
def ham_mat_mat(w):
return (w - 3) ** 2 + 2 # cực tiểu tại w = 3


def dao_ham(w):
return 2 * (w - 3)


# Gradient descent từ đầu
w = 0.0
learning_rate = 0.1
for buoc in range(50):
grad = dao_ham(w)
w -= learning_rate * grad
if buoc % 10 == 0:
print(f"Bước {buoc:>2}: w = {w:.6f}, loss = {ham_mat_mat(w):.6f}")

print(f"→ Hội tụ tại w = {w:.6f} (đáp án đúng: 3.0)")

Viết gradient descent bằng tay một lần giúp bạn hiểu optimizer.step() về sau.


📊 Tuần 8–10: EDA và trực quan hoá​

import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

df = pd.read_csv("du-lieu.csv")

# --- 1. Kiểm tra phân phối và outlier ---
fig, axes = plt.subplots(2, 3, figsize=(16, 9))
for ax, cot in zip(axes.ravel(), df.select_dtypes("number").columns[:6]):
sns.histplot(df[cot].dropna(), kde=True, ax=ax, color="#2563eb")
ax.set_title(f"Phân phối: {cot}")
plt.tight_layout()
plt.show()

# --- 2. Ma trận tương quan ---
plt.figure(figsize=(12, 10))
corr = df.select_dtypes("number").corr()
sns.heatmap(corr, annot=len(corr) <= 12, fmt=".2f", cmap="RdBu_r",
center=0, square=True, cbar_kws={"shrink": 0.8})
plt.title("Ma trận tương quan")
plt.show()

# --- 3. Tìm tương quan mạnh với biến mục tiêu ---
muc_tieu = "gia"
tuong_quan = (corr[muc_tieu].drop(muc_tieu)
.sort_values(key=abs, ascending=False))
print("Tương quan với biến mục tiêu:")
print(tuong_quan.head(10).round(3))

Ba điều cần nhớ về tương quan:

  1. Chỉ áp dụng cho quan hệ tuyến tính. Hai biến có quan hệ hình chữ U có thể có r ≈ 0.
  2. Nhạy với outlier. Một điểm dị thường có thể tạo tương quan giả.
  3. Tương quan ≠ nhân quả. Luôn luôn.

🤖 Tuần 11–15: Machine Learning với scikit-learn​

Đây là phần cốt lõi. scikit-learn vẫn là công cụ quan trọng nhất — không phải deep learning.

Quy trình 7 bước chuẩn​

import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, roc_auc_score, confusion_matrix

# --- 1. Tách dữ liệu TRƯỚC khi làm bất cứ gì khác ---
X = df.drop(columns=["nhan"])
y = df["nhan"]

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y # stratify giữ tỷ lệ lớp
)

# --- 2. Tiền xử lý bằng Pipeline (KHÔNG fit trên test!) ---
cot_so = X.select_dtypes("number").columns.tolist()
cot_chu = X.select_dtypes("object").columns.tolist()

tien_xu_ly = ColumnTransformer([
("so", Pipeline([
("dien_khuyet", SimpleImputer(strategy="median")),
("chuan_hoa", StandardScaler()),
]), cot_so),
("chu", Pipeline([
("dien_khuyet", SimpleImputer(strategy="constant", fill_value="Không rõ")),
("ma_hoa", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
]), cot_chu),
])

# --- 3. Pipeline hoàn chỉnh ---
model = Pipeline([
("tien_xu_ly", tien_xu_ly),
("phan_loai", RandomForestClassifier(random_state=42, n_jobs=-1)),
])

# --- 4. Cross-validation TRƯỚC khi tuning ---
diem_cv = cross_val_score(model, X_train, y_train, cv=5, scoring="roc_auc")
print(f"ROC-AUC CV: {diem_cv.mean():.4f} ± {diem_cv.std():.4f}")

# --- 5. Tuning siêu tham số ---
luoi = {
"phan_loai__n_estimators": [100, 300, 500],
"phan_loai__max_depth": [None, 10, 20],
"phan_loai__min_samples_leaf": [1, 2, 5],
}

tim_kiem = GridSearchCV(model, luoi, cv=5, scoring="roc_auc", n_jobs=-1, verbose=1)
tim_kiem.fit(X_train, y_train)
print(f"Bộ tham số tốt nhất: {tim_kiem.best_params_}")

# --- 6. Đánh giá trên tập test (CHỈ MỘT LẦN) ---
mo_hinh_tot_nhat = tim_kiem.best_estimator_
y_du_doan = mo_hinh_tot_nhat.predict(X_test)
y_xac_suat = mo_hinh_tot_nhat.predict_proba(X_test)[:, 1]

print(classification_report(y_test, y_du_doan))
print(f"ROC-AUC test: {roc_auc_score(y_test, y_xac_suat):.4f}")
print(f"Ma trận nhầm lẫn:\n{confusion_matrix(y_test, y_du_doan)}")

# --- 7. Feature importance ---
ten_dac_trung = mo_hinh_tot_nhat.named_steps["tien_xu_ly"].get_feature_names_out()
do_quan_trong = mo_hinh_tot_nhat.named_steps["phan_loai"].feature_importances_

for ten, diem in sorted(zip(ten_dac_trung, do_quan_trong),
key=lambda x: -x[1])[:10]:
print(f"{ten:<40} {diem:.4f}")

Những lỗi nghiêm trọng nhất trong ML​

1. Data leakage — lỗi chết người số 1:

# ❌ RÒ RỈ DỮ LIỆU: fit scaler trên TOÀN BỘ dữ liệu
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # đã "thấy" cả tập test!
X_train, X_test = train_test_split(X_scaled, ...)

# ✅ ĐÚNG: chỉ fit trên train (Pipeline làm việc này tự động)
X_train, X_test, y_train, y_test = train_test_split(X, y)
# rồi dùng Pipeline như ví dụ trên

Leakage làm model báo accuracy 95% khi validation nhưng chỉ 60% khi thực tế.

2. Không có baseline:

from sklearn.dummy import DummyClassifier

# Luôn so sánh với baseline trước
baseline = DummyClassifier(strategy="most_frequent")
baseline.fit(X_train, y_train)
print(f"Baseline accuracy: {baseline.score(X_test, y_test):.4f}")

# Nếu model của bạn = 0.72 và baseline = 0.70 → model gần như vô dụng!

3. Chỉ nhìn accuracy với dữ liệu mất cân bằng:

Nếu 99% giao dịch là hợp lệ, model "luôn dự đoán hợp lệ" đạt 99% accuracy — và bắt được 0% gian lận.

from sklearn.metrics import precision_recall_fscore_support

p, r, f1, _ = precision_recall_fscore_support(y_test, y_du_doan, average="binary")
print(f"Precision: {p:.4f} | Recall: {r:.4f} | F1: {f1:.4f}")
Chỉ sốÝ nghĩaKhi nào ưu tiên
AccuracyTỷ lệ đúngDữ liệu cân bằng
PrecisionTrong số dự đoán "positive", bao nhiêu đúngKhi false positive tốn kém (spam filter)
RecallTrong số "positive" thật, bắt được bao nhiêuKhi false negative nguy hiểm (ung thư, gian lận)
F1Trung bình điều hoà của hai cái trênCần cân bằng
ROC-AUCKhả năng phân biệt hai lớpSo sánh model tổng quát
PR-AUCNhư ROC nhưng tốt hơn khi mất cân bằngDữ liệu lệch mạnh

🧠 Tuần 16–19: Deep Learning với PyTorch​

pip install torch torchvision
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# --- Chuẩn bị dữ liệu ---
X, y = make_classification(n_samples=5000, n_features=20, n_informative=12,
random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

scaler = StandardScaler().fit(X_train)
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)

train_ds = TensorDataset(
torch.tensor(X_train, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.float32).unsqueeze(1),
)
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)


# --- Định nghĩa model ---
class MangPhanLoai(nn.Module):
def __init__(self, so_dac_trung: int):
super().__init__()
self.mang = nn.Sequential(
nn.Linear(so_dac_trung, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, 1),
)

def forward(self, x):
return self.mang(x)


thiet_bi = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MangPhanLoai(X_train.shape[1]).to(thiet_bi)

ham_mat_mat = nn.BCEWithLogitsLoss()
bo_toi_uu = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(bo_toi_uu, patience=5, factor=0.5)

# --- Vòng lặp huấn luyện ---
so_vong = 50
lich_su = {"train_loss": [], "val_loss": []}

X_test_t = torch.tensor(X_test, dtype=torch.float32).to(thiet_bi)
y_test_t = torch.tensor(y_test, dtype=torch.float32).unsqueeze(1).to(thiet_bi)

for vong in range(so_vong):
model.train()
tong_loss = 0.0
for xb, yb in train_loader:
xb, yb = xb.to(thiet_bi), yb.to(thiet_bi)

bo_toi_uu.zero_grad()
du_doan = model(xb)
loss = ham_mat_mat(du_doan, yb)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
bo_toi_uu.step()

tong_loss += loss.item() * len(xb)

# Đánh giá trên validation
model.eval()
with torch.no_grad():
val_loss = ham_mat_mat(model(X_test_t), y_test_t).item()

train_loss = tong_loss / len(train_ds)
lich_su["train_loss"].append(train_loss)
lich_su["val_loss"].append(val_loss)

scheduler.step(val_loss)

if (vong + 1) % 10 == 0:
print(f"Vòng {vong + 1:>3}/{so_vong} — "
f"train loss {train_loss:.4f} | val loss {val_loss:.4f} | "
f"lr {bo_toi_uu.param_groups[0]['lr']:.2e}")

# --- Đánh giá cuối ---
model.eval()
with torch.no_grad():
xac_suat = torch.sigmoid(model(X_test_t)).cpu().numpy().ravel()

from sklearn.metrics import roc_auc_score
print(f"\nROC-AUC: {roc_auc_score(y_test, xac_suat):.4f}")

Bốn khái niệm PyTorch phải nắm:

Khái niệmVai trò
nn.ModuleLớp cơ sở cho mọi model
forward()Định nghĩa luồng tính toán
loss.backward()Tính gradient
optimizer.step()Cập nhật trọng số
model.train() / .eval()Bật/tắt Dropout, BatchNorm
torch.no_grad()Tắt tính gradient khi suy luận (tiết kiệm bộ nhớ)

⚠️ Lỗi khiến 90% người mới mất hàng giờ: quên model.eval() khi đánh giá. Dropout vẫn hoạt động → kết quả validation sai và không tái lập được.

Khi nào dùng deep learning thay vì scikit-learn?

Dùng scikit-learnDùng deep learning
Dữ liệu dạng bảng (tabular)Ảnh, văn bản, âm thanh
Dữ liệu ít (< 100k dòng)Dữ liệu lớn (> 1M mẫu)
Cần giải thích được quyết địnhCần độ chính xác tối đa
Cần nhanh, ít tinh chỉnhCó GPU và thời gian
Baseline và prototypeBài toán thực sự phức tạp

💡 Sự thật quan trọng: với dữ liệu dạng bảng, XGBoost/LightGBM thường đánh bại neural network. Đừng dùng deep learning chỉ vì nó nghe "hiện đại" hơn.


💬 Tuần 20: NLP và LLM cơ bản​

# --- Cách 1: Dùng API (thực tế nhất hiện nay) ---
from openai import OpenAI

client = OpenAI()


def phan_tich_cam_xuc(doan_van: str) -> dict:
phan_hoi = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content":
"Bạn là chuyên gia phân tích cảm xúc. Trả về JSON với "
"'cam_xuc' (tich_cuc|tieu_cuc|trung_tinh) và 'ly_do'."},
{"role": "user", "content": doan_van},
],
response_format={"type": "json_object"},
temperature=0,
)
import json
return json.loads(phan_hoi.choices[0].message.content)


print(phan_tich_cam_xuc("Sản phẩm giao nhanh, đóng gói cẩn thận. Rất hài lòng!"))

Các khái niệm LLM cần biết:

Khái niệmÝ nghĩa
TokenĐơn vị văn bản model xử lý (~0.75 từ tiếng Anh)
EmbeddingVector biểu diễn ngữ nghĩa
Context windowSố token tối đa model xử lý một lần
Temperature0 = ổn định, 1+ = sáng tạo/ngẫu nhiên
RAGTruy xuất tài liệu liên quan rồi đưa vào prompt
Fine-tuningHuấn luyện thêm trên dữ liệu riêng
HallucinationModel bịa thông tin nghe hợp lý

RAG — kỹ thuật quan trọng nhất khi làm AI application:

import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("keepitreal/vietnamese-sbert")


def tim_tai_lieu_lien_quan(cau_hoi: str, kho_tai_lieu: list[str], top_k: int = 3):
"""Tìm đoạn tài liệu liên quan nhất bằng độ tương đồng cosine."""
vector_kho = model.encode(kho_tai_lieu, normalize_embeddings=True)
vector_hoi = model.encode([cau_hoi], normalize_embeddings=True)[0]

diem = vector_kho @ vector_hoi # cosine đã chuẩn hoá
idx = np.argsort(diem)[::-1][:top_k]

return [(kho_tai_lieu[i], float(diem[i])) for i in idx]


kho = [
"Chính sách đổi trả: khách hàng được đổi trong 7 ngày.",
"Phí vận chuyển nội thành là 30.000 đồng.",
"Thời gian bảo hành sản phẩm điện tử là 12 tháng.",
]

for noi_dung, diem in tim_tai_lieu_lien_quan("Tôi muốn đổi hàng thì sao?", kho):
print(f"[{diem:.3f}] {noi_dung}")

🚀 Tuần 21: MLOps — đưa model vào sản phẩm​

Đây là kỹ năng được trả lương cao nhất và cũng là thứ thiếu hụt nhất trên thị trường.

"""Đóng gói model thành API hoàn chỉnh."""
from contextlib import asynccontextmanager
from pathlib import Path

import joblib
import pandas as pd
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field


class DuLieuVao(BaseModel):
dac_trung: list[float] = Field(min_length=20, max_length=20)


class KetQuaRa(BaseModel):
du_doan: int
xac_suat: float
phien_ban_model: str


@asynccontextmanager
async def vong_doi(app: FastAPI):
"""Load model một lần khi khởi động — không load mỗi request."""
duong_dan = Path("model/xgb_v1.2.0.joblib")
if not duong_dan.exists():
raise RuntimeError(f"Không tìm thấy model: {duong_dan}")

app.state.model = joblib.load(duong_dan)
app.state.phien_ban = duong_dan.stem.split("_")[-1]
print(f"✅ Đã load model phiên bản {app.state.phien_ban}")
yield
app.state.model = None


app = FastAPI(title="API Dự đoán", lifespan=vong_doi)


@app.get("/suc-khoe")
async def suc_khoe():
return {"trang_thai": "ok", "model": app.state.phien_ban}


@app.post("/du-doan", response_model=KetQuaRa)
async def du_doan(du_lieu: DuLieuVao):
try:
X = pd.DataFrame([du_lieu.dac_trung])
du_doan = int(app.state.model.predict(X)[0])
xac_suat = float(app.state.model.predict_proba(X)[0][1])
except Exception as e:
raise HTTPException(500, f"Lỗi suy luận: {e}") from e

return KetQuaRa(du_doan=du_doan, xac_suat=xac_suat,
phien_ban_model=app.state.phien_ban)

Bốn trụ cột MLOps cần biết:

Trụ cộtCông cụMục đích
VersioningDVC, MLflowTheo dõi dữ liệu và model theo phiên bản
ServingFastAPI, TorchServeĐưa model thành API
MonitoringEvidently, PrometheusPhát hiện model "xuống cấp"
PipelineAirflow, PrefectTự động hoá train → test → deploy

Data drift — vấn đề mà người mới không biết:

Model train trên dữ liệu 2025 có thể sai dần vào 2026 vì hành vi người dùng thay đổi. Cần giám sát phân phối dữ liệu đầu vào:

from scipy.stats import ks_2samp
import numpy as np

def kiem_tra_drift(du_lieu_train: np.ndarray, du_lieu_hien_tai: np.ndarray,
nguong: float = 0.05) -> dict:
"""Kiểm định Kolmogorov-Smirnov cho từng đặc trưng."""
ket_qua = {}
for i in range(du_lieu_train.shape[1]):
_, p_value = ks_2samp(du_lieu_train[:, i], du_lieu_hien_tai[:, i])
ket_qua[f"dac_trung_{i}"] = {
"p_value": round(p_value, 4),
"co_drift": p_value < nguong,
}
so_drift = sum(1 for v in ket_qua.values() if v["co_drift"])
print(f"⚠️ {so_drift}/{len(ket_qua)} đặc trưng có drift")
return ket_qua

💼 Tuần 22–23: Dự án portfolio​

Ba dự án nên làm (chọn 2):

Dự ánThể hiện được
Dự đoán giá/churn có deployToàn bộ pipeline: EDA → model → API → deploy
Hệ thống RAG trả lời tài liệu nội bộLLM, embedding, vector DB, đánh giá
Pipeline MLOps có monitoringTự động train, versioning, phát hiện drift

Cấu trúc repo chuẩn:

du-an/
├── README.md ← quan trọng nhất
├── requirements.txt
├── data/
│ ├── raw/ ← dữ liệu gốc, không sửa
│ └── processed/
├── notebooks/
│ ├── 01_eda.ipynb
│ └── 02_modeling.ipynb
├── src/
│ ├── data/ ← hàm load + làm sạch
│ ├── features/ ← tạo đặc trưng
│ ├── models/ ← train + predict
│ └── api/ ← FastAPI serving
├── tests/
├── models/ ← model đã train (dùng DVC nếu lớn)
├── Dockerfile
└── .github/workflows/ci.yml

Điều nhà tuyển dụng tìm kiếm:

  1. Tái lập được — có seed, có requirements.txt, có hướng dẫn chạy.
  2. Code sạch, không chỉ notebook — notebook để khám phá, src/ để sản phẩm.
  3. Có deploy thật — link truy cập được, không chỉ screenshot.
  4. Hiểu hạn chế của model — bạn biết nó sai ở đâu.
  5. Có test — kể cả vài test đơn giản cho hàm tiền xử lý cũng gây ấn tượng.

🎤 Tuần 24: Phỏng vấn​

Cấu trúc phỏng vấn Data Scientist / ML Engineer:

VòngNội dungTrọng số
1Sàng lọc, giới thiệu dự án10%
2SQL + Python coding25%
3ML theory (bias-variance, regularization, metrics)25%
4ML system design25%
5Behavior + culture15%

Câu hỏi lý thuyết hay gặp:

  1. Bias-variance tradeoff — giải thích và cách xử lý từng phía.
  2. L1 và L2 regularization khác nhau thế nào? Khi nào dùng cái nào?
  3. Vì sao Random Forest giảm overfitting so với một cây đơn lẻ?
  4. Gradient boosting hoạt động thế nào? Khác Random Forest ở đâu?
  5. Xử lý dữ liệu mất cân bằng — kể 4 cách và ưu nhược điểm.
  6. Cross-validation dùng thế nào cho dữ liệu chuỗi thời gian?
  7. Precision và recall — khi nào ưu tiên cái nào? Cho ví dụ thực tế.
  8. Data leakage là gì? Cho 3 ví dụ tinh vi.

Câu hỏi ML system design (quan trọng nhất, chiếm 25%):

"Thiết kế hệ thống phát hiện gian lận giao dịch thẻ tín dụng cho ngân hàng có 10 triệu giao dịch/ngày."

Khung trả lời:

1. LÀM RÕ YÊU CẦU
- Độ trễ cho phép? (gợi ý: < 100ms cho giao dịch online)
- Chi phí false positive vs false negative?
- Có nhãn không? Bao lâu có nhãn? (chargeback mất 30-60 ngày)

2. DỮ LIỆU
- Nguồn: giao dịch, thiết bị, lịch sử khách hàng, vị trí
- Mất cân bằng cực nặng (~0,1% gian lận)
- Nhãn đến muộn → cần chiến lược cập nhật

3. ĐẶC TRƯNG
- Tần suất giao dịch 1h/24h/7d
- Lệch giữa vị trí giao dịch và vị trí thường ngày
- Số merchant khác nhau trong 24h
- Thời gian từ giao dịch trước

4. MODEL
- Baseline: rule-based + ngưỡng
- Chính: LightGBM (nhanh, xử lý mất cân bằng tốt, giải thích được)
- Metric: PR-AUC + recall@precision=0.9

5. TRIỂN KHAI
- Serving online < 100ms: model nhỏ, feature store
- Batch scoring cho phân tích hậu kiểm
- Fallback: nếu model lỗi → dùng rule cũ

6. GIÁM SÁT
- Data drift, model drift
- Tỷ lệ false positive theo thời gian (đo mức làm phiền khách)
- Retrain hàng tuần khi có nhãn mới

7. ĐÁNH ĐỔI
- Chặn nhiều → giảm gian lận nhưng tăng khách bực bội
- Cần quy trình review thủ công cho giao dịch nghi ngờ

🧪 Đánh giá model đúng cách​

Phần lớn người học dành 90% thời gian để tăng accuracy và 10% để kiểm tra xem con số đó có đáng tin không. Tỷ lệ này nên đảo ngược.

Chiến lược chia dữ liệu phải khớp với thực tế​

from sklearn.model_selection import (train_test_split, KFold, GroupKFold,
TimeSeriesSplit, LeaveOneGroupOut)

# --- Dữ liệu độc lập, phân phối giống nhau ---
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2,
random_state=42, stratify=y)

# --- Dữ liệu chuỗi thời gian: KHÔNG được chia ngẫu nhiên ---
# Chia ngẫu nhiên cho model "thấy tương lai" → kết quả đẹp giả tạo
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, val_idx in tscv.split(X):
...

# --- Dữ liệu nhóm: cùng một khách hàng không được xuất hiện ở cả train và test ---
gkf = GroupKFold(n_splits=5)
for train_idx, val_idx in gkf.split(X, y, groups=df["khach_hang_id"]):
...

# --- Leave-one-group-out: kiểm tra khả năng tổng quát hoá sang nhóm mới ---
logo = LeaveOneGroupOut()
for train_idx, val_idx in logo.split(X, y, groups=df["khu_vuc"]):
...

⚠️ Ví dụ điển hình của chia sai: bài toán dự đoán khách hàng rời bỏ dịch vụ. Nếu chia ngẫu nhiên theo dòng, cùng một khách hàng có thể nằm cả ở train và test. Model chỉ cần "nhớ" khách hàng thay vì học pattern → accuracy 0,95 khi validation, 0,65 khi thực tế.

Chọn metric khớp với chi phí nghiệp vụ​

Tình huốngMetric nên dùngVì sao
Dữ liệu cân bằng, sai sót ngang nhauAccuracy, F1Đơn giản, dễ hiểu
Mất cân bằng nhẹ (1:10)F1, ROC-AUCAccuracy bắt đầu gây hiểu nhầm
Mất cân bằng nặng (1:1000)PR-AUC, Recall@PrecisionROC-AUC bị lạc quan quá mức
Chi phí false positive caoPrecisionVí dụ: tự động chặn tài khoản
Chi phí false negative caoRecallVí dụ: tầm soát bệnh, phát hiện gian lận
Cần xếp hạng, không cần ngưỡngROC-AUC, NDCGGợi ý sản phẩm, tìm kiếm
Kết quả cần xác suất đáng tinLog loss, Brier score, calibrationDự đoán rủi ro, định giá
from sklearn.metrics import (average_precision_score, precision_recall_curve,
brier_score_loss, log_loss, roc_curve, auc)
import numpy as np

ap = average_precision_score(y_test, y_xac_suat) # PR-AUC
brier = brier_score_loss(y_test, y_xac_suat) # hiệu chuẩn
ll = log_loss(y_test, y_xac_suat)
print(f"PR-AUC: {ap:.4f} | Brier: {brier:.4f} | LogLoss: {ll:.4f}")

# Chọn ngưỡng theo mục tiêu nghiệp vụ, KHÔNG dùng 0.5 mặc định
precision, recall, nguong = precision_recall_curve(y_test, y_xac_suat)

# Ví dụ: cần precision >= 0.90, tìm recall tốt nhất có thể
idx = np.where(precision[:-1] >= 0.90)[0]
if len(idx):
i = idx[np.argmax(recall[i])]
print(f"Ngưỡng {nguong[i]:.3f} → precision {precision[i]:.3f}, recall {recall[i]:.3f}")

Ngưỡng 0,5 là mặc định của thư viện, không phải lựa chọn có căn cứ. Với bài toán gian lận, bạn cần ngưỡng cao hơn nhiều để giảm false positive.

Calibration — xác suất có đáng tin không?​

from sklearn.calibration import calibration_curve, CalibratedClassifierCV
import matplotlib.pyplot as plt

xac_suat_thuc, xac_suat_du_doan = calibration_curve(y_test, y_xac_suat, n_bins=10)

plt.figure(figsize=(7, 7))
plt.plot([0, 1], [0, 1], "k--", label="Hoàn hảo")
plt.plot(xac_suat_du_doan, xac_suat_thuc, "o-", label="Model")
plt.xlabel("Xác suất dự đoán")
plt.ylabel("Tỷ lệ dương thực tế")
plt.title("Biểu đồ hiệu chuẩn")
plt.legend()
plt.show()

# Hiệu chỉnh nếu model tự tin quá mức
model_hieu_chuan = CalibratedClassifierCV(model, method="isotonic", cv=5)
model_hieu_chuan.fit(X_train, y_train)

Vì sao quan trọng: nhãn "khách hàng này có 70% khả năng mua" chỉ có ích nếu 70% số khách hàng như vậy thực sự mua. Nếu chỉ 35% mua, mọi quyết định dựa trên con số đó đều sai.

Error analysis — bước mà hầu hết mọi người bỏ qua​

Đừng dừng ở việc biết accuracy. Hãy đọc từng dự đoán sai:

import pandas as pd

# Xây bảng dự đoán sai kèm thông tin ngữ cảnh
danh_gia = X_test.copy()
danh_gia["thuc_te"] = y_test.values
danh_gia["du_doan"] = y_du_doan
danh_gia["xac_suat"] = y_xac_suat

sai = danh_gia[danh_gia["thuc_te"] != danh_gia["du_doan"]]

# Phân tích lỗi theo từng nhóm
print(sai.groupby("khu_vuc").size().sort_values(ascending=False).head())
print(sai.groupby("nhom_tuoi").size().sort_values(ascending=False).head())

# Lỗi "tự tin nhưng sai" — nguy hiểm nhất
tu_tin_sai = sai[sai["xac_suat"].between(0.85, 0.999)]
print(f"Số dự đoán tự tin nhưng sai: {len(tu_tin_sai)} "
f"({len(tu_tin_sai)/len(sai):.1%} tổng số lỗi)")

# Xem vài ví dụ cụ thể để hiểu nguyên nhân
print(tu_tin_sai[['khu_vuc', 'nhom_tuoi', 'xac_suat']].head(10))

Câu hỏi cần trả lời sau error analysis:

  1. Lỗi tập trung ở một nhóm cụ thể không? (nếu có → model có định kiến)
  2. Có lỗi "tự tin nhưng sai" không? (nguy hiểm nhất trong thực tế)
  3. Lỗi có liên quan đến dữ liệu thiếu hoặc ngoài phân phối không?
  4. Nếu chỉ cải thiện được một loại lỗi, loại nào đáng cải thiện nhất?

Với bài toán phân loại, ma trận nhầm lẫn theo từng nhóm thường tiết lộ vấn đề mà metric tổng thể che mất.


📖 Cách tự học và cập nhật kiến thức​

Lĩnh vực này thay đổi nhanh đến mức khoá học bạn mua hôm nay sẽ lỗi thời sau 18 tháng. Kỹ năng quan trọng nhất là tự học có hệ thống.

Bốn nguồn học chất lượng nhất​

NguồnDùng đểGhi chú
Paper gốcHiểu thuật toán từ nguồnarXiv, paperswithcode
Documentation chính thứcDùng đúng APILuôn cập nhật nhất
Source codeHiểu cơ chế bên trongscikit-learn, PyTorch
Blog/khảo sát chất lượngNắm bức tranh tổng thểDistill, Lil'Log, Kaggle

Cách đọc một paper ML​

Đừng đọc từ đầu đến cuối. Đọc theo thứ tự này:

1. Tiêu đề + Abstract      (1 phút)  — bài này giải quyết vấn đề gì?
2. Hình 1 + bảng 1 (3 phút) — trực giác cốt lõi là gì?
3. Kết luận (2 phút) — kết quả có gì đáng chú ý?
4. Method (kỹ) (20 phút) — cách làm cụ thể
5. Thí nghiệm + bảng số liệu (15 phút) — có gì đáng nghi ngờ không?
6. Related work (5 phút) — đặt trong bối cảnh nào

Ba câu hỏi phải tự trả lời sau khi đọc:

  1. Ý tưởng cốt lõi, giải thích trong một câu.
  2. So với phương pháp trước đó, cải thiện ở điểm nào?
  3. Có dùng được cho bài toán của mình không, và cần điều kiện gì?

Duy trì kỹ năng code song song với kỹ năng mô hình​

# Mỗi tháng: đọc source của một thư viện bạn dùng hàng ngày
python -c "import sklearn; print(sklearn.__file__)"

Mở source sklearn/linear_model/_logistic.py và đọc cách họ tổ chức code, cách họ xử lý tham số, cách họ viết docstring. Đây là cách học nhanh nhất từ người giỏi mà hầu như không ai làm.

Lộ trình tự học 12 tháng song song công việc​

ThángHoạt độngSản phẩm
1–2Đọc 1 paper/tuần + tóm tắt8 bài tóm tắt
3–4Tái lập 1 paper đơn giảnRepo GitHub
5–6Tham gia 1 competition KaggleXếp hạng + notebook
7–8Đóng góp cho 1 thư viện mã nguồn mở1 PR được merge
9–10Viết 3 bài blog kỹ thuậtBlog cá nhân
11–12Xây 1 dự án end-to-end có deployPortfolio

Viết lại là cách học hiệu quả nhất. Khi bạn cố giải thích một thuật toán cho người khác, bạn phát hiện ra mình chưa hiểu chỗ nào. Đó chính là giá trị.


⚠️ 7 sai lầm khiến bạn học 1 năm mà không xin được việc​

  1. Chỉ học model, không học làm sạch dữ liệu. 80% công việc thật là xử lý dữ liệu.
  2. Không biết SQL. Mọi vòng phỏng vấn đều có SQL.
  3. Chỉ làm Kaggle, không deploy. Model không deploy = dự án chưa xong.
  4. Học deep learning trước machine learning cổ điển. Như học chạy trước khi biết đi.
  5. Không có baseline. Không biết model của mình tốt hay chỉ ngang đoán bừa.
  6. Không hiểu hạn chế của model. Nhà tuyển dụng hỏi câu này để kiểm tra bạn có thật sự làm hay copy.
  7. Bỏ qua kỹ năng kỹ thuật phần mềm. Git, Docker, testing — không có thì không qua vòng kỹ thuật.

❓ Câu hỏi thường gặp​

Cần bằng thạc sĩ/tiến sĩ không? Cho vị trí ML Engineer và AI Application Developer: không cần. Cho Research Scientist: thường cần. Đa số việc làm ở Việt Nam không yêu cầu bằng cao học.

Không giỏi toán thì có học được không? Được — cho vị trí Data Analyst, ML Engineer, AI Application Developer. Không được — cho Research Scientist. Bạn cần đại số tuyến tính và xác suất cơ bản ở mọi vị trí, nhưng không cần toán chuyên sâu.

Học Data Science bao lâu thì có việc? Nếu đã biết lập trình và SQL: 6–9 tháng. Nếu từ con số 0: 12–18 tháng. Bất kỳ ai hứa "3 tháng thành Data Scientist lương cao" đều đang bán khoá học, không bán sự thật.

AI có thay thế Data Scientist không? Thay thế phần viết code lặp lại và làm EDA cơ bản. Không thay thế phần hiểu vấn đề, chọn metric đúng, phán đoán về dữ liệu và đánh đổi nghiệp vụ.

Nên học TensorFlow hay PyTorch? PyTorch. Nó chiếm ưu thế trong nghiên cứu và ngày càng phổ biến trong công nghiệp. TensorFlow vẫn có chỗ đứng ở production mobile/embedded nhưng PyTorch là lựa chọn an toàn hơn để học.

Có nên học LLM trước Machine Learning truyền thống không? Không. Học ML cổ điển trước để hiểu overfitting, validation, metric, và đánh đổi. Những khái niệm này áp dụng nguyên vẹn cho LLM.


🎯 Tóm lại​

4 tuần  → Python: numpy, pandas
3 tuần → Toán: đại số tuyến tính, xác suất, đạo hàm
3 tuần → EDA và trực quan hoá
5 tuần → Machine Learning với scikit-learn ← phần quan trọng nhất
4 tuần → Deep Learning với PyTorch
1 tuần → NLP và LLM cơ bản
1 tuần → MLOps: đóng gói và deploy
2 tuần → Dự án portfolio
1 tuần → Phỏng vấn

Sự thật cuối cùng: người có việc làm trong ngành này không phải người biết nhiều thuật toán nhất, mà là người có thể đưa một model từ notebook ra production và chứng minh nó tạo ra giá trị. Hãy xây dựng theo hướng đó ngay từ dự án đầu tiên.


📚 Bài viết liên quan​

Lộ trình học Python để trở thành Backend Developer

· 26 phút để đọc

Backend developer là người xây dựng "phần chìm của tảng băng": API, database, xác thực, xử lý nghiệp vụ — tất cả những gì người dùng không nhìn thấy nhưng nếu hỏng thì cả sản phẩm sập.

Python là một trong những lựa chọn tốt nhất để bắt đầu, nhờ cú pháp rõ ràng và ba framework trưởng thành: Flask, Django, FastAPI.

Bài viết này là lộ trình 12 tháng thực tế, không phải danh sách từ khoá. Mỗi giai đoạn đều có: cần học gì, code mẫu, dự án phải làm, và tiêu chí để biết đã sẵn sàng bước tiếp.


🗺️ Bức tranh toàn cảnh​

Giai đoạn 1 (tuần 1–8)    Python nền tảng + OOP
Giai đoạn 2 (tuần 9–12) Git + Linux + công cụ
Giai đoạn 3 (tuần 13–20) Web cơ bản + Flask
Giai đoạn 4 (tuần 21–28) Database + SQL + ORM
Giai đoạn 5 (tuần 29–36) API chuyên nghiệp + Auth + Testing
Giai đoạn 6 (tuần 37–44) Django hoặc FastAPI (chọn 1)
Giai đoạn 7 (tuần 45–48) Docker + Deploy + CI/CD
Giai đoạn 8 (tuần 49–52) Dự án tổng hợp + phỏng vấn

Backend developer thực sự làm gì mỗi ngày?

Hoạt độngTỷ lệ thời gian
Viết code tính năng mới30%
Đọc và hiểu code cũ20%
Debug và sửa lỗi20%
Review code của đồng nghiệp10%
Viết test10%
Họp, trao đổi thiết kế, tài liệu10%

💡 Chú ý: chỉ 30% thời gian là "viết code mới". Vì vậy đọc code và debug là kỹ năng quan trọng ngang viết code.


🧱 Giai đoạn 1 (tuần 1–8): Python nền tảng và OOP​

Nội dung​

TuầnChủ đềKỹ năng đạt được
1Biến, kiểu dữ liệu, toán tử, input/printViết script tính toán
2if/elif/else, vòng lặpXử lý logic
3List, tuple, dict, setChọn cấu trúc dữ liệu đúng
4Hàm, tham số, *args/**kwargs, scopeChia nhỏ chương trình
5Xử lý file, CSV, JSON, pathlibĐọc/ghi dữ liệu
6Ngoại lệ, try/except/else/finallyCode không crash
7OOP: class, kế thừa, magic methodsThiết kế đối tượng
8Module, package, virtualenv, pipTổ chức dự án

Điều backend developer phải nắm chắc ở giai đoạn này​

1. Chọn đúng cấu trúc dữ liệu — đây là kỹ năng bị đánh giá thấp nhất:

Cấu trúcTruy cậpThêm/XoáDùng khi
listO(1) theo indexO(n) ở đầu, O(1) ở cuốiDữ liệu có thứ tự
dictO(1) theo keyO(1)Tra cứu theo khoá
setO(1) kiểm tra tồn tạiO(1)Loại trùng, kiểm tra membership
tupleO(1)Không đổiDữ liệu bất biến
# ❌ Tìm kiếm trong list — O(n), chậm khi dữ liệu lớn
danh_sach_email = ["a@x.com", "b@x.com", "c@x.com"]
if "c@x.com" in danh_sach_email: # quét từng phần tử
...

# ✅ Dùng set — O(1)
tap_email = {"a@x.com", "b@x.com", "c@x.com"}
if "c@x.com" in tap_email: # tra trực tiếp
...

Với 1 triệu bản ghi, khác biệt này là giây vs micro-giây.

2. try/except đúng cách — không nuốt lỗi:

# ❌ Bắt mọi lỗi, che giấu bug thật
try:
ket_qua = xu_ly_du_lieu(duong_dan)
except Exception:
pass

# ✅ Bắt lỗi cụ thể, log lại, xử lý đúng cách
import logging

logger = logging.getLogger(__name__)

try:
with open(duong_dan, encoding="utf-8") as f:
ket_qua = xu_ly(f.read())
except FileNotFoundError:
logger.warning("Không tìm thấy file %s, dùng cấu hình mặc định", duong_dan)
ket_qua = CAU_HINH_MAC_DINH
except json.JSONDecodeError as e:
logger.error("File %s không phải JSON hợp lệ: %s", duong_dan, e)
raise

3. OOP dùng cho việc thật, không phải class Animal:

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from decimal import Decimal


class PhuongThucThanhToan(ABC):
"""Interface cho mọi phương thức thanh toán."""

@abstractmethod
def thanh_toan(self, so_tien: Decimal) -> bool:
...

@property
@abstractmethod
def ten(self) -> str:
...


@dataclass
class ThanhToanThe(PhuongThucThanhToan):
so_the: str
han_dung: str

@property
def ten(self) -> str:
return f"Thẻ ****{self.so_the[-4:]}"

def thanh_toan(self, so_tien: Decimal) -> bool:
if so_tien <= 0:
raise ValueError("Số tiền phải dương")
# gọi cổng thanh toán...
return True


@dataclass
class ThanhToanViDienTu(PhuongThucThanhToan):
ma_vi: str
so_du: Decimal = field(default=Decimal("0"))

@property
def ten(self) -> str:
return f"Ví {self.ma_vi}"

def thanh_toan(self, so_tien: Decimal) -> bool:
if self.so_du < so_tien:
return False
self.so_du -= so_tien
return True


for pt in [ThanhToanThe("4111111111111234", "12/28"),
ThanhToanViDienTu("VI-001", Decimal("500000"))]:
ok = pt.thanh_toan(Decimal("200000"))
print(f"{pt.ten}: {'✅ thành công' if ok else '❌ thất bại'}")

Ở đây bạn học: ABC, @abstractmethod, @dataclass, Decimal cho tiền, @property — tất cả đều dùng hàng ngày ở backend.

Dự án giai đoạn 1​

Công cụ dòng lệnh quản lý chi tiêu — lưu vào JSON:

import json
from pathlib import Path
from datetime import date
from decimal import Decimal

FILE = Path("chi-tieu.json")


def doc_du_lieu() -> list[dict]:
if not FILE.exists():
return []
return json.loads(FILE.read_text(encoding="utf-8"))


def luu_du_lieu(ds: list[dict]) -> None:
FILE.write_text(json.dumps(ds, ensure_ascii=False, indent=2), encoding="utf-8")


def them(so_tien: str, danh_muc: str, ghi_chu: str = "") -> None:
ds = doc_du_lieu()
ds.append({
"ngay": date.today().isoformat(),
"so_tien": str(Decimal(so_tien)),
"danh_muc": danh_muc,
"ghi_chu": ghi_chu,
})
luu_du_lieu(ds)
print(f"✅ Đã thêm {so_tien} cho {danh_muc}")


def bao_cao_theo_danh_muc() -> None:
ds = doc_du_lieu()
if not ds:
print("Chưa có dữ liệu.")
return
tong: dict[str, Decimal] = {}
for item in ds:
tong[item["danh_muc"]] = tong.get(item["danh_muc"], Decimal(0)) + Decimal(item["so_tien"])
for danh_muc, so in sorted(tong.items(), key=lambda x: -x[1]):
print(f"{danh_muc:<16} {so:>12,.0f} đ")
print(f"{'TỔNG':<16} {sum(tong.values()):>12,.0f} đ")


if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Dùng: python chi_tieu.py them <số_tiền> <danh_mục>")
print(" python chi_tieu.py bao-cao")
sys.exit(1)

lenh = sys.argv[1]
if lenh == "them":
them(sys.argv[2], sys.argv[3], " ".join(sys.argv[4:]))
elif lenh == "bao-cao":
bao_cao_theo_danh_muc()
else:
print(f"Lệnh không hợp lệ: {lenh}")

Tiêu chí hoàn thành giai đoạn 1​

  • Viết được chương trình 150+ dòng chia thành nhiều hàm, không cần tra cú pháp
  • Giải thích được khi nào dùng list vs dict vs set
  • Dùng try/except đúng chỗ, không nuốt lỗi
  • Biết tạo venv và cài package

🛠️ Giai đoạn 2 (tuần 9–12): Git, Linux và công cụ​

Nhiều người bỏ qua giai đoạn này và trả giá suốt sự nghiệp.

Git — bắt buộc​

# Cấu hình lần đầu
git config --global user.name "Tên Bạn"
git config --global user.email "email@example.com"

# Workflow cơ bản hàng ngày
git status
git add .
git commit -m "feat: thêm chức năng đăng nhập"
git push origin main

# Làm việc nhánh — KỸ NĂNG QUAN TRỌNG NHẤT
git checkout -b feature/them-dang-nhap
# ... code ...
git commit -m "feat: hoàn thiện đăng nhập"
git push origin feature/them-dang-nhap
# rồi mở Pull Request trên GitHub

Conventional Commits — quy ước commit mà mọi team chuyên nghiệp dùng:

Tiền tốÝ nghĩa
feat:Thêm tính năng
fix:Sửa bug
docs:Sửa tài liệu
refactor:Tái cấu trúc, không đổi hành vi
test:Thêm/sửa test
chore:Việc lặt vặt (deps, config)

Viết commit rõ ràng giúp bạn được đánh giá cao hơn ngay từ ngày đầu đi làm.

Linux / command line cơ bản​

LệnhCông dụng
ls -laLiệt kê file, gồm file ẩn
cd, pwdDi chuyển, xem thư mục hiện tại
grep -rn "text" .Tìm text trong toàn bộ thư mục
find . -name "*.py"Tìm file theo mẫu
tail -f app.logXem log đang chạy (dùng hàng ngày!)
ps aux | grep pythonXem tiến trình Python
chmod +x script.shCấp quyền chạy
ssh user@serverKết nối server
curl -X POST ...Test API từ terminal

Công cụ nên cài​

Công cụVai trò
VS Code hoặc PyCharmIDE
Postman / InsomniaTest API trực quan
TablePlus / DBeaverXem database
Docker DesktopChạy database local dễ dàng

🌐 Giai đoạn 3 (tuần 13–20): Web hoạt động thế nào + Flask​

Kiến thức web bắt buộc​

HTTP request/response — nền tảng của mọi thứ:

GET /api/san-pham?page=2 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGc...
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json

{"data": [...], "page": 2, "total": 47}

Bảng mã trạng thái — phải thuộc lòng:

NhómMãÝ nghĩaKhi nào dùng
2xx200OKGET/PUT thành công
2xx201CreatedPOST tạo mới thành công
2xx204No ContentDELETE thành công
4xx400Bad RequestDữ liệu gửi lên sai định dạng
4xx401UnauthorizedChưa đăng nhập
4xx403ForbiddenĐã đăng nhập nhưng không có quyền
4xx404Not FoundKhông tìm thấy tài nguyên
4xx409ConflictTrùng dữ liệu (email đã tồn tại)
4xx422UnprocessableDữ liệu đúng định dạng nhưng sai logic
4xx429Too Many RequestsVượt rate limit
5xx500Internal Server ErrorLỗi phía server (bug)
5xx503Service UnavailableServer quá tải/bảo trì

⚠️ Lỗi kinh điển của người mới: trả về 200 OK kèm {"success": false, "error": "..."}. Điều này phá vỡ mọi thư viện client. Hãy dùng đúng mã trạng thái.

Flask — hiểu web từ gốc​

from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# "Database" trong bộ nhớ
SACH = {
1: {"id": 1, "ten": "Lập trình Python", "gia": 250000},
2: {"id": 2, "ten": "Cấu trúc dữ liệu", "gia": 180000},
}
_bo_dem = 2


@app.get("/api/sach")
def danh_sach_sach():
return jsonify(list(SACH.values()))


@app.get("/api/sach/<int:sach_id>")
def chi_tiet_sach(sach_id):
sach = SACH.get(sach_id)
if sach is None:
abort(404, description=f"Không tìm thấy sách id={sach_id}")
return jsonify(sach)


@app.post("/api/sach")
def them_sach():
global _bo_dem
du_lieu = request.get_json(silent=True) or {}

if not du_lieu.get("ten"):
abort(400, description="Thiếu trường 'ten'")
if not isinstance(du_lieu.get("gia"), int) or du_lieu["gia"] <= 0:
abort(400, description="'gia' phải là số nguyên dương")

_bo_dem += 1
SACH[_bo_dem] = {"id": _bo_dem, **du_lieu}
return jsonify(SACH[_bo_dem]), 201


@app.put("/api/sach/<int:sach_id>")
def cap_nhat_sach(sach_id):
if sach_id not in SACH:
abort(404)
du_lieu = request.get_json(silent=True) or {}
SACH[sach_id] = {"id": sach_id, **du_lieu}
return jsonify(SACH[sach_id])


@app.delete("/api/sach/<int:sach_id>")
def xoa_sach(sach_id):
if SACH.pop(sach_id, None) is None:
abort(404)
return "", 204


@app.errorhandler(404)
def khong_tim_thay(e):
return jsonify({"loi": str(e.description)}), 404


@app.errorhandler(400)
def du_lieu_sai(e):
return jsonify({"loi": str(e.description)}), 400

Đọc kỹ đoạn này — nó chứa gần hết những gì cần biết về REST API cơ bản: routing, path param, validate, mã trạng thái, error handler.

Dự án giai đoạn 3​

API quản lý thư viện — CRUD đầy đủ cho 2 bảng (sách, người mượn), có validate và error handler. Đây là dự án bạn sẽ nâng cấp qua các giai đoạn sau.


🗄️ Giai đoạn 4 (tuần 21–28): Database và ORM​

SQL — không thể bỏ qua​

Cho dù dùng ORM, bạn phải hiểu SQL. Không hiểu SQL nghĩa là không debug được query chậm.

-- Tạo bảng với ràng buộc đầy đủ
CREATE TABLE tac_gia (
id SERIAL PRIMARY KEY,
ten VARCHAR(120) NOT NULL,
email VARCHAR(120) UNIQUE NOT NULL
);

CREATE TABLE sach (
id SERIAL PRIMARY KEY,
ten VARCHAR(200) NOT NULL,
gia NUMERIC(12,0) NOT NULL CHECK (gia > 0),
tac_gia_id INTEGER NOT NULL REFERENCES tac_gia(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW()
);

-- INDEX — thứ quyết định query nhanh hay chậm
CREATE INDEX idx_sach_tac_gia ON sach(tac_gia_id);

-- JOIN + GROUP BY + HAVING
SELECT t.ten,
COUNT(s.id) AS so_sach,
SUM(s.gia) AS tong_gia_tri
FROM tac_gia t
LEFT JOIN sach s ON s.tac_gia_id = t.id
GROUP BY t.id, t.ten
HAVING COUNT(s.id) > 1
ORDER BY tong_gia_tri DESC
LIMIT 10;

Bốn khái niệm SQL quan trọng nhất:

Khái niệmÝ nghĩaVì sao quan trọng
INDEXCấu trúc tăng tốc tra cứuQuery 5 giây → 5 mili-giây
JOINKết hợp nhiều bảng90% query thật cần JOIN
TRANSACTIONNhóm thao tác "tất cả hoặc không gì cả"Chuyển tiền: trừ A và cộng B phải cùng thành công
EXPLAINXem kế hoạch thực thi queryCông cụ debug hiệu năng số 1
-- Luôn dùng EXPLAIN khi query chậm
EXPLAIN ANALYZE SELECT * FROM sach WHERE tac_gia_id = 5;

ORM với SQLAlchemy​

from datetime import datetime
from decimal import Decimal
from sqlalchemy import String, Numeric, ForeignKey, DateTime, func, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session


class Base(DeclarativeBase):
pass


class TacGia(Base):
__tablename__ = "tac_gia"

id: Mapped[int] = mapped_column(primary_key=True)
ten: Mapped[str] = mapped_column(String(120))
email: Mapped[str] = mapped_column(String(120), unique=True)

sach: Mapped[list["Sach"]] = relationship(back_populates="tac_gia", cascade="all, delete-orphan")

def __repr__(self) -> str:
return f"TacGia(id={self.id}, ten={self.ten!r})"


class Sach(Base):
__tablename__ = "sach"

id: Mapped[int] = mapped_column(primary_key=True)
ten: Mapped[str] = mapped_column(String(200))
gia: Mapped[Decimal] = mapped_column(Numeric(12, 0))
tac_gia_id: Mapped[int] = mapped_column(ForeignKey("tac_gia.id"))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())

tac_gia: Mapped[TacGia] = relationship(back_populates="sach")


# Sử dụng
with Session(engine) as session:
tg = TacGia(ten="Nguyễn Văn A", email="a@example.com")
tg.sach = [Sach(ten="Python cơ bản", gia=Decimal("250000"))]
session.add(tg)
session.commit()

# Truy vấn với eager loading — tránh N+1 query
stmt = (
select(TacGia)
.join(TacGia.sach)
.where(Sach.gia > 200000)
.options(selectinload(TacGia.sach))
)
for tac_gia in session.scalars(stmt):
print(tac_gia.ten, [s.ten for s in tac_gia.sach])

🔥 Vấn đề N+1 query là lỗi hiệu năng phổ biến nhất với ORM. Nếu bạn lặp qua 100 tác giả và mỗi lần truy cập tac_gia.sach lại phát sinh 1 query → 101 query. selectinload/joinedload giải quyết việc này.

Dự án giai đoạn 4​

Nâng cấp API thư viện: chuyển từ dict trong bộ nhớ sang PostgreSQL + SQLAlchemy, thêm relationship, thêm index, viết 5 query phức tạp có JOIN + GROUP BY.


🔐 Giai đoạn 5 (tuần 29–36): API chuyên nghiệp​

Xác thực với JWT​

from datetime import datetime, timedelta, timezone
from passlib.context import CryptContext
import jwt

SECRET = "đọc-từ-biến-môi-trường-trong-thực-tế"
THUAT_TOAN = "HS256"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def bam_mat_khau(mat_khau: str) -> str:
return pwd_context.hash(mat_khau)


def kiem_tra_mat_khau(mat_khau: str, da_bam: str) -> bool:
return pwd_context.verify(mat_khau, da_bam)


def tao_token(user_id: int, vai_tro: str = "user") -> str:
payload = {
"sub": str(user_id),
"vai_tro": vai_tro,
"exp": datetime.now(timezone.utc) + timedelta(hours=24),
"iat": datetime.now(timezone.utc),
}
return jwt.encode(payload, SECRET, algorithm=THUAT_TOAN)


def giai_ma_token(token: str) -> dict:
return jwt.decode(token, SECRET, algorithms=[THUAT_TOAN])

Ba quy tắc bảo mật không được vi phạm:

  1. Không bao giờ lưu mật khẩu dạng thô — luôn băm bằng bcrypt/argon2 (không dùng MD5/SHA1).
  2. Không bao giờ để secret trong code — dùng biến môi trường.
  3. Luôn kiểm tra quyền — xác thực (bạn là ai) khác uỷ quyền (bạn được làm gì).

Phân quyền​

from functools import wraps
from flask import request, jsonify, g


def yeu_cau_dang_nhap(f):
@wraps(f)
def wrapper(*args, **kwargs):
header = request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return jsonify({"loi": "Thiếu token"}), 401
try:
g.nguoi_dung = giai_ma_token(header[7:])
except jwt.ExpiredSignatureError:
return jsonify({"loi": "Token đã hết hạn"}), 401
except jwt.InvalidTokenError:
return jsonify({"loi": "Token không hợp lệ"}), 401
return f(*args, **kwargs)
return wrapper


def yeu_cau_quyen(vai_tro_can: str):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
if g.nguoi_dung.get("vai_tro") != vai_tro_can:
return jsonify({"loi": "Không có quyền truy cập"}), 403
return f(*args, **kwargs)
return wrapper
return decorator


@app.delete("/api/sach/<int:sach_id>")
@yeu_cau_dang_nhap
@yeu_cau_quyen("admin")
def xoa_sach(sach_id):
...

Validation với Pydantic​

from pydantic import BaseModel, Field, EmailStr, field_validator
from decimal import Decimal


class SachVao(BaseModel):
ten: str = Field(min_length=1, max_length=200)
gia: Decimal = Field(gt=0, le=100_000_000)
tac_gia_email: EmailStr

@field_validator("ten")
@classmethod
def chuan_hoa_ten(cls, v: str) -> str:
return " ".join(v.split())


sach = SachVao.model_validate({"ten": " Python cơ bản ", "gia": "250000",
"tac_gia_email": "a@example.com"})
print(sach.ten) # "Python cơ bản" (đã chuẩn hoá)

Testing — kỹ năng phân biệt junior và mid​

import pytest
from app import app, SACH


@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as c:
yield c


@pytest.fixture(autouse=True)
def reset_du_lieu():
"""Mỗi test chạy trên dữ liệu sạch."""
goc = SACH.copy()
yield
SACH.clear()
SACH.update(goc)


def test_lay_danh_sach(client):
r = client.get("/api/sach")
assert r.status_code == 200
assert len(r.get_json()) == 2


def test_lay_sach_khong_ton_tai(client):
r = client.get("/api/sach/9999")
assert r.status_code == 404
assert "loi" in r.get_json()


def test_them_sach_thieu_ten(client):
r = client.post("/api/sach", json={"gia": 100000})
assert r.status_code == 400


def test_them_sach_thanh_cong(client):
r = client.post("/api/sach", json={"ten": "Sách mới", "gia": 99000})
assert r.status_code == 201
assert r.get_json()["ten"] == "Sách mới"


@pytest.mark.parametrize("gia", [0, -100, "abc", None])
def test_them_sach_gia_khong_hop_le(client, gia):
r = client.post("/api/sach", json={"ten": "X", "gia": gia})
assert r.status_code == 400

Chạy: pytest -v --cov=app để xem độ phủ test.


🚀 Giai đoạn 6 (tuần 37–44): Chọn Django hoặc FastAPI​

Sau khi đã hiểu Flask và web từ gốc, hãy chọn một hướng chuyên sâu.

Chọn Django nếu bạn muốn​

  • Làm sản phẩm hoàn chỉnh nhanh: có admin, auth, ORM, form sẵn.
  • Làm việc ở công ty có nhiều dự án web truyền thống.
  • Cần CMS, ERP, hệ thống quản lý nội bộ.

Điểm mạnh độc nhất: Django Admin cho bạn giao diện quản trị hoàn chỉnh miễn phí.

Chọn FastAPI nếu bạn muốn​

  • Làm API cho mobile/frontend hoặc phục vụ model ML.
  • Cần hiệu năng cao, xử lý nhiều request đồng thời.
  • Thích code hiện đại với type hints và tự sinh tài liệu.

Điểm mạnh độc nhất: tài liệu Swagger tự sinh — bạn giao API cho team frontend và họ tự đọc.

Nếu chưa quyết được: học FastAPI trước (nhỏ hơn, học nhanh), rồi học Django sau (dễ hơn nhiều khi đã nắm web).


🐳 Giai đoạn 7 (tuần 45–48): Docker, deploy, CI/CD​

Docker — đóng gói để "chạy được ở mọi máy"​

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd --create-home appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]
# docker-compose.yml — chạy app + database cùng lúc
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s

api:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://app:secret@db:5432/mydb
depends_on:
db:
condition: service_healthy

volumes:
pgdata:
docker compose up --build     # chạy toàn bộ hệ thống bằng 1 lệnh

CI/CD với GitHub Actions​

name: CI

on:
push:
branches: [main]
pull_request:

jobs:
kiem-tra:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 5

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Cài phụ thuộc
run: pip install -r requirements.txt -r requirements-dev.txt

- name: Kiểm tra định dạng
run: ruff check .

- name: Kiểm tra kiểu
run: mypy app

- name: Chạy test
run: pytest -v --cov=app --cov-report=term-missing
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb

- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .

Checklist bảo mật trước khi deploy​

  • DEBUG = False trên production
  • Mọi secret đọc từ biến môi trường
  • HTTPS bắt buộc
  • CORS chỉ cho domain cụ thể (không *)
  • Rate limiting trên endpoint đăng nhập
  • Validate toàn bộ input, dùng tham số hoá SQL
  • Có logging và health check
  • Backup database tự động

🎯 Giai đoạn 8 (tuần 49–52): Dự án tổng hợp và phỏng vấn​

Dự án tổng hợp — chọn 1, làm thật sâu​

Ý tưởngCông nghệĐiểm mạnh khi phỏng vấn
Nền tảng blog có APIFastAPI + PostgreSQL + JWT + DockerThể hiện mọi kỹ năng
Hệ thống đặt hàngDjango + DRF + Celery + RedisThể hiện xử lý nghiệp vụ phức tạp
API phân tích dữ liệu công khaiFastAPI + pandas + cacheThể hiện xử lý dữ liệu + hiệu năng
Hệ thống quản lý nội bộDjango + Admin + PostgreSQLGần với công việc thật nhất

Checklist dự án đủ tốt:

  • Có README với ảnh chụp màn hình
  • Có Docker, chạy được bằng 1 lệnh
  • Có test với coverage > 70%
  • Có CI chạy tự động
  • Có tài liệu API (Swagger/OpenAPI)
  • Có deploy thật, có link truy cập được
  • Có migration database
  • Có xử lý lỗi và logging tử tế

Bộ câu hỏi phỏng vấn backend Python​

Python:

  1. List comprehension và generator expression khác nhau thế nào?
  2. *args và **kwargs — dùng khi nào?
  3. Vì sao def f(x=[]) là lỗi? Giải thích cơ chế.
  4. Shallow copy và deep copy — phân biệt và ví dụ.
  5. Decorator hoạt động ra sao? Viết decorator đo thời gian.
  6. GIL ảnh hưởng gì tới đa luồng? Khi nào dùng multiprocessing?
  7. __slots__ dùng để làm gì?
  8. Context manager là gì? Viết một cái.

Web & API:

  1. Khi nào dùng PUT và khi nào dùng PATCH?
  2. Làm sao chống spam/brute-force cho endpoint đăng nhập?
  3. Xử lý file upload lớn như thế nào?
  4. Idempotency trong API là gì? Vì sao quan trọng với thanh toán?
  5. CORS là gì? Vì sao browser chặn request?
  6. Làm sao versioning API?

Database:

  1. Index hoạt động thế nào? Khi nào index làm chậm?
  2. Transaction isolation level — có mấy mức?
  3. N+1 query là gì và cách phát hiện?
  4. Vì sao dùng connection pool?
  5. Optimistic vs pessimistic locking?

Kiến trúc:

  1. Khi nào cần cache? Cache invalidation ra sao?
  2. Xử lý tác vụ nặng (gửi email, xử lý ảnh) — dùng gì?
  3. Làm sao scale ứng dụng khi có 1 triệu người dùng?
  4. Monolith vs microservices — khi nào chọn cái nào?

📖 Kỹ năng đọc code — thứ không ai dạy bạn​

Trường học dạy bạn viết code. Không ai dạy bạn đọc code — nhưng đó là 50% công việc thật.

Khi vào công ty, việc đầu tiên của bạn gần như chắc chắn là: "Đọc codebase này đi, rồi sửa một bug nhỏ." Codebase có thể 50.000 dòng, do 20 người viết trong 5 năm, không có tài liệu.

Cách đọc một codebase lạ​

Bước 1 — Chạy được nó trước. Đừng đọc. Hãy chạy:

git clone <repo>
cd <repo>
python -m venv .venv && .venv\Scripts\activate
pip install -r requirements.txt
# đọc README để biết cách chạy
pytest

Nếu test chạy được, bạn đã có "lưới an toàn": sửa sai sẽ bị test phát hiện.

Bước 2 — Tìm điểm vào (entry point). Với web app, đó là nơi route được đăng ký:

# Tìm tất cả route
grep -rn "@app.route\|@app.get\|@app.post" app/
grep -rn "urlpatterns\|APIRouter" .

Bước 3 — Đi theo một luồng duy nhất. Đừng đọc lan man. Chọn một API endpoint, rồi lần theo:

Request → Route → Validation → Business logic → Database → Response

Ví dụ với POST /api/don-hang:

  1. Route nằm ở file nào?
  2. Hàm route gọi hàm nào tiếp?
  3. Dữ liệu được validate ở đâu?
  4. Logic nghiệp vụ nằm ở service nào, hay nằm luôn trong route?
  5. Truy vấn database nằm ở đâu?

Chỉ cần làm điều này với 3–5 endpoint là bạn đã hiểu kiến trúc dự án.

Bước 4 — Vẽ sơ đồ. Vẽ ra giấy cấu trúc thư mục và luồng dữ liệu. Nghe có vẻ trẻ con, nhưng nó giúp bạn nhớ và giúp bạn đặt câu hỏi đúng khi hỏi đồng nghiệp.

Bước 5 — Sửa bug nhỏ đầu tiên. Chọn bug nhỏ nhất trong issue tracker. Sửa nó, viết test cho nó, mở Pull Request. Đây là cách nhanh nhất để được coi là thành viên thật của team.

Đọc source của thư viện bạn dùng​

Đây là cách học nhanh nhất mà ít người làm. Khi bạn không hiểu Flask hoặc FastAPI xử lý một thứ gì đó, hãy mở source:

# Xem source ngay trong terminal
python -c "import flask, inspect; print(inspect.getsourcefile(flask))"

# Hoặc trong Python
import requests, inspect
print(inspect.getsource(requests.Session.request))

Đọc code của thư viện nổi tiếng giúp bạn thấy cách người giỏi viết code: cách họ đặt tên, cách họ xử lý lỗi, cách họ tổ chức module.


🧪 Cách luyện tập hiệu quả trong 12 tháng​

Quy tắc 70/20/10​

Tỷ lệHoạt độngGhi chú
70%Tự viết code, làm dự ánPhần quan trọng nhất
20%Đọc code người khác, đọc docs/sourceHọc từ người giỏi
10%Xem video, đọc sách, học lý thuyếtChỉ để gợi mở

Hầu hết người mới làm ngược lại: 80% xem video, 20% gõ code. Đó là lý do họ học mãi không tiến bộ.

Cách luyện "không copy-paste"​

Cách duy nhất để thực sự nhớ:

  1. Đọc ví dụ trong tài liệu.
  2. Đóng tài liệu lại.
  3. Viết lại từ đầu, không nhìn.
  4. Bí quá thì mở ra xem 5 giây rồi đóng lại.
  5. Sau khi chạy được, so sánh code bạn viết với code mẫu — khác chỗ nào, vì sao?

Cách này đau đớn hơn nhiều so với copy-paste. Nhưng nó là sự khác biệt giữa "biết" và "biết làm".

Lịch luyện tập mẫu theo tuần​

NgàyThời lượngNội dung
Thứ 290 phútHọc khái niệm mới + bài tập
Thứ 390 phútLuyện thuật toán (LeetCode easy)
Thứ 490 phútLàm dự án cá nhân
Thứ 590 phútĐọc code người khác hoặc đọc source thư viện
Thứ 690 phútLàm dự án cá nhân
Thứ 7120 phútHoàn thiện dự án + viết README
Chủ nhậtNghỉNão cần thời gian củng cố

Tổng: khoảng 10–11 giờ/tuần. Con số này thực tế hơn nhiều so với kế hoạch "học 4 giờ mỗi ngày" mà không ai duy trì được quá 2 tuần.

Ba câu hỏi tự kiểm tra mỗi tuần​

  1. Tuần này tôi viết được bao nhiêu dòng code tự nghĩ, không copy?
  2. Tôi có thể giải thích thứ tôi vừa học cho người khác không?
  3. Dự án tôi đang làm có tiến triển nhìn thấy được không?

Nếu cả ba câu đều "không", bạn đang lãng phí thời gian — hãy đổi cách học.


⚠️ 8 sai lầm khiến bạn tốn thêm 1 năm​

  1. Học 3 framework cùng lúc. Chọn 1, đi đến nơi đến chốn.
  2. Không học SQL vì "đã có ORM". ORM không cứu bạn khi query chạy 30 giây.
  3. Không viết test. Bạn sẽ không được nhận vào vị trí mid/senior.
  4. Không học Docker. Mọi team chuyên nghiệp đều dùng.
  5. Chỉ làm dự án todo list. Nhà tuyển dụng đã thấy 1000 cái như thế.
  6. Không đọc code của người khác. Đọc Django/FastAPI source dạy bạn nhiều hơn tutorial.
  7. Học mà không deploy. Dự án không deploy = dự án chưa xong.
  8. Không chuẩn bị phỏng vấn. Biết code mà không diễn đạt được thì vẫn trượt.

❓ Câu hỏi thường gặp​

Backend Python có còn dễ xin việc không? Có, đặc biệt ở mảng API, data platform, automation. Nhu cầu cao nhưng yêu cầu cũng cao hơn 5 năm trước — biết CRUD cơ bản không còn đủ, cần Docker, testing, cloud.

Cần học frontend không? Không cần thành thạo, nhưng hiểu HTML/CSS/JS cơ bản giúp bạn phối hợp với frontend tốt hơn nhiều. Biết React là lợi thế lớn.

Có cần bằng đại học? Không bắt buộc, nhưng nếu không có, portfolio và kinh nghiệm phải thật nổi bật.

Bao lâu để thành mid-level? Thường 2–3 năm làm việc thật. Học 12 tháng đưa bạn tới junior; từ junior lên mid cần kinh nghiệm dự án thật, không phải học thêm khoá học.

Nên học async Python không? Có, nhưng chỉ sau khi vững sync. Hiểu async/await và khi nào không nên dùng async quan trọng hơn là biết cú pháp.


🎯 Tóm lại​

Lộ trình backend Python trong 12 tháng:

8 tuần  → Python + OOP vững
4 tuần → Git + Linux + công cụ
8 tuần → Web + Flask
8 tuần → Database + SQL + ORM
8 tuần → API chuyên nghiệp + Auth + Test
8 tuần → Django HOẶC FastAPI
4 tuần → Docker + Deploy + CI/CD
4 tuần → Dự án tổng hợp + phỏng vấn

Điều duy nhất quyết định thành công không phải là lộ trình này, mà là bạn có gõ code mỗi ngày hay không.


📚 Bài viết liên quan​

Lộ trình học Python cho người muốn làm Data Analyst

· 25 phút để đọc

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 độngTỷ lệ thời gianGhi chú
Viết SQL lấy dữ liệu30%Kỹ năng #1, không phải Python
Làm sạch và kiểm tra dữ liệu20%Phần "không ai thấy" nhưng tốn thời gian nhất
Phân tích, tìm insight20%Phần thú vị nhất
Trực quan hoá và làm báo cáo20%Dashboard, slide, báo cáo định kỳ
Họp, trao đổi với stakeholder10%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àmCú pháp
Chọn cộtdf[["ten", "gia"]]
Lọc dòngdf[df["gia"] > 100000]
Lọc nhiều điều kiệndf[(df.gia > 1e5) & (df.thanh_pho == "Hà Nội")]
Lọc theo danh sáchdf[df.thanh_pho.isin(["Hà Nội", "Đà Nẵng"])]
Sắp xếpdf.sort_values("gia", ascending=False)
Tạo cột mớidf["gia_vnd"] = df["gia_usd"] * 25400
Đổi tên cộtdf.rename(columns={"ten_kh": "khach_hang"})
Nhóm và tổng hợpdf.groupby("thanh_pho")["gia"].sum()
Nhóm nhiều hàmdf.groupby("thanh_pho").agg(so_don=("id","count"), dt=("gia","sum"))
Pivotdf.pivot_table(index="thang", columns="thanh_pho", values="gia", aggfunc="sum")
Nối dọcpd.concat([df1, df2], ignore_index=True)
Nối ngangdf1.merge(df2, on="khach_hang_id", how="left")
Ngày thángdf["ngay_dat"].dt.to_period("M"), .dt.dayofweek
Chuỗidf["ten"].str.strip().str.title()
Áp dụng hàmdf["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 WHERE thay vì ON sẽ 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 khiKHÔNG dùng khi
CộtSo sánh giữa các nhómNhóm > 12 loại
Cột ngangTên nhóm dài—
ĐườngXu hướng theo thời gianDữ liệu không có thứ tự
TrònTối đa 4–5 phầnSo sánh chính xác giữa các phần
HistogramPhân phối một biến sốSo sánh nhóm
Box plotSo sánh phân phối nhiều nhómNgười xem không quen thống kê
ScatterTương quan hai biếnBiến phân loại
HeatmapMa trận quan hệBảng nhỏ

Bốn quy tắc trực quan hoá chuyên nghiệp:

  1. 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.
  2. 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".
  3. Bỏ mọi mực thừa. Xoá gridline thừa, khung, 3D, đổ bóng.
  4. 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:

  1. 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.
  2. 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.
  3. 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ànhCách đặt lịch
WindowsTask Scheduler
macOS / Linuxcron: 0 8 * * 1 python /path/bao_cao.py
CloudGitHub Actions schedule:
Nâng caoAirflow, 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 BIMiễn phí bản desktopPhổ biến nhất ở doanh nghiệp VN
TableauCó phí (đắt)Doanh nghiệp lớn, quốc tế
StreamlitMiễn phí, mã nguồn mởTeam kỹ thuật, cần tuỳ biến
Looker StudioMiễ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ự ánKỹ 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ự độngTrực quan hoá, Streamlit/Power BI, tự động hoá
Phân tích A/B testThố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ả
![Biểu đồ RFM](anh-rfm.png)
- 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òngNội dungChuẩn bị gì
1Sàng lọc với HRKể câu chuyện nghề nghiệp 2 phút
2SQL test (thường live coding)Luyện 50 bài SQL
3Python/pandas testgroupby, merge, làm sạch dữ liệu
4Case study nghiệp vụ"Doanh thu giảm 20%, bạn kiểm tra gì?"
5Văn hoá & hành viCâu hỏi STAR

Câu hỏi SQL hay được hỏi:

  1. Viết truy vấn tìm khách hàng có trên 5 đơn trong 30 ngày qua.
  2. Tính doanh thu tích luỹ theo tháng (running total).
  3. Tìm khách hàng chưa từng mua trong 90 ngày.
  4. Tính tỷ lệ giữ chân khách hàng theo cohort.
  5. Tìm sản phẩm bán chạy thứ 2 trong mỗi danh mục.
  6. 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​

  1. 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.
  2. 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.
  3. 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ị.
  4. Kết luận nhân quả từ tương quan. Sai lầm nghiêm trọng nhất.
  5. 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ì.
  6. 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.


📚 Bài viết liên quan​