Phân bổ ngân sách biết tự giải thích: Sức mạnh của shadow price trong bài toán tối ưu

Công nghệ11 tháng 8, 2026·6 phút đọc

Bài viết chỉ ra cách dùng quy hoạch tuyến tính liên tục để phân bổ ngân sách mà vẫn giữ được shadow price – đại lượng cho biết mỗi ràng buộc đang giúp hay hại kế hoạch bao nhiêu. Thay vì thêm biến nhị phân khiến mô hình thành MIP và mất khả năng giải thích, tác giả đề xuất chia ngân sách thành các mức lợi suất giảm dần. Đi kèm là dự án mã nguồn mở CLARO giúp áp dụng ngay vào thực tế.

Phân bổ ngân sách biết tự giải thích: Sức mạnh của shadow price trong bài toán tối ưu

Phân bổ ngân sách biết tự giải thích: Sức mạnh của shadow price trong bài toán tối ưu

Khi để một mô hình tối ưu tự quyết, nó có thể dồn toàn bộ 20.000 GBP vào một kênh duy nhất. Thêm một ràng buộc "tối thiểu 3.000 GBP cho kênh thứ hai", mô hình nghe lời, nhưng kèm theo đó là một con số âm lặng lẽ bên cạnh ràng buộc – chi phí thực sự của chính sách an toàn mà bạn tự đặt ra. Đó chính là giá trị ẩn trong mô hình, và bài viết này sẽ chỉ ra cách không vứt bỏ nó.

Minh hoạ ý tưởng CLARO – phân bổ ngân sách tuyến tính có ràng buộcMinh hoạ ý tưởng CLARO – phân bổ ngân sách tuyến tính có ràng buộc

Tại sao cách xếp hạng ROI đơn giản lại thất bại

Cách tiếp cận hiển nhiên nhất là tính lợi nhuận trên mỗi pound cho từng kênh, xếp hạng và cấp ngân sách cho các kênh dẫn đầu. Bảng kết quả sạch sẽ nhưng kế hoạch thì khó dùng. Xếp hạng giả định rằng mỗi lựa chọn độc lập, trong khi phân bổ ngân sách là một quyết định liên kết: mỗi pound bạn đưa cho một kênh là một pound mà các kênh khác mất đi. Thêm một quy tắc đơn giản "giữ tối thiểu 3.000 GBP cho kênh này", danh sách đã xếp hạng sẽ không còn ý nghĩa gì.

Vì vậy đây là bài toán tối ưu có ràng buộc, và quy hoạch tuyến tính (Linear Programming – LP) là công cụ chuẩn. Nhưng lý do thực sự nên dùng LP không nằm ở kết quả phân bổ, mà nằm ở một sản phẩm phụ gần như ai cũng bỏ qua: dual value hay shadow price của các ràng buộc.

Shadow price: Lời giải thích có sẵn trong mô hình

Khi một mô hình LP liên tục hội tụ, nó có thể cung cấp giá trị đối ngẫu của các ràng buộc: nếu bạn nới lỏng một quy tắc thêm một đơn vị, hàm mục tiêu sẽ thay đổi bao nhiêu. Con số đó chính là lời giải thích cho mọi quyết định của mô hình. Nhưng lời giải thích này chỉ còn giá trị khi mô hình vẫn là LP liên tục.

Và đây là nơi xuất hiện một quyết định trông rất vô hại.

Cạm bẫy của biến on/off

Một LP thuần túy thường dồn toàn bộ ngân sách vào kênh tốt nhất và bỏ đói các kênh còn lại. Cách sửa tự nhiên là thêm biến nhị phân kiểu "chạy kênh này hay không" (on/off). Biến này giải quyết việc dồn ngân sách, nhưng biến mô hình thành một mixed-integer program (MIP). Khi đó, shadow price của LP không còn được cung cấp trực tiếp. Bạn có được con số phân bổ và mất đi lý do vì sao có con số đó.

Cách thoát ra là đa dạng hóa mà không cần công tắc bật/tắt: chia ngân sách của mỗi kênh thành nhiều dải (bracket), và mỗi dải sau có lợi suất biên thấp hơn dải trước.

Dải đầu tiên của một kênh mạnh có giá trị rất cao. Dải thứ ba của nó có thể thấp hơn dải đầu tiên của một đối thủ yếu hơn, nên bản thân bộ tối ưu sẽ tự động dàn trải tiền. Không cần biến nhị phân, shadow price quen thuộc vẫn còn nguyên. Nguyên tắc "lợi suất giảm dần" trở thành hình học, thay vì logic cứng nhắc.

