100本ノック / マーケティングサイエンス / マーケティングサイエンス100本ノック
製造業の推薦システムと数理最適化|A/Bテスト・LTV・在庫・生産計画をPythonで実践
推薦で需要を創り、制約下で利益を守る:製造業の施策検証と最適配分
タイトル・概要
本記事では、架空の産業部品メーカー「工房テック」を題材に、推薦施策のA/BテストからLTV評価、EC・営業・AIエージェントへの応用、さらに生産・在庫・サプライチェーン・マーケティング予算の最適化までを一気通貫で扱います。対象はマーケティングサイエンス100本ノックの No.071〜No.080 です。
狙いは、予測精度だけで終わらず、施策で増える需要を検証し、供給能力と利益を踏まえて実行案に変換することです。数値は説明用の架空データであり、同じコードを実行すれば結果を再現できます。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。 掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
はじめに:この記事で扱う製造業の実務課題
製造業のマーケティングでは「何を勧めれば受注しやすいか」と「受注後に約束どおり供給できるか」を別々に扱いがちです。しかし、欠品する製品を強く推薦すれば顧客体験を損ない、売上最大化だけの計画は段取り・能力・在庫・輸送の制約で実行不能になります。本記事では需要側と供給側を同じ意思決定の鎖として捉えます。
現場でよくある状況
- EC、営業、保守の各チャネルで提案ロジックがばらばら
- クリック率は上がったが、粗利や継続購入への寄与が不明
- Excel上の配分案は、最小ロットや工数上限を後から手修正
- 部門ごとのKPI最適化により、全社利益や納期遵守率が低下
なぜこの問題は判断が難しいのか
推薦の表示対象には選択バイアスがあり、相関と因果を区別する必要があります。また、短期の受注率と長期LTVは一致しません。供給側では複数の制約が同時に効き、整数条件や不確実な需要も含まれます。したがって、実験・価値評価・最適化・運用統制の四つを接続する必要があります。
今回扱うノックの全体像
| No. | テーマ | 主な意思決定 |
|---|---|---|
| 071 | A/Bテスト | 推薦を本番展開するか |
| 072 | 推薦とLTV | 誰への推薦を優先するか |
| 073 | ECへの応用 | 何を上位表示するか |
| 074 | 製造業への応用 | 営業がどの部品を提案するか |
| 075 | AIエージェントとの統合 | 自動提案を許可・保留するか |
| 076 | 線形計画法 | 限られた能力をどう配分するか |
| 077 | 整数計画法 | 段取りを含む生産案は何か |
| 078 | 在庫最適化 | 安全在庫と発注点はいくつか |
| 079 | サプライチェーン最適化 | どこからどこへ何個運ぶか |
| 080 | マーケティング予算配分 | 予算をどの施策へ振り向けるか |
Python 環境の準備
numpy と pandas でデータを扱い、推定と最適化には scipy、可視化には matplotlib を使います。乱数生成器は seed=42 に固定します。金額は特記がなければ千円単位です。
import sys
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import japanize_matplotlib
from scipy import stats
from scipy.optimize import linprog, milp, Bounds, LinearConstraint
from IPython.display import display
rng = np.random.default_rng(42)
pd.set_option("display.max_columns", 20)
pd.set_option("display.float_format", lambda x: f"{x:,.2f}")
print("Python:", sys.version.split()[0])
print("pandas:", pd.__version__, "matplotlib:", matplotlib.__version__)
Python: 3.13.1
pandas: 3.0.3 matplotlib: 3.11.0
架空データの作成
顧客企業ごとの業種、企業規模、過去粗利、取引月数、推薦対象群、受注、推薦後粗利を生成します。実験群への割付は無作為とし、企業規模による基礎受注確率の違いも含めます。後続ノックでは、この顧客データと製品・工場のマスタを共通利用します。
n = 2000
customers = pd.DataFrame({
"customer_id": [f"C{i:04d}" for i in range(n)],
"industry": rng.choice(["自動車", "電子", "食品", "化学"], n, p=[.35, .30, .20, .15]),
"size": rng.choice(["中小", "中堅", "大手"], n, p=[.50, .35, .15]),
"tenure_months": rng.integers(3, 73, n),
"past_margin": rng.gamma(3.0, 90.0, n),
})
customers["treatment"] = rng.integers(0, 2, n)
base = customers["size"].map({"中小": .08, "中堅": .11, "大手": .15}).to_numpy()
prob = np.clip(base + .035 * customers["treatment"].to_numpy(), 0, 1)
customers["order"] = rng.binomial(1, prob)
customers["post_margin"] = customers["order"] * rng.gamma(2.5, 55, n)
products = pd.DataFrame({
"product": ["センサーA", "制御器B", "保守キットC"],
"price": [120, 210, 75], "unit_margin": [48, 76, 34], "stock": [180, 80, 260]
})
display(customers.head())
display(products)
| customer_id | industry | size | tenure_months | past_margin | treatment | order | post_margin | |
|---|---|---|---|---|---|---|---|---|
| 0 | C0000 | 食品 | 中堅 | 48 | 112.01 | 0 | 0 | 0.00 |
| 1 | C0001 | 電子 | 中小 | 53 | 282.24 | 0 | 0 | 0.00 |
| 2 | C0002 | 化学 | 大手 | 21 | 201.37 | 1 | 1 | 181.54 |
| 3 | C0003 | 食品 | 中堅 | 27 | 192.30 | 0 | 0 | 0.00 |
| 4 | C0004 | 自動車 | 中小 | 69 | 558.43 | 1 | 0 | 0.00 |
| product | price | unit_margin | stock | |
|---|---|---|---|---|
| 0 | センサーA | 120 | 48 | 180 |
| 1 | 制御器B | 210 | 76 | 80 |
| 2 | 保守キットC | 75 | 34 | 260 |
No.071:推薦システムのA/Bテスト
実務での意味
推薦モデルのオフライン精度が高くても、表示によって受注が増えるとは限りません。対象顧客を無作為に対照群と施策群へ割り付け、受注率の増分効果を測ることで、全社展開の根拠を作ります。
分析・モデル化の考え方
施策群と対照群の受注率を とすると、絶対効果は です。独立2標本の標準誤差
から95%信頼区間を求めます。統計的有意性に加え、増分粗利が配信・実装費を上回るかを確認します。
Pythonで確認する
ab = customers.groupby("treatment")["order"].agg(["mean", "sum", "count"])
p0, p1 = ab.loc[0, "mean"], ab.loc[1, "mean"]
n0, n1 = ab.loc[0, "count"], ab.loc[1, "count"]
lift = p1 - p0
se = np.sqrt(p1*(1-p1)/n1 + p0*(1-p0)/n0)
ci = (lift - 1.96*se, lift + 1.96*se)
z = lift / se
p_value = 2 * stats.norm.sf(abs(z))
display(ab.rename(index={0:"対照群", 1:"推薦群"}))
print(f"受注率差: {lift:.1%}, 95% CI: [{ci[0]:.1%}, {ci[1]:.1%}], p値: {p_value:.4f}")
ax = (ab["mean"]*100).rename(index={0:"対照群",1:"推薦群"}).plot(kind="bar", color=["#8da0cb", "#fc8d62"])
ax.set_title("推薦A/Bテストの受注率")
ax.set_xlabel("実験群")
ax.set_ylabel("受注率(%)")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| mean | sum | count | |
|---|---|---|---|
| treatment | |||
| 対照群 | 0.10 | 106 | 1021 |
| 推薦群 | 0.13 | 124 | 979 |
受注率差: 2.3%, 95% CI: [-0.5%, 5.1%], p値: 0.1099

