100本ノック / マーケティングサイエンス / マーケティングサイエンス100本ノック
製造業の推薦システム入門|協調フィルタリングからオンライン学習までPythonで実践
製造業B2B営業を変える推薦システム:購買履歴から「次に提案すべき商材」を設計する10本ノック
本Notebookでは、産業部品メーカーの架空の受注データを使い、顧客企業ごとに次に提案すべき商材を選ぶ推薦システムを段階的に構築します。協調フィルタリング、Matrix Factorization、Implicit Feedback、グラフ推薦、推薦リストの多様性・意外性、ランキング学習、オンライン学習を、営業・マーケティングの意思決定につなげます。
対象はマーケティングサイエンス100本ノックの No.061〜No.070 です。数式だけで終わらせず、モデルの出力を「重点提案先」「提案商材」「評価指標」「運用ルール」へ翻訳します。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。 掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
1. はじめに:この記事で扱う製造業の実務課題
製造業の法人営業では、顧客の設備、工程、過去の採用品によって提案候補が大きく変わります。営業担当者の経験は重要ですが、取扱商材や顧客数が増えると、全組合せを人だけで把握することは困難です。
ここで推薦システムを使う目的は営業を自動化することではありません。購買履歴から提案仮説を優先順位づけし、担当者が技術適合性や顧客事情を確認する時間を増やすことです。
2. 現場でよくある状況
- 部門ごとに購入履歴が分散し、全社横断のクロスセル余地が見えない
- 売上上位品ばかりを提案し、顧客固有のニーズを取り逃がす
- 未購入を「関心なし」と誤解するが、実際は未認知や未提案かもしれない
- 推薦精度だけを追い、似た商材ばかりの提案になる
- 新しい反応をモデルへ戻す仕組みがなく、推薦が陳腐化する
3. なぜこの問題は判断が難しいのか
受注データは評価点ではなく、購入した/していないという暗黙的フィードバックです。ゼロは不満ではなく「未観測」であり、購入頻度には企業規模の影響も混ざります。また、精度、利益、多様性、在庫、技術的適合性は必ずしも同時に最大化できません。
したがって、モデルは次の判断を補助する部品として扱います。
- 候補生成:提案可能性のある商材を広く抽出する
- ランキング:商談化確率や期待粗利で並べる
- 制約・再ランキング:適合性、在庫、多様性を反映する
- 学習:提案後の閲覧、問い合わせ、受注を次回へ戻す
4. 今回扱うノックの全体像
| No. | テーマ | 実務上の問い |
|---|---|---|
| 061 | 協調フィルタリング | 類似顧客の購買から何を提案するか |
| 062 | Matrix Factorization | 潜在的な用途・工程をどう捉えるか |
| 063 | Implicit Feedback | 購入回数を信頼度としてどう扱うか |
| 064 | LightGCN | 顧客・商材グラフをどう伝播するか |
| 065 | Graph Neural Network | ノード属性と関係性をどう統合するか |
| 066 | 多様性 | 似た推薦への偏りをどう抑えるか |
| 067 | セレンディピティ | 有用で意外な提案をどう測るか |
| 068 | 量子ランダムウォーク | 新しい探索手法をどう評価するか |
| 069 | ランキング学習 | 複数KPIで候補順をどう決めるか |
| 070 | オンライン学習 | 顧客反応の変化へどう追随するか |
5. Python環境の準備
外部データは使わず、NumPy、pandas、matplotlib、scikit-learn、NetworkXで再現します。乱数シードは固定します。グラフの日本語表示には japanize_matplotlib を利用します。
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
import networkx as nx
from sklearn.decomposition import NMF
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.preprocessing import StandardScaler
SEED = 42
rng = np.random.default_rng(SEED)
pd.set_option("display.max_columns", 20)
pd.set_option("display.precision", 3)
plt.rcParams["figure.figsize"] = (8, 4.5)
print(f"Python {sys.version.split()[0]} / seed={SEED}")
Python 3.11.9 / seed=42
6. 架空データの作成
80社の顧客企業と18商材を想定します。顧客には業界・規模、商材にはカテゴリ・粗利率があり、潜在的な「加工」「保全」「自動化」ニーズから購入回数が生じるようにします。後半10社を評価対象とし、その各社で購入済み商材を1件だけ隠して、推薦が当てられるか検証します。
n_customers, n_items, latent_dim = 80, 18, 3
customer_ids = [f"C{i:03d}" for i in range(1, n_customers + 1)]
item_ids = [f"P{i:02d}" for i in range(1, n_items + 1)]
categories = np.array(["切削工具", "保全部品", "自動化機器"])
customer_latent = rng.gamma(1.8, 1.0, size=(n_customers, latent_dim))
item_latent = rng.gamma(1.5, 1.0, size=(n_items, latent_dim))
item_category = np.repeat(categories, 6)
for j, cat in enumerate(item_category):
item_latent[j, np.where(categories == cat)[0][0]] += 2.2
rate = np.exp(-3.0 + (customer_latent @ item_latent.T) / 3.0)
counts_full = rng.poisson(np.clip(rate, 0, 5))
counts_full = np.clip(counts_full, 0, 12)
customers = pd.DataFrame({
"customer": customer_ids,
"industry": rng.choice(["自動車", "電機", "食品", "化学"], n_customers),
"employees": rng.integers(80, 1800, n_customers)
})
items = pd.DataFrame({
"item": item_ids,
"category": item_category,
"gross_margin": rng.uniform(0.18, 0.48, n_items).round(3)
})
train = counts_full.copy()
test_pairs = []
for u in range(70, 80):
bought = np.flatnonzero(train[u] > 0)
if len(bought):
i = bought[-1]
test_pairs.append((u, i))
train[u, i] = 0
print(f"購買ありセル比率: {(counts_full > 0).mean():.1%}, 評価用holdout: {len(test_pairs)}件")
pd.concat([customers.head(5), pd.DataFrame(train[:5], columns=item_ids)], axis=1)
購買ありセル比率: 68.0%, 評価用holdout: 10件
| customer | industry | employees | P01 | P02 | P03 | P04 | P05 | P06 | P07 | ... | P09 | P10 | P11 | P12 | P13 | P14 | P15 | P16 | P17 | P18 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | C001 | 食品 | 1726 | 9 | 0 | 7 | 1 | 4 | 0 | 7 | ... | 3 | 2 | 1 | 6 | 1 | 1 | 3 | 4 | 4 | 1 |
| 1 | C002 | 食品 | 1682 | 5 | 1 | 8 | 0 | 5 | 1 | 8 | ... | 2 | 0 | 0 | 3 | 1 | 0 | 2 | 1 | 0 | 0 |
| 2 | C003 | 電機 | 1139 | 1 | 2 | 5 | 1 | 3 | 2 | 1 | ... | 1 | 1 | 1 | 1 | 7 | 2 | 6 | 1 | 1 | 7 |
| 3 | C004 | 電機 | 992 | 2 | 2 | 0 | 1 | 6 | 0 | 3 | ... | 7 | 2 | 0 | 5 | 1 | 1 | 3 | 3 | 3 | 1 |
| 4 | C005 | 電機 | 383 | 2 | 4 | 4 | 2 | 3 | 1 | 5 | ... | 5 | 0 | 0 | 6 | 0 | 0 | 1 | 2 | 1 | 0 |
5 rows × 21 columns
category_sales = pd.DataFrame(train, columns=item_ids).sum().rename("purchase_count").to_frame().join(items.set_index("item"))
category_sales.groupby("category")["purchase_count"].sum().sort_values().plot(kind="barh", color="#2878B5")
plt.title("架空データ:商材カテゴリ別の購入回数")
plt.xlabel("購入回数")
plt.ylabel("商材カテゴリ")
plt.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.show()

