100本ノック / マーケティングサイエンス / マーケティングサイエンス100本ノック

製造業のマーケティング投資最適化|アトリビューションからAI・デジタルツインまで

製造業の営業・マーケティング投資を最適化する10の実践

No.081〜No.090:施策貢献度からデジタルツイン、Decision Intelligenceまで

産業機器メーカーを想定し、展示会、技術セミナー、Web広告、代理店支援という複数施策を、受注確率・粗利・供給制約・不確実性まで含めて評価します。個別の分析手法を並べるのではなく、「施策の寄与を測る → 配分案を作る → 揺らぎに耐えるか確認する → 会議で意思決定する」という実務の流れとして扱います。

[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。

はじめに:この記事で扱う製造業の実務課題

B2B製造業では、マーケティング費用を投じてから受注まで数か月かかり、顧客は複数の接点を経由します。さらに営業人員、工場能力、製品別粗利、部材不足が成果を左右します。本稿の問いは、単純な「リード件数最大化」ではなく、限られた予算をどこに配り、どの不確実性を許容し、誰がどの条件で意思決定するかです。

現場でよくある状況

  • 展示会は受注に近いが高額、Web広告は接点数が多いが受注への距離が遠い
  • 各部門が自部門のKPIで施策を評価し、全社粗利との接続が弱い
  • 平均値ベースの計画が、需要変動や供給制約で崩れる
  • 高度なモデルを作っても、会議で採否を説明できず運用されない

なぜこの問題は判断が難しいのか

顧客接点の順序、施策間の重複、効果逓減、予算の整数性、将来需要の分布、モデル誤差が同時に存在するためです。相関だけで配分すると過大評価が起き、期待値だけで最適化すると悪条件で大きく外れます。そこで、本稿では複数の見方を比較し、目的関数と制約を明示します。

今回扱うノックの全体像

No.テーマ意思決定への役割
081アトリビューション受注に至る接点の貢献を配賦する
082ポートフォリオ最適化収益と変動のバランスを取る
083シミュレーション最適化複雑な業務を反復計算で探索する
084ロバスト最適化最悪側の条件でも壊れにくい案を選ぶ
085確率最適化制約違反確率を管理する
086強化学習観測結果に応じて逐次配分する
087デジタルツイン施策から生産までを仮想再現する
088DIとの統合分析を意思決定プロセスへ接続する
089AI時代の最適化AI予測を監視可能な最適化へ使う
090実務事例導入判断まで一気通貫でまとめる

Python環境の準備

外部データには依存せず、NumPy、pandas、matplotlibだけを使います。乱数シードを固定し、同じ結果を再現できるようにします。金額の単位は特記がない限り万円です。

%matplotlib inline
import sys
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
from itertools import product

SEED = 42
rng = np.random.default_rng(SEED)
pd.set_option("display.float_format", lambda x: f"{x:,.2f}")
print("Python:", sys.version.split()[0])
print("numpy:", np.__version__, "pandas:", pd.__version__, "matplotlib:", matplotlib.__version__)
Python: 3.13.1
numpy: 2.5.1 pandas: 3.0.3 matplotlib: 3.11.0

架空データの作成

産業用ポンプメーカー「工房ポンプ」の直近400商談を作ります。各商談には複数の顧客接点があり、案件規模、製品、受注結果、粗利を持たせます。真の受注確率はデータ生成時だけ利用し、実務では観測できないものとします。

channels = ["Exhibition", "WebAds", "Webinar", "Distributor"]
products = ["Standard", "HighPressure", "IoT"]
n = 400
deal_size = rng.lognormal(mean=4.5, sigma=0.45, size=n)
product_category = rng.choice(products, size=n, p=[0.45, 0.30, 0.25])
touches = rng.poisson([0.7, 1.4, 0.9, 0.8], size=(n, 4))
touches = np.clip(touches, 0, 4)
linear = -2.7 + touches @ np.array([0.55, 0.18, 0.40, 0.48]) + 0.20 * (product_category == "IoT")
win_prob = 1 / (1 + np.exp(-linear))
won = rng.binomial(1, win_prob)
margin_rate = np.select([product_category == "Standard", product_category == "HighPressure"], [0.28, 0.34], default=0.42)
deals = pd.DataFrame(touches, columns=channels)
deals.insert(0, "deal_id", [f"D{i:04d}" for i in range(1, n + 1)])
deals["product"] = product_category
deals["deal_size"] = deal_size
deals["won"] = won
deals["gross_profit"] = deal_size * margin_rate * won
print("架空データ件数:", len(deals), " / 受注率:", f"{deals.won.mean():.1%}")
deals.head()
架空データ件数: 400  / 受注率: 22.0%
deal_id Exhibition WebAds Webinar Distributor product deal_size won gross_profit
0 D0001 1 1 0 1 Standard 103.25 0 0.00
1 D0002 2 1 1 1 IoT 56.37 0 0.00
2 D0003 0 3 2 0 Standard 126.18 0 0.00
3 D0004 1 1 1 1 IoT 137.45 0 0.00
4 D0005 1 0 2 1 HighPressure 37.41 0 0.00

No.081:アトリビューション

実務での意味

最終接点だけに受注を帰属させると、案件育成に効いた上流施策を過小評価します。一方、全接点へ均等配分すると、順序や受注との関連を無視します。ここでは受注案件の粗利を「最終接点」と「接点回数按分」の2方式で比較し、評価ルールが予算判断を変えることを確認します。

分析・モデル化の考え方

案件 ii の粗利を GiG_i、チャネル jj の接点数を xijx_{ij} とすると、線形配賦額は

Aj=iGixijkxikA_j=\sum_i G_i\frac{x_{ij}}{\sum_k x_{ik}}

です。これは因果効果ではなく、観測された接点への会計的な配賦です。因果判断には実験や統制群が別途必要です。

Pythonで確認する

won_deals = deals.query("won == 1").copy()
denom = won_deals[channels].sum(axis=1).replace(0, np.nan)
linear_credit = won_deals[channels].div(denom, axis=0).mul(won_deals["gross_profit"], axis=0).sum().fillna(0)
last_touch = won_deals[channels].apply(lambda r: channels[np.flatnonzero(r.to_numpy() > 0)[-1]] if r.sum() else "None", axis=1)
last_credit = won_deals.groupby(last_touch)["gross_profit"].sum().reindex(channels, fill_value=0)
attr = pd.DataFrame({"Linear credit": linear_credit, "Last-touch credit": last_credit})
display(attr.round(1))
ax = attr.plot(kind="bar", figsize=(8, 4), color=["#2F6690", "#D95F59"])
ax.set(title="Gross-profit attribution by rule", xlabel="Channel", ylabel="Attributed gross profit (10k JPY)")
ax.grid(axis="y", alpha=.3); plt.xticks(rotation=0); plt.tight_layout(); plt.show()
Linear credit Last-touch credit
Exhibition 590.80 33.80
WebAds 944.50 392.40
Webinar 666.80 681.70
Distributor 566.00 1,660.30

png

結果の読み取り

配賦方式によってチャネルの順位と金額が変わります。したがって「貢献額」は事実そのものではなく、ルールを含む指標です。実務では、配賦指標を予算削減の唯一の根拠にせず、接点順序、案件属性、未接触の対照群を併記します。

No.082:ポートフォリオ最適化

実務での意味

施策別の平均ROIが高くても、同じ景気要因で同時に悪化する施策へ集中すると計画が不安定になります。投資ポートフォリオの考え方を応用し、期待粗利と変動リスクを同時に見ます。

分析・モデル化の考え方

配分比率を ww、期待収益率を μ\mu、共分散行列を Σ\Sigma とすると、期待収益は wTμw^T\mu、リスクは wTΣw\sqrt{w^T\Sigma w} です。ここでは5%刻みの配分案を全探索し、リスク調整後スコア wTμλwTΣww^T\mu-\lambda\sqrt{w^T\Sigma w} を最大化します。

Pythonで確認する

scenario_returns = pd.DataFrame({
    "Exhibition": [0.55, 0.30, 0.12, 0.40, 0.20, 0.48],
    "WebAds": [0.38, 0.42, 0.18, 0.35, 0.28, 0.31],
    "Webinar": [0.44, 0.36, 0.25, 0.40, 0.32, 0.39],
    "Distributor": [0.32, 0.24, 0.40, 0.29, 0.45, 0.35],
}, index=["Boom", "Normal-A", "Recession", "Normal-B", "SupplyShock", "Recovery"])
mu, cov = scenario_returns.mean().to_numpy(), scenario_returns.cov().to_numpy()
candidates = []
for units in product(range(21), repeat=4):
    if sum(units) == 20:
        w = np.array(units) / 20
        ret, risk = w @ mu, np.sqrt(w @ cov @ w)
        candidates.append([*w, ret, risk, ret - 1.2 * risk])
portfolio = pd.DataFrame(candidates, columns=channels + ["expected_return", "risk", "score"])
best_port = portfolio.loc[portfolio.score.idxmax()]
display(best_port.to_frame("value").round(3))
plt.figure(figsize=(7, 4)); plt.scatter(portfolio.risk, portfolio.expected_return, s=10, alpha=.35)
plt.scatter(best_port.risk, best_port.expected_return, s=90, color="#D95F59", label="Selected")
plt.title("Marketing portfolio: return and risk"); plt.xlabel("Risk (standard deviation)"); plt.ylabel("Expected return")
plt.grid(alpha=.3); plt.legend(); plt.tight_layout(); plt.show()
value
Exhibition 0.00
WebAds 0.00
Webinar 0.55
Distributor 0.45
expected_return 0.35
risk 0.03
score 0.31

png

結果の読み取り

選択案は平均収益だけの最大化ではなく、景況シナリオ間の変動を抑えた組み合わせです。リスク係数 λ\lambda は経営の許容度なので、分析担当者が暗黙に決めず、経営会議で複数案を比較します。

No.083:シミュレーション最適化

実務での意味

展示会の来場者、商談化、受注は確率的です。閉じた数式を作りにくい場合でも、候補予算を何度も仮想実行すれば、粗利分布を比較できます。

分析・モデル化の考え方

各施策の投資単位数 bjb_j に対して商談数をポアソン分布、受注数を二項分布で生成します。目的は期待純粗利 E[G(b)C(b)]E[G(b)-C(b)] の最大化です。ただし過学習を避けるため、候補数、反復数、乱数管理を記録します。

Pythonで確認する

def simulate_plan(units, repeats=800, seed=SEED):
    local = np.random.default_rng(seed)
    units = np.asarray(units)
    leads_per_unit = np.array([7, 12, 9, 8])
    win_rate = np.array([.24, .11, .19, .22])
    gp_per_win = np.array([70, 48, 62, 58])
    cost_per_unit = np.array([80, 35, 45, 50])
    leads = local.poisson(units * leads_per_unit, size=(repeats, 4))
    wins = local.binomial(leads, win_rate)
    return (wins * gp_per_win).sum(axis=1) - units @ cost_per_unit

plans = []
for u in product(range(5), repeat=4):
    if np.dot(u, [80, 35, 45, 50]) <= 400:
        profit = simulate_plan(u)
        plans.append([u, profit.mean(), np.quantile(profit, .10)])
sim_result = pd.DataFrame(plans, columns=["units", "mean_profit", "p10_profit"]).sort_values("mean_profit", ascending=False)
display(sim_result.head(8))
top = sim_result.head(3)
plt.figure(figsize=(8, 4))
for _, row in top.iterrows():
    plt.hist(simulate_plan(row.units), bins=25, alpha=.35, label=str(row.units))
plt.title("Profit distribution of top simulated plans"); plt.xlabel("Net gross profit (10k JPY)"); plt.ylabel("Frequency")
plt.grid(alpha=.3); plt.legend(title="Units"); plt.tight_layout(); plt.show()
units mean_profit p10_profit
71 (0, 2, 4, 3) 457.61 157.20
24 (0, 0, 4, 4) 436.67 158.00
93 (0, 3, 4, 2) 433.58 146.60
48 (0, 1, 4, 3) 431.04 168.80
90 (0, 3, 3, 3) 424.48 148.00
44 (0, 1, 3, 4) 422.85 155.80
155 (1, 1, 4, 2) 413.90 143.00
86 (0, 3, 2, 4) 410.49 133.00

png

結果の読み取り

期待値が近い案でも下位10%点は異なります。経営が予算下振れを嫌う場合、平均最大案ではなくP10が高い案を選ぶ余地があります。また、乱数誤差が順位を左右しないよう反復数を増やして再確認します。

No.084:ロバスト最適化

実務での意味

受注率の推定値は将来も同じとは限りません。ロバスト最適化は「想定範囲内で最も不利な状況」を置き、その状況でも成果を確保できる案を選びます。

分析・モデル化の考え方

不確実なシナリオ集合 SS に対し、maxbminsSG(b,s)\max_b\min_{s\in S}G(b,s) を解きます。悲観し過ぎると投資停止になるため、不確実集合は過去変動や専門家判断から説明可能に設定します。

Pythonで確認する

base = np.array([.24, .11, .19, .22])
shocks = pd.DataFrame([
    base,
    base * [.70, .90, .85, 1.00],
    base * [.95, .65, .80, .95],
    base * [.80, .85, .70, .90],
], index=["Base", "EventWeak", "DigitalWeak", "BroadDownturn"], columns=channels)
robust_rows = []
for u in product(range(5), repeat=4):
    cost = np.dot(u, [80, 35, 45, 50])
    if cost <= 400:
        scenario_profit = (np.asarray(u) * np.array([7,12,9,8]) * shocks.to_numpy() * np.array([70,48,62,58])).sum(axis=1) - cost
        robust_rows.append([u, scenario_profit.mean(), scenario_profit.min()])
robust = pd.DataFrame(robust_rows, columns=["units", "average", "worst_case"])
best_robust = robust.loc[robust.worst_case.idxmax()]
display(best_robust.to_frame("robust plan").round(1))
comparison = pd.DataFrame({"Mean-optimal": sim_result.iloc[0][["mean_profit", "p10_profit"]],
                           "Robust": [best_robust.average, best_robust.worst_case]}, index=["Average-like", "Downside metric"])
comparison.plot(kind="bar", figsize=(7,4), color=["#2F6690", "#E6A23C"])
plt.title("Mean-oriented and robust plans"); plt.xlabel("Metric"); plt.ylabel("Net gross profit (10k JPY)")
plt.grid(axis="y", alpha=.3); plt.xticks(rotation=0); plt.tight_layout(); plt.show()
robust plan
units (0, 0, 4, 4)
average 368.18
worst_case 284.34

png

結果の読み取り

ロバスト案は平均的な伸びを一部手放し、弱いシナリオでの損失を抑えます。全予算を常に使う必要はなく、限界利益が不確実なら投資余力を残すことも最適解になり得ます。

No.085:確率最適化

実務での意味

営業が獲得した受注を工場が処理できなければ、納期遅延と機会損失が生じます。「平均では能力内」ではなく、能力超過の確率を一定以下に抑える必要があります。

分析・モデル化の考え方

生産負荷を確率変数 L(b,ξ)L(b,\xi)、月間能力を CC として、機会制約 P(LC)1αP(L\le C)\ge 1-\alpha を置きます。ここではモンテカルロ標本で充足率を推定し、95%以上の案だけを採用します。

Pythonで確認する

def capacity_check(units, repeats=2000, seed=123):
    local = np.random.default_rng(seed)
    units = np.asarray(units)
    leads = local.poisson(units * [7,12,9,8], size=(repeats,4))
    wins = local.binomial(leads, [.24,.11,.19,.22])
    load = wins @ np.array([18, 12, 16, 15])
    profit = wins @ np.array([70,48,62,58]) - units @ np.array([80,35,45,50])
    return profit.mean(), (load <= 135).mean(), np.quantile(load, .95)

chance_rows = []
for u in product(range(5), repeat=4):
    if np.dot(u, [80,35,45,50]) <= 400:
        mean_profit, service_prob, load_p95 = capacity_check(u)
        chance_rows.append([u, mean_profit, service_prob, load_p95])
chance = pd.DataFrame(chance_rows, columns=["units", "mean_profit", "capacity_probability", "load_p95"])
feasible = chance.query("capacity_probability >= 0.95").sort_values("mean_profit", ascending=False)
display(feasible.head(8))
plt.figure(figsize=(7,4)); plt.scatter(chance.capacity_probability, chance.mean_profit, alpha=.45)
plt.axvline(.95, color="#D95F59", linestyle="--", label="Required probability")
plt.title("Profit versus capacity reliability"); plt.xlabel("P(load <= capacity)"); plt.ylabel("Expected net gross profit (10k JPY)")
plt.grid(alpha=.3); plt.legend(); plt.tight_layout(); plt.show()
units mean_profit capacity_probability load_p95
35 (0, 1, 2, 0) 151.88 0.96 132.00
3 (0, 0, 0, 3) 149.74 0.96 135.00
77 (0, 3, 1, 0) 146.38 0.96 128.00
31 (0, 1, 1, 1) 141.55 0.97 127.05
73 (0, 3, 0, 1) 139.68 0.96 129.00
27 (0, 1, 0, 2) 132.30 0.97 126.00
140 (1, 1, 1, 0) 126.77 0.96 134.00
136 (1, 1, 0, 1) 119.75 0.96 132.00

png

結果の読み取り

右側の点だけが95%の能力制約を満たします。許容超過確率5%は技術的定数ではなく、残業、外注、納期契約、顧客重要度を踏まえた経営判断です。

No.086:強化学習

実務での意味

施策効果が不明な新市場では、初期推定だけで年間予算を固定するより、少額で学びながら配分を更新する方が合理的です。ここでは強化学習の入口として多腕バンディットを扱います。

分析・モデル化の考え方

ε\varepsilon-greedy法では、確率 1ε1-\varepsilon で推定価値最大の施策を選び、確率 ε\varepsilon で探索します。価値は Qt+1(a)=Qt(a)+(RtQt(a))/Nt(a)Q_{t+1}(a)=Q_t(a)+(R_t-Q_t(a))/N_t(a) で逐次更新します。

Pythonで確認する

true_value = np.array([18, 12, 16, 20])
bandit_rng = np.random.default_rng(SEED)
q = np.zeros(4); counts = np.zeros(4, dtype=int); history = []
epsilon = .15
for t in range(300):
    if bandit_rng.random() < epsilon or counts.min() == 0:
        a = bandit_rng.integers(4)
    else:
        a = int(np.argmax(q))
    reward = bandit_rng.normal(true_value[a], 12)
    counts[a] += 1
    q[a] += (reward - q[a]) / counts[a]
    history.append((t, a, reward, q[a]))
bandit = pd.DataFrame({"channel": channels, "trials": counts, "estimated_value": q, "true_value_demo_only": true_value})
display(bandit.round(2))
plt.figure(figsize=(8,4))
for j, ch in enumerate(channels):
    series = pd.Series([h[2] if h[1] == j else np.nan for h in history]).expanding().mean()
    plt.plot(series, label=ch)
plt.title("Online learning of reward by channel"); plt.xlabel("Decision round"); plt.ylabel("Observed cumulative mean reward")
plt.grid(alpha=.3); plt.legend(); plt.tight_layout(); plt.show()
channel trials estimated_value true_value_demo_only
0 Exhibition 14 18.23 18
1 WebAds 14 12.91 12
2 Webinar 17 18.49 16
3 Distributor 255 19.52 20

png

結果の読み取り

高い報酬が観測された施策に試行が集まりつつ、探索も残ります。実務では報酬が遅れて観測される、顧客属性が異なる、施策変更が顧客体験を損なう点に注意し、ガードレールと人による停止権限を設けます。

No.087:デジタルツイン

実務での意味

マーケティングの意思決定は営業と生産へ波及します。デジタルツインは、施策投入から商談、受注、生産負荷、納期までの状態遷移を仮想空間で再現し、部門横断の影響を確認する仕組みです。

分析・モデル化の考え方

ここでは簡易な月次状態モデルを作ります。状態は案件残 PtP_t と生産残 BtB_t、入力は施策量 utu_t です。Pt+1=Pt+Leads(ut)WinstP_{t+1}=P_t+Leads(u_t)-Wins_tBt+1=max(0,Bt+Load(Winst)Capacity)B_{t+1}=\max(0,B_t+Load(Wins_t)-Capacity) と更新します。

Pythonで確認する

def run_twin(monthly_units, months=12, seed=77):
    local = np.random.default_rng(seed); pipeline = 20; backlog = 10; rows = []
    for month in range(1, months+1):
        leads = local.poisson(np.dot(monthly_units, [7,12,9,8]))
        available = pipeline + leads
        wins = local.binomial(available, .12)
        pipeline = available - wins
        new_load = wins * local.integers(12, 20)
        capacity = 135 + local.integers(-10, 11)
        backlog = max(0, backlog + new_load - capacity)
        rows.append([month, leads, wins, pipeline, new_load, capacity, backlog])
    return pd.DataFrame(rows, columns=["month","leads","wins","pipeline","new_load","capacity","backlog"])

twin = run_twin(feasible.iloc[0].units)
display(twin)
fig, ax = plt.subplots(figsize=(8,4))
ax.plot(twin.month, twin.pipeline, marker="o", label="Sales pipeline")
ax.plot(twin.month, twin.backlog, marker="s", label="Production backlog")
ax.set(title="Simplified marketing-to-production digital twin", xlabel="Month", ylabel="Cases / load index")
ax.grid(alpha=.3); ax.legend(); plt.tight_layout(); plt.show()
month leads wins pipeline new_load capacity backlog
0 1 35 5 50 90 132 0
1 2 27 12 65 156 126 30
2 3 28 13 80 221 137 114
3 4 23 16 87 240 145 209
4 5 33 19 101 247 128 328
5 6 27 14 114 238 127 439
6 7 24 11 127 209 133 515
7 8 21 21 127 294 129 680
8 9 36 23 140 345 134 891
9 10 25 14 151 238 134 995
10 11 24 16 159 224 134 1085
11 12 20 27 152 513 141 1457

png

結果の読み取り

商談が増えても、生産残が累積する月があれば納期リスクが高まります。実装時はCRM、受注、MES/ERPの定義と更新時刻をそろえ、現実との差を継続監視する必要があります。精密な3Dモデルだけがデジタルツインではありません。

No.088:DIとの統合

実務での意味

DI(Decision Intelligence)は、予測や最適化の結果を、意思決定者、選択肢、根拠、承認、実績検証まで含む業務プロセスへ統合する考え方です。モデル精度だけでなく「誰が、何を見て、いつ決めるか」を設計します。

分析・モデル化の考え方

DIでは、目的、制約、選択肢、推奨、信頼度、ガードレールを意思決定レコードとして残します。これにより、結果が悪かったときに、モデル、入力、制約、承認判断のどこを改善すべきか追跡できます。

Pythonで確認する

selected = feasible.iloc[0]
decision_record = pd.DataFrame([
    ["Objective", "Expected net gross profit", f"{selected.mean_profit:.1f}"],
    ["Decision", "Investment units", dict(zip(channels, selected.units))],
    ["Constraint", "Budget", "<= 400 (10k JPY)"],
    ["Guardrail", "P(production load <= capacity)", f"{selected.capacity_probability:.1%}"],
    ["Owner", "Approval", "Sales, Marketing, Production, Finance"],
    ["Review", "Re-estimation", "Monthly / stop if reliability < 90%"],
], columns=["record_type", "item", "value"])
display(decision_record)
plt.figure(figsize=(7,3.5)); plt.bar(channels, selected.units, color="#2F6690")
plt.title("DI decision card: recommended investment units"); plt.xlabel("Channel"); plt.ylabel("Investment units")
plt.grid(axis="y", alpha=.3); plt.tight_layout(); plt.show()
record_type item value
0 Objective Expected net gross profit 151.9
1 Decision Investment units {'Exhibition': 0, 'WebAds': 1, 'Webinar': 2, '...
2 Constraint Budget <= 400 (10k JPY)
3 Guardrail P(production load <= capacity) 95.8%
4 Owner Approval Sales, Marketing, Production, Finance
5 Review Re-estimation Monthly / stop if reliability < 90%

png

結果の読み取り

推奨値だけでなく制約、信頼性、責任者、再評価条件が1つのレコードになります。これが「分析資料」から「運用可能な意思決定」へ移す最小単位です。DIは特定製品名ではなく、意思決定を設計・記録・改善する枠組みとして扱います。

No.089:AI時代の最適化

実務での意味

生成AIや予測AIは、案件要約、受注確率、施策案の生成を支援できます。しかし、AIの予測値をそのまま最適化へ渡すと、過信や分布変化が配分を歪めます。

分析・モデル化の考え方

AI予測を p^\hat p とすると、校正誤差やドリフトを踏まえて安全側に psafe=clip(p^m,0,1)p_{safe}=\mathrm{clip}(\hat p-m,0,1) と補正し、制約付き最適化へ入力します。さらに、推奨理由、入力版、承認者、実績を監査ログへ残します。

Pythonで確認する

ai_pred = pd.Series([.27, .14, .23, .25], index=channels, name="AI predicted win rate")
uncertainty_margin = pd.Series([.05, .04, .06, .05], index=channels)
safe_pred = (ai_pred - uncertainty_margin).clip(0, 1).rename("Safety-adjusted rate")
ai_table = pd.concat([ai_pred, uncertainty_margin.rename("Uncertainty margin"), safe_pred], axis=1)
display(ai_table)
ax = ai_table[["AI predicted win rate", "Safety-adjusted rate"]].plot(kind="bar", figsize=(8,4), color=["#7E57C2", "#2F6690"])
ax.set(title="AI predictions with safety adjustment", xlabel="Channel", ylabel="Win probability")
ax.grid(axis="y", alpha=.3); plt.xticks(rotation=0); plt.tight_layout(); plt.show()
AI predicted win rate Uncertainty margin Safety-adjusted rate
Exhibition 0.27 0.05 0.22
WebAds 0.14 0.04 0.10
Webinar 0.23 0.06 0.17
Distributor 0.25 0.05 0.20

png

結果の読み取り

不確実性幅を引いた安全側予測は、AIの点予測より控えめです。重要なのは一律にAIを疑うことではなく、校正誤差、対象外データ、入力欠損を測り、信頼度に応じて自動化レベルを変えることです。

No.090:実務事例—四半期投資会議を再現する

実務での意味

最後に、架空の四半期投資会議として候補案を比較します。平均粗利、下振れ、能力充足率、予算を同じ表にし、単一KPIではなく複数の判断軸で合意形成します。

分析・モデル化の考え方

候補は「期待値重視」「ロバスト」「能力制約付き」の3案です。意思決定ではパレート優越、ハード制約、許容リスクを順に確認します。モデルは意思決定を代替するのではなく、トレードオフを可視化します。

Pythonで確認する

candidate_units = {
    "Expected-value plan": tuple(sim_result.iloc[0].units),
    "Robust plan": tuple(best_robust.units),
    "Capacity-safe plan": tuple(feasible.iloc[0].units),
}
rows = []
for name, u in candidate_units.items():
    sims = simulate_plan(u, repeats=3000, seed=999)
    mean_profit, cap_prob, load_p95 = capacity_check(u, repeats=3000, seed=999)
    rows.append([name, u, np.dot(u,[80,35,45,50]), sims.mean(), np.quantile(sims,.10), cap_prob, load_p95])
scorecard = pd.DataFrame(rows, columns=["plan","units","budget","mean_profit","p10_profit","capacity_probability","load_p95"]).set_index("plan")
display(scorecard.round(2))
plot_data = scorecard[["mean_profit", "p10_profit"]]
plot_data.plot(kind="bar", figsize=(9,4), color=["#2F6690", "#E6A23C"])
plt.title("Quarterly investment decision scorecard"); plt.xlabel("Candidate plan"); plt.ylabel("Net gross profit (10k JPY)")
plt.grid(axis="y", alpha=.3); plt.xticks(rotation=10); plt.tight_layout(); plt.show()
units budget mean_profit p10_profit capacity_probability load_p95
plan
Expected-value plan (0, 2, 4, 3) 400 456.64 176.00 0.06 314.05
Robust plan (0, 0, 4, 4) 380 450.52 162.00 0.07 311.00
Capacity-safe plan (0, 1, 2, 0) 125 150.09 -1.00 0.96 128.00

png

結果の読み取り

推奨は「能力制約付き案」です。理由は、期待粗利を確保しつつ生産能力を95%以上の確率で守るという、明示したガードレールを満たすためです。ただし、外注能力を確保できるなら制約を更新し、再最適化すべきです。最適解は永久に固定された答えではなく、前提に対する最良案です。

対象ノックを通して見える実務上の示唆

  1. 測定と最適化を分ける:アトリビューションは配賦、因果効果は実験・準実験で確認する。
  2. 期待値だけで決めない:分散、下位点、最悪ケース、制約充足確率を併記する。
  3. 部門間の状態をつなぐ:マーケティング施策を営業パイプラインと生産負荷まで追う。
  4. 推奨の運用条件を残す:責任者、承認、停止条件、再推定周期をDecision Recordにする。
  5. AIには信頼度を添える:点予測を鵜呑みにせず、校正、ドリフト、監査ログを組み込む。

実務導入する場合に必要なこと

  • CRM・MA・ERP/MES間で案件ID、製品、施策費、粗利、能力の定義を統一する
  • 目的関数とハード制約を営業・マーケティング・生産・財務で合意する
  • 過去再現、感度分析、バックテスト、小規模実験の順で妥当性を確認する
  • 推奨を覆せる人、停止基準、例外処理、モデル更新責任者を決める
  • 投資効果だけでなく、納期、顧客体験、安全、公平性もガードレールとして監視する

まとめ

No.081〜No.090では、接点の寄与を測るところから、リスクを含む配分、逐次学習、デジタルツイン、DI、AI統合までを一連の意思決定として確認しました。実務で価値を生むのは、複雑なアルゴリズム単体ではなく、前提・目的・制約・不確実性・責任を可視化し、実績から更新できる仕組みです。

法人向けのご相談

数理工房では、マーケティング投資配分、需要・受注予測、営業パイプラインと生産計画の連携、シミュレーション最適化、Decision Intelligenceの設計をご支援します。手元データの棚卸しや、小規模な検証設計からご相談いただけます。

📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。