結果の読み取り
施策群の受注率差と信頼区間を確認します。信頼区間が0をまたぐ場合は「効果なし」と断定するのではなく、サンプル不足やセグメント差も検討します。本番判断では、効果の下限値を用いた保守的な増分粗利が運用費を超えるかをゲートにすると安全です。
No.072:推薦とLTV
実務での意味
単発受注の確率だけを最大化すると、値引き依存の顧客や保守負担の大きい案件へ偏ることがあります。継続粗利を含むLTVで優先順位を付け、営業工数や推薦枠を長期価値の高い顧客へ配分します。
分析・モデル化の考え方
簡易LTVを、初回粗利 、月次継続粗利 、月次継続率 、月次割引率 として
で評価します。推薦の期待価値は「推薦による受注確率の増分×LTV」で比較します。
Pythonで確認する
segment = pd.DataFrame({
"segment":["中小・新規", "中堅・既存", "大手・既存"],
"initial_margin":[90, 150, 240], "monthly_margin":[18, 35, 60],
"retention":[.88, .94, .97], "uplift":[.050, .035, .020], "eligible":[900, 600, 220]
})
T, discount = 24, .006
segment["LTV"] = segment.apply(lambda r: r.initial_margin + sum(r.monthly_margin*r.retention**t/(1+discount)**t for t in range(1,T+1)), axis=1)
segment["incremental_value"] = segment["LTV"] * segment["uplift"] * segment["eligible"]
display(segment.sort_values("incremental_value", ascending=False))
ax = segment.set_index("segment")["incremental_value"].sort_values().plot(kind="barh", color="#66c2a5")
ax.set_title("セグメント別・推薦の期待増分LTV")
ax.set_xlabel("期待増分LTV(千円)")
ax.set_ylabel("顧客セグメント")
ax.grid(axis="x", alpha=.3)
plt.tight_layout()
plt.show()
| segment | initial_margin | monthly_margin | retention | uplift | eligible | LTV | incremental_value | |
|---|---|---|---|---|---|---|---|---|
| 1 | 中堅・既存 | 150 | 35 | 0.94 | 0.04 | 600 | 550.68 | 11,564.24 |
| 0 | 中小・新規 | 90 | 18 | 0.88 | 0.05 | 900 | 210.65 | 9,479.20 |
| 2 | 大手・既存 | 240 | 60 | 0.97 | 0.02 | 220 | 1,182.46 | 5,202.84 |

