Quay lại Blog
Data PlatformCập nhật: 1 tháng 2, 202625 phút đọc

Demand Forecasting với ML: giảm 20% chi phí tồn kho cho doanh nghiệp Việt Nam

Case study: chuỗi bán lẻ VN giảm 20% chi phí tồn kho (1,2 tỷ VNĐ/năm). So sánh Prophet, XGBoost, LSTM. MAPE từ 35-45% (Excel) → 18-25% (ML). Hướng dẫn triển khai end-to-end.

Đặng Quỳnh Hương

Đặng Quỳnh Hương

Senior Data Scientist

Biểu đồ time series forecasting với historical sales data, ML predictions và inventory optimization
#Demand Forecasting#Inventory Optimization#Time Series#Prophet#XGBoost#Machine Learning#Supply Chain

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ánKích thước dữ liệuMùa vụFeatures ngoàiDễ dùngMAPEKhuyến nghị
Moving AvgBất kỳKhôngKhôngRất cao35-45%Chỉ làm baseline
ARIMA100-1.000ĐơnKhôngThấp25-35%Dữ liệu nhỏ, đơn giản
Prophet100-10.000NhiềuHạn chếCao20-30%Bắt đầu từ đây
XGBoost1.000-100.000Qua featuresTốtTrung bình18-25%Tốt nhất cho đa số
LSTMTrên 10.000NhiềuThấp15-22%Quy mô lớn, phức tạp

Khuyến nghị của Carptech:

  1. Bắt đầu với Prophet (nhanh, kết quả tốt, triển khai 2-4 tuần)
  2. 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)
  3. 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:
      1. lag_7 (doanh số tuần trước): 22%
      2. is_promotion (có khuyến mãi): 18%
      3. rolling_mean_14 (trung bình 14 ngày): 15%
      4. month (tháng): 12%
      5. 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ướcSauThay đổi
MAPE42%21%-50% cải thiện
Tỷ lệ tồn dư (quá 90 ngày)28%12%-57%
Tỷ lệ hết hàng18%9%-50%
Tồn kho trung bình60 tỷ VNĐ48 tỷ VNĐ-20% (giải phóng 12 tỷ)
Vòng quay tồn kho6 lần/năm7,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:


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í.

Đăng ký nhận bài viết mới

Nhận thông báo khi chúng tôi publish bài viết mới về Data Platform, Analytics và AI.

Có câu hỏi về Data Platform?

Đội ngũ chuyên gia của Carptech sẵn sàng tư vấn miễn phí về giải pháp phù hợp nhất cho doanh nghiệp của bạn. Đặt lịch tư vấn 60 phút qua Microsoft Teams hoặc gửi form liên hệ.

✓ Miễn phí 100% • ✓ Microsoft Teams • ✓ Không cam kết dài hạn