業界別最適化Example集: 小売業における在庫管理の最適化 (小売業)
nAG数値計算ライブラリ > 業界別最適化Example集 > 小売業における在庫管理の最適化 (小売業)

Keyword: 小売業, 在庫管理, 最適化, 混合整数計画法, MILP

小売業分野の最適化問題:在庫管理の最適化

問題の概要

在庫管理

小売業において、在庫管理は重要な課題の一つです。在庫が多すぎると保管コストがかかり、少なすぎると機会損失が発生します。したがって、需要の不確実性を考慮しながら、在庫維持コストと品切れコストのバランスを取ることが求められます。

ここでは、需要の不確実性を考慮しながら、発注コスト、在庫コスト、品切れコストなどの要因を考慮して、最適な在庫量と発注量を決定することで、コスト削減と顧客満足度の向上を目指します。

需要変動に対応した在庫管理の最適化(具体例)

ある小売業者が、5期間にわたって在庫管理を行う問題を考えます。各期間の需要量は以下の通りであり、固定発注コストは500円、単位発注コストは10円、単位在庫維持コストは1円とします。ここでの目的は、需要の変動に柔軟に対応しながら、発注と在庫保持の総コストを最小化する発注計画を立てることです。

期間 需要量
1 100
2 150
3 120
4 200
5 180

問題の定式化

パラメータ

パラメータ 説明
T 計画期間の総数 5
dt 期間 t における予測される需要量 表参照
K 発注を行う際の固定コスト 500
c 単位あたりの発注コスト 10
h 単位あたりの在庫保持コスト 1
M 十分に大きな数(発注量の上限を示す) 10000

決定変数

変数 説明 範囲
xt 期間 t における発注量 非負の整数
yt 期間 t に発注を行う場合は1、そうでない場合は0 バイナリ変数
It 期間 t の終わりにおける在庫量 非負の実数

目的関数

目的
最小化 t=1T(Kyt+cxt+hIt)

目的関数は、全期間にわたる固定発注コスト、発注コスト、および在庫維持コストの合計を最小化することを表しています。

制約条件

制約 説明
1 It=It1+xtdt,t 各期間の終わりにおける在庫量は、前期の在庫量に現期の発注量を加えたものから、現期の需要量を引いたものと等しくなります。
2 xtMyt,t 発注が行われる場合(yt=1)のみ、発注量 xt が正の値を取り得ます。M は発注量の上限値です。
3 It,xt0,t 在庫量と発注量は非負です。
4 yt{0,1},t 発注の有無は二値で表されます。

コード例

以下に、この問題を nAG Library for Python の 混合整数計画問題専用ソルバー関数 handle_solve_milp を用いて解くコード例を示します。

今回の問題は整数の変数を含む問題であることから、handle_solve_milp のような汎用的な MILP ソルバーを選択しました。

import numpy as np
from naginterfaces.library import opt, mip

# 問題のパラメータを定義
periods = [1, 2, 3, 4, 5]
demand = [100, 150, 120, 200, 180]
fixed_order_cost = 500
unit_order_cost = 10
unit_holding_cost = 1
initial_inventory = 0
M = 10000  # 十分に大きな数

# 変数の数を計算
num_vars = len(periods) * 3

# 問題のハンドルを作成
handle = opt.handle_init(nvar=num_vars)

# 目的関数の係数を設定
c = [unit_order_cost] * len(periods)  # 発注量に対するコスト
c += [fixed_order_cost] * len(periods)  # 発注を行う際の固定コスト
c += [unit_holding_cost] * len(periods)  # 在庫保持コスト
opt.handle_set_linobj(handle, cvec=c)

# 変数の下限と上限を設定
lbounds = [0] * num_vars
ubounds_x = [np.inf] * len(periods)  # x_tの上限は無限大(または運用上の最大発注量に設定)
ubounds_y = [1] * len(periods)  # y_tはバイナリ変数であるため、上限は1
ubounds_I = [np.inf] * len(periods)  # I_tの上限は無限大(または実際の最大在庫量に設定)
ubounds = ubounds_x + ubounds_y + ubounds_I
opt.handle_set_simplebounds(handle, bl=lbounds, bu=ubounds)

# 変数の数を計算
num_vars = len(periods) * 3

# 制約式のインデックスと係数を設定
cstr_indices_row = []
cstr_indices_col = []
cstr_coeffs = []
cstr_lbounds = []
cstr_ubounds = []
row_index = 1