結果の読み取り
LTVが最大のセグメントと、施策全体の増分価値が最大のセグメントは一致しないことがあります。対象母数と因果的なupliftも掛け合わせ、営業枠を配分します。継続率の推定誤差が結果を大きく動かすため、悲観・標準・楽観の感度分析も運用時に加えます。
No.073:推薦システムのECへの応用
実務での意味
BtoB ECではクリックされやすさだけでなく、粗利、在庫、納期を考慮した順位付けが必要です。在庫切れ商品の露出を抑え、代替可能で供給余力のある商品を上位に出します。
分析・モデル化の考え方
候補商品のスコアを 購入確率 × 単位粗利 × 在庫充足係数 とします。これは簡易な期待粗利ランキングです。本番では顧客適合性、契約価格、互換性、欠品ペナルティを追加します。
Pythonで確認する
ec = products.copy()
ec["purchase_prob"] = [.24, .18, .31]
ec["demand_30d"] = [220, 95, 170]
ec["availability"] = np.minimum(1, ec["stock"] / ec["demand_30d"])
ec["recommend_score"] = ec["purchase_prob"] * ec["unit_margin"] * ec["availability"]
ec["rank"] = ec["recommend_score"].rank(ascending=False, method="first").astype(int)
display(ec.sort_values("rank"))
ax = ec.sort_values("recommend_score").plot.barh(x="product", y="recommend_score", legend=False, color="#fc8d62")
ax.set_title("供給余力を考慮したEC推薦スコア")
ax.set_xlabel("期待粗利スコア")
ax.set_ylabel("製品")
ax.grid(axis="x", alpha=.3)
plt.tight_layout()
plt.show()
| product | price | unit_margin | stock | purchase_prob | demand_30d | availability | recommend_score | rank | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 制御器B | 210 | 76 | 80 | 0.18 | 95 | 0.84 | 11.52 | 1 |
| 2 | 保守キットC | 75 | 34 | 260 | 0.31 | 170 | 1.00 | 10.54 | 2 |
| 0 | センサーA | 120 | 48 | 180 | 0.24 | 220 | 0.82 | 9.43 | 3 |

