Модель многокритериальной оптимизации выбора поставщика
Автор
In [ ]:
# ================================================================
# Многокритериальная оптимизация выбора поставщика ЭКБ
# Среда: Engee / Julia
# Реализует модель, описанную в статье (формулы 1–28)
# ================================================================
using Plots
using Statistics
using Printf
# ----------------------------------------------------------------
# БЛОК 1. ПАРАМЕТРЫ ЗАКУПКИ
# ----------------------------------------------------------------
Q = 30 # Требуемое количество, шт. (формула 1)
α = 3.0 # Порог допустимого избытка MOQ (формула 6)
p_expert = 1.5 # Экспертное соотношение β₁/β₂ (формула 11)
# ----------------------------------------------------------------
# БЛОК 2. ИСХОДНЫЕ ДАННЫЕ КОММЕРЧЕСКИХ ПРЕДЛОЖЕНИЙ
# ----------------------------------------------------------------
suppliers = ["ООО Альфа", "ИП Бета", "АО Гамма",
"ООО Дельта", "ЗАО Эпсилон"]
# Столбцы: c (цена), m (MOQ), t (срок)
offers = [
120.0 10 14; # ООО Альфа
95.0 100 21; # ИП Бета
110.0 25 10; # АО Гамма
130.0 1 7; # ООО Дельта
100.0 50 30; # ЗАО Эпсилон
]
# ----------------------------------------------------------------
# БЛОК 3. ИСТОРИЯ ВЗАИМОДЕЙСТВИЯ ИЗ ERP-СИСТЕМЫ
# ----------------------------------------------------------------
# Столбцы: N (всего), N_t (в срок), N_q (без рекламаций)
history = [
10 9 10; # ООО Альфа
5 3 4; # ИП Бета
8 8 7; # АО Гамма
15 15 15; # ООО Дельта
2 1 2; # ЗАО Эпсилон
]
n_sup = length(suppliers)
# ----------------------------------------------------------------
# БЛОК 4. ЗАЩИТА ВХОДНЫХ ДАННЫХ
# ----------------------------------------------------------------
@assert all(isfinite, offers) "Таблица КП содержит NaN или Inf!"
@assert all(isfinite, history) "Таблица истории содержит NaN или Inf!"
@assert Q > 0 "Потребность Q должна быть > 0!"
@assert α >= 1.0 "Порог α должен быть >= 1!"
# ----------------------------------------------------------------
# БЛОК 5. ВЫЧИСЛЕНИЕ УРОВНЯ ДОВЕРИЯ r_i
# Формулы (7)–(15) статьи
# ----------------------------------------------------------------
# Частные показатели надёжности (формулы 7–8)
p_t = zeros(n_sup)
p_q = zeros(n_sup)
r = zeros(n_sup)
for i in 1:n_sup
N, Nt, Nq = history[i, 1], history[i, 2], history[i, 3]
if N == 0
# Новый поставщик — нейтральный балл (формула 10)
r[i] = 0.5
else
p_t[i] = Nt / N
p_q[i] = Nq / N
end
end
# Энтропийные веса β (формулы 12–14)
function entropy_weights(x1, x2)
n = length(x1)
ε = 1e-12 # защита от log(0)
s1 = sum(x1); s2 = sum(x2)
p1 = (s1 > 0) ? x1 ./ s1 : ones(n) ./ n
p2 = (s2 > 0) ? x2 ./ s2 : ones(n) ./ n
H1 = -sum(p1 .* log.(p1 .+ ε)) / log(n)
H2 = -sum(p2 .* log.(p2 .+ ε)) / log(n)
d1, d2 = 1 - H1, 1 - H2
total = d1 + d2
return (total > 0) ? (d1/total, d2/total) : (0.5, 0.5)
end
β1_exp = p_expert / (1 + p_expert) # формула (11)
β2_exp = 1.0 / (1 + p_expert)
# Энтропийные веса только по поставщикам с историей
has_history = history[:, 1] .> 0
if sum(has_history) >= 2
β1_ent, β2_ent = entropy_weights(p_t[has_history],
p_q[has_history])
else
β1_ent
β1_ent, β2_ent = 0.5, 0.5
end
# Комбинированные веса (формула 15)
denom = β1_exp * β1_ent + β2_exp * β2_ent
β1 = (denom > 0) ? (β1_exp * β1_ent) / denom : 0.5
β2 = (denom > 0) ? (β2_exp * β2_ent) / denom : 0.5
# Уровень доверия (формула 9)
for i in 1:n_sup
if history[i, 1] > 0
r[i] = β1 * p_t[i] + β2 * p_q[i]
end
# иначе r[i] = 0.5 уже задан выше
end
println("=" ^ 60)
println(" БЛОК: УРОВЕНЬ ДОВЕРИЯ")
println("=" ^ 60)
@printf("%-16s | β₁=%.3f (эксп.), β₁_ent=%.3f → β₁=%.3f\n",
"", β1_exp, β1_ent, β1)
@printf("%-16s | β₂=%.3f (эксп.), β₂_ent=%.3f → β₂=%.3f\n",
"", β2_exp, β2_ent, β2)
println("-" ^ 60)
@printf("%-16s | %-8s | %-8s | %-8s\n",
"Поставщик", "p_t", "p_q", "r_i")
println("-" ^ 60)
for i in 1:n_sup
@printf("%-16s | %-8.3f | %-8.3f | %-8.3f\n",
suppliers[i], p_t[i], p_q[i], r[i])
end
# ----------------------------------------------------------------
# БЛОК 6. ПРИВЕДЁННАЯ ЦЕНА И ФИЛЬТРАЦИЯ ПО MOQ
# Формулы (3)–(6) статьи
# ----------------------------------------------------------------
c_real = zeros(n_sup)
Q_real = zeros(Int, n_sup)
excluded = falses(n_sup)
for i in 1:n_sup
c_i = offers[i, 1]
m_i = offers[i, 2]
Q_real[i] = max(Q, m_i) # формула (3)
c_real[i] = c_i * Q_real[i] / Q # формула (4)
ratio = Q_real[i] / Q
if ratio > α # формула (6)
excluded[i] = true
end
end
println()
println("=" ^ 65)
println(" БЛОК: ФИЛЬТРАЦИЯ ПО MOQ (Q=$(Q) шт., α=$(α))")
println("=" ^ 65)
@printf("%-16s | %-6s | %-8s | %-6s | %-12s | %s\n",
"Поставщик", "m_i", "Q_real", "ratio", "c_real,руб", "Статус")
println("-" ^ 65)
for i in 1:n_sup
ratio = Q_real[i] / Q
status = excluded[i] ? "✗ ИСКЛЮЧЁН" : "✓ допустим"
@printf("%-16s | %-6d | %-8d | %-6.2f | %-12.2f | %s\n",
suppliers[i], offers[i,2], Q_real[i],
ratio, c_real[i], status)
end
# Индексы допустимых поставщиков
active = findall(.!excluded)
@assert length(active) >= 1 "Все поставщики исключены! Увеличьте α."
# ----------------------------------------------------------------
# БЛОК 7. МАТРИЦА ПАРНЫХ СРАВНЕНИЙ МАИ
# Формулы (20)–(26) статьи
# ----------------------------------------------------------------
# Матрица задаётся экспертом:
# критерии: [1]=цена, [2]=срок, [3]=доверие
A_ahp = [
1.0 2.0 3.0;
1/2 1.0 2.0;
1/3 1/2 1.0;
]
function ahp_weights(A)
k = size(A, 1)
# Нормировка столбцов (формулы 21–22)
col_sums = sum(A, dims=1)
A_norm = A ./ col_sums
# Веса = среднее по строкам (формула 23)
w = vec(mean(A_norm, dims=2))
# Проверка согласованности (формулы 24–26)
λ_max = sum((A * w) ./ w) / k
CI = (λ_max - k) / (k - 1)
RI_table = Dict(1=>0.00, 2=>0.00, 3=>0.58,
4=>0.90, 5=>1.12, 6=>1.24, 7=>1.32)
RI = get(RI_table, k, 1.49)
CR = (RI > 0) ? CI / RI : 0.0
return w, λ_max, CI, CR
end
w, λ_max, CI, CR = ahp_weights(A_ahp)
println()
println("=" ^ 60)
println(" БЛОК: МАИ — ВЕСА КРИТЕРИЕВ")
println("=" ^ 60)
@printf(" w₁ (цена) = %.4f\n", w[1])
@printf(" w₂ (срок) = %.4f\n", w[2])
@printf(" w₃ (доверие) = %.4f\n", w[3])
@printf(" Σw = %.4f\n", sum(w))
println("-" ^ 60)
@printf(" λ_max = %.4f\n", λ_max)
@printf(" CI = %.4f\n", CI)
@printf(" CR = %.4f\n", CR)
if CR < 0.1
println(" ✅ Суждения согласованы (CR < 0.1)")
else
println(" ❌ Суждения НЕ согласованы — пересмотрите матрицу A!")
end
# ----------------------------------------------------------------
# БЛОК 8. НОРМИРОВКА КРИТЕРИЕВ
# Формулы (16)–(19) статьи
# ----------------------------------------------------------------
# Матрица критериев для активных поставщиков:
# столбцы: c_real, t, r
data_active = hcat(
c_real[active],
Float64.(offers[active, 3]),
r[active]
)
dirs = [:min, :min, :max] # направления оптимизации
n_active = length(active)
n_crit = 3
normalized = zeros(n_active, n_crit)
for j in 1:n_crit
v = data_active[:, j]
v_min = minimum(v)
v_max = maximum(v)
for i in 1:n_active
if v_max ≈ v_min # вырожденный случай (формула 19)
normalized[i, j] = 1.0
elseif dirs[j] == :min # формула (17)
normalized[i, j] = (v_max - v[i]) / (v_max - v_min)
else # формула (18)
normalized[i, j] = (v[i] - v_min) / (v_max - v_min)
end
end
end
# ----------------------------------------------------------------
# БЛОК 9. ЦЕЛЕВАЯ ФУНКЦИЯ И РАНЖИРОВАНИЕ
# Формулы (27)–(28) статьи
# ----------------------------------------------------------------
F_scores = normalized * w # формула (27)
best_local = argmax(F_scores)
best_idx = active[best_local]
order_local = sortperm(F_scores, rev=true)
println()
println("=" ^ 70)
println(" БЛОК: РАНЖИРОВАНИЕ ПОСТАВЩИКОВ")
println("=" ^ 70)
@printf("%-4s %-16s | %-10s | %-8s | %-8s | %-8s | %s\n",
"Ранг", "Поставщик",
"c_real", "Срок", "Доверие", "F(s_i)", "")
println("-" ^ 70)
for (rank, idx_local) in enumerate(order_local)
idx = active[idx_local]
marker = (idx == best_idx) ? "← ОПТИМАЛЬНЫЙ" : ""
@printf("%-4d %-16s | %-10.2f | %-8.0f | %-8.3f | %-8.4f | %s\n",
rank,
suppliers[idx],
c_real[idx],
offers[idx, 3],
r[idx],
F_scores[idx_local],
marker)
end
println()
println(" Исключены по MOQ:")
for i in findall(excluded)
@printf(" ✗ %-16s (MOQ=%d, ratio=%.2f, c_real=%.2f руб./шт.)\n",
suppliers[i], offers[i,2],
Q_real[i]/Q, c_real[i])
end
# ----------------------------------------------------------------
# БЛОК 10. АНАЛИЗ ЧУВСТВИТЕЛЬНОСТИ
# Варьируем веса — проверяем устойчивость победителя
# ----------------------------------------------------------------
scenarios = [
"Базовый (МАИ)" w[1] w[2] w[3];
"Приоритет цены" 0.70 0.20 0.10;
"Приоритет срока" 0.20 0.60 0.20;
"Приоритет доверия" 0.20 0.20 0.60;
"Равные веса" 0.333 0.333 0.333;
]
println()
println("=" ^ 65)
println(" БЛОК: АНАЛИЗ ЧУВСТВИТЕЛЬНОСТИ")
println("=" ^ 65)
@printf("%-22s | %-6s %-6s %-6s | %s\n",
"Сценарий", "w1", "w2", "w3", "Победитель")
println("-" ^ 65)
for row in eachrow(scenarios)
name = row[1]
ws = Float64.([row[2], row[3], row[4]])
ws ./= sum(ws) # нормировка на случай округлений
f_sens = normalized * ws
winner = suppliers[active[argmax(f_sens)]]
@printf("%-22s | %-6.3f %-6.3f %-6.3f | %s\n",
name, ws[1], ws[2], ws[3], winner)
end