100本ノック / 機械学習 / 実務機械学習100本ノック
製造業の顧客クラスタリングと製品推薦をPythonで実践|機械学習100本ノック
製造業の提案力を高める顧客セグメント分析と製品推薦
機械学習100本ノック No.081〜No.090:クラスタリング・類似検索・協調フィルタリング
設備・部品メーカーが保有する顧客属性、取引実績、製品仕様を使い、**「どの顧客に、どの製品を、なぜ提案するか」**を再現可能な分析へ変える実務編です。KMeans、PCA、類似度、ユーザー×アイテム行列、協調フィルタリングを、一つの営業企画シナリオで接続します。
この notebook で扱うデータ・分析結果は研修用の架空例です。推薦結果は営業判断を支援する候補であり、自動的に採用すべき結論ではありません。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
1. はじめに:この記事で扱う製造業の実務課題
製造業の法人営業では、顧客数と製品数が増えるほど、担当者の経験だけで提案機会を拾うことが難しくなります。本稿では、産業機器メーカーの営業企画部が、顧客企業を理解し、似た顧客や製品を探し、購買履歴から次の提案候補を作る状況を想定します。
目標は「推薦モデルを作ること」ではありません。限られた営業工数を、顧客課題との適合度が高い候補へ配分するための判断材料を作ることです。
2. 現場でよくある状況
- 顧客区分が売上規模だけで、設備構成や保全課題の違いを表せていない
- 製品検索が型番や担当者の記憶に依存し、代替品・関連品を見落とす
- 購買履歴は基幹システムにあるが、未購入品の提案に活用できていない
- 推薦件数は出せても、正しかったかを定量評価していない
3. なぜこの問題は判断が難しいのか
「似ている」には、売上規模、設備台数、停止損失、品質要求、製品仕様、購買パターンなど複数の定義があります。また、購買がないことは不要を意味せず、単に未提案・更新前・他社購入かもしれません。したがって、距離や予測スコアを業務文脈と切り離して解釈しないことが重要です。
4. 今回扱うノックの全体像
| No. | 手法 | 意思決定への接続 |
|---|---|---|
| 081〜085 | KMeans、クラスタ数評価、PCA | 顧客群の理解と営業施策の設計 |
| 086〜087 | コサイン類似度・近傍探索 | 類似製品、類似顧客の探索 |
| 088〜089 | 購買行列・協調フィルタリング | クロスセル候補の順位付け |
| 090 | Precision@K・Recall@K | 推薦施策を導入するかの検証 |
5. Python 環境の準備
外部データや seaborn は使いません。乱数生成器を 42 に固定し、同じ結果を再現できるようにします。標準化は単位の違う特徴量が距離を支配しないために必要です。
import sys
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.preprocessing import StandardScaler
SEED = 42
rng = np.random.default_rng(SEED)
plt.rcParams["figure.figsize"] = (8, 4.8)
plt.rcParams["axes.unicode_minus"] = False
print(f"Python : {sys.version.split()[0]}")
print(f"NumPy : {np.__version__}")
print(f"pandas : {pd.__version__}")
print(f"matplotlib : {matplotlib.__version__}")
Python : 3.13.1
NumPy : 2.5.1
pandas : 3.0.3
matplotlib : 3.11.0
6. 架空データの作成
120社の顧客企業について、年間購買額、設備台数、予防保全比率、品質要求度、停止損失を生成します。潜在的に「小規模・価格重視」「中規模・バランス」「大規模・安定稼働重視」の3群がある設定ですが、分析時には正解ラベルを使いません。
別に12製品の仕様データと、過去の購買有無も作ります。購買確率は顧客特性と製品カテゴリの適合度で変化させ、現実の疎な購買行列を模します。
n_customers = 120
segment = np.repeat([0, 1, 2], [40, 45, 35])
params = {
0: ([25, 12, 0.30, 45, 1.5], [7, 4, .10, 9, .5]),
1: ([70, 32, 0.58, 68, 4.5], [13, 7, .10, 8, 1.0]),
2: ([155, 68, 0.82, 87, 10.0], [25, 12, .07, 5, 2.0]),
}
raw = np.vstack([rng.normal(params[s][0], params[s][1]) for s in segment])
raw[:, 0:2] = np.maximum(raw[:, 0:2], 1)
raw[:, 2] = np.clip(raw[:, 2], 0.05, 0.98)
raw[:, 3] = np.clip(raw[:, 3], 20, 100)
raw[:, 4] = np.maximum(raw[:, 4], 0.2)
feature_cols = ["annual_purchase_mjpy", "installed_machines", "preventive_ratio", "quality_requirement", "downtime_loss_mjpy"]
customers = pd.DataFrame(raw, columns=feature_cols)
customers.insert(0, "customer_id", [f"C{i:03d}" for i in range(1, n_customers + 1)])
customers.head().round(2)
| customer_id | annual_purchase_mjpy | installed_machines | preventive_ratio | quality_requirement | downtime_loss_mjpy | |
|---|---|---|---|---|---|---|
| 0 | C001 | 27.13 | 7.84 | 0.38 | 53.47 | 0.52 |
| 1 | C002 | 15.88 | 12.51 | 0.27 | 44.85 | 1.07 |
| 2 | C003 | 31.16 | 15.11 | 0.31 | 55.15 | 1.73 |
| 3 | C004 | 18.98 | 13.48 | 0.20 | 52.91 | 1.48 |
| 4 | C005 | 23.71 | 9.28 | 0.42 | 43.61 | 1.29 |
products = pd.DataFrame({
"product_id": [f"P{i:02d}" for i in range(1, 13)],
"product_name": ["標準センサー", "高精度センサー", "耐熱センサー", "標準モーター", "省エネモーター", "高トルクモーター",
"簡易監視端末", "予知保全端末", "品質検査カメラ", "交換部品キット", "遠隔保守契約", "省エネ診断"],
"category": ["sensor"]*3 + ["motor"]*3 + ["digital"]*3 + ["service"]*3,
"price_index": [25, 58, 62, 45, 70, 76, 30, 82, 88, 18, 55, 48],
"precision": [45, 88, 72, 50, 65, 70, 42, 78, 95, 35, 60, 55],
"durability": [50, 68, 92, 64, 75, 90, 45, 80, 68, 55, 70, 50],
"digital_level": [25, 45, 35, 20, 45, 25, 65, 95, 85, 15, 90, 72],
})
affinity = np.column_stack([
0.8 - customers["quality_requirement"].to_numpy()/150,
customers["installed_machines"].to_numpy()/100,
(customers["preventive_ratio"].to_numpy() + customers["quality_requirement"].to_numpy()/100)/2,
customers["preventive_ratio"].to_numpy(),
])
cat_index = products["category"].map({"sensor":0, "motor":1, "digital":2, "service":3}).to_numpy()
base = affinity[:, cat_index] + (100-products["price_index"].to_numpy())[None, :] / 500
prob = np.clip(0.08 + 0.48*base + rng.normal(0, .04, (n_customers, 12)), .03, .82)
purchase = (rng.random((n_customers, 12)) < prob).astype(int)
purchase_df = pd.DataFrame(purchase, index=customers.customer_id, columns=products.product_id)
print("顧客数:", len(customers), "製品数:", len(products))
print("購買行列の密度:", f"{purchase.mean():.1%}")
products
顧客数: 120 製品数: 12
購買行列の密度: 35.4%
| product_id | product_name | category | price_index | precision | durability | digital_level | |
|---|---|---|---|---|---|---|---|
| 0 | P01 | 標準センサー | sensor | 25 | 45 | 50 | 25 |
| 1 | P02 | 高精度センサー | sensor | 58 | 88 | 68 | 45 |
| 2 | P03 | 耐熱センサー | sensor | 62 | 72 | 92 | 35 |
| 3 | P04 | 標準モーター | motor | 45 | 50 | 64 | 20 |
| 4 | P05 | 省エネモーター | motor | 70 | 65 | 75 | 45 |
| 5 | P06 | 高トルクモーター | motor | 76 | 70 | 90 | 25 |
| 6 | P07 | 簡易監視端末 | digital | 30 | 42 | 45 | 65 |
| 7 | P08 | 予知保全端末 | digital | 82 | 78 | 80 | 95 |
| 8 | P09 | 品質検査カメラ | digital | 88 | 95 | 68 | 85 |
| 9 | P10 | 交換部品キット | service | 18 | 35 | 55 | 15 |
| 10 | P11 | 遠隔保守契約 | service | 55 | 60 | 70 | 90 |
| 11 | P12 | 省エネ診断 | service | 48 | 55 | 50 | 72 |
No.081:KMeansで顧客をクラスタリングする
実務での意味
顧客群をデータで整理すると、担当割当、セミナー内容、提案テンプレートを群ごとに設計できます。売上だけでなく保全成熟度や停止損失も含め、営業施策に意味のある軸を作ります。
分析・モデル化の考え方
KMeansは各顧客を最も近い重心へ割り当て、クラスタ内平方和を最小化します。特徴量を標準化した後で3群に分け、元単位の平均値に戻して解釈します。
Pythonで確認する
scaler = StandardScaler()
X = scaler.fit_transform(customers[feature_cols])
kmeans = KMeans(n_clusters=3, random_state=SEED, n_init=20)
customers["cluster"] = kmeans.fit_predict(X)
cluster_profile = customers.groupby("cluster")[feature_cols].agg(["mean", "count"])
cluster_profile.round(2)
| annual_purchase_mjpy | installed_machines | preventive_ratio | quality_requirement | downtime_loss_mjpy | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| mean | count | mean | count | mean | count | mean | count | mean | count | |
| cluster | ||||||||||
| 0 | 69.28 | 45 | 31.49 | 45 | 0.60 | 45 | 67.09 | 45 | 4.50 | 45 |
| 1 | 148.36 | 35 | 69.05 | 35 | 0.83 | 35 | 86.00 | 35 | 10.15 | 35 |
| 2 | 25.22 | 40 | 11.85 | 40 | 0.28 | 40 | 46.73 | 40 | 1.42 | 40 |
結果の読み取り
クラスタ平均を見ることで、規模だけでなく保全・品質要求の組合せを言語化できます。番号自体に順序はありません。営業企画では、各群へ『効率運用型』『成長設備型』『安定稼働重視型』など、行動につながる名称を関係者と付けます。
No.082:エルボー法でクラスタ数を決める
実務での意味
クラスタ数を先に3と決め打ちすると、組織都合に合わせた分類になりがちです。複雑さを増やしたとき、群内のばらつきがどこまで改善するかを確認します。
分析・モデル化の考え方
目的関数は です。クラスタ数を増やせばSSEは必ず下がるため、低下が緩やかになる折れ曲がりを候補にします。
Pythonで確認する
ks = range(1, 9)
inertias = [KMeans(n_clusters=k, random_state=SEED, n_init=20).fit(X).inertia_ for k in ks]
plt.plot(list(ks), inertias, marker="o")
plt.title("Elbow method for customer clusters")
plt.xlabel("Number of clusters k")
plt.ylabel("Within-cluster SSE")
plt.grid(True, alpha=.3)
plt.tight_layout()
plt.show()
pd.DataFrame({"k": list(ks), "SSE": np.round(inertias, 1)})