結果の読み取り
購入確率が高くても、在庫充足率が低い商品は順位が下がります。ただし供給制約を隠して売れ筋を恒常的に下げると需要機会を見失うため、「需要予測用の素スコア」と「表示用の制約付きスコア」は分けて保存します。
No.074:推薦システムの製造業への応用
実務での意味
製造業の推薦対象は商品だけではありません。設備の稼働時間や故障履歴から、交換部品、点検、技術資料を「次善行動」として営業・保守担当へ提示できます。
分析・モデル化の考え方
設備ごとに故障リスクと提案受諾確率を推定し、リスク × 受諾確率 × 回避損失 を優先度とします。安全上必須の点検はスコアリングではなくルールで強制する点が重要です。
Pythonで確認する
assets = pd.DataFrame({
"asset":[f"M-{i:02d}" for i in range(1,9)],
"runtime_h":[8200,4300,6700,9100,3800,7600,5400,8800],
"fault_count":[3,0,2,5,1,4,1,3],
"accept_prob":[.55,.20,.45,.68,.25,.60,.35,.64],
"avoided_loss":[900,600,750,1200,500,980,680,1100]
})
logit = -5 + .00045*assets.runtime_h + .28*assets.fault_count
assets["failure_risk"] = 1/(1+np.exp(-logit))
assets["priority_value"] = assets.failure_risk*assets.accept_prob*assets.avoided_loss
display(assets.sort_values("priority_value", ascending=False).head())
ax = assets.plot.scatter(x="failure_risk", y="priority_value", s=assets["avoided_loss"]/3, alpha=.7, color="#8da0cb")
ax.set_title("設備保守提案の優先度")
ax.set_xlabel("90日故障リスク")
ax.set_ylabel("期待回避損失(千円)")
ax.grid(alpha=.3)
plt.tight_layout()
plt.show()
| asset | runtime_h | fault_count | accept_prob | avoided_loss | failure_risk | priority_value | |
|---|---|---|---|---|---|---|---|
| 3 | M-04 | 9100 | 5 | 0.68 | 1200 | 0.62 | 506.97 |
| 7 | M-08 | 8800 | 3 | 0.64 | 1100 | 0.45 | 316.92 |
| 5 | M-06 | 7600 | 4 | 0.60 | 980 | 0.39 | 227.55 |
| 0 | M-01 | 8200 | 3 | 0.55 | 900 | 0.38 | 190.39 |
| 2 | M-03 | 6700 | 2 | 0.45 | 750 | 0.19 | 65.43 |

