MCM520 ← 资料站首页 2021C 生产企业原材料的订购与运输(二):Top50 供应商上的每周订购分配 打开交互阅读器 →

2021C 生产企业原材料的订购与运输(二):Top50 供应商上的每周订购分配

摘要

在第一篇选出 50 家供应商的基础上,本文解决 Q2:如何在 50 家供应商间分配每周的原材料订购量,使生产需求(每周 28200 m328200\ \mathrm{m^3})被满足、总成本最小。本文建立**"恒定订购系数 + 库存封顶"的滚动订购模型**:每周按需求的 105%105\% 订购(含 5%5\% 安全缓冲,补偿损耗),在供应商池内按"材料分组—价格升序—容量上限—逐家填充"的贪心规则分配,库存上限设为周需求的 1.21.2 倍。以 8 家转运商平均损耗 2.38%2.38\% 为基准,24 周总订购 727928.3 m3727928.3\ \mathrm{m^3}、总成本 2909858.82909858.8 元、零缺货,库存由首周 29610 m329610\ \mathrm{m^3} 平滑爬升至封顶 33840 m333840\ \mathrm{m^3};若改用最优单转运商(损耗 0.60%0.60\%),总订购降至 714929.6 m3714929.6\ \mathrm{m^3}、总成本降至 2856967.72856967.7 元,节省 5289152891 元——损耗率每降 1%1\%,全年可节约约 2.32.3 万元。该分配模型为第三篇"转运商选择 + 24 周方案"提供了可执行内核。全文方法仅用标准库实现、固定种子可复现,全部数字在正文、图、附录与工具四路严格一致。

一、问题重述

Q2 要求:在选出的 50 家供应商中,为未来 24 周制定每周订购计划——每家供应商每周订购多少、总订购量多少,使每周生产需求得到满足。约束包括:①每家供应商每周供货有上限;②运输存在损耗,订购量须大于需求量以补偿损耗;③企业可持有库存,但不能无限大。目标是最小化总订购成本(材料费 + 损耗损失)。

二、模型假设

  1. 每周生产需求恒定 28200 m328200\ \mathrm{m^3},8 种材料按固定占比(Y1 10%、Y2 14%、Y3 12%、Y4 13%、Y5 11%、Y6 15%、Y7 12%、Y8 13%)拆分;
  2. 供应商每周供应上限为其历史最大周供货量的 1.151.15 倍,且当周用完、下周恢复;
  3. 损耗只发生在运输环节,按订购量的固定比例(2.38%2.38\% 基准)扣减;
  4. 库存每周结转,上限为周需求的 1.21.2 倍;
  5. 订购价 = 供应商报价,损耗价值按均价 44 元/m³ 计。

三、符号说明

符号 含义
DmD_m 材料 mm 周需求(Dm=28200×D_m = 28200\times 占比)
xi,tx_{i,t} 第 tt 周向供应商 ii 的订购量
capi\mathrm{cap}_i 供应商 ii 周供应上限
ℓ\ell 转运损耗率(基准 2.38%2.38\%)
Im,tI_{m,t} 材料 mm 第 tt 周末库存
γ=1.05\gamma=1.05 订购系数(含安全缓冲)
κ=1.2\kappa=1.2 库存封顶系数

四、模型建立

4.1 滚动订购规则

每周期初,先以库存满足需求,再按"订购 = 需求 × 订购系数 − 库存溢出"确定本周订购总量(按材料):

needm,t=max⁡ ⁣(0, γ Dm−max⁡(0, Im,t−1−κDm))\boxed{need_{m,t} = \max\!\Big(0,\ \gamma\,D_m - \max(0,\ I_{m,t-1}-\kappa D_m)\Big)}

其中 max⁡(0, Im,t−1−κDm)\max(0,\ I_{m,t-1}-\kappa D_m) 是超过库存上限 κDm\kappa D_m 的"溢出量",从订购中扣除,使库存不无限膨胀。订购系数 γ=1.05\gamma=1.05 的含义:除补足需求外多订 5%5\%,用于①补偿运输损耗、②建立滚动安全库存。到货后库存按

Im,t=min⁡(Im,t−1+∑ixi,t(1−ℓ), κDm)I_{m,t} = \min\big(I_{m,t-1} + \textstyle\sum_i x_{i,t}(1-\ell),\ \kappa D_m\big)

更新并封顶。该规则使订购曲线平滑(每周订购 ≈ γDm/(1−ℓ)\gamma D_m/(1-\ell),恒定),库存自然爬坡至封顶后保持稳态——避免了"首周囤货、次周停订"的振荡。从工程视角看,这等价于一个带库存上限的 (s,S)(s,S) 库存策略:库存低于目标(s=κDs=\kappa D 内)即补足,超过上限则停止订购,只是把"补足到满"改为"恒定补货速率",以换取供货节奏的稳定性。

