Linear regression: dự đoán thời gian giao hàng
Xem thửLan phụ trách điều phối shipper của Giao Hàng Anh Tí. Mỗi sáng chị phải trả lời những câu hỏi rất cụ thể: "Hôm nay mưa thì đơn chậm thêm bao nhiêu phút?", "Đơn vào Khu Cong Nghiep chậm hơn đơn ở Dau Pho bao nhiêu?", "Giao xe đạp thay xe máy thì mất thêm bao lâu?". Bài trước, mô hình của Minh sai trung bình 10,46 phút, nhưng Lan không cần một con số sai số. Chị cần những con số có thể nói với shipper.
Bộ dữ liệu mô phỏng có một lợi thế hiếm gặp: ta biết luật sinh dữ liệu (data card ở Chương 1). Ví dụ trời mưa cộng trung bình 12 phút trước khi thêm nhiễu, giờ cao điểm cộng 15 phút, mỗi km cộng 6,5 phút. Mô hình không được xem data card; nó chỉ thấy 33.312 đơn tháng 1 đến tháng 4 với nhiễu ngẫu nhiên.
Hãy dự đoán trước khi đọc tiếp: mô hình hồi quy tuyến tính học từ 33.312 đơn sẽ ước lượng "trời mưa cộng bao nhiêu phút": gần 12, gần 6, hay một con số không đọc được? Và nếu hỏi nó về một shipper có 120 tháng kinh nghiệm (dữ liệu chỉ có tới 36 tháng), nó sẽ nói gì?
Sau bài này, bạn có thể
- Viết công thức dự đoán của hồi quy tuyến tính và tính tay một dự đoán từ các hệ số.
- Đọc mỗi hệ số thành câu "thêm một đơn vị đặc trưng thì cộng bao nhiêu phút, khi các đặc trưng khác giữ nguyên", kể cả hệ số của cột one-hot.
- Khớp mô hình bằng NumPy (bình phương tối thiểu) và bằng scikit-learn, rồi đối chiếu hệ số học được với luật sinh dữ liệu.
- Nhận ra khi mô hình tuyến tính ngoại suy sai ngoài vùng dữ liệu, và sửa bằng một đặc trưng có hình dạng đúng.
Cần biết trước: bài Học có giám sát: train, validation, test và baseline (bài trước), bài Gradient descent và learning rate (Chương 5) và bài Mã hóa biến phân loại và chuẩn hóa thang đo (Chương 6).
Mức độ: Cơ bản · Thời gian: khoảng 20 phút · Từ khóa: hồi quy tuyến tính, hệ số, intercept, bình phương tối thiểu, ngoại suy
Một tổng có trọng số
Hồi quy tuyến tính (linear regression) dự đoán bằng một tổng: một hằng số cộng với mỗi đặc trưng nhân một hệ số.
ŷ = b + w_1 · x_1 + w_2 · x_2 + ... + w_d · x_d
Trong đó x_1 đến x_d là các đặc trưng của một đơn (km, mưa, cao điểm, express...), w_1 đến w_d là các hệ số (coefficient), b là hệ số chặn (intercept), tức dự đoán khi mọi đặc trưng bằng 0. Bạn đã gặp đúng dạng này ở Chương 3: đó là tích vô hướng giữa vector đặc trưng và vector hệ số, cộng thêm b.
Với bốn đặc trưng distance_km, is_rain, is_peak, is_express, mô hình học từ train cho:
ŷ = 33,83 + 6,32 · km + 13,26 · mưa + 15,22 · cao_điểm - 20,05 · express
Ví dụ số: một đơn standard 5 km, trời mưa, ngoài giờ cao điểm.
ŷ = 33,83 + 6,32 × 5 + 13,26 × 1 + 15,22 × 0 - 20,05 × 0
= 33,83 + 31,60 + 13,26 = 78,69 phút
Mô hình tư duy của bài: mỗi hệ số là câu trả lời cho "thêm một đơn vị đặc trưng thì cộng bao nhiêu phút", khi mọi đặc trưng khác giữ nguyên. Hệ số 6,32 nghĩa là mỗi km cộng 6,32 phút. Hệ số -20,05 của is_express nghĩa là cùng quãng đường, cùng thời tiết, đơn express nhanh hơn khoảng 20 phút. Đây chính là loại câu Lan cần.
Mô hình chọn hệ số thế nào: bình phương tối thiểu
Có vô số bộ hệ số có thể. Hồi quy tuyến tính chọn bộ làm cho tổng bình phương phần dư (residual, hiệu giữa số phút thật và số phút dự đoán) nhỏ nhất:
loss(w, b) = Σ (y_i - ŷ_i)² với i chạy qua mọi đơn trong train
Bình phương làm mọi phần dư thành số dương và phạt nặng các đơn bị đoán lệch xa. Ở Chương 5 bạn tối thiểu một hàm như thế bằng gradient descent, đi từng bước nhỏ. Với hồi quy tuyến tính có một lối tắt: bài toán có lời giải đóng (closed-form), giải trực tiếp bằng đại số tuyến tính, gọi là bình phương tối thiểu (least squares). np.linalg.lstsq của NumPy và LinearRegression của scikit-learn đều dùng lối tắt này, nên không cần chọn learning rate hay số vòng lặp.
Một hệ quả nên nhớ: vì phạt bình phương, mô hình nhắm tới trung bình có điều kiện của số phút. Nhiễu của dữ liệu là nhiễu nhân (đơn càng lâu, dao động càng lớn) cộng thêm một phần nhiễu luôn dương, nên trung bình thật cao hơn luật gốc một chút. Đừng ngạc nhiên khi hệ số học được hơi lớn hơn con số trong data card.
Đọc hệ số của cột one-hot và cột đã chuẩn hóa
Không phải hệ số nào cũng đọc theo "mỗi km". Sơ đồ sau là cách Lan nên đọc một hệ số bất kỳ trong bảng kết quả.
Đang tải sơ đồ…
Cách đọc hình: bắt đầu từ ô cyan (một hệ số trong bảng). Ô cam đầu tiên hỏi cột có phải one-hot không. Cột one-hot như district_Khu Cong Nghiep đọc là "chênh bao nhiêu phút so với quận mốc", ở đây quận mốc là Dau Pho. Nếu cột đã chuẩn hóa (trừ trung bình, chia độ lệch chuẩn), hệ số là số phút cho mỗi độ lệch chuẩn, không phải mỗi đơn vị gốc. Cột số để nguyên đọc theo đơn vị gốc. Cả ba nhánh gặp nhau ở ô cam cuối: giá trị bạn hỏi có nằm trong vùng dữ liệu đã học không. Nếu không, đi sang ô đỏ: đó là ngoại suy (extrapolation), mô hình không có bằng chứng nào cho câu trả lời.
Ô đỏ là điều dễ quên nhất. Animation dưới đây cho bạn thấy nó trên đúng câu hỏi của Lan về shipper 120 tháng kinh nghiệm. Bấm biến thể để so với một mô hình có đặc trưng được sửa hình dạng.
Đang tải Animation…
Làm cùng mình
Bước 1: tự giải bình phương tối thiểu bằng NumPy.
COLS = ["distance_km", "is_rain", "is_peak", "is_express"]
def design(df):
# Cột 1 đầu tiên để mô hình có hệ số chặn (intercept)
return np.column_stack([np.ones(len(df)), df[COLS].to_numpy(dtype=float)])
X, y = design(train), train[TARGET].to_numpy()
w, *_ = np.linalg.lstsq(X, y, rcond=None)
for name, value in zip(["intercept"] + COLS, w):
print(f"{name:12} {value:7.2f}")
pred = design(valid) @ w
print(f"valid MAE {np.abs(valid[TARGET].to_numpy() - pred).mean():.2f} min")
| Dòng code | Máy làm gì | Vì sao |
|---|---|---|
np.column_stack([np.ones(...), ...]) | Thêm cột toàn số 1 trước các đặc trưng | Hệ số của cột này chính là intercept b |
np.linalg.lstsq(X, y, rcond=None) | Giải hệ cho tổng bình phương phần dư nhỏ nhất | Lời giải đóng, không cần vòng lặp |
design(valid) @ w | Nhân ma trận đặc trưng với vector hệ số | Mỗi hàng là một tích vô hướng, như Chương 3 |
np.abs(...).mean() | MAE trên tháng 5 | Đo trên dữ liệu chưa học |
Kết quả [Đã chạy] Python 3.12, pandas 3.1, scikit-learn 1.9, CPU x86_64, ngày 04/11/2026:
intercept 33.83
distance_km 6.32
is_rain 13.26
is_peak 15.22
is_express -20.05
valid MAE 12.18 min
Chỉ với bốn đặc trưng, mô hình tự tìm ra: mỗi km khoảng 6,3 phút, mưa khoảng 13 phút, cao điểm khoảng 15 phút, express nhanh hơn khoảng 20 phút. MAE 12,18 phút đã tốt hơn nhiều baseline 22,06 của bài trước.
Bước 2: đủ đặc trưng, dùng scikit-learn, đối chiếu luật sinh dữ liệu.
FEATURES = NUMERIC + CATEGORICAL
# Lấy Dau Pho và motorbike làm mốc, khớp với luật sinh dữ liệu
encoder = OneHotEncoder(drop=[["Dau Pho"], ["motorbike"]])
model = make_pipeline(make_column_transformer((encoder, CATEGORICAL), remainder="passthrough",
verbose_feature_names_out=False),
LinearRegression())
model.fit(train[FEATURES], train[TARGET])
| Dòng code | Máy làm gì | Vì sao |
|---|---|---|
OneHotEncoder(drop=[["Dau Pho"], ["motorbike"]]) | One-hot quận và xe, bỏ một cột mốc mỗi nhóm | Hệ số đọc thành "chênh so với Dau Pho, so với xe máy" |
remainder="passthrough" | Giữ nguyên các cột số | Hệ số của cột số đọc theo đơn vị gốc |
make_pipeline(..., LinearRegression()) | Mã hóa rồi khớp trong một đối tượng | Bước mã hóa chỉ học từ train, như Chương 6 |
Kết quả [Đã chạy] Python 3.12, pandas 3.1, scikit-learn 1.9, CPU x86_64, ngày 04/11/2026:
learned rule
distance_km 6.63 6.5
is_rain 12.48 12.0
is_peak 15.22 15.0
is_weekend -5.39 -5.0
is_express -20.28 -20.0
district_Khu Cong Nghiep 15.78 15.0
district_Doi Thong 12.69 12.0
district_Xom Vui 4.67 4.0
vehicle_bike 9.95 10.0
vehicle_van 7.77 8.0
shipper_exp_months -0.21 -0.3
items 0.93 NaN
weight_kg 0.02 0.0
intercept 24.97
valid MAE 10.46 min
Cột learned là hệ số mô hình học được, cột rule là luật sinh dữ liệu trước khi thêm nhiễu. Mưa 12,48 so với 12; cao điểm 15,22 so với 15; Khu Cong Nghiep 15,78 so với 15; xe đạp 9,95 so với 10. Mô hình tìm lại gần đúng mọi luật, và weight_kg (không có trong luật) được 0,02 phút mỗi kg, gần như bằng 0. Đây là trường hợp lý tưởng: dữ liệu thật sự được sinh ra từ một tổng có trọng số, nên mô hình tuyến tính là mô hình đúng hình dạng.
Hai chỗ lệch đáng chú ý. items không có hệ số trong luật vì luật dùng 4 · căn(items), một đường cong; mô hình xấp xỉ nó bằng một đường thẳng 0,93 phút mỗi món. shipper_exp_months học được -0,21 thay vì -0,3, vì luật nói kinh nghiệm chỉ giúp tới 24 tháng rồi dừng, còn mô hình bắt buộc một đường thẳng cho cả 0 đến 36 tháng. Thêm toàn bộ đặc trưng đưa MAE xuống 10,46 phút, đúng con số Minh mang đi họp ở bài trước.
Bước 3: ngoại suy, tình huống thất bại.
train = train.assign(exp_capped=np.minimum(train["shipper_exp_months"], 24))
BASE = ["distance_km", "is_rain", "is_peak", "is_express"]
straight = LinearRegression().fit(train[BASE + ["shipper_exp_months"]], train[TARGET])
capped = LinearRegression().fit(train[BASE + ["exp_capped"]], train[TARGET])
print(f"shipper_exp_months in train: {train.shipper_exp_months.min()} to {train.shipper_exp_months.max()}")
print(f"straight coef {straight.coef_[-1]:.3f} min/month, capped coef {capped.coef_[-1]:.3f} min/month")
for months in [0, 12, 24, 36, 120]:
# Luật sinh dữ liệu: kinh nghiệm chỉ giúp tới 24 tháng
rule = -0.3 * min(months, 24)
print(f"{months:3} months rule {rule:6.1f} straight model {straight.coef_[-1] * months:6.1f}"
f" capped model {capped.coef_[-1] * min(months, 24):6.1f}")
Mô hình straight dùng số tháng kinh nghiệm như cũ. Mô hình capped dùng min(tháng, 24), một đặc trưng có cùng hình dạng với luật. Kết quả [Đã chạy] Python 3.12, pandas 3.1, scikit-learn 1.9, CPU x86_64, ngày 04/11/2026:
shipper_exp_months in train: 0 to 36
straight coef -0.217 min/month, capped coef -0.287 min/month
0 months rule -0.0 straight model -0.0 capped model -0.0
12 months rule -3.6 straight model -2.6 capped model -3.4
24 months rule -7.2 straight model -5.2 capped model -6.9
36 months rule -7.2 straight model -7.8 capped model -6.9
120 months rule -7.2 straight model -26.0 capped model -6.9
Ở 120 tháng, mô hình straight nói kinh nghiệm giúp giảm 26,0 phút, gấp hơn ba lần mức thật 7,2 phút. Nó không sai về phép tính: đường thẳng cứ thế kéo dài, vì không có dữ liệu nào ngoài 36 tháng để bảo nó dừng. Mô hình capped dừng ở mức giảm 6,9 phút, sát luật vì chính đặc trưng đã dừng ở 24. Bài học: mô hình tuyến tính chỉ có thể học hình dạng mà bạn cho nó qua đặc trưng.
Áp dụng vào Giao Hàng Anh Tí
Bảng hệ số ở bước 2 là thứ Lan có thể dán lên bảng điều phối: mưa cộng khoảng 12,5 phút, cao điểm 15 phút, Khu Cong Nghiep chậm hơn Dau Pho gần 16 phút, xe đạp chậm hơn xe máy 10 phút. Khi một shipper hỏi "vì sao đơn này dự kiến 79 phút?", mô hình trả lời được bằng cách liệt kê từng phần cộng, điều mà nhiều mô hình phức tạp hơn ở các bài sau không làm dễ như vậy.
Hai điều kiện đi kèm khi Lan dùng bảng này. Thứ nhất, chỉ hỏi trong vùng dữ liệu đã có: quãng đường tới khoảng 34 km, kinh nghiệm 0 đến 36 tháng. Thứ hai, mỗi hệ số là "khi các thứ khác giữ nguyên". Nếu hai đặc trưng luôn đi cùng nhau (như số món và cân nặng), mô hình có thể chia ảnh hưởng giữa chúng theo cách khó đọc; ở đây weight_kg gần 0 vì số món đã mang thông tin đó.
Hiểu lầm thường gặp
Hiểu lầm 1: "Hệ số lớn nghĩa là đặc trưng quan trọng." Sai khi các cột có đơn vị khác nhau. 6,63 phút mỗi km và 0,02 phút mỗi kg không so được trực tiếp, vì 1 km và 1 kg là hai thước khác nhau. Muốn so độ quan trọng, chuẩn hóa cột trước (hệ số thành số phút trên mỗi độ lệch chuẩn) hoặc đo MAE khi bỏ từng cột.
Hiểu lầm 2: "Hồi quy tuyến tính chỉ vẽ được đường thẳng nên không học được đường cong." Mô hình tuyến tính theo hệ số, không bắt buộc tuyến tính theo dữ liệu gốc. Đưa vào min(tháng, 24), căn(items) hay bình phương km, nó vẫn là hồi quy tuyến tính nhưng học được hình dạng cong. Cái giá là bạn phải biết hình dạng để tạo đặc trưng.
Hiểu lầm 3: "Mô hình đã học thì trả lời được mọi đầu vào." Ngoài vùng dữ liệu, mô hình chỉ kéo dài quy luật trong vùng. Shipper 120 tháng được dự đoán nhanh hơn 26 phút, không có bằng chứng nào ủng hộ.
| Triệu chứng | Nguyên nhân | Cách kiểm chứng | Cách sửa |
|---|---|---|---|
| Dự đoán âm hoặc rất nhỏ cho đơn lạ | Ngoại suy ngoài vùng train | So đầu vào với min, max của từng cột trong train | Chặn đầu vào, cảnh báo, hoặc tạo đặc trưng có hình dạng đúng |
| Hệ số one-hot khó đọc, rất lớn ngược dấu | Không bỏ cột mốc, các cột one-hot cộng lại luôn bằng 1 | Đếm cột one-hot mỗi nhóm so với số giá trị | drop một giá trị mốc có ý nghĩa nghiệp vụ |
| Hệ số khác xa hiểu biết nghiệp vụ | Hai đặc trưng tương quan mạnh chia nhau ảnh hưởng | Tính tương quan giữa các cột | Bỏ bớt một cột, hoặc dùng Ridge (bài Overfitting) |
Tự kiểm tra (có đáp án)
Câu 1 (giải thích). Hệ số của district_Doi Thong là 12,69. Đọc thành một câu cho Lan.
Đáp án: cùng quãng đường, thời tiết, giờ, dịch vụ và xe, đơn ở Doi Thong giao lâu hơn đơn ở Dau Pho (quận mốc) khoảng 12,7 phút.
Câu 2 (dự đoán). Với công thức bốn đặc trưng ở đầu bài, đơn express 2 km, trời khô, đúng giờ cao điểm được dự đoán bao nhiêu phút?
Đáp án: 33,83 + 6,32 × 2 + 15,22 - 20,05 = 41,64 phút.
Câu 3 (vận dụng). Một shipper mới chạy xe van, chưa có trong dữ liệu train vì chưa từng có đơn xe van ở quận đó. Mô hình có dự đoán được không, và cần cẩn thận điều gì?
Đáp án: dự đoán được, vì mô hình cộng hệ số của xe van và hệ số của quận một cách độc lập. Nhưng đó là giả định rằng ảnh hưởng của xe và quận cộng dồn mà không tương tác; nếu xe van ở quận đó chậm theo cách đặc biệt, mô hình tuyến tính không biết.
Tự tay làm
Trong linreg_coefs.py, thêm cột sqrt_items = np.sqrt(items) vào bảng (gợi ý: train.assign(...), valid.assign(...)) và dùng nó thay cho items. Trước khi chạy, dự đoán hệ số của sqrt_items sẽ gần con số nào trong luật sinh dữ liệu.
Kết quả mong đợi để tự kiểm: hệ số của sqrt_items khoảng 3,99 (luật là 4), và valid MAE khoảng 10,45 phút. MAE gần như không đổi vì trên khoảng 1 đến 15 món, đường cong căn bậc hai khá gần một đường thẳng; cái được là hệ số giờ đọc đúng theo luật.
Điều cần nhớ
- Hồi quy tuyến tính dự đoán bằng intercept cộng tổng các đặc trưng nhân hệ số, tức một tích vô hướng cộng hằng số.
- Mỗi hệ số đọc là "thêm một đơn vị thì cộng bao nhiêu phút, khi các thứ khác giữ nguyên"; cột one-hot đọc so với nhóm mốc.
- Hệ số được chọn bằng bình phương tối thiểu, có lời giải đóng; trên dữ liệu này nó tìm lại gần đúng luật sinh (mưa 12,48 so với 12).
- Mô hình chỉ học được hình dạng bạn đưa vào qua đặc trưng và không đáng tin ngoài vùng dữ liệu. Bài tiếp theo dùng cùng tổng có trọng số này, nhưng biến nó thành xác suất để quyết định đơn nào cần cảnh báo trễ.
Nguồn tham khảo
- scikit-learn, Linear Models, Ordinary Least Squares: https://scikit-learn.org/stable/modules/linear_model.html
- scikit-learn,
LinearRegression: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html - scikit-learn, Common pitfalls in the interpretation of coefficients of linear models: https://scikit-learn.org/stable/auto_examples/inspection/plot_linear_model_coefficient_interpretation.html
- NumPy,
numpy.linalg.lstsq: https://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html - AI Engineering from Scratch (MIT), phase 02, Linear regression: https://github.com/rohitg00/ai-engineering-from-scratch/tree/main/phases/02-ml-fundamentals/02-linear-regression