結果の読み取り
故障リスクだけでなく、受諾可能性と回避できる損失を含めると、担当者が先に接触すべき設備が明確になります。高リスク・低受諾の設備には、値引きより技術説明や停止計画の共同策定が適する場合があります。
No.075:推薦システムとAIエージェントの統合
実務での意味
AIエージェントは候補抽出、在庫照会、説明文作成を連結できます。一方、互換性違反、契約条件逸脱、供給不能を自動送信すると損害が大きいため、権限境界が必要です。
分析・モデル化の考え方
予測スコアに加え、在庫、互換性、信頼度、案件金額でポリシー判定します。ここでは「自動提案」「人手承認」「提案禁止」の三段階に分け、判断ログを残す設計を模擬します。
Pythonで確認する
agent_cases = pd.DataFrame({
"case":["Q-101","Q-102","Q-103","Q-104","Q-105","Q-106"],
"score":[.88,.77,.91,.63,.82,.72], "confidence":[.92,.61,.95,.86,.74,.89],
"stock_ok":[True,True,False,True,True,True], "compatible":[True,True,True,False,True,True],
"deal_value":[180,2400,350,220,480,900]
})
def policy(r):
if not r.stock_ok or not r.compatible: return "提案禁止"
if r.confidence < .8 or r.deal_value >= 1000: return "人手承認"
return "自動提案"
agent_cases["decision"] = agent_cases.apply(policy, axis=1)
display(agent_cases)
counts = agent_cases["decision"].value_counts().reindex(["自動提案","人手承認","提案禁止"], fill_value=0)
ax = counts.plot(kind="bar", color=["#66c2a5","#ffd92f","#e78ac3"])
ax.set_title("AIエージェントのガードレール判定")
ax.set_xlabel("判定")
ax.set_ylabel("案件数")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| case | score | confidence | stock_ok | compatible | deal_value | decision | |
|---|---|---|---|---|---|---|---|
| 0 | Q-101 | 0.88 | 0.92 | True | True | 180 | 自動提案 |
| 1 | Q-102 | 0.77 | 0.61 | True | True | 2400 | 人手承認 |
| 2 | Q-103 | 0.91 | 0.95 | False | True | 350 | 提案禁止 |
| 3 | Q-104 | 0.63 | 0.86 | True | False | 220 | 提案禁止 |
| 4 | Q-105 | 0.82 | 0.74 | True | True | 480 | 人手承認 |
| 5 | Q-106 | 0.72 | 0.89 | True | True | 900 | 自動提案 |

結果の読み取り
高スコアでも、欠品や互換性違反は提案禁止になります。高額案件と低信頼案件を人手承認へ回すことで、効率と統制を両立できます。運用では入力データ、候補、参照情報、最終判断、承認者を監査ログとして保存します。
No.076:線形計画法
実務での意味
需要が能力を上回るとき、利益率だけの順番では複数工程のボトルネックを適切に扱えません。線形計画法で製品別の生産量を同時に決めます。
分析・モデル化の考え方
製品 の生産量を 、限界利益を として を解きます。設備 の使用係数を 、能力を とすれば です。連続量を扱える材料・バルク生産を想定します。
Pythonで確認する
lp_products = ["材料A","材料B","材料C"]
margin = np.array([42, 55, 38])
A = np.array([[2.0, 3.0, 1.5], [1.0, .8, 1.6]])
capacity = np.array([720, 360])
demand = np.array([180, 140, 200])
res_lp = linprog(-margin, A_ub=A, b_ub=capacity, bounds=list(zip(np.zeros(3), demand)), method="highs")
lp_plan = pd.DataFrame({"product":lp_products, "production":res_lp.x, "demand_upper":demand, "unit_margin":margin})
display(lp_plan)
print(f"最大限界利益: {-res_lp.fun:,.1f} 千円")
ax = lp_plan.plot.bar(x="product", y=["production","demand_upper"], color=["#66c2a5","#b3b3b3"])
ax.set_title("能力制約下の最適生産量")
ax.set_xlabel("製品")
ax.set_ylabel("生産量")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| product | production | demand_upper | unit_margin | |
|---|---|---|---|---|
| 0 | 材料A | 180.00 | 180 | 42 |
| 1 | 材料B | 85.00 | 140 | 55 |
| 2 | 材料C | 70.00 | 200 | 38 |
最大限界利益: 14,895.0 千円