| k | SSE | |
|---|---|---|
| 0 | 1 | 600.0 |
| 1 | 2 | 190.7 |
| 2 | 3 | 77.4 |
| 3 | 4 | 67.6 |
| 4 | 5 | 59.2 |
| 5 | 6 | 53.8 |
| 6 | 7 | 49.2 |
| 7 | 8 | 46.2 |
結果の読み取り
SSEの改善が大きい区間と、その後の緩やかな区間を比較します。折れ曲がりは唯一の正解ではありません。営業組織が運用できる施策数や、各群の最低顧客数も合わせて決めます。
No.083:シルエット係数を確認する
実務での意味
エルボー法の目視だけでなく、各顧客が自群にまとまり、他群から離れているかを数値化します。境界顧客を例外扱いする判断にも使えます。
分析・モデル化の考え方
各点について、同じクラスタへの平均距離を 、最も近い他クラスタへの平均距離を とすると、 です。1に近いほど分離が明瞭です。
Pythonで確認する
silhouette_rows = []
for k in range(2, 9):
labels = KMeans(n_clusters=k, random_state=SEED, n_init=20).fit_predict(X)
silhouette_rows.append((k, silhouette_score(X, labels)))
silhouette_df = pd.DataFrame(silhouette_rows, columns=["k", "silhouette"])
print(silhouette_df.round(3).to_string(index=False))
best_k = int(silhouette_df.loc[silhouette_df.silhouette.idxmax(), "k"])
print(f"\n最大値となる候補: k={best_k}")
k silhouette
2 0.597
3 0.600
4 0.507
5 0.385
6 0.276
7 0.255
8 0.256
最大値となる候補: k=3
結果の読み取り
係数が最大のkは統計的な有力候補です。ただし、僅差なら運用の簡潔さを優先できます。低い係数は失敗とは限らず、顧客が連続的に変化して明瞭な群がない可能性を示します。
No.084:PCAで次元削減する
実務での意味
5つの顧客指標を2軸へ圧縮し、顧客差を説明しやすくします。ダッシュボードの見やすさのためだけでなく、どの変数が軸を作るかも確認します。
分析・モデル化の考え方
PCAは分散が最大となる直交方向へデータを射影します。第1・第2主成分の寄与率と負荷量を確認し、2次元で失われる情報量を明示します。主成分の符号は反転しても意味は同じです。
Pythonで確認する
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
loadings = pd.DataFrame(pca.components_.T, index=feature_cols, columns=["PC1", "PC2"])
print("寄与率:", np.round(pca.explained_variance_ratio_, 3))
print("累積寄与率:", round(pca.explained_variance_ratio_.sum(), 3))
loadings.round(3)
寄与率: [0.885 0.04 ]
累積寄与率: 0.926
| PC1 | PC2 | |
|---|---|---|
| annual_purchase_mjpy | 0.450 | -0.347 |
| installed_machines | 0.456 | -0.346 |
| preventive_ratio | 0.441 | 0.492 |
| quality_requirement | 0.440 | 0.610 |
| downtime_loss_mjpy | 0.449 | -0.382 |
結果の読み取り
累積寄与率がどの程度かを見て、2軸表示が元情報をどれだけ保持するか判断します。負荷量の絶対値が大きい特徴量から、PC1・PC2を『規模・重要度』『保全姿勢』などと業務用語へ翻訳します。
No.085:クラスタを2次元で可視化する
実務での意味
クラスタの重なりや外れた顧客を可視化すると、標準施策を適用しにくい境界顧客を営業レビューへ回せます。
分析・モデル化の考え方
PCA座標を散布図にし、KMeansの割当を色で表します。PCAは可視化用、クラスタリングは標準化済み5次元で行っており、2次元だけで再学習していない点が重要です。
Pythonで確認する
fig, ax = plt.subplots()
for c in sorted(customers.cluster.unique()):
mask = customers.cluster.to_numpy() == c
ax.scatter(X_pca[mask, 0], X_pca[mask, 1], label=f"Cluster {c}", alpha=.75)
ax.set_title("Customer clusters projected onto PCA space")
ax.set_xlabel("Principal component 1")
ax.set_ylabel("Principal component 2")
ax.grid(True, alpha=.3)
ax.legend()
plt.tight_layout()
plt.show()