データは疎で、カテゴリごとに購入量も異なります。この状態は、少数の購買だけを手掛かりに未購入商材を順位づけするB2B推薦の難しさを簡略化して表しています。以下では同じデータを使い、手法ごとの役割を比較します。
7. No.061:協調フィルタリング
実務での意味
顧客ベース協調フィルタリングは「自社と似た購買をしている企業が導入済みだが、自社は未購入の商材」を提案候補にします。用途が明示されていなくても、購買パターンから横展開候補を作れる点が強みです。
分析・モデル化の考え方
購入有無ベクトルを とし、顧客 のコサイン類似度を
で計算します。未購入商材 のスコアは、類似顧客の購入有無を類似度で加重した値です。
Pythonで確認する
binary = (train > 0).astype(float)
norm = np.linalg.norm(binary, axis=1, keepdims=True)
similarity = binary @ binary.T / np.maximum(norm @ norm.T, 1e-12)
np.fill_diagonal(similarity, 0)
cf_scores = similarity @ binary / np.maximum(similarity.sum(axis=1, keepdims=True), 1e-12)
cf_scores[train > 0] = -np.inf
u = 72
top_cf = np.argsort(cf_scores[u])[-5:][::-1]
pd.DataFrame({"推薦商材": np.array(item_ids)[top_cf], "CFスコア": cf_scores[u, top_cf]}).merge(items, left_on="推薦商材", right_on="item")
| 推薦商材 | CFスコア | item | category | gross_margin | |
|---|---|---|---|---|---|
| 0 | P07 | 0.908 | P07 | 保全部品 | 0.231 |
| 1 | P13 | 0.876 | P13 | 自動化機器 | 0.231 |
| 2 | P09 | 0.856 | P09 | 保全部品 | 0.214 |
| 3 | P08 | 0.828 | P08 | 保全部品 | 0.332 |
| 4 | P17 | 0.778 | P17 | 自動化機器 | 0.210 |
結果の読み取り
スコアは購入確率そのものではなく、類似企業での採用の強さです。営業担当者は上位候補について設備仕様と既存契約を確認し、導入事例を添えて提案できます。一方、新規顧客や購入が少ない顧客には類似度が安定しないため、属性情報や人気商材による補完が必要です。
8. No.062:Matrix Factorization
実務での意味
Matrix Factorization(行列因子分解)は、顧客×商材の大きな表を少数の潜在軸へ圧縮します。潜在軸は加工用途、保全重視、自動化投資などの需要構造として解釈できる場合があり、明示的なカテゴリを越えた候補生成に役立ちます。
分析・モデル化の考え方
購買行列 を顧客因子 と商材因子 の積で近似します。
今回は購入回数が非負なので、説明しやすい非負値行列因子分解(NMF)を使います。
Pythonで確認する
nmf = NMF(n_components=3, init="nndsvda", random_state=SEED, max_iter=1000)
P = nmf.fit_transform(train)
Q = nmf.components_.T
mf_scores = P @ Q.T
mf_scores[train > 0] = -np.inf
factor_profile = pd.DataFrame(Q, index=item_ids, columns=["潜在軸1", "潜在軸2", "潜在軸3"])
for col in factor_profile:
print(col, ":", ", ".join(factor_profile[col].nlargest(3).index))
top_mf = np.argsort(mf_scores[u])[-5:][::-1]
pd.DataFrame({"推薦商材": np.array(item_ids)[top_mf], "MFスコア": mf_scores[u, top_mf]})
潜在軸1 : P13, P15, P18
潜在軸2 : P05, P01, P09
潜在軸3 : P12, P07, P09
| 推薦商材 | MFスコア | |
|---|---|---|
| 0 | P13 | 0.365 |
| 1 | P02 | 0.343 |
| 2 | P09 | 0.312 |
| 3 | P08 | 0.305 |
| 4 | P07 | 0.299 |
結果の読み取り
各潜在軸で重みが大きい商材を並べると、その軸が表す用途を業務知識で命名できます。ただし因子は統計的に得られたもので、必ずしも一意な意味を持ちません。営業会議では「因子名」を断定せず、上位商材の共通用途を仮説として確認します。
9. No.063:Implicit Feedback
実務での意味
B2Bの受注履歴には星評価がありません。購入回数、見積依頼、資料閲覧などの行動を、好みではなく関心の強さに対する信頼度として扱う必要があります。
分析・モデル化の考え方
購入有無 と信頼度 を分け、重み付き誤差を考えます。
ここでは信頼度を掛けた行列をNMFへ入力し、考え方を短いコードで確認します。
Pythonで確認する
alpha = 3.0
confidence = 1 + alpha * np.log1p(train)
weighted_signal = (train > 0) * confidence
implicit_nmf = NMF(n_components=3, init="nndsvda", random_state=SEED, max_iter=1000)
Pu = implicit_nmf.fit_transform(weighted_signal)
Qi = implicit_nmf.components_.T
implicit_scores = Pu @ Qi.T
implicit_scores[train > 0] = -np.inf
sample = pd.DataFrame({"購入回数": np.arange(0, 11)})
sample["信頼度"] = 1 + alpha * np.log1p(sample["購入回数"])
sample
| 購入回数 | 信頼度 | |
|---|---|---|
| 0 | 0 | 1.000 |
| 1 | 1 | 3.079 |
| 2 | 2 | 4.296 |
| 3 | 3 | 5.159 |
| 4 | 4 | 5.828 |
| 5 | 5 | 6.375 |
| 6 | 6 | 6.838 |
| 7 | 7 | 7.238 |
| 8 | 8 | 7.592 |
| 9 | 9 | 7.908 |
| 10 | 10 | 8.194 |
結果の読み取り
購入回数に対して対数を使うため、1回から2回への増加は重く、10回から11回への増加は相対的に軽くなります。大企業の大量購入が推薦を独占することを抑えつつ、反復購入を強い証拠として使えます。実運用では閲覧、見積、受注に異なる重みを設定し、商談化率で検証します。
10. No.064:LightGCN
実務での意味
購買を「顧客ノードと商材ノードを結ぶ辺」とみなすと、似た顧客・似た商材という二段先、三段先の関係を使えます。LightGCNは複雑な変換を省き、推薦に重要な近傍伝播へ焦点を当てた手法です。
分析・モデル化の考え方
二部グラフの隣接行列を 、次数行列を とすると、正規化隣接行列は
です。埋め込みを と伝播し、各層の平均を最終表現にします。ここでは潜在因子を初期埋め込みにして2層伝播を確認します。
Pythonで確認する
A = np.block([[np.zeros((n_customers, n_customers)), binary],
[binary.T, np.zeros((n_items, n_items))]])
degree = A.sum(axis=1)
D_inv_sqrt = np.diag(1 / np.sqrt(np.maximum(degree, 1)))
A_norm = D_inv_sqrt @ A @ D_inv_sqrt
E0 = np.vstack([P, Q])
E1 = A_norm @ E0
E2 = A_norm @ E1
E = (E0 + E1 + E2) / 3
lightgcn_scores = E[:n_customers] @ E[n_customers:].T
lightgcn_scores[train > 0] = -np.inf
pd.DataFrame({
"指標": ["ノード数", "エッジ数", "行列密度"],
"値": [A.shape[0], int(binary.sum()), round(A.mean(), 4)]
})
| 指標 | 値 | |
|---|---|---|
| 0 | ノード数 | 98.000 |
| 1 | エッジ数 | 969.000 |
| 2 | 行列密度 | 0.202 |
結果の読み取り
グラフは顧客80社・商材18点を購買辺で結んでいます。伝播を深くしすぎると全ノードの表現が似る過平滑化が起きるため、層数は検証データで決めます。実務では販売停止品や技術的に非互換な辺を除くデータ品質管理も重要です。
11. No.065:Graph Neural Network
実務での意味
GNNは購買関係だけでなく、顧客の業界・規模、商材カテゴリ・粗利などの属性を統合できます。購買履歴が少ない新規顧客や新商材のコールドスタートを緩和できる可能性があります。
分析・モデル化の考え方
一般的なメッセージパッシングは、近傍 の情報を集約します。
本格的な学習にはPyTorch等を使いますが、ここでは顧客の業界を商材側へ一段伝播し、「どの業界で採用される商材か」を可視化します。
Pythonで確認する
industry_onehot = pd.get_dummies(customers["industry"]).astype(float)
industry_signal = binary.T @ industry_onehot.to_numpy()
industry_share = industry_signal / np.maximum(industry_signal.sum(axis=1, keepdims=True), 1)
industry_df = pd.DataFrame(industry_share, index=item_ids, columns=industry_onehot.columns)
industry_df.plot(kind="bar", stacked=True, colormap="tab20c")
plt.title("GNNの一段集約に相当する商材別・採用業界構成")
plt.xlabel("商材")
plt.ylabel("購入企業に占める業界構成比")
plt.grid(axis="y", alpha=0.3)
plt.legend(title="業界", bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
plt.show()

結果の読み取り
特定業界に偏る商材と、複数業界に広がる商材を区別できます。履歴が少ない顧客には同業界で採用される商材を初期候補にできます。ただし業界属性は便利な一方、過去の偏りを固定化する恐れがあります。採用不可の技術条件を明示的制約とし、新規用途を試す探索枠も残します。
12. No.066:多様性
実務での意味
推薦上位がすべて切削工具なら、顧客の保全・自動化ニーズを見落とします。多様性は、精度を保ちながら提案カテゴリの偏りを抑える指標です。
分析・モデル化の考え方
リスト内多様性(Intra-list Diversity)を、異なるカテゴリの組合せ比率として計算します。
再ランキングには、関連度と既選択品との類似性を調整するMMRの考え方を使います。
Pythonで確認する
cat_map = dict(zip(item_ids, item_category))
def diversity(indices):
pairs = [(a, b) for x, a in enumerate(indices) for b in indices[x+1:]]
return np.mean([item_category[a] != item_category[b] for a, b in pairs]) if pairs else 0
def diverse_rerank(scores, k=5, penalty=0.35):
available, selected = set(np.flatnonzero(np.isfinite(scores))), []
scaled = (scores - np.nanmin(scores[np.isfinite(scores)])) / (np.ptp(scores[np.isfinite(scores)]) + 1e-9)
while available and len(selected) < k:
best = max(available, key=lambda i: scaled[i] - penalty * sum(item_category[i] == item_category[j] for j in selected))
selected.append(best); available.remove(best)
return selected
base = np.argsort(mf_scores[u])[-5:][::-1].tolist()
reranked = diverse_rerank(mf_scores[u])
pd.DataFrame({
"方式": ["関連度のみ", "多様性再ランキング"],
"推薦": [", ".join(np.array(item_ids)[base]), ", ".join(np.array(item_ids)[reranked])],
"多様性": [diversity(base), diversity(reranked)]
})
| 方式 | 推薦 | 多様性 | |
|---|---|---|---|
| 0 | 関連度のみ | P13, P02, P09, P08, P07 | 0.7 |
| 1 | 多様性再ランキング | P13, P02, P09, P08, P18 | 0.8 |
結果の読み取り
再ランキングにより、関連度上位だけのリストよりカテゴリの広がりを持たせられます。多様性を上げすぎると無関係な提案が混ざるため、制約ではなく目的関数の重みとして調整し、クリック率だけでなく商談の幅や複数カテゴリ受注率で評価します。
13. No.067:セレンディピティ
実務での意味
セレンディピティは「顧客にとって意外だが有用」な推薦です。単なる人気品ではなく、担当者が見落としていた用途を提示できれば、クロスセルや新工程への展開につながります。
分析・モデル化の考え方
有用性をモデルスコア、意外性を非人気度 とし、簡易指標を
とします。実際の有用性は受注・商談化で事後評価すべきであり、「珍しいだけ」の商材を高評価しないことが重要です。
Pythonで確認する
popularity = binary.mean(axis=0)
finite = np.isfinite(mf_scores[u])
rel = np.zeros(n_items)
rel[finite] = (mf_scores[u, finite] - mf_scores[u, finite].min()) / (np.ptp(mf_scores[u, finite]) + 1e-9)
unexpectedness = -np.log(np.maximum(popularity, 1 / n_customers))
serendipity_score = rel * unexpectedness
serendipity_score[~finite] = -np.inf
top_ser = np.argsort(serendipity_score)[-5:][::-1]
pd.DataFrame({
"商材": np.array(item_ids)[top_ser],
"関連度": rel[top_ser],
"採用企業率": popularity[top_ser],
"セレンディピティ": serendipity_score[top_ser]
})
| 商材 | 関連度 | 採用企業率 | セレンディピティ | |
|---|---|---|---|---|
| 0 | P02 | 0.935 | 0.588 | 0.498 |
| 1 | P04 | 0.480 | 0.362 | 0.488 |
| 2 | P18 | 0.735 | 0.575 | 0.406 |
| 3 | P06 | 0.638 | 0.550 | 0.381 |
| 4 | P14 | 0.374 | 0.375 | 0.367 |
結果の読み取り
採用企業率が低くても関連度が高い商材が上位になります。これらは通常提案枠とは別の「発見枠」として、技術根拠や導入事例を添えて提示するのが安全です。オフライン指標だけで有用性は確定できないため、営業担当者の採否理由と顧客反応を記録します。
14. No.068:量子ランダムウォーク
実務での意味
ランダムウォークは、顧客・商材グラフ上を移動して関連候補を探索します。量子ランダムウォークでは振幅の重ね合わせと干渉により、古典的手法と異なる探索分布を作れる可能性があります。ただし、現時点の実務では量子優位を前提にせず、比較実験として扱うのが妥当です。
分析・モデル化の考え方
古典的ウォークは確率ベクトルを と更新します。量子的な離散時間ウォークは複素振幅 をユニタリ演算子 で
と更新します。ここでは量子回路を実装せず、比較基準となる再始動付き古典ランダムウォークを実装し、研究導入時のベースラインを明確にします。
Pythonで確認する
transition = A / np.maximum(A.sum(axis=1, keepdims=True), 1)
start = np.zeros(A.shape[0]); start[u] = 1
p = start.copy(); restart = 0.2
for _ in range(30):
p = (1 - restart) * transition.T @ p + restart * start
walk_scores = p[n_customers:].copy()
walk_scores[train[u] > 0] = -np.inf
top_walk = np.argsort(walk_scores)[-5:][::-1]
pd.DataFrame({"商材": np.array(item_ids)[top_walk], "到達確率": walk_scores[top_walk]})
| 商材 | 到達確率 | |
|---|---|---|
| 0 | P07 | 0.021 |
| 1 | P13 | 0.020 |
| 2 | P09 | 0.020 |
| 3 | P08 | 0.019 |
| 4 | P17 | 0.018 |
結果の読み取り
到達確率は顧客からグラフ上で近い未購入商材を示します。量子手法を検討する場合も、この古典ベースラインに対する精度、計算時間、実装・運用コストの改善を測るべきです。シミュレータ上の小規模な差だけで本番導入を決めず、再現可能な評価設計を先に置きます。
15. No.069:ランキング学習
実務での意味
候補生成後は、推薦スコアだけでなく、粗利、人気度、顧客属性、在庫や提案履歴を使って「どれを先に見せるか」を決めます。ランキング学習は過去の反応からこの順序を学びます。
分析・モデル化の考え方
実務ではpointwise、pairwise、listwiseの方法があります。ここでは各顧客・商材を1行とし、購入有無を目的変数、CF・MFスコア、人気度、粗利率を説明変数とするpointwiseモデルを使います。評価には順位品質を見るHitRate@Kを使います。
Pythonで確認する
rows = []
raw_mf = P @ Q.T
for cu in range(70):
for i in range(n_items):
rows.append([cu, i, similarity[cu] @ binary[:, i], raw_mf[cu, i], popularity[i], items.loc[i, "gross_margin"], binary[cu, i]])
rank_df = pd.DataFrame(rows, columns=["u", "i", "cf", "mf", "pop", "margin", "label"])
X = rank_df[["cf", "mf", "pop", "margin"]]
y = rank_df["label"]
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
ranker = LogisticRegression(class_weight="balanced", random_state=SEED, max_iter=1000).fit(X_scaled, y)
feature_table = pd.DataFrame({"特徴量": X.columns, "係数": ranker.coef_[0]}).sort_values("係数", ascending=False)
feature_table
| 特徴量 | 係数 | |
|---|---|---|
| 1 | mf | 1.926 |
| 0 | cf | 1.503 |
| 3 | margin | -0.206 |
| 2 | pop | -0.686 |
def hit_rate_at_k(score_matrix, pairs, k=5):
hits = []
for cu, true_i in pairs:
top = np.argsort(score_matrix[cu])[-k:]
hits.append(true_i in top)
return np.mean(hits) if hits else np.nan
rank_scores = np.full((n_customers, n_items), -np.inf)
for cu in range(70, 80):
feat = pd.DataFrame({
"cf": similarity[cu] @ binary,
"mf": raw_mf[cu],
"pop": popularity,
"margin": items["gross_margin"]
})
rank_scores[cu] = ranker.predict_proba(scaler.transform(feat))[:, 1]
rank_scores[cu, train[cu] > 0] = -np.inf
comparison = pd.DataFrame({
"モデル": ["協調フィルタリング", "行列因子分解", "ランキング学習"],
"HitRate@5": [hit_rate_at_k(cf_scores, test_pairs), hit_rate_at_k(mf_scores, test_pairs), hit_rate_at_k(rank_scores, test_pairs)]
})
comparison
| モデル | HitRate@5 | |
|---|---|---|
| 0 | 協調フィルタリング | 1.0 |
| 1 | 行列因子分解 | 0.8 |
| 2 | ランキング学習 | 0.9 |
結果の読み取り
係数は標準化後なので、どの特徴が順位へ効いたかを方向と大きさで比較できます。小規模な架空データのHitRateは手法の優劣を一般化するものではありません。実務では時系列で学習・評価期間を分け、HitRate、NDCG、商談化率、期待粗利を併記し、利益だけで顧客適合性を損なっていないか監視します。
16. No.070:オンライン学習
実務での意味
設備更新、予算期、新製品投入で顧客の関心は変化します。オンライン学習は、閲覧や問い合わせなど新しい反応を受け取り、推薦方針を逐次更新します。探索を残すことで、過去実績が少ない商材にも学習機会を与えます。
分析・モデル化の考え方
多腕バンディットのUCB(Upper Confidence Bound)では、商材 の平均報酬 と試行回数 から
を最大にする商材を提示します。第1項が活用、第2項が探索です。ここでは顧客反応率が未知という設定で、500回の提案をシミュレーションします。
Pythonで確認する
true_response = np.array([0.06, 0.10, 0.04, 0.16, 0.08])
n_arms, rounds, c = len(true_response), 500, 1.2
trials = np.zeros(n_arms); rewards = np.zeros(n_arms)
cumulative = []
for t in range(1, rounds + 1):
if t <= n_arms:
arm = t - 1
else:
mean = rewards / trials
ucb = mean + c * np.sqrt(np.log(t) / trials)
arm = np.argmax(ucb)
reward = rng.random() < true_response[arm]
trials[arm] += 1; rewards[arm] += reward
cumulative.append(rewards.sum())
pd.DataFrame({
"候補": [f"候補{i+1}" for i in range(n_arms)],
"真の反応率(検証用)": true_response,
"提示回数": trials.astype(int),
"推定反応率": rewards / trials
})
| 候補 | 真の反応率(検証用) | 提示回数 | 推定反応率 | |
|---|---|---|---|---|
| 0 | 候補1 | 0.06 | 78 | 0.064 |
| 1 | 候補2 | 0.10 | 113 | 0.124 |
| 2 | 候補3 | 0.04 | 78 | 0.064 |
| 3 | 候補4 | 0.16 | 171 | 0.175 |
| 4 | 候補5 | 0.08 | 60 | 0.017 |
plt.plot(np.arange(1, rounds + 1), cumulative, color="#D95319")
plt.title("UCBによるオンライン提案の累積反応数")
plt.xlabel("提案回数")
plt.ylabel("累積反応数")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

結果の読み取り
反応率の高い候補へ提示が集まりつつ、他候補も探索されています。本番では受注だけではフィードバックが遅いため、資料閲覧、問い合わせ、商談化を中間報酬として設計します。探索は顧客体験を損なわない範囲に制限し、販売停止品・不適合品を候補から除外するガードレールが必須です。
17. 対象ノックを通して見える実務上の示唆
10本の手法は競合する単一モデルではなく、推薦業務の異なる部品です。
| 層 | 主な手法 | 意思決定 |
|---|---|---|
| 候補生成 | CF、MF、Implicit、LightGCN/GNN、ランダムウォーク | 提案漏れを減らす |
| 再ランキング | 多様性、セレンディピティ、ランキング学習 | 順番と提案の幅を決める |
| 学習・運用 | オンライン学習 | 反応変化へ追随する |
最も重要なのは、高いオフライン精度ではなく、推薦理由を営業が確認でき、顧客に不利益な提案を除外でき、結果が次回学習へ戻る一連の業務設計です。
18. 実務導入する場合に必要なこと
- 目的の合意:売上、粗利、商談化率、休眠顧客再活性化のどれを主目的にするか決める
- IDとデータの整備:顧客・拠点・商材コードを統合し、返品、廃番、無償サンプルを識別する
- 適合性制約:設備仕様、認証、地域、在庫、契約条件を推薦前後のルールにする
- 時系列評価:過去で学習し未来を予測する形で、人気モデルや営業現行運用と比較する
- 説明と画面設計:「類似企業で採用」「既存品との併用が多い」など推薦根拠を表示する
- フィードバックループ:採用・見送り理由、顧客反応、受注を記録する
- 監視と統制:精度、偏り、探索率、データドリフト、販売禁止条件を継続監視する
19. まとめ
No.061〜070では、購買履歴を起点に、潜在因子、暗黙的フィードバック、グラフ構造、属性、多様性、意外性、ランキング、逐次学習へと推薦システムを拡張しました。
製造業の推薦は「当たりそうな商材を出す」だけでは不十分です。技術適合性を守りながら、営業が検証可能な提案仮説を作り、顧客反応を学習へ戻すことで、初めて継続的な意思決定基盤になります。まずは対象顧客・商材を限定した小規模検証から始め、現行営業との比較で効果を確認することが現実的です。
20. 法人向けのご相談
数理工房では、製造業の受注・顧客・商材データを用いた推薦システムの構想整理、PoC、評価設計、業務実装をご支援します。「データはあるが提案に使えていない」「モデル精度と営業現場の運用をつなげたい」といった段階からご相談いただけます。
📩 お問い合わせ: surikobo.co.jp/contact まずはお気軽にご相談ください。