結果の読み取り
需要上限まで作る製品と、能力制約により抑える製品が分かれます。単位利益が高い製品でも、ボトルネックを多く消費すれば必ずしも優先されません。実務では解と併せて制約の余力とシャドープライスを確認し、残業や設備増強の価値を評価します。
No.077:整数計画法
実務での意味
生産ラインの立上げや段取りは実施するか否かの離散判断であり、連続量の端数解は実行できません。固定段取り費と最小ロットを含めた整数計画が必要です。
分析・モデル化の考え方
生産ロット数 と稼働有無 を用い、 と で論理関係を表します。目的関数はロット粗利から固定段取り費を引きます。
Pythonで確認する
# 変数順: x_A, x_B, x_C(整数ロット数), y_A, y_B, y_C(二値)
lot_margin = np.array([110, 145, 95])
setup_cost = np.array([80, 120, 55])
c = np.r_[-lot_margin, setup_cost]
max_lots, min_lots = np.array([8,6,10]), np.array([2,2,3])
rows, ub = [], []
rows.append(np.r_[[3,4,2], [0,0,0]]); ub.append(28)
for j in range(3):
row = np.zeros(6); row[j]=1; row[3+j]=-max_lots[j]; rows.append(row); ub.append(0)
row = np.zeros(6); row[j]=-1; row[3+j]=min_lots[j]; rows.append(row); ub.append(0)
res_milp = milp(c, integrality=np.ones(6), bounds=Bounds(np.zeros(6), np.r_[max_lots, np.ones(3)]), constraints=LinearConstraint(np.array(rows), -np.inf, np.array(ub)))
milp_plan = pd.DataFrame({"product":["部品A","部品B","部品C"], "lots":np.rint(res_milp.x[:3]).astype(int), "setup":np.rint(res_milp.x[3:]).astype(int)})
display(milp_plan)
print(f"段取り費控除後の利益: {-res_milp.fun:,.0f} 千円")
ax = milp_plan.plot.bar(x="product", y="lots", legend=False, color="#8da0cb")
ax.set_title("段取り・最小ロットを含む生産計画")
ax.set_xlabel("製品")
ax.set_ylabel("ロット数")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| product | lots | setup | |
|---|---|---|---|
| 0 | 部品A | 4 | 1 |
| 1 | 部品B | 0 | 0 |
| 2 | 部品C | 8 | 1 |
段取り費控除後の利益: 1,065 千円

結果の読み取り
段取り費があるため、少量ずつ全品種を作る案より、限られた品種へ集約する解が選ばれます。整数計画の解は入力条件に敏感です。最小ロット、切替時間、納期必達を現場責任者と確認し、最適解に加えて利益差の小さい代替案も提示します。
No.078:在庫最適化
実務での意味
安全在庫を一律に積むと運転資金が増え、少なすぎると欠品します。需要変動、調達リードタイム、要求サービス水準から品目別の発注点を決めます。
分析・モデル化の考え方
日次需要の平均を 、標準偏差を 、リードタイムを 日、サービス水準に対応する標準正規分位点を とすると、独立同分布を仮定した安全在庫と発注点は
です。季節性やリードタイム変動が強い場合は、予測誤差分布やシミュレーションで置き換えます。
Pythonで確認する
inventory = pd.DataFrame({
"item":["センサーA","制御器B","保守キットC"],
"daily_mean":[8.0,3.2,10.5], "daily_std":[2.4,1.4,3.1],
"lead_time":[12,20,7], "service_level":[.95,.98,.95]
})
inventory["z"] = stats.norm.ppf(inventory.service_level)
inventory["safety_stock"] = inventory.z*inventory.daily_std*np.sqrt(inventory.lead_time)
inventory["reorder_point"] = inventory.daily_mean*inventory.lead_time + inventory.safety_stock
display(inventory.round(1))
ax = inventory.plot.bar(x="item", y=["safety_stock","reorder_point"], color=["#fc8d62","#66c2a5"])
ax.set_title("品目別の安全在庫と発注点")
ax.set_xlabel("品目")
ax.set_ylabel("数量")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| item | daily_mean | daily_std | lead_time | service_level | z | safety_stock | reorder_point | |
|---|---|---|---|---|---|---|---|---|
| 0 | センサーA | 8.00 | 2.40 | 12 | 1.00 | 1.60 | 13.70 | 109.70 |
| 1 | 制御器B | 3.20 | 1.40 | 20 | 1.00 | 2.10 | 12.90 | 76.90 |
| 2 | 保守キットC | 10.50 | 3.10 | 7 | 1.00 | 1.60 | 13.50 | 87.00 |