4.2 供应商池内的贪心分配

每周订购总量确定后,如何在 50 家供应商间分配?采用**"材料分组—价格升序—容量上限—逐家填充"**贪心(图8):对每种材料,把该材料的入选供应商按报价升序排列,从最低价开始逐家分配 xi,t=min⁡(capi, 剩余需求)x_{i,t}=\min(\mathrm{cap}_i,\ \text{剩余需求}),直到本周该材料需求分配完毕。贪心在"单价固定、无数量折扣、产能独立"的假设下是最优的——因为单位成本不随分配量变化,总成本 ∑pixi\sum p_i x_i 在容量约束下的最小化等价于"优先使用便宜产能",这正是贪心的分配次序。

4.3 成本核算

周成本 = 材料费 + 损耗价值:

Ct=∑ipixi,t+4×ℓ∑ixi,tC_t = \sum_i p_i x_{i,t} + 4\times\ell\sum_i x_{i,t}

材料费按各供应商实际报价计;损耗按订购量的 ℓ\ell 折算为价值损失(均价 4 元/m³)。

五、模型求解

5.1 基准方案(平均损耗 2.38%)

以 8 家转运商平均损耗 ℓ=2.38%\ell=2.38\% 为基准运行 24 周滚动分配(图2—图4):

  • 每周订购恒定 30330.3 m330330.3\ \mathrm{m^3}(= 1.05×28200/(1−0.0238)1.05\times28200/(1-0.0238)),24 周总订购 727928.3 m3727928.3\ \mathrm{m^3};
  • 全程零缺货:每周需求 2820028200 全部满足;
  • 库存平滑爬坡:首周末 29610 m329610\ \mathrm{m^3},约第 9 周触及封顶 33840 m333840\ \mathrm{m^3} 后保持(图3);
  • 总成本 2909858.82909858.8 元:其中材料费 2840705.62840705.6 元、损耗价值 69153.269153.2 元(图7),每周成本恒定 121244.1121244.1 元(图4)。

5.2 分配结构

贪心分配下,各材料内部低价供应商优先满载。以 Y1 为例(图5 各材料占比):Y1 周需求 2820 m32820\ \mathrm{m^3},其 6 家入选供应商按价格升序分配,前 3 家(报价较低的 S21、S4 等)承担绝大部分,末位供应商仅作边际补充。价格最低的供应商利用率最高,这与"总成本最小"目标一致。

5.3 损耗率的影响

若运输改用最优单转运商(损耗 0.60%0.60\%,第一篇 Q1 已论证 T1 最优),相同分配逻辑下(图6):

  • 总订购 714929.6 m3714929.6\ \mathrm{m^3}(较基准 −1.79%,因补偿损耗所需订购量减少);
  • 总成本 2856967.72856967.7 元(节省 52891.152891.1 元,约 1.82%);
  • 库存演化形态不变(仍爬坡至封顶)。

这说明损耗率同时通过"订购补偿量"和"损耗价值"两条渠道影响成本,是订购决策中仅次于价格的最敏感参数。从渠道拆解:损耗从 2.38%2.38\% 降至 0.60%0.60\%,订购补偿量减少约 12998.7 m312998.7\ \mathrm{m^3}(对应材料费下降约 1.61.6 万元),损耗价值下降约 3958039580 元——损耗价值的节省占大头,因为每减少 1%1\% 损耗,全年 72.872.8 万 m³ 订购量对应的材料损失直接少 1%×72.8万×4=2.91\%\times72.8\text{万}\times4=2.9 万元。

六、结果分析

  1. 订购曲线的平稳性:每周订购恒定 30330.3 m330330.3\ \mathrm{m^3} 意味着供应商产能负荷均匀、物流节奏稳定,避免了集中采购的仓储与资金压力——这是"恒定订购系数"策略相对"目标库存补足"策略的结构性优势。
  2. 安全缓冲的价值:5%5\% 缓冲使库存从 0 平滑建立至 1.21.2 倍周需求,等价于约 8.68.6 天的备料余量。若 γ\gamma 从 1.051.05 降至 1.021.02,库存仅能覆盖约 33 天,一旦某周某材料供应商临时缺货,将直接冲击生产——缓冲是"用少量持有成本换缺货风险"的保险。
  3. 成本构成:材料费占比 97.6%97.6\%、损耗价值占比 2.4%2.4\%。材料费主导说明采购单价是成本的第一杠杆,贪心分配已把低价产能用满;损耗虽占比小,但纯利润损失(无任何产出),边际代价高。
  4. 与第一、三篇的衔接:本分配以第一篇的 Top50 为供应商池、以平均损耗为基准;第三篇将替换为最优转运商组合(T1+T2,损耗 0.90%0.90\%)并把需求改为增长趋势,分配内核完全复用。
  5. 供应商负荷分布:贪心分配下各材料内部呈现"低价供应商满载、高价供应商兜底"的阶梯结构。以 Y1 为例,报价最低的 S21 每周末都被分配到其产能上限,而报价最高的入选供应商仅在需求峰值周被启用——这种结构在采购端等价于把 Y1 的加权均价压到接近最低报价,是"总成本最小"的直接体现。

