Nếu bạn muốn bổ sung thêm các kiến thức về Tối Ưu Hóa thì có thể đăng ký khóa học Python Data Analyst Thực Chiến nhé. Trong khóa học này, riêng phần OR Challenges bạn sẽ được thực chiến qua 081+ Challenges từ cơ bản đến nâng cao. Các Challenges & Cases này sẽ giúp bạn có thêm nhiều ý tưởng áp dụng vào trong công việc thực tế cũng như giúp bạn tạo ấn tượng tốt trước nhà tuyển dụng.
A – Team Data Analysis Nhận Được Ticket
Cửa hàng đang có nhiều Combo khuyến mãi giá tốt
- Mua combo thường rẻ hơn mua lẻ.
- Nhưng nhân viên hay quên gợi ý combo cho khách.
- Khách khi phát hiện ra thì không hài lòng, đặc biệt khách VIP.
Yêu cầu: Xây dựng hệ thống tự động gợi ý cách mua rẻ nhất cho mỗi đơn để khách hàng có trải nghiệm tốt nhất.
- Input: Đơn hàng (Sản phẩm, combo, đơn hàng)
- Output: Gợi ý mua gì, tiết kiệm bao nhiêu.
B – Giải Thích Bằng Ví Dụ Đời Thường Cho Dễ Hình Dung
C – Data Input Đầu Vào
File excel gồm 3 sheets dữ liệu: Products, SpecialOffers, Orders
D – Data Output Mong Muốn
Hệ thống sẽ xuất ra kết quả gợi ý nên chọn sản phẩm nào tốt nhất cho từng đơn hàng
E – Dưới Góc Nhìn Vừa Toán Học Vừa Đời Thường
F – Code Python
import pandas as pd
from pulp import *
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
# ═══════════════════════════════════════════════════════════════════════════════
# LOAD DATA: Đọc 3 sheets từ Excel (Products, SpecialOffers, Orders)
# ═══════════════════════════════════════════════════════════════════════════════
def load_data(filepath):
return (pd.read_excel(filepath, sheet_name='Products'),
pd.read_excel(filepath, sheet_name='SpecialOffers'),
pd.read_excel(filepath, sheet_name='Orders'))
# ═══════════════════════════════════════════════════════════════════════════════
# GET ORDER NEEDS: Lấy nhu cầu 1 đơn → {ProductID: SoLuong}
# Ví dụ DH001: {'P01': 3, 'P02': 2} = cần 3 Táo + 2 Lê
# ═══════════════════════════════════════════════════════════════════════════════
def get_order_needs(df_orders, order_id):
lines = df_orders[df_orders['OrderID'] == order_id]
needs = {row['ProductID']: row['SoLuong'] for _, row in lines.iterrows()}
return needs, lines.iloc[0]['TenKhachHang'], lines.iloc[0]['NgayDat']
# ═══════════════════════════════════════════════════════════════════════════════
# BUILD OFFER MATRIX: Chuyển combo → list tuple (ID, Tên, {PID: Qty}, Giá)
# Ví dụ: ('C02', 'Combo Hàn Mix', {'P01':2, 'P02':2}, 140000)
# ═══════════════════════════════════════════════════════════════════════════════
def build_offer_matrix(df_offers, pids):
offers = []
for _, row in df_offers.iterrows():
qty = {pid: int(row.get(f'{pid}_Qty', 0)) for pid in pids}
offers.append((row['OfferID'], row['TenCombo'], qty, row['ComboPrice_VND']))
return offers
# ═══════════════════════════════════════════════════════════════════════════════
# SOLVE ORDER: Giải ILP tìm cách mua rẻ nhất
# ─────────────────────────────────────────────────────────────────────────────
# CÁCH GIẢI (Integer Linear Programming):
# ĐẶT ẨN: x[combo] = số lần mua combo, y[pid] = số mua lẻ
# MINIMIZE: Σ(giá_combo × x) + Σ(giá_lẻ × y)
# RÀNG BUỘC: Với mỗi sản phẩm: Σ(qty_combo × x) + y = nhu_cầu
# ═══════════════════════════════════════════════════════════════════════════════
def solve_order(order_id, needs, customer, date, df_products, offers):
# Chuẩn bị giá và tên sản phẩm
prices = {r['ProductID']: r['DonGia_VND'] for _, r in df_products.iterrows()}
names = {r['ProductID']: r['TenSanPham'] for _, r in df_products.iterrows()}
pids = list(prices.keys())
# TẠO MODEL: Minimize tổng chi phí
model = LpProblem(f"Order_{order_id}", LpMinimize)
# BIẾN QUYẾT ĐỊNH: x[combo], y[sản phẩm] (số nguyên ≥ 0)
x = {o[0]: LpVariable(f"x_{o[0]}", lowBound=0, cat='Integer') for o in offers}
y = {p: LpVariable(f"y_{p}", lowBound=0, cat='Integer') for p in pids}
# HÀM MỤC TIÊU: Min(tiền sp combo + tiền sp lẻ)
model += lpSum(o[3]*x[o[0]] for o in offers) + lpSum(prices[p]*y[p] for p in pids)
# RÀNG BUỘC: Đủ số lượng từng sản phẩm (không thừa, không thiếu)
for p in pids:
model += lpSum(o[2].get(p,0)*x[o[0]] for o in offers) + y[p] == needs.get(p,0)
# GIẢI
model.solve(PULP_CBC_CMD(msg=0))
if model.status != LpStatusOptimal:
return None
# TRÍCH XUẤT KẾT QUẢ
total = int(value(model.objective))
individual = sum(prices[p] * needs.get(p, 0) for p in pids)
combos = [{'OfferID': o[0], 'TenCombo': o[1], 'SoLan': int(x[o[0]].varValue),
'DonGiaCombo': o[3], 'ThanhTien': o[3]*int(x[o[0]].varValue)}
for o in offers if x[o[0]].varValue and x[o[0]].varValue > 0]
singles = [{'ProductID': p, 'TenSanPham': names[p], 'SoLuong': int(y[p].varValue),
'DonGia': prices[p], 'ThanhTien': prices[p]*int(y[p].varValue)}
for p in pids if y[p].varValue and y[p].varValue > 0]
return {'OrderID': order_id, 'NgayDat': date, 'TenKhachHang': customer,
'ChiPhiToiUu': total, 'ChiPhiMuaLe': individual,
'TietKiem': individual - total,
'TietKiem_Pct': (individual - total) / individual * 100 if individual else 0,
'Combos': combos, 'MuaLe': singles}
# ═══════════════════════════════════════════════════════════════════════════════
# EXPORT TO EXCEL: Xuất 2 sheets (Summary + Details)
# ═══════════════════════════════════════════════════════════════════════════════
def export_to_excel(results, path):
wb = Workbook()
# Styles
H_FILL = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
H_FILL_G = PatternFill(start_color='70AD47', end_color='70AD47', fill_type='solid')
S_FILL = PatternFill(start_color='C6EFCE', end_color='C6EFCE', fill_type='solid')
H_FONT = Font(bold=True, color='FFFFFF')
BORDER = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def cell(ws, r, c, v, money=False):
cell = ws.cell(row=r, column=c, value=v)
cell.border = BORDER
if money and isinstance(v, (int, float)): cell.number_format = '#,##0'
return cell
# ─── SHEET 1: SUMMARY ────────────────────────────────────────────────────
ws = wb.active
ws.title = 'Summary'
ws.merge_cells('A1:H1')
ws.cell(1, 1, 'TỔNG KẾT TỐI ƯU ĐƠN HÀNG').font = Font(bold=True, size=14)
headers = ['Mã ĐH', 'Ngày', 'Khách Hàng', 'Chi Phí Tối Ưu', 'Mua Lẻ Hết', 'Tiết Kiệm', '%', 'Gợi Ý']
for c, h in enumerate(headers, 1):
cell(ws, 3, c, h).fill, cell(ws, 3, c, h).font = H_FILL, H_FONT
tot_opt, tot_ind, tot_sav = 0, 0, 0
for r, res in enumerate(results, 4):
cell(ws, r, 1, res['OrderID'])
cell(ws, r, 2, str(res['NgayDat'])[:10] if res['NgayDat'] else '')
cell(ws, r, 3, res['TenKhachHang'])
cell(ws, r, 4, res['ChiPhiToiUu'], True)
cell(ws, r, 5, res['ChiPhiMuaLe'], True)
c = cell(ws, r, 6, res['TietKiem'], True)
c.fill, c.font = S_FILL, Font(bold=True, color='006100')
cell(ws, r, 7, f"{res['TietKiem_Pct']:.1f}%")
sugg = [f"{c['TenCombo']} x{c['SoLan']}" for c in res['Combos']]
sugg += [f"{s['TenSanPham']} x{s['SoLuong']} (lẻ)" for s in res['MuaLe']]
cell(ws, r, 8, '; '.join(sugg))
tot_opt += res['ChiPhiToiUu']
tot_ind += res['ChiPhiMuaLe']
tot_sav += res['TietKiem']
tr = len(results) + 4
ws.cell(tr, 1, 'TỔNG').font = Font(bold=True)
ws.cell(tr, 4, tot_opt).number_format = '#,##0'
ws.cell(tr, 5, tot_ind).number_format = '#,##0'
c = ws.cell(tr, 6, tot_sav)
c.number_format, c.fill, c.font = '#,##0', S_FILL, Font(bold=True, color='006100')
for i, w in enumerate([10, 11, 20, 16, 14, 12, 8, 45], 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ─── SHEET 2: DETAILS ────────────────────────────────────────────────────
ws = wb.create_sheet('Details')
ws.merge_cells('A1:G1')
ws.cell(1, 1, 'CHI TIẾT GỢI Ý MUA HÀNG').font = Font(bold=True, size=14)
headers = ['Mã ĐH', 'Khách', 'Loại', 'Mã/Tên', 'SL', 'Đơn Giá', 'Thành Tiền']
for c, h in enumerate(headers, 1):
cell(ws, 3, c, h).fill, cell(ws, 3, c, h).font = H_FILL_G, H_FONT
row = 4
for res in results:
for c in res['Combos']:
cell(ws, row, 1, res['OrderID'])
cell(ws, row, 2, res['TenKhachHang'])
cell(ws, row, 3, 'COMBO')
cell(ws, row, 4, f"{c['OfferID']} - {c['TenCombo']}")
cell(ws, row, 5, c['SoLan'])
cell(ws, row, 6, c['DonGiaCombo'], True)
cell(ws, row, 7, c['ThanhTien'], True)
row += 1
for s in res['MuaLe']:
cell(ws, row, 1, res['OrderID'])
cell(ws, row, 2, res['TenKhachHang'])
cell(ws, row, 3, 'LẺ')
cell(ws, row, 4, f"{s['ProductID']} - {s['TenSanPham']}")
cell(ws, row, 5, s['SoLuong'])
cell(ws, row, 6, s['DonGia'], True)
cell(ws, row, 7, s['ThanhTien'], True)
row += 1
ws.cell(row, 3, 'TỔNG').font = Font(bold=True)
ws.cell(row, 7, res['ChiPhiToiUu']).number_format = '#,##0'
row += 2
for i, w in enumerate([10, 18, 8, 32, 6, 12, 12], 1):
ws.column_dimensions[get_column_letter(i)].width = w
wb.save(path)
# ═══════════════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════════════
def main():
df_products, df_offers, df_orders = load_data("shopping_offers_dataset.xlsx")
offers = build_offer_matrix(df_offers, df_products['ProductID'].tolist())
results = []
for oid in df_orders['OrderID'].unique():
needs, customer, date = get_order_needs(df_orders, oid)
result = solve_order(oid, needs, customer, date, df_products, offers)
if result: results.append(result)
export_to_excel(results, "shopping_offers_results.xlsx")
if __name__ == "__main__":
main()
Kiến thức chi tiết về OR và Python sẽ được hướng dẫn và giải thích chi tiết trong khóa học.