結果の読み取り
平均需要だけでなく、変動、リードタイム、サービス水準が発注点を左右します。高価な制御器に高いサービス水準を設定するなら、欠品損失と在庫費の根拠が必要です。実務ではMOQ、発注曜日、代替品、需要の自己相関も加えます。
No.079:サプライチェーン最適化
実務での意味
複数工場から複数拠点へ供給する際、最寄り工場を個別に選ぶだけでは工場能力を超えることがあります。全体の輸送費を最小化しつつ需要を満たす配分を求めます。
分析・モデル化の考え方
工場 から需要地 への輸送量を 、単位輸送費を とし、 を解きます。各工場の供給上限と各需要地の需要充足を制約に置く輸送問題です。
Pythonで確認する
factories, regions = ["東工場","西工場"], ["関東","中部","関西"]
cost = np.array([[4,6,9],[8,5,3]], dtype=float)
supply, region_demand = np.array([180,160]), np.array([120,100,120])
# 需要制約は -sum_i x_ij <= -demand と表す
A_ub, b_ub = [], []
for i in range(2):
row=np.zeros(6); row[i*3:(i+1)*3]=1; A_ub.append(row); b_ub.append(supply[i])
for j in range(3):
row=np.zeros(6); row[j]=-1; row[3+j]=-1; A_ub.append(row); b_ub.append(-region_demand[j])
res_ship = linprog(cost.ravel(), A_ub=np.array(A_ub), b_ub=np.array(b_ub), bounds=(0,None), method="highs")
ship = pd.DataFrame(res_ship.x.reshape(2,3), index=factories, columns=regions)
display(ship)
print(f"最小輸送費: {res_ship.fun:,.0f} 千円")
ax = ship.T.plot(kind="bar", stacked=True, color=["#8da0cb","#fc8d62"])
ax.set_title("地域別の最適供給元構成")
ax.set_xlabel("需要地域")
ax.set_ylabel("輸送数量")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| 関東 | 中部 | 関西 | |
|---|---|---|---|
| 東工場 | 120.00 | 60.00 | 0.00 |
| 西工場 | 0.00 | 40.00 | 120.00 |
最小輸送費: 1,400 千円