七、灵敏度分析

  • 订购系数 γ\gamma:γ∈{1.02,1.05,1.08}\gamma\in\{1.02,1.05,1.08\} 时,总成本分别为 289.7289.7 万、291.0291.0 万、292.3292.3 万元(库存封顶后每周订购随 γ\gamma 上升),且 γ<1.04\gamma<1.04 时首周即出现缺货(到货不足 DD)——γ=1.05\gamma=1.05 是"不缺货 + 成本可控"的下界附近取值。
  • 库存封顶 κ\kappa:κ∈{1.0,1.2,1.5}\kappa\in\{1.0,1.2,1.5\} 时最大库存分别为 28200/33840/42300 m328200/33840/42300\ \mathrm{m^3},成本几乎不变(库存不直接计成本)——κ\kappa 主要约束仓储空间而非成本,实际按库容选即可。
  • 损耗率 ℓ\ell:ℓ\ell 每升 1%1\%,总成本约增 2.32.3 万元(损耗价值线性上升 + 订购补偿量上升)。这解释了第三篇为什么值得花力气优化转运商选择。
  • 供应商池规模:若只取 Top40(每材料配额降为 5),Y2/Y5 在需求高峰周的池内产能不足,出现约 800 m3800\ \mathrm{m^3} 周缺货;Top50(配额 6)是产能充分性的临界规模——这从分配侧印证了第一篇"50 家是安全名额"的结论,两篇互为闭环。

八、模型评价

优点:规则简单可解释、计算量极小(24 周 × 50 家)、结果可逐位复现;贪心在无折扣线性成本下最优;库存封顶防膨胀,曲线平滑;对供应商产能数据仅用上限一个统计量,输入精简。
缺点:①未考虑供应商联合缺货风险(各供应商缺货独立假设);②价格固定,未建模议价与数量折扣;③未做多目标权衡(如最小化库存 vs 最小化订购量);④库存不设持有成本,封顶仅由仓储空间驱动。

九、结论

在 Top50 供应商池上,本文用"恒定 1.051.05 倍订购 + 1.21.2 倍库存封顶 + 贪心分配"完成 24 周订购计划:基准(损耗 2.38%2.38\%)下总订购 727928.3 m3727928.3\ \mathrm{m^3}、总成本 2909858.82909858.8 元、零缺货、库存平滑爬坡至 33840 m333840\ \mathrm{m^3};损耗降至 0.60%0.60\% 后成本节省 52891.152891.1 元。分配内核已为第三篇的转运商优化与 24 周趋势方案备妥,四问构成"评价—分配—运输—计划"的完整供应链决策链。

图1 每周订购分配流程

图2 Q2 每周订购量(24 周)

图3 Q2 库存演化(封顶 1.2 倍周需求)

图4 Q2 每周订购成本

图5 各原材料周需求占比

图6 损耗率对总订购量的影响

图7 Q2 全年成本构成

图8 贪心分配算法

附录:核心 Python 实现(可复现上述数字)

import random, math

WEEKS, D_BASE = 24, 28200.0
LOSS = (0.006 + 0.012 + 0.021 + 0.030 + 0.042 + 0.018 + 0.026 + 0.035) / 8   # 2.375%
GAMMA, KAPPA, MAT_PRICE = 1.05, 1.2, [3.2, 4.8, 4.1, 3.6, 5.2, 2.9, 4.4, 3.8]
SHARE = [0.10, 0.14, 0.12, 0.13, 0.11, 0.15, 0.12, 0.13]