結果の読み取り
群が概ね分かれていれば施策の説明がしやすくなります。重なる点は『誤分類』と即断せず、複数施策の対象候補です。図上の距離は2成分への射影後なので、元の5次元距離とは一致しません。
No.086:類似商品を検索する
実務での意味
廃番代替、上位機種、セット提案では、仕様の近い製品を素早く列挙する必要があります。属人的な型番知識を補助できます。
分析・モデル化の考え方
価格、精度、耐久性、デジタル度を標準化し、ベクトルの向きが近い製品をコサイン類似度で検索します。カテゴリは意図的に除外し、カテゴリを越えた候補も発見できるようにします。
Pythonで確認する
product_features = ["price_index", "precision", "durability", "digital_level"]
Xp = StandardScaler().fit_transform(products[product_features])
product_sim = cosine_similarity(Xp)
query_idx = products.index[products.product_name == "予知保全端末"][0]
order = np.argsort(product_sim[query_idx])[::-1]
similar_products = products.loc[order[order != query_idx][:4], ["product_id", "product_name", "category"]].copy()
similar_products["cosine_similarity"] = product_sim[query_idx, order[order != query_idx][:4]]
similar_products.round(3)
| product_id | product_name | category | cosine_similarity | |
|---|---|---|---|---|
| 8 | P09 | 品質検査カメラ | digital | 0.866 |
| 10 | P11 | 遠隔保守契約 | service | 0.670 |
| 4 | P05 | 省エネモーター | motor | 0.500 |
| 1 | P02 | 高精度センサー | sensor | 0.314 |
結果の読み取り
上位候補は『同等品』ではなく、仕様空間で近い候補です。互換性、安全規格、設備接続条件は別途ルールで絞り込みます。類似検索は営業の探索範囲を広げ、最終判断は技術担当が行う設計が安全です。
No.087:類似顧客を検索する
実務での意味
新規顧客や担当変更時に、似た既存顧客の導入事例を参照できれば、初回提案の仮説を短時間で作れます。
分析・モデル化の考え方
標準化した顧客特徴量のユークリッド距離を使います。購買履歴そのものではなく経営・設備特性が似た顧客を探すため、後の協調フィルタリングとは異なる根拠の推薦になります。
Pythonで確認する
query_customer = "C050"
q = customers.index[customers.customer_id == query_customer][0]
dist = np.linalg.norm(X - X[q], axis=1)
nearest = np.argsort(dist)[1:6]
similar_customers = customers.loc[nearest, ["customer_id", "cluster"] + feature_cols].copy()
similar_customers.insert(2, "distance", dist[nearest])
print("検索元")
display(customers.loc[[q], ["customer_id", "cluster"] + feature_cols].round(2))
print("近い顧客")
similar_customers.round(2)
検索元
| customer_id | cluster | annual_purchase_mjpy | installed_machines | preventive_ratio | quality_requirement | downtime_loss_mjpy | |
|---|---|---|---|---|---|---|---|
| 49 | C050 | 0 | 53.26 | 24.32 | 0.76 | 91.24 | 3.33 |
近い顧客
| customer_id | cluster | distance | annual_purchase_mjpy | installed_machines | preventive_ratio | quality_requirement | downtime_loss_mjpy | |
|---|---|---|---|---|---|---|---|---|
| 46 | C047 | 0 | 1.04 | 50.98 | 27.87 | 0.61 | 77.65 | 3.77 |
| 67 | C068 | 0 | 1.15 | 54.18 | 27.30 | 0.61 | 77.24 | 5.11 |
| 54 | C055 | 0 | 1.25 | 60.62 | 34.22 | 0.57 | 84.75 | 6.07 |
| 56 | C057 | 0 | 1.29 | 76.24 | 19.79 | 0.67 | 71.64 | 3.39 |
| 61 | C062 | 0 | 1.37 | 56.31 | 29.66 | 0.71 | 72.66 | 6.23 |
結果の読み取り
距離が近い顧客の成功事例は提案仮説になりますが、契約条件や業界規制が同じとは限りません。検索結果には『似ている根拠となった特徴量』も併記し、営業が説明できる状態にします。
No.088:ユーザー×アイテム行列を作る
実務での意味
推薦の入力となる購買履歴を、顧客を行、製品を列にした行列へ変換します。基幹データの粒度・重複・返品処理を決める実務上の重要工程です。
分析・モデル化の考え方
今回は購入ありを1、なしを0とする暗黙的フィードバックを使います。0は低評価ではなく未観測です。購入数量や金額を使う場合は、大口顧客だけが距離を支配しない変換が必要です。
Pythonで確認する
matrix_preview = purchase_df.iloc[:10].copy()
matrix_preview["purchased_items"] = matrix_preview.sum(axis=1)
matrix_preview
| product_id | P01 | P02 | P03 | P04 | P05 | P06 | P07 | P08 | P09 | P10 | P11 | P12 | purchased_items |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| customer_id | |||||||||||||
| C001 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 4 |
| C002 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 4 |
| C003 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 6 |
| C004 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 5 |
| C005 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 2 |
| C006 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 5 |
| C007 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| C008 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 3 |
| C009 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 5 |
| C010 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 3 |
結果の読み取り
行列は多くが0になる疎行列です。密度、購入ゼロ顧客、購入ゼロ製品を監視します。閲覧や見積履歴も統合すれば情報は増えますが、行動ごとの重みと個人情報・契約上の利用目的を定義する必要があります。
No.089:協調フィルタリングを実装する
実務での意味
似た購買パターンの顧客が購入した製品を候補にすることで、属性データだけでは見えないクロスセル機会を発見します。
分析・モデル化の考え方
顧客間コサイン類似度を 、顧客 の製品 購買を とし、 でスコア化します。既購入品は候補から除外します。
Pythonで確認する
def recommend_user_based(matrix, customer_id, k=5, neighbor_n=15):
sim = cosine_similarity(matrix)
u = matrix.index.get_loc(customer_id)
neighbors = np.argsort(sim[u])[::-1]
neighbors = neighbors[(neighbors != u)][:neighbor_n]
weights = sim[u, neighbors]
scores = weights @ matrix.iloc[neighbors].to_numpy() / (np.abs(weights).sum() + 1e-12)
scores[matrix.iloc[u].to_numpy() > 0] = -np.inf
top = np.argsort(scores)[::-1][:k]
result = products.set_index("product_id").loc[matrix.columns[top], ["product_name", "category"]].copy()
result["recommendation_score"] = scores[top]
return result
recommend_user_based(purchase_df, "C050", k=5).round(3)
| product_name | category | recommendation_score | |
|---|---|---|---|
| product_id | |||
| P08 | 予知保全端末 | digital | 0.511 |
| P05 | 省エネモーター | motor | 0.439 |
| P12 | 省エネ診断 | service | 0.421 |
| P10 | 交換部品キット | service | 0.377 |
| P07 | 簡易監視端末 | digital | 0.337 |
結果の読み取り
スコアは購買確率そのものではなく順位付けの指標です。推薦理由として、近傍顧客数や類似事例を提示します。新規顧客・新製品には履歴がないコールドスタートがあるため、No.086・087の属性ベース候補と併用します。
No.090:推薦結果をPrecision@K・Recall@Kで評価する
実務での意味
推薦を導入する前に、過去の一部購入を隠し、上位K件で再発見できるかを測ります。営業接触枠が有限ならPrecision、機会損失を避けたいならRecallを重視します。
分析・モデル化の考え方
顧客 の上位K集合を 、テスト期間の購入集合を とすると、、 です。
Pythonで確認する
def offline_evaluate(matrix, k=3, seed=42):
local_rng = np.random.default_rng(seed)
train = matrix.copy()
heldout = {}
for cid in matrix.index:
bought = np.flatnonzero(matrix.loc[cid].to_numpy())
if len(bought) >= 2:
test_i = int(local_rng.choice(bought))
train.iloc[train.index.get_loc(cid), test_i] = 0
heldout[cid] = matrix.columns[test_i]
sim = cosine_similarity(train)
precisions, recalls = [], []
for cid, true_item in heldout.items():
u = train.index.get_loc(cid)
neighbors = np.argsort(sim[u])[::-1]
neighbors = neighbors[(neighbors != u)][:15]
w = sim[u, neighbors]
scores = w @ train.iloc[neighbors].to_numpy() / (np.abs(w).sum() + 1e-12)
scores[train.iloc[u].to_numpy() > 0] = -np.inf
recs = set(train.columns[np.argsort(scores)[::-1][:k]])
hit = int(true_item in recs)
precisions.append(hit / k)
recalls.append(hit) # held-out item is one item
return len(heldout), np.mean(precisions), np.mean(recalls)
rows = []
for k in [1, 3, 5]:
n, precision, recall = offline_evaluate(purchase_df, k=k)
rows.append((k, n, precision, recall))
evaluation = pd.DataFrame(rows, columns=["K", "evaluated_customers", "Precision@K", "Recall@K"])
evaluation.round(3)
| K | evaluated_customers | Precision@K | Recall@K | |
|---|---|---|---|---|
| 0 | 1 | 118 | 0.169 | 0.169 |
| 1 | 3 | 118 | 0.144 | 0.432 |
| 2 | 5 | 118 | 0.132 | 0.661 |
結果の読み取り
Kを増やすと一般にRecallは上がりやすく、Precisionは下がりやすくなります。今回の単純なランダム保留は学習用デモです。実務では時点で分割し、人気順推薦や現行営業ルールをベースラインにして、最終的に商談化率・粗利・解約抑制をA/Bテストします。
7. 対象ノックを通して見える実務上の示唆
- セグメントと推薦は役割が違う:クラスタは施策設計の単位、推薦は顧客ごとの候補順位です。両方をつなぐと、全体施策と個別提案を両立できます。
- 類似性を一つに固定しない:設備・経営特性、製品仕様、購買パターンは異なる証拠です。複数の候補を統合し、根拠を表示する方が実務で信頼されます。
- 未購入を拒否とみなさない:購買0には未提案、他社購入、更新時期前などが混在します。商談履歴や設備適合条件を加える必要があります。
- 評価指標を営業能力と結ぶ:Kはモデル都合ではなく、1担当者が確認・接触できる候補数から決めます。
8. 実務導入する場合に必要なこと
- データ定義:顧客・製品の名寄せ、返品・無償品の扱い、購買時点、廃番・互換情報を管理する
- 安全な候補生成:設備互換、安全規格、供給可否、契約制約をルールで先に除外する
- 時間軸を守る検証:未来情報の混入を防ぎ、過去で学習して将来購買を評価する
- 現場運用:CRMへ候補、根拠、除外理由、フィードバック入力欄を実装する
- KPI設計:Precision@Kだけでなく、採用率、商談化率、粗利、顧客満足、推薦の偏りを監視する
- ガバナンス:利用目的、アクセス権、保存期間、モデル更新責任、停止基準を定める
小規模な導入では、まず1営業チーム・1製品群で、月次の候補リストと人手レビューから始めると効果とリスクを確認しやすくなります。
9. まとめ
No.081〜090では、顧客の構造をクラスタリングで理解し、PCAで説明し、類似検索と協調フィルタリングで提案候補を作り、Precision@K・Recall@Kで評価しました。重要なのは高度なアルゴリズムよりも、類似性の定義、業務制約、時間を守った評価、営業が説明できる推薦理由です。
10. 法人向けのご相談
数理工房では、製造業における顧客セグメント設計、類似製品検索、クロスセル推薦、PoC評価設計から業務実装までご相談いただけます。保有データの棚卸しや、既存の営業ルールをベースラインにした小規模検証からでも対応可能です。
📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。