NPS с доверительным интервалом: считаем неопределённость в NumPy

За месяц NPS поднялся с 20 до 25 пунктов. Команда готова считать это улучшением сервиса, но на новый опрос ответили всего двадцать клиентов. Прежде чем обсуждать разницу, оценим неопределённость самого показателя. Для этого достаточно NumPy, исходных оценок и понимания, кого именно представляют полученные ответы.

Превращаем ответы в показатель

NPS строится из ответов по шкале от 0 до 10. Оценки 9–10 считаются промоутерами, 0–6 — критиками, 7–8 — нейтральными. Показатель равен доле промоутеров минус доля критиков, умноженной на 100. Он меняется от −100 до 100 пунктов. Рост с 20 до 25 означает пять пунктов, а не пять процентов от исходного значения.

Для расчёта удобно закодировать вклад каждого ответа: промоутер даёт +1, критик −1, нейтральный 0. Среднее таких значений, умноженное на 100, в точности даёт NPS. Нейтральные ответы остаются в знаменателе, хотя их вклад в числитель нулевой. Удаление нейтральных завысило бы абсолютную величину оценки.

В примере каждый ответ принадлежит отдельному клиенту. Если человек отвечает несколько раз, нужно заранее выбрать правило: последний ответ за период, первый ответ после обращения или повторная выборка клиентов со всеми ответами. Иначе активные респонденты будут иметь больший вес, чем остальные. Идентификатор клиента поэтому полезно сохранять уже при сборе опроса.

Повторяем выборку ответов

Bootstrap создаёт много выборок такого же размера из наблюдавшихся ответов с возвращением. В каждой заново рассчитывается NPS. Процентильный интервал берёт квантили 2,5% и 97,5% полученного распределения. Это простой способ увидеть, какой разброс оценки совместим с похожими выборками из доступных ответов.

Фиксированное зерно генератора делает учебный результат воспроизводимым. Двадцать тысяч повторов достаточно для демонстрации, но увеличение числа повторов не добавляет новых клиентов: оно только точнее описывает bootstrap-распределение по уже собранной информации. Основную неопределённость здесь задаёт маленький опрос.

На очень малых или почти однородных выборках процентильный bootstrap может давать грубую оценку границ. Если все ответившие поставили 10, повторная выборка не создаст критиков и получится вырожденный интервал. Для ответственного решения нужны достаточный объём и проверка подходящей модели интервала, а не только больше повторов в коде.

import numpy as np
ratings = np.array([10,9,10,8,7,6,5,9,10,8,9,4,10,7,9,6,8,10,9,5])
assert ((ratings>=0)&(ratings<=10)).all()
score = np.where(ratings>=9,1,np.where(ratings<=6,-1,0))
rng = np.random.default_rng(42)
boot = rng.choice(score,size=(20000,len(score)),replace=True).mean(axis=1)*100
point = score.mean()*100
ci = np.quantile(boot,[0.025,0.975])
print({"NPS":point,"CI":ci.tolist(),"n":len(score),"promoters":int((score==1).sum()),"detractors":int((score==-1).sum())})
assert np.isclose(point,25)
assert ci[0] < point < ci[1]
assert len(score) == 20

Что сообщить руководителю

В примере десять промоутеров, пять критиков и пять нейтральных ответов. NPS равен 25 пунктам, а интервал получается широким. Поэтому само движение на несколько пунктов ещё не даёт уверенного основания связать результат с изменением сервиса. В отчёте рядом с NPS покажите интервал, число ответов и распределение трёх категорий. Так читатель видит данные, из которых получилась оценка.

Для сравнения месяцев нужно оценивать разницу двух NPS. В независимых опросах повторно выбирают ответы каждого месяца отдельно и вычитают результаты. Для одних и тех же клиентов можно сохранить пары. Пересечение двух отдельных интервалов не является прямым расчётом интервала разницы.

Что проверить в самом опросе

Сравните способ приглашения и долю ответивших. Если раньше опрос получали все покупатели, а теперь только завершившие обращение в поддержку, состав респондентов изменился. Bootstrap не исправляет такой отбор. Даже узкая граница относится к наблюдавшимся ответам при принятой схеме, а не автоматически ко всем клиентам магазина.

После проверки данных полезно разобрать комментарии критиков по повторяющимся причинам и связать их с процессами: задержка доставки, качество товара, работа поддержки. Затем изменение конкретного процесса оценивают отдельной метрикой и продуманным сравнением. Так NPS помогает выбрать направление работы, а не становится единственным доказательством её успеха.

Для метрики с несколькими событиями одного клиента пригодится пример сохранения клиентской единицы наблюдения: Средний чек в A/B-тесте: как сделать bootstrap по пользователям.

Связываем интервал с реальным решением

До следующего опроса договоритесь, какое изменение заслуживает действия. Для запуска проекта по улучшению доставки могут потребоваться одновременно снижение числа жалоб и заметное движение NPS. Тогда заранее понятно, какие данные собирать, а отдельный колеблющийся балл не становится автоматическим поводом менять процесс.

Полезно сохранить число приглашений и ответов по клиентским группам. Если новый месяц дал больше ответов от недавно купивших и меньше от постоянных клиентов, сначала сравните группы отдельно. При повторном опросе одинаковых людей можно также посмотреть, кто перешёл из критиков в нейтральные и промоутеры. Это содержательнее одной общей разницы и помогает выбрать, какие причины комментариев проверить в операционных данных. Набор клиентов для такого анализа должен определяться одинаково в обоих периодах.

Продолжить практику

В «A/B тестах и анализе данных на Python» вы применяете bootstrap и другие статистические методы, оцениваете доверительные интервалы и формулируете вывод для продуктовой команды. Курс охватывает выбор метрик, планирование эксперимента и полный анализ результата в финальном проекте. Откройте программу курса.

Продолжить на практике

  • A/B тесты и анализ данных на Python ↗

    Практический курс по A/B-тестированию и продуктовым метрикам: формулируем гипотезы, считаем размер выборки, запускаем эксперименты и анализируем результаты. От первой идеи до внедрения изменений в продукт — без «воды» и лишней теории.