# 在庫バランス制約の設定
for t in range(1, len(periods) + 1):
    # 今期の発注量 x_t に関する設定
    cstr_indices_row.append(row_index)
    cstr_indices_col.append(t)  # x_t のインデックス
    cstr_coeffs.append(1)  # x_t の係数

    # 今期の在庫量 I_t に関する設定
    cstr_indices_row.append(row_index)
    cstr_indices_col.append(10 + t)  # I_t のインデックス
    cstr_coeffs.append(-1)  # I_t の係数を-1に

    # 前期の在庫量 I_{t-1} に関する設定(初期在庫を除く)
    if t > 1:
        cstr_indices_row.append(row_index)
        cstr_indices_col.append(10 + t - 1)  # I_{t-1} のインデックス
        cstr_coeffs.append(1)  # I_{t-1} の係数

    # 制約の下限と上限の設定を修正
    if t == 1:
        cstr_lbounds.append(demand[0] - initial_inventory)  # 初期在庫を考慮
        cstr_ubounds.append(np.inf)
    else:
        cstr_lbounds.append(demand[t - 1])  # その期間の需要
        cstr_ubounds.append(np.inf)

    row_index += 1

# 発注量制約の設定
for t in range(1, len(periods) + 1):
    # x_t - M * y_t <= 0
    cstr_indices_row.append(row_index)
    cstr_indices_col.append(t)  # x_t のインデックス
    cstr_coeffs.append(1)  # x_t の係数

    cstr_indices_row.append(row_index)
    cstr_indices_col.append(5 + t)  # y_t のインデックス
    cstr_coeffs.append(-M)  # y_t の係数は -M

    cstr_lbounds.append(-np.inf)  # 下限はなし(または0でも良い)
    cstr_ubounds.append(0)  # 上限は0

    row_index += 1

# 上述の制約を問題に追加
opt.handle_set_linconstr(handle, bl=cstr_lbounds, bu=cstr_ubounds,
                         irowb=cstr_indices_row, icolb=cstr_indices_col, b=cstr_coeffs)

# 変数をバイナリに設定
startIdx = len(periods) + 1  # 1ベースインデックスでx_tの後の最初のy_t
endIdx = startIdx + len(periods)  # y_t の範囲の終わり
opt.handle_set_property(handle=handle, ptype='Binary', idx=list(range(startIdx, endIdx)))
opt.handle_set_property(handle=handle, ptype='Integer', idx=list(range(1,len(periods)+1)))

# ソルバーのオプションを設定
opt.handle_opt_set(handle, 'Print Level = 0')

# 問題を解く
x = mip.handle_solve_milp(handle)[0]

# ハンドルを解放
opt.handle_free(handle)

# 結果を表示
print("最適発注計画:")
for t in periods:
    if x[t - 1] > 0:
        print(f"期間 {t}: 発注量={x[t - 1]:.0f}")
    else:
        print(f"期間 {t}: 発注なし")
print(f"総コスト: {sum(c[i - 1] * x[i - 1] for i in range(num_vars))}")

# 各期間の在庫を計算して表示
inventory = [initial_inventory]  # 初期在庫
for t in periods:
    inventory.append(inventory[-1] + x[t - 1] - demand[t - 1])
print(f"在庫履歴: {inventory}")

結果例

上記のコードを実行すると、以下のような出力が得られます。

最適発注計画:
期間 1: 発注量=370
期間 2: 発注なし
期間 3: 発注なし
期間 4: 発注量=380
期間 5: 発注なし
総コスト: 9070.0
在庫履歴: [0, 270.0, 120.0, 0.0, 180.0, 0.0]

この結果から、最適な発注計画は、期間1で370単位、期間4で380単位を発注し、他の期間では発注を行わないことがわかります。この発注計画に従うと、総コストは9070となります。

在庫量の推移を見ると、期間1の発注により在庫が270まで増加し、その後需要に応じて減少していきます。期間3の終わりには在庫が0になりますが、期間4で再び発注が行われ、在庫が180まで増加します。最終的に、期間5の終わりには在庫が0になっています。

まとめ

ここでは、混合整数計画法を用いて、需要の変動に対応しつつ、発注と在庫保持に関わる総コストを最小化する最適な在庫管理計画を求めました。

この手法は、在庫管理問題に幅広く適用可能であり、需要の不確実性を考慮した複数シナリオの導入、リードタイムの考慮、在庫切れによる機会損失の目的関数への追加などにより、より現実的な応用も可能です。


関連情報
© 日本ニューメリカルアルゴリズムズグループ株式会社 2024
Privacy Policy  /  Trademarks