Một chuỗi bán lẻ điện tử 50 cửa hàng tại Việt Nam đang đối mặt với bài toán quen thuộc: 28% hàng tồn kho nằm quá 90 ngày trong khi 18% mặt hàng thường xuyên hết hàng mỗi tuần. Sau 16 tuần triển khai demand forecasting bằng ML, họ giảm 20% tồn kho, giải phóng 12 tỷ VNĐ vốn, và tăng tỷ lệ có hàng từ 82% lên 91%.
Theo McKinsey, ứng dụng ML trong supply chain giúp giảm 20-50% chi phí tồn kho và tăng 65% mức độ hài lòng của khách hàng. Tại Việt Nam, qua khảo sát của Carptech với hơn 40 doanh nghiệp bán lẻ và sản xuất, 72% vẫn dùng Excel để dự báo nhu cầu, chỉ 15% đã triển khai ML. Đây là cơ hội rất lớn mà nhiều doanh nghiệp đang bỏ lỡ.
Bài viết này hướng dẫn triển khai demand forecasting end-to-end: từ nền tảng time series, so sánh thuật toán (ARIMA, Prophet, XGBoost, LSTM), feature engineering, evaluation metrics, đến production deployment. Kèm case study thực tế giảm tồn kho 1,2 tỷ VNĐ/năm.
Cho Business Leaders: Nếu bạn không cần đọc chi tiết kỹ thuật, đây là tóm tắt:
- Vấn đề: Tồn kho dư (đọng vốn, hao hụt) vs Hết hàng (mất doanh thu) -- dự báo giúp cân bằng cả hai
- Kết quả: ML giảm sai số dự báo từ 35-45% xuống 18-25%, tương đương tiết kiệm 15-25% chi phí tồn kho
- Thời gian: Bắt đầu với Prophet trong 2-4 tuần, mở rộng với XGBoost nếu cần chính xác hơn
- ROI: 500-2.000% sau 3 năm, hoàn vốn trong 3-6 tháng
- Bước tiếp theo: Đặt lịch tư vấn miễn phí để đánh giá hiện trạng dự báo của doanh nghiệp bạn
Tóm tắt -- Key Takeaways
- Vấn đề: Overstock (đọng vốn, hao hụt) vs Stockout (mất doanh thu) -- dự báo giúp cân bằng cả hai
- Thuật toán: Prophet (dễ nhất, tốt cho mùa vụ), XGBoost (chính xác nhất khi có features), LSTM (phức tạp, cần dữ liệu lớn)
- Độ chính xác: ML giảm MAPE từ 35-45% (truyền thống) xuống 18-25%, tương đương tiết kiệm 15-25% chi phí tồn kho
- Features: Dữ liệu bán hàng lịch sử, mùa vụ, khuyến mãi, thời tiết, ngày lễ, sự kiện -- thường 15-25 features
- Triển khai: Bắt đầu với Prophet (2-4 tuần), nâng cấp XGBoost nếu cần chính xác hơn
- ROI: Giảm 15-25% tồn kho, đạt 90-95% service level (tỷ lệ có hàng)
Tại sao Demand Forecasting quan trọng: tác động kinh doanh
Bài toán cân bằng tồn kho
Tồn kho quá nhiều (Overstock):
Chi phí:
- Vốn bị đọng: 100 triệu VNĐ tồn kho x 12% lãi suất = 12 triệu VNĐ/năm
- Lưu kho: Thuê kho, bốc xếp, bảo hiểm (~8-10% giá trị tồn kho/năm)
- Lỗi thời: Thời trang (giảm giá 50%), Điện tử (giảm 20%/năm), Thực phẩm (hư hỏng)
- Tổng chi phí: 25-30% giá trị tồn kho/năm
Tồn kho quá ít (Stockout):
Chi phí:
- Mất doanh thu: Khách mua hàng đối thủ (4-8% doanh thu bị mất)
- Khách hàng không hài lòng: Có thể không quay lại (ảnh hưởng LTV)
- Đặt hàng khẩn: Chi phí vận chuyển gấp 2-3 lần bình thường
Tồn kho tối ưu: Cân bằng giữa hai chi phí
Mục tiêu: Tối thiểu hóa (Chi phí giữ hàng + Chi phí hết hàng)
Demand forecasting giúp:
- Đặt hàng đúng số lượng
- Đặt hàng đúng thời điểm
- Đạt target service level (ví dụ: 95% tỷ lệ có hàng)
Ví dụ ROI: chuỗi bán lẻ
Kịch bản:
- 100 cửa hàng, 1.000 SKU/cửa hàng
- Tồn kho hiện tại: trung bình 500 triệu VNĐ
- MAPE hiện tại: 40% (dự báo truyền thống)
- Tỷ lệ hết hàng: 15%, tỷ lệ tồn dư: 25%
Sau khi triển khai ML forecasting (MAPE 22%):
- Giảm tồn kho: 20% (500 triệu → 400 triệu) = 100 triệu VNĐ giải phóng
- Tiết kiệm chi phí vốn: 100 triệu x 12% = 12 triệu VNĐ/năm
- Tiết kiệm chi phí kho: 100 triệu x 10% = 10 triệu VNĐ/năm
- Giảm hết hàng: 15% → 8% = thêm 7% doanh số được nắm bắt
- Doanh thu tăng thêm: 2 tỷ doanh thu x 7% = 140 triệu VNĐ/năm
- Tổng lợi ích: 12 triệu + 10 triệu + 140 triệu = 162 triệu VNĐ/năm
- Chi phí dự án ML: 50 triệu VNĐ (một lần) + 10 triệu VNĐ/năm (bảo trì)
- ROI: 324% năm đầu tiên
Nếu doanh nghiệp bạn đang sử dụng Excel để dự báo, khả năng cao bạn đang bỏ lỡ hàng tỷ VNĐ mỗi năm. Để hiểu rõ hơn về nền tảng dữ liệu cần thiết, đọc thêm bài Giới thiệu về Data Platform.
Nền tảng Time Series Forecasting
Time Series là gì?
Time series là chuỗi dữ liệu được đánh chỉ mục theo thời gian (doanh số theo ngày, nhiệt độ theo giờ, v.v.)
Ngày Doanh số
2024-01-01 1.200
2024-01-02 1.350
2024-01-03 1.180
...
Mục tiêu: Dự đoán giá trị tương lai dựa trên các pattern trong quá khứ.
Các thành phần chính của Time Series
Trend (xu hướng dài hạn):
- Tăng: Doanh số tăng theo thời gian
- Giảm: Sản phẩm đang suy giảm
- Phẳng: Sản phẩm ổn định, trưởng thành
Seasonality (mùa vụ -- các pattern lặp lại):
- Hàng tuần: Cuối tuần vs ngày thường (bán lẻ)
- Hàng tháng: Cuối tháng nhận lương (thương mại điện tử)
- Hàng năm: Mùa hè vs mùa đông (kem, áo khoác)
Cyclic patterns (chu kỳ bất thường):
- Chu kỳ kinh tế (suy thoái, tăng trưởng)
- Các pattern nhiều năm
Irregular/Noise (biến động ngẫu nhiên):
- Sự kiện không dự đoán được (thời tiết, đình công)
- Lỗi đo lường
Ví dụ phân tách chuỗi thời gian:
from statsmodels.tsa.seasonal import seasonal_decompose
import pandas as pd
# Tải dữ liệu bán hàng
df = pd.read_csv('daily_sales.csv', parse_dates=['date'], index_col='date')
# Phân tách thành phần
decomposition = seasonal_decompose(
df['sales'],
model='multiplicative',
period=7 # Mùa vụ theo tuần
)
# Vẽ biểu đồ
decomposition.plot()
plt.show()
Kết quả: 4 biểu đồ (Observed, Trend, Seasonal, Residual)
Ý nghĩa: Hiểu rõ các thành phần giúp chọn thuật toán phù hợp. Nếu bạn chưa có hệ thống dữ liệu hoàn chỉnh, hãy tham khảo tổng quan Modern Data Stack để xây dựng nền tảng trước.
So sánh thuật toán: truyền thống vs ML
Moving Average (Baseline)
Simple Moving Average (SMA):
# Trung bình trượt 7 ngày
df['forecast_sma'] = df['sales'].rolling(window=7).mean().shift(1)
- Ưu điểm: Đơn giản, nhanh
- Nhược điểm: Không nắm bắt trend, không nắm bắt mùa vụ, chậm phản ứng với thay đổi
- MAPE: 35-45%
ARIMA (AutoRegressive Integrated Moving Average)
Thuật toán time series truyền thống
Khái niệm:
- AR (AutoRegressive): Sử dụng giá trị quá khứ (doanh số hôm qua dự đoán hôm nay)
- I (Integrated): Differencing để loại bỏ trend
- MA (Moving Average): Sử dụng sai số dự báo quá khứ
Tham số: ARIMA(p, d, q)
- p: Số quan sát lag (bậc AR)
- d: Bậc differencing (bậc I)
- q: Kích thước cửa sổ trung bình (bậc MA)
Triển khai:
from statsmodels.tsa.arima.model import ARIMA
# Huấn luyện ARIMA
model = ARIMA(df['sales'], order=(7, 1, 7))
fitted_model = model.fit()
# Dự báo 30 ngày tới
forecast = fitted_model.forecast(steps=30)
print(forecast)
- Ưu điểm: Kinh điển, được nghiên cứu kỹ, xử lý trend tốt
- Nhược điểm: Không xử lý tốt nhiều mùa vụ cùng lúc, cần tinh chỉnh tham số thủ công
- MAPE: 25-35%
Khi nào dùng: Tập dữ liệu nhỏ, pattern đơn giản, cần giải thích được kết quả
Prophet (Meta)
Thư viện time series hiện đại, thân thiện với người dùng
Tính năng chính:
- Tự động phát hiện mùa vụ: Ngày, tuần, năm
- Hiệu ứng ngày lễ: Calendar ngày lễ sẵn có + sự kiện tùy chỉnh
- Xử lý tốt dữ liệu thiếu: Chấp nhận gaps trong dữ liệu
- Mùa vụ cộng hoặc nhân
- Trend changepoints: Phát hiện khi xu hướng thay đổi
Triển khai:
from prophet import Prophet
import pandas as pd
# Chuẩn bị dữ liệu (Prophet yêu cầu cột 'ds' và 'y')
df_prophet = df.rename(columns={'date': 'ds', 'sales': 'y'})
# Khởi tạo model
model = Prophet(
yearly_seasonality=True,
weekly_seasonality=True,
daily_seasonality=False,
seasonality_mode='multiplicative'
)
# Thêm mùa vụ tùy chỉnh (hiệu ứng lương cuối tháng)
model.add_seasonality(name='monthly', period=30.5, fourier_order=5)
# Thêm ngày lễ Việt Nam
vietnam_holidays = pd.DataFrame({
'holiday': 'tet',
'ds': pd.to_datetime(['2024-02-10', '2025-01-29', '2026-02-17']),
'lower_window': -3, # 3 ngày trước
'upper_window': 7 # 7 ngày sau
})
model = Prophet(holidays=vietnam_holidays)
# Huấn luyện
model.fit(df_prophet)
# Dự báo 30 ngày tới
future = model.make_future_dataframe(periods=30)
forecast = model.predict(future)
# Vẽ biểu đồ
model.plot(forecast)
model.plot_components(forecast) # Phân tách trend, mùa vụ
plt.show()
- Ưu điểm: Dễ sử dụng, xử lý mùa vụ tốt, dễ giải thích, ổn định
- Nhược điểm: Khó tích hợp features bên ngoài (khuyến mãi, thời tiết)
- MAPE: 20-30%
Khi nào dùng: Khởi động nhanh, mùa vụ mạnh, cần giải thích được kết quả
XGBoost (Gradient Boosting)
Tiếp cận ML: Biến forecasting thành bài toán regression
Khái niệm:
- Tạo features từ chuỗi thời gian (lags, rolling stats, date features)
- Huấn luyện XGBoost để dự đoán:
doanh_so = f(features)
Feature engineering:
import pandas as pd
import numpy as np
def create_time_series_features(df):
df = df.copy()
# Features từ ngày tháng
df['day_of_week'] = df.index.dayofweek
df['day_of_month'] = df.index.day
df['week_of_year'] = df.index.isocalendar().week
df['month'] = df.index.month
df['quarter'] = df.index.quarter
df['is_weekend'] = (df.index.dayofweek >= 5).astype(int)
# Lag features (doanh số quá khứ)
for lag in [1, 7, 14, 30]:
df[f'lag_{lag}'] = df['sales'].shift(lag)
# Rolling statistics
for window in [7, 14, 30]:
df[f'rolling_mean_{window}'] = df['sales'].rolling(window=window).mean()
df[f'rolling_std_{window}'] = df['sales'].rolling(window=window).std()
# Trend
df['days_since_start'] = (df.index - df.index[0]).days
return df
df_features = create_time_series_features(df)
Thêm features bên ngoài:
# Khuyến mãi
df_features['is_promotion'] = df_features.index.isin(promotion_dates).astype(int)
# Thời tiết
df_features = df_features.merge(
weather_df, left_index=True, right_on='date', how='left'
)
# Ngày lễ
df_features['is_holiday'] = df_features.index.isin(vietnam_holidays).astype(int)
Huấn luyện XGBoost:
from xgboost import XGBRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_percentage_error
# Features và target
feature_cols = [
'day_of_week', 'day_of_month', 'week_of_year', 'month', 'is_weekend',
'lag_1', 'lag_7', 'lag_14', 'lag_30',
'rolling_mean_7', 'rolling_mean_14', 'rolling_std_7',
'is_promotion', 'temperature', 'is_holiday', 'days_since_start'
]
# Loại bỏ dòng NaN (từ lag/rolling features)
df_clean = df_features.dropna()
X = df_clean[feature_cols]
y = df_clean['sales']
# Chia train/test (theo thời gian -- không shuffle!)
split_idx = int(len(df_clean) * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
# Huấn luyện
model = XGBRegressor(
n_estimators=500,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
random_state=42
)
model.fit(X_train, y_train)
# Đánh giá
y_pred = model.predict(X_test)
mape = mean_absolute_percentage_error(y_test, y_pred) * 100
print(f"MAPE: {mape:.2f}%")
# Mức độ quan trọng của features
feature_importance = pd.DataFrame({
'feature': feature_cols,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
print(feature_importance.head(10))
- Ưu điểm: Chính xác nhất (với features tốt), xử lý biến ngoài, linh hoạt
- Nhược điểm: Cần feature engineering, khó giải thích hơn Prophet
- MAPE: 18-25%
Khi nào dùng: Cần độ chính xác cao nhất, có dữ liệu ngoài (khuyến mãi, thời tiết), sẵn sàng đầu tư vào feature engineering
LSTM (Long Short-Term Memory Neural Networks)
Deep learning cho time series
Khái niệm:
- Biến thể RNN được thiết kế cho dữ liệu tuần tự
- Học được các phụ thuộc dài hạn (long-term dependencies)
Triển khai:
import tensorflow as tf
from tensorflow import keras
import numpy as np
# Chuẩn bị dữ liệu cho LSTM (dạng sequences)
def create_sequences(data, seq_length=30):
X, y = [], []
for i in range(len(data) - seq_length):
X.append(data[i:i+seq_length])
y.append(data[i+seq_length])
return np.array(X), np.array(y)
# Chuẩn hóa dữ liệu
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
sales_scaled = scaler.fit_transform(df[['sales']])
# Tạo sequences (dùng 30 ngày gần nhất để dự đoán ngày tiếp theo)
X, y = create_sequences(sales_scaled, seq_length=30)
# Chia dữ liệu
split_idx = int(len(X) * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
# Xây dựng model LSTM
model = keras.Sequential([
keras.layers.LSTM(128, activation='relu', return_sequences=True,
input_shape=(30, 1)),
keras.layers.Dropout(0.2),
keras.layers.LSTM(64, activation='relu'),
keras.layers.Dropout(0.2),
keras.layers.Dense(32, activation='relu'),
keras.layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
# Huấn luyện
history = model.fit(
X_train, y_train,
epochs=50,
batch_size=32,
validation_split=0.2,
verbose=1
)
# Dự đoán
y_pred_scaled = model.predict(X_test)
y_pred = scaler.inverse_transform(y_pred_scaled)
# Đánh giá
mape = mean_absolute_percentage_error(
scaler.inverse_transform(y_test), y_pred
) * 100
print(f"MAPE: {mape:.2f}%")
- Ưu điểm: Nắm bắt pattern rất phức tạp, xử lý multivariate time series
- Nhược điểm: Cần tập dữ liệu lớn (trên 10.000 data points), huấn luyện chậm, black box, rủi ro overfitting
- MAPE: 15-22% (nếu tinh chỉnh tốt và có đủ dữ liệu)
Khi nào dùng: Tập dữ liệu rất lớn, pattern phức tạp, có đội ngũ ML engineering chuyên môn
Hướng dẫn chọn thuật toán
| Thuật toán | Kích thước dữ liệu | Mùa vụ | Features ngoài | Dễ dùng | MAPE | Khuyến nghị |
|---|---|---|---|---|---|---|
| Moving Avg | Bất kỳ | Không | Không | Rất cao | 35-45% | Chỉ làm baseline |
| ARIMA | 100-1.000 | Đơn | Không | Thấp | 25-35% | Dữ liệu nhỏ, đơn giản |
| Prophet | 100-10.000 | Nhiều | Hạn chế | Cao | 20-30% | Bắt đầu từ đây |
| XGBoost | 1.000-100.000 | Qua features | Tốt | Trung bình | 18-25% | Tốt nhất cho đa số |
| LSTM | Trên 10.000 | Nhiều | Có | Thấp | 15-22% | Quy mô lớn, phức tạp |
Khuyến nghị của Carptech:
- Bắt đầu với Prophet (nhanh, kết quả tốt, triển khai 2-4 tuần)
- Nâng cấp lên XGBoost nếu có dữ liệu khuyến mãi/thời tiết/sự kiện (4-8 tuần)
- Cân nhắc LSTM chỉ khi có trên 10.000 SKUs và đội ML engineer chuyên biệt
Feature Engineering nâng cao
Ngoài các lag features cơ bản, cần thêm các features đặc thù theo ngành:
Promotion Features (khuyến mãi)
# Có khuyến mãi hôm nay không?
df['is_promotion'] = df.index.isin(promotion_dates).astype(int)
# Mức giảm giá
df['discount_percent'] = df['date'].map(promotions_dict)
# Loại khuyến mãi
df['promo_type'] = df['date'].map(promo_types_dict)
df = pd.get_dummies(df, columns=['promo_type'])
# Số ngày đến khuyến mãi tiếp theo
next_promo_dates = sorted(promotion_dates)
def days_until_next_promo(date):
future_promos = [d for d in next_promo_dates if d > date]
return (future_promos[0] - date).days if future_promos else 365
df['days_until_promo'] = df.index.map(days_until_next_promo)
Tác động: Khuyến mãi có thể tăng doanh số 2-5 lần, thường là top 3 features quan trọng nhất.
Weather Features (thời tiết)
# Lấy dữ liệu thời tiết (OpenWeatherMap API, Visual Crossing)
weather = fetch_weather_data(location='Hanoi', start_date='2024-01-01')
# Ghép với dữ liệu bán hàng
df = df.merge(weather, left_index=True, right_on='date', how='left')
# Features
df['temperature'] # Nhiệt độ (Celsius)
df['precipitation'] # Lượng mưa (mm)
df['is_rainy'] = (df['precipitation'] > 1).astype(int)
# Thời tiết hôm qua ảnh hưởng doanh số hôm nay
df['temperature_lag1'] = df['temperature'].shift(1)
Ví dụ thực tế:
- Doanh số kem tăng khi nóng
- Doanh số áo mưa tăng khi mưa
- Doanh số cà phê tăng khi lạnh
Events và ngày lễ
import pandas as pd
# Ngày lễ Việt Nam
holidays = pd.DataFrame({
'date': pd.to_datetime([
'2024-01-01', # Tết Dương lịch
'2024-02-10', '2024-02-11', '2024-02-12', # Tết Nguyên đán
'2024-04-30', # Ngày Giải phóng miền Nam
'2024-05-01', # Ngày Quốc tế Lao động
'2024-09-02', # Quốc khánh
])
})
df['is_holiday'] = df.index.isin(holidays['date']).astype(int)
# Số ngày cách ngày lễ gần nhất (lượng mua sắm tăng trước lễ)
df['days_to_holiday'] = df.index.map(
lambda d: min([abs((h - d).days) for h in holidays['date']])
)
df['is_pre_holiday'] = (df['days_to_holiday'] <= 3).astype(int)
df['is_post_holiday'] = df.index.isin(
holidays['date'] + pd.Timedelta(days=1)
).astype(int)
Tác động: Doanh số thường tăng mạnh 2-3 ngày trước Tết, giảm trong và sau Tết.
Sự kiện (thể thao, giải trí)
# Sự kiện lớn ảnh hưởng doanh số
# Ví dụ: World Cup -> doanh số bia tăng
events = pd.DataFrame({
'date': ['2024-06-15', '2024-07-15'],
'event': ['vietnam_vs_thailand', 'world_cup_final']
})
df['has_event'] = df.index.isin(pd.to_datetime(events['date'])).astype(int)
Store/Product Features (dự báo đa cấp)
Cho chuỗi bán lẻ (dự báo theo cửa hàng x SKU):
# Features cửa hàng
stores = pd.DataFrame({
'store_id': ['HN001', 'HCM002'],
'store_size_sqm': [500, 800],
'location_type': ['mall', 'street'],
'parking_available': [1, 0]
})
# Features sản phẩm
products = pd.DataFrame({
'sku': ['SKU001', 'SKU002'],
'category': ['electronics', 'fashion'],
'brand': ['Samsung', 'Nike'],
'price': [15000000, 2000000]
})
# Ghép dữ liệu cho dự báo cấp cửa hàng x SKU
df_multi = sales.merge(stores, on='store_id').merge(products, on='sku')
Việc chuẩn bị và chuyển đổi dữ liệu từ nhiều nguồn đòi hỏi pipeline ETL/ELT hiệu quả. Tham khảo bài ETL vs ELT: Sự Thay Đổi Mô Hình để hiểu cách thiết kế pipeline dữ liệu phù hợp.
Đánh giá độ chính xác: các Metrics quan trọng
MAPE (Mean Absolute Percentage Error)
Metric phổ biến nhất trong doanh nghiệp:
from sklearn.metrics import mean_absolute_percentage_error
mape = mean_absolute_percentage_error(y_true, y_pred) * 100
print(f"MAPE: {mape:.2f}%")
Công thức:
MAPE = (1/n) x Tong |thuc_te - du_bao| / |thuc_te| x 100
Cách hiểu:
-
MAPE = 10%: Dự báo sai trung bình 10%
-
Dưới 10%: Xuất sắc
-
10-20%: Tốt
-
20-30%: Chấp nhận được
-
Trên 30%: Kém
-
Ưu điểm: Dễ hiểu, không phụ thuộc tỷ lệ (so sánh được giữa các sản phẩm khác nhau)
-
Nhược điểm: Không xác định khi actual = 0, thiên về under-forecasting
RMSE (Root Mean Squared Error)
from sklearn.metrics import mean_squared_error
import numpy as np
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
print(f"RMSE: {rmse:.2f}")
Công thức:
RMSE = can_bac_2[(1/n) x Tong(thuc_te - du_bao)^2]
- Ưu điểm: Phạt nặng các sai số lớn (tốt khi lỗi lớn gây tốn kém)
- Nhược điểm: Phụ thuộc tỷ lệ (không so sánh được giữa các sản phẩm khác magnitude)
MAE (Mean Absolute Error)
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(y_true, y_pred)
print(f"MAE: {mae:.2f}")
- Ưu điểm: Đơn giản, ổn định với outliers
- Nhược điểm: Phụ thuộc tỷ lệ
Bias (dự báo dư vs dự báo thiếu)
bias = (y_pred - y_true).mean()
print(f"Bias: {bias:.2f}")
# Bias dương = dự báo dư (tồn kho nhiều)
# Bias âm = dự báo thiếu (hết hàng)
Ý nghĩa kinh doanh:
- Dự báo dư: An toàn hơn (tránh hết hàng), nhưng tốn chi phí (tồn kho dư thừa)
- Dự báo thiếu: Rủi ro cao (hết hàng, mất doanh thu)
Bias tối ưu: Phụ thuộc vào cân bằng chi phí
# Nếu chi phí hết hàng > chi phí giữ hàng -> Nên dự báo dư nhẹ
optimal_bias = (stockout_cost - holding_cost) / total_cost
Đánh giá theo nhóm SKU
Không phải tất cả SKU đều giống nhau -- cần đánh giá riêng biệt:
# Phân nhóm SKU theo doanh số
df['sales_tier'] = pd.qcut(
df['total_sales'], q=3, labels=['Thap', 'Trung_binh', 'Cao']
)
# Đánh giá MAPE theo nhóm
for tier in ['Thap', 'Trung_binh', 'Cao']:
tier_mask = df['sales_tier'] == tier
mape_tier = mean_absolute_percentage_error(
y_true[tier_mask], y_pred[tier_mask]
) * 100
print(f"MAPE ({tier}): {mape_tier:.2f}%")
Kết quả điển hình:
- SKU doanh số cao: MAPE 15-20% (nhiều dữ liệu, pattern ổn định)
- SKU doanh số trung bình: MAPE 20-30%
- SKU doanh số thấp (long-tail): MAPE 40-60% (bất thường, khó dự báo)
Chiến lược: Tập trung độ chính xác vào SKU doanh số cao (quy tắc 80/20).
Triển khai Production: từ model đến hành động
Bước 1: Pipeline dự báo tự động
import schedule
import time
from datetime import datetime
def run_weekly_forecast():
"""
Tạo dự báo cho tất cả SKU
Chạy mỗi Chủ nhật lúc 2 giờ sáng
"""
print(f"[{datetime.now()}] Bắt đầu dự báo hàng tuần...")
# 1. Lấy dữ liệu bán hàng mới nhất
sales_data = fetch_sales_data(last_days=365)
# 2. Với mỗi SKU, huấn luyện model và dự báo
forecasts = []
for sku in sku_list:
sku_data = sales_data[sales_data['sku'] == sku]
# Huấn luyện model (Prophet hoặc XGBoost)
model = train_forecast_model(sku_data)
# Dự báo 30 ngày tới
forecast = model.predict(periods=30)
forecasts.append({
'sku': sku,
'forecast_date': datetime.now().date(),
'predictions': forecast
})
# 3. Lưu dự báo vào database
save_forecasts_to_db(forecasts)
print(f"[{datetime.now()}] Hoàn thành dự báo cho {len(sku_list)} SKUs")
# Lên lịch chạy hàng tuần (Chủ nhật 2 giờ sáng)
schedule.every().sunday.at("02:00").do(run_weekly_forecast)
while True:
schedule.run_pending()
time.sleep(3600)
Bước 2: tối ưu hóa tồn kho
Dùng kết quả dự báo để tính lượng đặt hàng tối ưu:
def calculate_order_quantity(sku, forecast, current_inventory,
lead_time_days=7):
"""
Tính lượng đặt hàng dựa trên dự báo
"""
# Nhu cầu dự kiến trong thời gian lead time + chu kỳ đặt hàng
forecast_period = 14 # Đặt cho 2 tuần tới
expected_demand = forecast[:forecast_period].sum()
# Safety stock (dự phòng cho sai số dự báo)
# Quy tắc: 1.65 x forecast_std x can(lead_time) (95% service level)
forecast_std = forecast[:forecast_period].std()
safety_stock = 1.65 * forecast_std * np.sqrt(lead_time_days)
# Lượng tồn kho tối ưu
optimal_inventory = expected_demand + safety_stock
order_qty = max(0, optimal_inventory - current_inventory)
return {
'sku': sku,
'current_inventory': current_inventory,
'expected_demand': expected_demand,
'safety_stock': safety_stock,
'optimal_inventory': optimal_inventory,
'order_quantity': order_qty
}
Bước 3: tự động tạo đơn đặt hàng
def generate_purchase_orders():
"""
Tạo POs cho tất cả SKU cần bổ hàng
"""
forecasts = load_forecasts_from_db()
current_inventory = load_current_inventory()
pos = []
for sku in sku_list:
forecast = forecasts[sku]
inventory = current_inventory[sku]
order_calc = calculate_order_quantity(sku, forecast, inventory)
if order_calc['order_quantity'] > 0:
pos.append({
'sku': sku,
'quantity': order_calc['order_quantity'],
'expected_delivery_date': (
datetime.now() + timedelta(days=7)
),
'reason': (
f"Nhu cau du kien: {order_calc['expected_demand']:.0f}, "
f"Ton kho hien tai: {inventory}"
)
})
# Gửi đến hệ thống mua hàng
if pos:
send_purchase_orders_to_erp(pos)
print(f"Da tao {len(pos)} don dat hang")
else:
print("Khong can bo hang")
Bước 4: cảnh báo và giám sát
def monitor_forecast_accuracy():
"""
So sánh dự báo tuần trước vs doanh số thực tế
Cảnh báo nếu độ chính xác giảm
"""
last_week = datetime.now() - timedelta(days=7)
# Lấy dự báo từ 7 ngày trước
past_forecasts = load_forecasts_from_db(forecast_date=last_week)
# Lấy doanh số thực tế
actual_sales = load_sales_data(
start_date=last_week, end_date=datetime.now()
)
# Tính MAPE
mape = calculate_mape(past_forecasts, actual_sales)
print(f"Do chinh xac du bao 7 ngay qua: MAPE = {mape:.2f}%")
# Cảnh báo nếu giảm
if mape > 30: # Ngưỡng cảnh báo
send_alert(
subject="Do chinh xac du bao giam",
message=f"MAPE tang len {mape:.2f}% (nguong: 30%)"
)
# Chạy hàng ngày
schedule.every().day.at("08:00").do(monitor_forecast_accuracy)
Case Study: chuỗi bán lẻ giảm 3,15 tỷ VNĐ/năm
Bối cảnh:
- Doanh nghiệp: Chuỗi bán lẻ điện tử, 50 cửa hàng
- SKUs: 800 sản phẩm
- Cách làm cũ: Dự báo thủ công (Excel, kinh nghiệm cá nhân)
- Vấn đề:
- MAPE: 42% (rất thiếu chính xác)
- Overstock: 28% tồn kho quá 90 ngày
- Stockout: 18% SKU hết hàng hàng tuần
Triển khai (16 tuần):
Tuần 1-4: Chuẩn bị dữ liệu
- Thu thập 24 tháng dữ liệu bán hàng (cấp cửa hàng x SKU x ngày)
- Tích hợp dữ liệu bên ngoài:
- Lịch khuyến mãi (từ bộ phận marketing)
- Ngày lễ (lịch Việt Nam)
- Thời tiết (dữ liệu lịch sử Hà Nội, TP.HCM)
- Chất lượng dữ liệu: Xử lý dữ liệu thiếu, outliers
Tuần 5-8: Phát triển model
- Bắt đầu với Prophet: Baseline nhanh
- MAPE: 28% (cải thiện lớn so với 42%!)
- Dễ giải thích cho lãnh đạo
- Nâng cấp lên XGBoost: Thêm features khuyến mãi/thời tiết
- MAPE: 21% (cải thiện thêm)
- Top features quan trọng:
lag_7(doanh số tuần trước): 22%is_promotion(có khuyến mãi): 18%rolling_mean_14(trung bình 14 ngày): 15%month(tháng): 12%temperature(nhiệt độ): 8%
Tuần 9-12: Pilot testing
- Pilot: 10 SKU doanh số cao tại 5 cửa hàng
- A/B test: Dự báo ML vs dự báo thủ công
- Kết quả:
- Dự báo ML MAPE: 22%
- Dự báo thủ công MAPE: 41%
- Cửa hàng pilot giảm overstock từ 28% xuống 15%
Tuần 13-16: Triển khai toàn bộ và tự động hóa
- Triển khai cho tất cả 800 SKU, 50 cửa hàng
- Pipeline tự động: Dự báo hàng tuần, tạo đơn hàng hàng ngày
- Tích hợp với ERP (SAP)
Kết quả sau 6 tháng:
| Chỉ số | Trước | Sau | Thay đổi |
|---|---|---|---|
| MAPE | 42% | 21% | -50% cải thiện |
| Tỷ lệ tồn dư (quá 90 ngày) | 28% | 12% | -57% |
| Tỷ lệ hết hàng | 18% | 9% | -50% |
| Tồn kho trung bình | 60 tỷ VNĐ | 48 tỷ VNĐ | -20% (giải phóng 12 tỷ) |
| Vòng quay tồn kho | 6 lần/năm | 7,5 lần/năm | +25% |
| Service level (tỷ lệ có hàng) | 82% | 91% | +9 điểm % |
Tác động tài chính (hàng năm):
- Vốn giải phóng: 12 tỷ VNĐ x 12% lãi suất = 1,44 tỷ VNĐ/năm
- Tiết kiệm chi phí kho: 12 tỷ x 8% = 960 triệu VNĐ/năm
- Giảm markdown (hàng cũ): 300 triệu VNĐ/năm
- Doanh thu tăng thêm (ít hết hàng hơn): 18% xuống 9% = thêm 9% hàng có sẵn
- Tác động doanh thu: khoảng 100 tỷ doanh thu x 9% x 5% conversion = 450 triệu VNĐ/năm
- Tổng lợi ích: 1,44 tỷ + 960 triệu + 300 triệu + 450 triệu = 3,15 tỷ VNĐ/năm
- Chi phí dự án ML: 300 triệu VNĐ (một lần) + 50 triệu VNĐ/năm
- ROI: hơn 1.000% năm đầu tiên
Theo Gartner, các doanh nghiệp triển khai demand sensing (dự báo nhu cầu thời gian thực) giảm trung bình 30-50% sai số dự báo so với phương pháp truyền thống. Kết quả case study này hoàn toàn phù hợp với benchmark quốc tế.
Bài học rút ra:
- Promotion feature = game-changer: Feature quan trọng nhất (18% importance)
- SKU doanh số cao hưởng lợi nhiều nhất: MAPE cải thiện 45% xuống 18%, SKU doanh số thấp 48% xuống 35% (vẫn thách thức)
- Sản phẩm mùa vụ khó hơn: Máy lạnh (đỉnh mùa hè) MAPE 30% vs sản phẩm ổn định 15%
- Huấn luyện lại hàng tuần rất quan trọng: Huấn luyện lại hàng tháng khiến MAPE tăng lên 26%, hàng tuần giữ ổn định ở 21%
Phát hiện bất ngờ: Dự báo dư nhẹ tốt hơn dự báo thiếu
- Model ban đầu tối ưu cho MAPE (tối thiểu sai số)
- Nhưng thực tế kinh doanh: Chi phí hết hàng lớn hơn chi phí giữ hàng
- Điều chỉnh: Thêm bias (forecast x 1.05) -- dự báo dư trung bình 5%
- Kết quả: Tỷ lệ hết hàng giảm thêm (9% xuống 6%), tồn kho tăng nhẹ nhưng đáng đánh đổi
Khi nào KHÔNG nên dùng ML
ML quá mức cần thiết cho các trường hợp sau:
Sản phẩm mới (dưới 3 tháng lịch sử):
- Vấn đề: Không đủ dữ liệu cho ML
- Giải pháp: Dùng dự báo từ sản phẩm tương tự, hoặc quy tắc đơn giản
Nhu cầu bất thường/gián đoạn:
- Ví dụ: SKU bán 0, 0, 0, 50, 0, 0, 100, 0... (rất bất thường)
- Vấn đề: ML gặp khó, thường chỉ dự đoán giá trị trung bình (vô ích)
- Giải pháp: Dùng quy tắc (safety stock = nhu cầu cao nhất trong 90 ngày)
Sản phẩm rất ổn định:
- Ví dụ: Sữa bán 100 +/- 5 đơn vị mỗi ngày
- Vấn đề: ML thêm phức tạp mà không cải thiện
- Giải pháp: Trung bình trượt đơn giản (100 đơn vị/ngày)
Quy tắc ngón tay cái: Dùng ML khi hệ số biến thiên (CV) dưới 1.5
cv = std(sales) / mean(sales)
# CV dưoi 0.5: Rat on dinh -> Trung binh truot
# CV 0.5-1.5: Bien dong vua -> ML (XGBoost, Prophet)
# CV tren 1.5: Bat thuong -> Quy tac safety stock
Kết luận: dự báo nhu cầu = kho vàng tồn kho
Demand forecasting bằng ML không phải công nghệ xa vời -- với các công cụ hiện đại (Prophet, XGBoost), SMEs có thể triển khai trong 4-8 tuần và thấy ROI trong 3-6 tháng.
Tóm tắt:
- Bắt đầu đơn giản: Prophet baseline (MAPE 20-30%) trong 2-4 tuần
- Nâng cấp nếu cần: XGBoost với features (MAPE 18-25%) trong 4-8 tuần
- Tập trung vào SKU doanh số cao: Quy tắc 80/20 (20% SKU = 80% doanh thu)
- Tự động hóa: Dự báo hàng tuần, tạo đơn hàng hàng ngày, giải phóng nhân sự cho các exception
- Giám sát và cải tiến: Theo dõi MAPE hàng tuần, huấn luyện lại hàng tháng, thêm features mới hàng quý
ROI kỳ vọng thực tế:
- Giảm 15-25% tồn kho (điển hình)
- Giảm 30-50% tỷ lệ hết hàng
- Hoàn vốn trong 12-24 tháng
- ROI 500-2.000% sau 3 năm
Các bước tiếp theo:
- Kiểm tra độ chính xác dự báo hiện tại (tính MAPE)
- Thu thập dữ liệu lịch sử (12 tháng trở lên doanh số + khuyến mãi/ngày lễ)
- Chạy pilot Prophet (top 20 SKU, 1 tháng)
- Đo lường tác động (tồn kho, hết hàng, MAPE)
- Mở rộng tất cả SKU, tự động hóa
- Liên hệ Carptech nếu cần hỗ trợ triển khai hoặc đặt lịch tư vấn miễn phí
Tài liệu tham khảo:
- Prophet Documentation -- Thư viện time series forecasting
- XGBoost for Time Series (Kaggle) -- Hướng dẫn thực hành
- statsmodels ARIMA Guide -- Tài liệu ARIMA
- McKinsey: Supply Chain 4.0 -- Báo cáo supply chain
- Gartner: Demand Planning -- Nghiên cứu demand planning
Bài viết này là phần 4 của series "Advanced Analytics". Đọc thêm về Giới thiệu Data Platform, ETL vs ELT, và Modern Data Stack toàn cảnh.
Carptech -- Data Platform cho Doanh Nghiệp Việt Nam. Liên hệ tư vấn miễn phí.




