Доля promoters минус доля detractors по вопросу «порекомендуете нам?».
$$\text{NPS} = %,Promoters - %,Detractors$$ где Promoters — оценка 9-10, Detractors — 0-6 по шкале 0-10.
Что измеряет: lojal'nost' пользователей и word-of-mouth потенциал.
Шкала: 0-10. Группировка:
NPS = % Promoters - % Detractors. Диапазон от -100 до +100.
Бенчмарки (Bain & Co):
| Индустрия | Average NPS | Top 10% |
|---|---|---|
| Banking (Каспи лидер) | 30-50 | 70+ |
| Telecom (KCell, Beeline) | 0-20 | 40+ |
| Insurance | 20-40 | 60+ |
| Tech consumer (Apple) | 60+ | 80+ |
| Airlines (Air Astana) | 10-30 | 50+ |
Каспи имеет NPS ~75 — один из highest в банкинге Казахстана.
Когда полезен и когда нет:
✅ Полезен:
❌ Не полезен:
Pitfalls:
Selection bias: на опрос отвечают чаще довольные ИЛИ очень недовольные. Result — bimodal, не отражает «среднего» юзера.
Goodhart's law: если KPI команды — NPS, они оптимизируют под опрос, не реальное удовлетворение. Видел сценарии где компания просит «9-10 ответы плиз» — это деградация метрики.
NPS не predictive в B2B: часто решение остаться/уйти принимает не respondent (junior), а его boss. NPS низкий, но клиент остаётся.
-- Месячный NPS
SELECT
date_trunc('month', survey_date) AS month,
COUNT(*) AS responses,
ROUND(100.0 * COUNT(*) FILTER (WHERE score >= 9) / COUNT(*), 1) AS promoters_pct,
ROUND(100.0 * COUNT(*) FILTER (WHERE score <= 6) / COUNT(*), 1) AS detractors_pct,
ROUND(100.0 * COUNT(*) FILTER (WHERE score >= 9) / COUNT(*) -
100.0 * COUNT(*) FILTER (WHERE score <= 6) / COUNT(*), 1) AS nps
FROM nps_surveys
WHERE survey_date >= '2026-01-01'
GROUP BY 1
ORDER BY 1;
def nps(scores: pd.Series) -> float:
promoters = (scores >= 9).mean() * 100
detractors = (scores <= 6).mean() * 100
return promoters - detractors
monthly_nps = (surveys
.assign(month=surveys["survey_date"].dt.to_period("M"))
.groupby("month")["score"]
.apply(nps))