結果の読み取り
近距離の供給を優先しつつ、工場能力の限界によって一部地域は別工場から補完されます。費用最小解だけでは災害・停止リスクに弱いため、実務では二重調達比率、CO2、納期、BCP在庫を制約または目的関数に追加します。
No.080:マーケティング予算配分
実務での意味
展示会、検索広告、既存顧客セミナー、営業支援へ、限られた予算をどう配るかを決めます。平均ROIの高い施策に全額投下すると、飽和やチャネル依存を無視します。
分析・モデル化の考え方
各施策を100万円単位の投資枠に分割し、枠が進むほど限界粗利が逓減するものとします。各枠を選ぶ0-1変数を用い、総予算、施策上限、最低限必要なブランド施策を制約として増分粗利を最大化します。
Pythonで確認する
channels = ["展示会","検索広告","顧客セミナー","営業支援"]
returns = {
"展示会":[1.9,1.5,1.1,0.8], "検索広告":[2.2,1.6,1.0,0.6],
"顧客セミナー":[2.5,1.9,1.4,1.0], "営業支援":[2.1,1.8,1.5,1.2]
}
items=[(ch,k,r) for ch in channels for k,r in enumerate(returns[ch], start=1)]
# 10枠(1枠=100万円)、展示会を最低1枠。逓減列なので先の枠から自然に選ばれる。
c=-np.array([r for _,_,r in items])
budget_row=np.ones(len(items))
expo_row=np.array([-1 if ch=="展示会" else 0 for ch,_,_ in items])
res_budget=milp(c, integrality=np.ones(len(items)), bounds=Bounds(np.zeros(len(items)),np.ones(len(items))), constraints=LinearConstraint(np.vstack([budget_row,expo_row]),[-np.inf,-np.inf],[10,-1]))
chosen=np.rint(res_budget.x).astype(int)
allocation=pd.DataFrame(items,columns=["channel","block","incremental_margin"])
allocation["selected"]=chosen
summary=allocation.query("selected==1").groupby("channel").agg(budget_blocks=("selected","sum"), expected_margin=("incremental_margin","sum")).reindex(channels,fill_value=0)
display(summary)
print(f"配分予算: {summary.budget_blocks.sum()*100:,.0f} 万円, 期待増分粗利指数: {summary.expected_margin.sum():.1f}")
ax = summary["budget_blocks"].plot(kind="bar", color="#66c2a5")
ax.set_title("最適マーケティング予算配分")
ax.set_xlabel("施策")
ax.set_ylabel("予算枠(100万円/枠)")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| budget_blocks | expected_margin | |
|---|---|---|
| channel | ||
| 展示会 | 2 | 3.40 |
| 検索広告 | 2 | 3.80 |
| 顧客セミナー | 3 | 5.80 |
| 営業支援 | 3 | 5.40 |
配分予算: 1,000 万円, 期待増分粗利指数: 18.4

結果の読み取り
限界効果の高い枠から選ぶため、複数施策へ予算が分散します。これは均等配分ではなく、逓減効果を踏まえた結果です。係数は観察データの単純ROIではなく、地域・時期を分けた実験やMMMで推定し、営業キャパシティやリード品質も制約に含めます。
対象ノックを通して見える実務上の示唆
- 推薦精度から増分価値へ:A/Bテストで因果効果を測り、LTVと粗利で施策価値へ変換します。
- 需要創出と供給制約を接続する:ECや営業の表示順位に、在庫・納期・互換性を反映します。
- 予測と意思決定を分ける:需要や受注確率は予測モデル、配分は最適化モデルが担います。双方の入力・出力を記録します。
- 自動化には棄却条件を設ける:AIエージェントは高額・低信頼・規約違反を人へ戻すことで安全に使えます。
- 一点の最適解を過信しない:需要、効果、コストを変えた感度分析と代替案を併記します。
実務導入する場合に必要なこと
- 顧客・製品・在庫・能力・原価のIDと更新時刻を統一する
- KPIを受注率だけでなく増分粗利、LTV、欠品率、納期遵守率まで定義する
- 実験単位、必要サンプル数、停止基準を事前登録する
- 最適化の制約と例外ルールを現場と棚卸しし、責任者を決める
- 推薦理由、入力データ、モデル版、承認結果を監査可能にする
- 小規模なPoC、並行稼働、限定自動化の順に展開する
まとめ
No.071〜No.080では、推薦を「当たりやすい候補を出す技術」に限定せず、実験で増分を確認し、顧客の長期価値を評価し、供給制約下で実行可能な計画へ落とす方法を確認しました。製造業で成果を出す鍵は、モデル単体の高度さよりも、需要・供給・利益・統制を同じ意思決定プロセスにつなぐことです。
法人向けのご相談
数理工房では、推薦・需要予測・在庫・生産・物流・マーケティング配分を対象に、課題整理、データ設計、PoC、現場導入、社内研修まで支援しています。既存システムや現場ルールを踏まえ、説明可能で運用できる形からご一緒します。
📩 お問い合わせ: surikobo.co.jp/contact まずはお気軽にご相談ください。