📊 2. NPSの分析と活用
2種類のNPS調査
📌 トランザクショナルNPS vs リレーショナルNPS
tNPS(トランザクショナルNPS):
・特定の体験直後に測定
・購入後、サポート対応後、配送完了後など
・「この体験」についての評価
・リアルタイムで問題を検知できる
・頻繁に測定(日次〜週次)
rNPS(リレーショナルNPS):
・定期的に測定(四半期〜年次)
・「ブランド全体」についての評価
・長期的なロイヤルティを把握
・戦略的な意思決定に活用
・ベンチマーク比較に適している
使い分けの例:
・ECサイト: 購入後にtNPS + 四半期でrNPS
・SaaS: オンボーディング後にtNPS + 月次でrNPS
・店舗: 来店後にtNPS + 半年でrNPS
両方使うのがベストプラクティス
クローズドループ・フィードバック
💡 批判者への即時対応が最重要
クローズドループとは:
NPS調査 → 批判者を検知 → 即座にフォローアップ → 問題解決 → 結果を記録
→ この「ループを閉じる」プロセス
なぜ重要か:
・批判者を放置すると、ネガティブ口コミが拡散
・1人の批判者は平均9〜15人に不満を伝える
・逆に、問題解決すれば推奨者に転換する可能性も
実践のポイント:
1. スピード(24〜48時間以内)
・批判者には即日連絡が理想
・「ご意見ありがとうございます」だけでも効果あり
・遅れるほど問題は深刻化
2. 担当者を決める
・0〜3点: マネージャーが対応
・4〜6点: カスタマーサポートが対応
・役割を明確に
3. 解決策を提示
・謝罪だけでなく、具体的な解決策を
・「次回〇〇させていただきます」
・補償やクーポンも検討
4. 結果を記録
・何が原因だったか
・どう解決したか
・顧客の反応は
→ 傾向分析に活用
自由記述質問の活用
📌 「なぜ?」を聞く重要性
NPSの限界:
0〜10点の数字だけでは「なぜその点数か」が分からない
解決策: 自由記述質問を追加
「その点数をつけた理由を教えてください」
活用方法:
1. テキストマイニング
・頻出キーワードを抽出
・「遅い」「高い」「分かりにくい」など
・セグメント別に傾向を分析
2. カテゴリ分類
・価格、品質、サポート、配送、UIなど
・どのカテゴリの不満が多いか
・改善優先度を決める
3. 生の声を共有
・経営会議で具体的なコメントを紹介
・数字だけより説得力がある
・「お客様の声」として全社共有
推奨フォーマット:
Q1: 「この商品を友人に勧める可能性は?」(0〜10点)
Q2: 「その点数をつけた理由は?」(自由記述・任意)
統計的な注意点
💡 サンプルサイズと信頼性
最低サンプルサイズの目安:
・全体NPS: 最低100件(理想は300件以上)
・セグメント別: 各セグメント最低30件
・少なすぎると誤差が大きくなる
誤差範囲(信頼区間):
・100件: ±10ポイント程度
・300件: ±5ポイント程度
・1000件: ±3ポイント程度
例:
NPS = +20(n=100)の場合
→ 実際のNPSは+10〜+30の範囲にある可能性
→ 「NPS +20」と断言するのは危険
文化的バイアス(重要):
・日本人は極端な点数をつけにくい
・欧米: 9〜10点をつける人が多い
・日本: 7〜8点に集中しやすい
・結果、日本企業のNPSは低く出やすい
対策:
・自社の過去データとの比較を重視
・同じ国・業界内でベンチマーク
・グローバル比較は慎重に
NPS向上と収益の関係
📌 NPSが業績に与えるインパクト
研究結果(Bain & Company):
・NPS業界1位の企業は、平均の2倍以上の成長率
・NPSが12ポイント上昇すると、成長率が2倍に
推奨者の行動(平均的な傾向):
・リピート購入率: 批判者の5倍
・客単価: 批判者より20〜30%高い
・紹介: 平均2〜3人を紹介
・解約率: 批判者の1/5
批判者の行動:
・9〜15人にネガティブな口コミ
・SNSでは拡散が加速
・サポートコストが高い(問い合わせが多い)
LTVへの影響(例):
・推奨者のLTV: 50万円
・中立者のLTV: 30万円
・批判者のLTV: 10万円
→ 推奨者を増やすことの経済的価値は非常に大きい
Pythonでの実装
# ============================================
# NPS分析の実装
# ============================================
# NPS(Net Promoter Score)とは?
# → 顧客ロイヤルティを測る最もシンプルな指標
# → 「友人に勧めますか?」の1問だけで測定
# → 業績との相関が高く、多くの企業が採用
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# ============================================
# サンプルデータ作成
# ============================================
# np.random.seed(): 乱数の再現性を確保
# → 同じコードを実行すれば同じ結果が得られる
# → 42は慣習的によく使われる数値(意味はない)
np.random.seed(42)
n_responses = 1000 # 回答数
# ============================================
# np.random.choice(): 指定した選択肢からランダムに選ぶ
# ============================================
# パラメータ:
# range(11): 選択肢(0〜10点の11段階)
# size: 生成する数
# p: 各選択肢の確率(合計1.0になる必要あり)
# → 実際のNPS調査に近い分布を再現
scores = np.random.choice(range(11), size=n_responses,
p=[0.02, 0.03, 0.05, 0.08, 0.10,
0.12, 0.15, 0.18, 0.15, 0.08, 0.04])
# p配列の意味:
# 0点: 2%, 1点: 3%, 2点: 5%, … 10点: 4%
# → 7点付近が最も多い(現実的な分布)
# ============================================
# pd.DataFrame(): データフレームを作成
# ============================================
# 辞書形式で列名と値を指定
# ‘customer_id’: 顧客ID(1〜1000)
# ‘nps_score’: NPSスコア(0〜10)
df = pd.DataFrame({
‘customer_id’: range(1, n_responses + 1),
‘nps_score’: scores
})
# ============================================
# NPSセグメント分類関数
# ============================================
# NPSの3分類ルール(世界共通の基準):
# 推奨者(Promoter): 9〜10点 → 熱狂的なファン
# 中立者(Passive): 7〜8点 → 満足だが熱狂的でない
# 批判者(Detractor): 0〜6点 → 不満を持っている
def classify_nps(score):
if score >= 9:
return ‘Promoter’ # 推奨者
elif score >= 7:
return ‘Passive’ # 中立者
else:
return ‘Detractor’ # 批判者
# ============================================
# .apply(): 各行に関数を適用
# ============================================
# df[‘nps_score’]の各値にclassify_nps関数を適用
# → 結果を新しい列’segment’として追加
df[‘segment’] = df[‘nps_score’].apply(classify_nps)
# ============================================
# NPS計算
# ============================================
# (df[‘segment’] == ‘Promoter’) の仕組み:
# → 各行が条件を満たすかをTrue/Falseで返す
# → [True, False, True, False, …]のような配列
#
# .sum() の仕組み:
# → True=1, False=0として合計
# → つまり、条件を満たす行数をカウント
promoters = (df[‘segment’] == ‘Promoter’).sum()
passives = (df[‘segment’] == ‘Passive’).sum()
detractors = (df[‘segment’] == ‘Detractor’).sum()
# len(df): データフレームの行数(総回答数)
# 割合を計算(%)
promoter_pct = promoters / len(df) * 100
passive_pct = passives / len(df) * 100
detractor_pct = detractors / len(df) * 100
# ============================================
# NPS = 推奨者% – 批判者%
# ============================================
# 中立者は計算に含めない!(これがNPSの特徴)
# 範囲: -100(全員批判者)〜 +100(全員推奨者)
nps = promoter_pct – detractor_pct
print(“【NPS分析結果】”)
print(f”総回答数: {len(df)}件\n”)
print(f”推奨者(9-10点): {promoters}人 ({promoter_pct:.1f}%)”)
print(f”中立者(7-8点): {passives}人 ({passive_pct:.1f}%)”)
print(f”批判者(0-6点): {detractors}人 ({detractor_pct:.1f}%)\n”)
print(f”NPS = {promoter_pct:.1f}% – {detractor_pct:.1f}% = {nps:.1f}”)
print()
# ============================================
# スコア分布の確認
# ============================================
# .value_counts(): 各値の出現回数をカウント
# → {7: 180, 6: 150, 8: 150, …}のような結果
# .sort_index(): インデックス(スコア)順にソート
# → 0, 1, 2, … 10の順に並べ替え
score_dist = df[‘nps_score’].value_counts().sort_index()
print(“【スコア別分布】”)
# .items(): Seriesのインデックスと値をペアで取得
# → (0, 20), (1, 30), (2, 50), …のように順番に取得
for score, count in score_dist.items():
pct = count / len(df) * 100
# ‘█’ * int(pct): 文字列の繰り返し
# → pct=15なら ‘███████████████’(15個)
bar = ‘█’ * int(pct)
print(f”{score}点: {count:3d}人 ({pct:4.1f}%) {bar}”)
print()
# ============================================
# 可視化
# ============================================
# plt.subplots(): 複数のグラフを1つの図に配置
# 2, 2: 2行2列のグリッド(計4つのグラフ)
# figsize: 図全体のサイズ(幅14インチ × 高さ10インチ)
#
# 戻り値:
# fig: 図全体のオブジェクト
# axes: 各グラフへのアクセス用(axes[行, 列])
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# ============================================
# グラフ1: スコア分布(棒グラフ)
# ============================================
# 色の指定: [‘red’]*7 + [‘yellow’]*2 + [‘green’]*2
# [‘red’]*7 → [‘red’, ‘red’, ‘red’, ‘red’, ‘red’, ‘red’, ‘red’]
# + で結合 → 11色のリスト(0-6点=赤, 7-8点=黄, 9-10点=緑)
axes[0, 0].bar(score_dist.index, score_dist.values,
color=[‘red’]*7 + [‘yellow’]*2 + [‘green’]*2,
edgecolor=’black’)
axes[0, 0].set_xlabel(‘NPSスコア’, fontsize=11)
axes[0, 0].set_ylabel(‘回答数’, fontsize=11)
axes[0, 0].set_title(‘NPSスコア分布’, fontsize=13, fontweight=’bold’)
# set_xticks(): X軸の目盛りを設定(0〜10を明示)
axes[0, 0].set_xticks(range(11))
# grid(): グリッド線を表示(alpha=0.3で薄く)
axes[0, 0].grid(axis=’y’, alpha=0.3)
# axvline(): 垂直線を追加(セグメント境界を明示)
# x=6.5: 批判者(0-6)と中立者(7-8)の境界
# x=8.5: 中立者(7-8)と推奨者(9-10)の境界
axes[0, 0].axvline(x=6.5, color=’red’, linestyle=’–‘, linewidth=2, alpha=0.5)
axes[0, 0].axvline(x=8.5, color=’green’, linestyle=’–‘, linewidth=2, alpha=0.5)
# ============================================
# グラフ2: セグメント別割合(円グラフ)
# ============================================
segment_counts = df[‘segment’].value_counts()
colors_pie = [‘#ff6b6b’, ‘#ffd93d’, ‘#6bcf7f’] # 批判者=赤, 中立者=黄, 推奨者=緑
# explode: 各要素を中心から離す距離
# (0.05, 0, 0.05) → 1番目と3番目を少し外側に
# → 批判者と推奨者を強調表示
explode = (0.05, 0, 0.05)
# pie()のパラメータ:
# autopct=’%1.1f%%’: 割合を小数点1桁で表示(例: 45.2%)
# startangle=90: 12時の位置から開始
# explode: 要素を外側にずらす
# textprops: ラベルのフォント設定
axes[0, 1].pie(segment_counts.values, labels=segment_counts.index,
autopct=’%1.1f%%’, startangle=90, colors=colors_pie,
explode=explode, textprops={‘fontsize’: 11, ‘fontweight’: ‘bold’})
axes[0, 1].set_title(f’セグメント別割合\nNPS = {nps:.0f}’,
fontsize=13, fontweight=’bold’)
# ============================================
# グラフ3: NPSの解釈(テキスト表示)
# ============================================
# axis(‘off’): 軸を非表示にしてテキスト表示用スペースに
axes[1, 0].axis(‘off’)
nps_interpretation = f”””
【NPS = {nps:.0f} の解釈】
業界別ベンチマーク(一般的な目安):
• 優秀: NPS 50以上
• 良好: NPS 30〜50
• 普通: NPS 10〜30
• 要改善: NPS 0〜10
• 危険: NPS 0未満
現在のNPS: {nps:.0f}点
→ {“優秀!” if nps >= 50 else “良好!” if nps >= 30 else “普通” if nps >= 10 else “要改善” if nps >= 0 else “危険!”}
改善の余地:
• 批判者を中立者に: +{detractor_pct:.0f}ポイント向上
• 中立者を推奨者に: +{passive_pct:.0f}ポイント向上
• 両方実現: +{detractor_pct + passive_pct:.0f}ポイント向上
“””
# text()のパラメータ:
# 0.1, 0.5: テキストの位置(左から10%, 上下中央)
# verticalalignment=’center’: 垂直方向の中央揃え
# bbox: テキストを囲むボックスの設定
# boxstyle=’round’: 角丸の四角形
# facecolor=’wheat’: 背景色(小麦色)
# alpha=0.3: 透明度(0.3で薄く)
axes[1, 0].text(0.1, 0.5, nps_interpretation,
fontsize=11, verticalalignment=’center’,
fontfamily=’monospace’,
bbox=dict(boxstyle=’round’, facecolor=’wheat’, alpha=0.3))
# ============================================
# グラフ4: 改善シミュレーション
# ============================================
# 「もし改善したらNPSはどうなるか」を可視化
scenarios = [‘現状’, ‘批判者\n-50%’, ‘中立者\n→推奨者\n30%’, ‘両方\n改善’]
nps_values = [
nps, # 現状
promoter_pct – (detractor_pct * 0.5), # 批判者半減
(promoter_pct + passive_pct * 0.3) – detractor_pct, # 中立者30%が推奨者に
(promoter_pct + passive_pct * 0.3) – (detractor_pct * 0.5) # 両方改善
]
bars = axes[1, 1].bar(scenarios, nps_values,
color=[‘gray’, ‘orange’, ‘lightblue’, ‘lightgreen’],
edgecolor=’black’)
axes[1, 1].set_ylabel(‘NPS’, fontsize=11)
axes[1, 1].set_title(‘改善シナリオ別NPS’, fontsize=13, fontweight=’bold’)
# axhline(): 水平線を追加(y=0のベースライン)
axes[1, 1].axhline(y=0, color=’red’, linestyle=’-‘, linewidth=1)
axes[1, 1].grid(axis=’y’, alpha=0.3)
# ============================================
# zip(): 複数のリストを同時にループ
# ============================================
# zip(bars, nps_values)の動作:
# 1回目: bar=1番目の棒, value=nps_values[0]
# 2回目: bar=2番目の棒, value=nps_values[1]
# … と順番にペアで取得
for bar, value in zip(bars, nps_values):
height = bar.get_height() # 棒の高さ(=NPSの値)
# 棒の上にNPS値を表示
axes[1, 1].text(bar.get_x() + bar.get_width()/2, height + 2,
f'{value:.0f}’, ha=’center’, fontweight=’bold’, fontsize=10)
# tight_layout(): グラフ間の余白を自動調整
# → グラフ同士が重ならないように配置
plt.tight_layout()
# show(): グラフを画面に表示
plt.show()
# ============================================
# 批判者の詳細分析
# ============================================
# 批判者を改善するには、まず詳細を知る必要がある
#
# df[df[‘segment’] == ‘Detractor’] の仕組み:
# 1. df[‘segment’] == ‘Detractor’
# → 各行がDetractorかどうかをTrue/Falseで判定
# 2. df[…]
# → Trueの行だけを抽出(フィルタリング)
# 3. [‘nps_score’]
# → その中からnps_score列だけを取得
detractor_scores = df[df[‘segment’] == ‘Detractor’][‘nps_score’]
print(“【批判者の詳細】”)
print(f”批判者数: {len(detractor_scores)}人”)
# .mean(): 平均値を計算
print(f”平均スコア: {detractor_scores.mean():.1f}点”)
# .mode(): 最頻値(最も多い値)を取得
# → 結果はSeriesで返るので[0]で最初の値を取得
print(f”最頻値: {detractor_scores.mode()[0]}点”)
print()
print(“スコア別内訳:”)
for score in range(7): # 0〜6点(批判者の範囲)
count = (detractor_scores == score).sum()
if count > 0:
print(f” {score}点: {count}人”)
print(“\n【分析のポイント】”)
print(“・0〜3点: 深刻な不満(優先対応が必要)”)
print(“・4〜6点: 改善の余地あり(中立者に転換可能)”)
業界別NPSベンチマーク
📌 業界別の目安(参考値)
非常に高い(NPS 50以上):
・Apple: 72
・Amazon: 62
・Netflix: 68
→ 顧客が熱狂的なファン
高い(NPS 30〜50):
・ECサイト(優良): 35〜45
・SaaS(優良): 30〜40
→ 業界平均を上回る
平均的(NPS 10〜30):
・小売業: 10〜25
・金融業: 15〜30
→ 業界標準レベル
低い(NPS 0〜10):
・通信業: 0〜15
・航空業: 10〜20
→ 改善の余地が大きい
非常に低い(NPS 0未満):
・ケーブルTV: -5
・一部の公共サービス: -10
→ 緊急の対応が必要
重要:
・業界によって大きく異なる
・自社の過去データと比較が最重要
・競合との相対比較も有用
🎯 3. NPS改善のアクションプラン
3つの改善戦略
💡 セグメント別アプローチ
戦略1: 批判者を減らす(最優先)
アクション:
・批判者に直接連絡(電話、メール)
・不満の原因をヒアリング
・迅速な問題解決
・特別オファーで関係修復
効果:
・ネガティブ口コミの削減
・解約阻止
・中立者や推奨者への転換可能
戦略2: 中立者を推奨者に変える
アクション:
・「あと一歩」で推奨者になる層
・追加価値の提供
・パーソナライズドサービス
・ロイヤリティプログラム
効果:
・NPSの直接的な向上
・口コミの増加
・LTVの向上
戦略3: 推奨者を活用する
アクション:
・紹介プログラムの導入
・レビュー投稿の依頼
・事例紹介への協力依頼
・アンバサダープログラム
効果:
・新規顧客獲得
・ブランド認知度向上
・マーケティングコスト削減
セグメント別NPS分析
# ============================================
# セグメント別NPS分析
# ============================================
# なぜセグメント別に分析するか?
# → 全体のNPSだけでは改善ポイントが分からない
# → 「どの顧客層に問題があるか」を特定する
# → 効果的な施策を打てる
# ============================================
# 顧客属性を追加(サンプル)
# ============================================
# np.random.choice(): ランダムにカテゴリを割り当て
# p: 各カテゴリの割合(合計1.0)
#
# customer_type: 顧客タイプ
# 新規30%, リピーター50%, VIP20%の割合で生成
df[‘customer_type’] = np.random.choice([‘新規’, ‘リピーター’, ‘VIP’],
size=len(df),
p=[0.3, 0.5, 0.2])
# age_group: 年代
# 20代20%, 30代35%, 40代30%, 50代以上15%
df[‘age_group’] = np.random.choice([’20代’, ’30代’, ’40代’, ’50代以上’],
size=len(df),
p=[0.2, 0.35, 0.3, 0.15])
# ============================================
# NPS計算関数
# ============================================
# セグメント別にNPSを計算するための汎用関数
# → 同じ計算を何度も書かなくて済む
def calculate_nps(data):
“””
データフレームからNPSを計算
Parameters:
———–
data : DataFrame – ‘segment’列を含むデータ
Returns:
——–
float – NPS値(-100〜+100)
計算式:
NPS = (推奨者数 / 総数 – 批判者数 / 総数) × 100
“””
# 推奨者と批判者の人数をカウント
promoters = (data[‘segment’] == ‘Promoter’).sum()
detractors = (data[‘segment’] == ‘Detractor’).sum()
total = len(data)
# NPS = 推奨者% – 批判者%
return (promoters / total – detractors / total) * 100
# ============================================
# 顧客タイプ別NPS
# ============================================
print(“【顧客タイプ別NPS】”)
for ctype in [‘新規’, ‘リピーター’, ‘VIP’]:
# df[df[‘customer_type’] == ctype] の仕組み:
# 1. df[‘customer_type’] == ctype
# → 各行がctypeと一致するかをTrue/Falseで判定
# 2. df[…]
# → Trueの行だけを抽出
subset = df[df[‘customer_type’] == ctype]
nps_value = calculate_nps(subset)
print(f”{ctype}: NPS = {nps_value:.1f} ({len(subset)}人)”)
print()
# ============================================
# 年代別NPS
# ============================================
print(“【年代別NPS】”)
for age in [’20代’, ’30代’, ’40代’, ’50代以上’]:
subset = df[df[‘age_group’] == age]
nps_value = calculate_nps(subset)
print(f”{age}: NPS = {nps_value:.1f} ({len(subset)}人)”)
print()
# ============================================
# 批判者が多いセグメントを特定
# ============================================
# これが最も重要な分析!
# → 「どのセグメントに不満を持つ顧客が多いか」を特定
# → 優先的に対応すべきセグメントが分かる
print(“【詳細分析:批判者が多いセグメント】”)
# Step 1: 批判者だけを抽出し、顧客タイプ別にカウント
# df[df[‘segment’] == ‘Detractor’]: 批判者だけを抽出
# .groupby(‘customer_type’): 顧客タイプでグループ化
# .size(): 各グループの件数をカウント
detractor_by_type = df[df[‘segment’] == ‘Detractor’].groupby(‘customer_type’).size()
# Step 2: 各顧客タイプの総数を取得
total_by_type = df.groupby(‘customer_type’).size()
# Step 3: 批判者率を計算(批判者数 / 総数 × 100)
detractor_by_type_pct = detractor_by_type / total_by_type * 100
# Step 4: 批判者率が高い順にソートして表示
# .sort_values(ascending=False): 降順(大きい順)にソート
# .items(): インデックス(顧客タイプ)と値(批判者率)をペアで取得
for ctype, pct in detractor_by_type_pct.sort_values(ascending=False).items():
print(f”{ctype}: 批判者率 {pct:.1f}%”)
print(“\n【改善の優先順位】”)
print(“批判者率が高いセグメント = 優先的に対応すべき”)
print(“→ 不満の原因をヒアリングし、改善策を実施”)
print(“\n【次のアクション】”)
print(“1. 批判者率が最も高いセグメントの顧客にインタビュー”)
print(“2. 不満の原因を特定(価格?品質?サポート?)”)
print(“3. 具体的な改善策を実施”)
print(“4. 改善後のNPSを再測定”)