Nhìn vào con số: Khi ràng buộc lên tiếng

Mô hình giữ ở mức ngắn gọn. Thư viện PuLP trong Python mô tả sát vấn đề:

import pulp

def allocate(budget, platforms):
    model = pulp.LpProblem("budget_allocation", pulp.LpMaximize)
    slices = {}
    for p in platforms:
        slices[p.name] = [
            (pulp.LpVariable(f"x_{p.name}_b{i}", lowBound=0, upBound=frac * budget), y)
            for i, (frac, y) in enumerate(BRACKETS)
        ]
    model += pulp.lpSum(
        p.productivity * y * var for p in platforms for (var, y) in slices[p.name]
    )
    # ... thêm ràng buộc tổng ngân sách và ràng buộc tối thiểu ...
    model.solve(pulp.PULP_CBC_CMD(msg=False))
    return model, slices

Mọi ràng buộc cần có tên. Ràng buộc không tên vẫn ràng buộc, nhưng sau đó không thể "kể" rằng nó đã tác động thế nào. Đặt tên cho ràng buộc là cách để bản phân bổ biết nói chuyện lại.

Khi đọc kết quả, mỗi ràng buộc có tên sẽ báo hai điều: nó có đang bó buộc (tight) hay không, và shadow price của nó là bao nhiêu.

def interpret(model):
    for name, con in model.constraints.items():
        print(name, "tight:", abs(con.slack) < 1e-6, "shadow:", con.pi)

Cách đọc dấu rất quan trọng:

  • Một ràng buộc tổng ngân sách đang bó buộc có shadow price dương: thêm một pound sẽ tạo ra thêm chừng này giá trị.
  • Một ràng buộc mức sàn tối thiểu có shadow price âm: ép tiền vào kênh yếu hơn đang tốn chừng này.
  • Shadow price bằng 0 nghĩa là quy tắc bạn viết ra không hề ảnh hưởng đến quyết định.

Ví dụ với 20.000 GBP, hai nền tảng và mục tiêu tạo khách hàng tiềm năng. Nếu LinkedIn tạo được 300 khách hàng tiềm năng từ 9.500 GBP, Facebook tạo 150 từ 8.000 GBP, hai kênh khá sát nhau nên ngân sách chia gần 60/40 nghiêng về LinkedIn. Chỉ có ràng buộc ngân sách hoạt động, cả hai mức sàn 3.000 GBP đều không bị chạm tới.

Nhưng nếu LinkedIn mạnh hơn hẳn: 380 khách hàng tiềm năng từ 9.500 GBP, còn Facebook chỉ 30 từ 4.200 GBP, và bạn hạ sàn Facebook xuống 500 GBP, mô hình sẽ đưa LinkedIn tới 19.500 GBP, giữ Facebook đúng 500 GBP. Lúc này ràng buộc sàn bó buộc và shadow price âm. Con số âm đó chính là thông điệp mở đầu bài viết: mô hình đang nói rằng quy tắc an toàn của bạn có cái giá, và in nó ngay bên cạnh bản kế hoạch thay vì giấu bên trong.

Từ bộ tối ưu thành công cụ ra quyết định

Một hàm 30 dòng chưa phải là công cụ hoàn chỉnh. Cần thêm các lớp bổ trợ:

  • Kiểm tra dữ liệu đầu vào để phát hiện mâu thuẫn trước khi chạy solver, vì hầu hết kết quả "infeasible" thực ra là do mức sàn đặt cao hơn trần.
  • Giải lại mô hình với các kịch bản thận trọng, cơ sở và lạc quan để kiểm tra độ vững của khuyến nghị.
  • Dự báo biến phân bổ trở lại thành kết quả kỳ vọng kèm biên độ bất định.
  • Liệt kê các ràng buộc đang bó buộc và đề xuất một phương án đa dạng hơn, kèm chi phí hiệu quả so với điểm tối ưu.

Tác giả đã xây dựng những ý tưởng này thành dự án mã nguồn mở tên CLARO (Constrained Linear Allocation and Resource Optimiser). CLARO bọc quanh bài toán tối ưu với phân tích kịch bản, dự báo và diễn giải ràng buộc, trong khi vẫn giữ phần lõi LP để tái sử dụng cho các bài toán phân bổ nguồn lực khác.

pip install claro-engine

Thư viện có sẵn trên PyPI, còn mã nguồn, bài kiểm thử và ví dụ nằm trên GitHub.

Bài học cho người làm tối ưu

Việc thêm bi

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