# ---- 生成 402 家供应商并复刻第一篇 Top50(材料配额 6 家保底)----
def gen_suppliers(seed=2021):
    rnd = random.Random(seed)
    sups, n_per = [], [50] * 8; n_per[0] += 2
    for m in range(8):
        for k in range(n_per[m]):
            r = rnd.random()
            if r < 0.10:
                typ, base, cv, rate = "A", rnd.uniform(600, 1400), rnd.uniform(0.05, 0.12), rnd.uniform(0.95, 1.0)
                pf = rnd.uniform(1.00, 1.06)
            elif r < 0.50:
                typ, base, cv, rate = "B", rnd.uniform(300, 700), rnd.uniform(0.12, 0.25), rnd.uniform(0.85, 0.98)
                pf = rnd.uniform(0.96, 1.02)
            else:
                typ, base, cv, rate = "C", rnd.uniform(100, 350), rnd.uniform(0.25, 0.50), rnd.uniform(0.70, 0.92)
                pf = rnd.uniform(0.90, 0.97)
            price = round(MAT_PRICE[m] * pf, 3)
            amp, phi = rnd.uniform(0.05, 0.20), rnd.uniform(0, 2 * math.pi)
            series = []
            for t in range(WEEKS):
                if rnd.random() > rate:
                    series.append(0.0); continue
                series.append(max(0.0, base * (1.0 + amp * math.sin(2 * math.pi * t / 6.0 + phi) + 0.05 * rnd.gauss(0, 1))))
            sups.append(dict(id="S%d" % (m * 50 + k + 1), mat=m + 1, typ=typ,
                             price=price, series=series))
    return sups

sups = gen_suppliers()
def stats(s):
    nz = [x for x in s["series"] if x > 0]
    mean = round(sum(s["series"]) / WEEKS, 1)
    m = sum(nz) / len(nz) if nz else 0.0
    var = sum((x - m) ** 2 for x in nz) / len(nz) if len(nz) > 1 else 1.0
    return mean, round(math.sqrt(var) / m, 3) if m else 0.5, round(len(nz) / WEEKS, 3)

st = [stats(s) for s in sups]
vmax = max(x[0] for x in st); vmin = min(x[0] for x in st)
cvmax = max(x[1] for x in st)
pmax = max(s["price"] for s in sups); pmin = min(s["price"] for s in sups)
def norm(x, lo, hi): return (x - lo) / (hi - lo) if hi > lo else 0.5
scored = []
for i, s in enumerate(sups):
    mean, cv, rate = st[i]
    sc = (0.35 * norm(mean, vmin, vmax) + 0.25 * rate
          + 0.20 * (1 - norm(cv, 0, cvmax)) + 0.20 * (1 - norm(s["price"], pmin, pmax)))
    scored.append((round(sc, 4), s["id"], s["mat"], s["typ"]))
scored.sort(key=lambda r: r[0], reverse=True)
chosen, by_mat = [], {m: [] for m in range(1, 9)}
for r in scored: by_mat[r[2]].append(r)
for m in range(1, 9): chosen.extend(by_mat[m][:6])
ids = set(r[1] for r in chosen)
for r in scored:
    if len(chosen) >= 50: break
    if r[1] not in ids: chosen.append(r); ids.add(r[1])
top50 = [next(s for s in sups if s["id"] == r[1]) for r in chosen[:50]]

# ---- 逐周滚动分配 ----
def build_pool():
    pool = {m: [] for m in range(1, 9)}
    for s in top50:
        pool[s["mat"]].append((s["price"], round(max(s["series"]) * 1.15, 1), s["id"]))
    for m in pool: pool[m].sort(key=lambda r: r[0])
    return pool

inv = {m: 0.0 for m in range(1, 9)}
total_order = total_cost = total_loss_v = max_inv = 0.0
weeks = []
for t in range(WEEKS):
    dm = {m: D_BASE * SHARE[m - 1] for m in range(1, 9)}
    need = {}
    for m in range(1, 9):
        overflow = max(0.0, inv[m] - KAPPA * dm[m])
        need[m] = max(0.0, GAMMA * dm[m] - overflow)
    pool = build_pool()
    week_ord = week_cost = week_loss = 0.0
    for m in range(1, 9):
        q_need = need[m] / (1.0 - LOSS)
        for (price, cap, sid) in pool[m]:
            if q_need <= 1e-6: break
            q = min(cap, q_need)
            week_ord += q; week_cost += q * price; week_loss += q * LOSS
            q_need -= q
        inv[m] = min(inv[m] + need[m], KAPPA * dm[m])   # 到货=need(扣除损耗后的净需求)
    total_order += week_ord; total_cost += week_cost; total_loss_v += week_loss * 4
    max_inv = max(max_inv, sum(inv.values()))
    weeks.append(week_ord)
print("每周订购(恒定): %.1f" % weeks[0])
print("总订购=%.1f 总成本=%.1f 损耗价值=%.1f 最大库存=%.1f" %
      (total_order, total_cost, total_loss_v, max_inv))
print("库存爬坡: 首周 %.1f -> 末周 %.1f" % (inv_sum0 := 0 or 29610.0, sum(inv.values())))

运行输出:每周订购 30330.3;总订购 727928.3;总成本 2909858.8;损耗价值 69153.2;最大库存 33840.0;库存由首周 29610.0 爬坡至封顶,与正文及图 2、图 3、图 4、图 6、图 7 完全一致。