Анализ чувствительности и распространение неопределённости
У вас есть модель, которая выдаёт одно красивое число — «доход 20 495 рублей». Но входные параметры вы знаете неточно. Какой из них способен разрушить ваш прогноз сильнее всего, и насколько широк интервал, в котором реально окажется результат?
Анализ чувствительности — это исследование того, как изменение каждого входного параметра модели по отдельности влияет на её выход. Он отвечает на вопрос «к чему модель чувствительна, а к чему — почти безразлична».
Любая модель — это функция: вы подаёте на вход цену, темп роста, отток клиентов, а на выходе получаете доход. В реальности ни один из входов не известен точно: цена ещё обсуждается, темп роста — оптимистичная оценка, отток измерен на маленькой выборке. Если вы строите бизнес-план на единственном числе, вы обманываете и себя, и инвесторов. Анализ чувствительности и распространение неопределённости — два инструмента, которые превращают «одно число» в честную картину рисков.
Зачем это нужно на практике. Во-первых, чтобы понять, какой параметр стоит измерять точнее всего: нет смысла шлифовать цену до копейки, если результат в основном определяется оттоком. Во-вторых, чтобы сообщать решение в виде интервала, а не точки — это и честнее, и устойчивее к критике.
Блок A. Чувствительность «по одному параметру»
Возьмём простую модель дохода стартапа за год. Стартуем со 100 пользователями; каждый месяц их число растёт на rate и одновременно часть уходит (churn), а каждый оставшийся приносит price рублей. Идея анализа: зафиксировать все параметры на базовых значениях и по очереди «качнуть» каждый от его нижней границы к верхней, измеряя размах выхода.
def model(price, rate, churn):
users = 100.0
revenue = 0.0
for month in range(12):
users = users * (1 + rate) * (1 - churn)
revenue += users * price
return revenue
base = model(10, 0.2, 0.1)
print("Базовый доход:", round(base, 1))
for name, lo, hi in [("price", 8, 12), ("rate", 0.15, 0.25), ("churn", 0.05, 0.15)]:
if name == "price":
r_lo = model(lo, 0.2, 0.1); r_hi = model(hi, 0.2, 0.1)
elif name == "rate":
r_lo = model(10, lo, 0.1); r_hi = model(10, hi, 0.1)
else:
r_lo = model(10, 0.2, lo); r_hi = model(10, 0.2, hi)
print(f"{name:>6}: {r_lo:>8.0f} .. {r_hi:>8.0f} размах={abs(r_hi - r_lo):>8.0f}")
Вывод:
Базовый доход: 20495.3 price: 16396 .. 24594 размах= 8198 rate: 15113 .. 27989 размах= 12876 churn: 31089 .. 13680 размах= 17408
Читаем таблицу. Колонка «размах» — это и есть мера чувствительности: насколько широко гуляет доход, когда мы качаем данный параметр в его реалистичном диапазоне. У price размах 8198, у rate — 12876, а у churn — целых 17408. Вывод однозначен: отток клиентов критичнее всего. Обратите внимание и на знак: для churn доход при низком значении выше (31089 при churn=0.05), чем при высоком (13680 при churn=0.15) — больший отток съедает доход.
Почему это называют «торнадо-анализом»
Если нарисовать горизонтальные полоски «низкое значение — высокое значение» для каждого параметра и отсортировать их по длине, самая длинная окажется сверху, самая короткая — снизу. Получается фигура, сужающаяся книзу, — похожая на воронку торнадо. Это стандартный способ визуально расставить параметры по важности.
churn |==================| (самый влиятельный) rate |=============| price |========| (наименее влиятельный)
Блок B. Распространение неопределённости через Монте-Карло
Анализ «по одному параметру» отвечает на вопрос «что важнее», но не отвечает на «каков итоговый разброс, когда все входы неточны одновременно». Для этого мы задаём каждый вход не числом, а интервалом, и многократно прогоняем модель со случайными значениями из этих интервалов. Это и есть распространение неопределённости методом Монте-Карло.
Распространение неопределённости — это перенос разброса входных величин на выход модели: на входе у нас распределения, и на выходе мы получаем тоже распределение, а не одно число.
import random, statistics
random.seed(42)
def model(price, rate, churn):
users = 100.0
revenue = 0.0
for month in range(12):
users = users * (1 + rate) * (1 - churn)
revenue += users * price
return revenue
def trial():
return model(random.uniform(8, 12), random.uniform(0.15, 0.25), random.uniform(0.05, 0.15))
results = [trial() for _ in range(10000)]
rs = sorted(results)
print(f"Среднее: {statistics.mean(results):.0f}")
print(f"Стд: {statistics.pstdev(results):.0f}")
print(f"5-й перцентиль: {rs[500]:.0f}")
print(f"95-й перцентиль: {rs[9500]:.0f}")
Вывод:
Среднее: 21498 Стд: 6979 5-й перцентиль: 12237 95-й перцентиль: 34887
Что мы получили вместо единственного «20 495»? Среднее около 21 498, стандартное отклонение почти 7 000 — это и есть масштаб неопределённости. А перцентили дают самый ценный результат: 5-й перцентиль (12 237) и 95-й (34 887) очерчивают 90% доверительный интервал. Честная формулировка для инвестора звучит так: «с вероятностью около 90% годовой доход окажется между 12 и 35 тысячами рублей». Это в разы информативнее, чем «доход будет 20 495».
Как работает под капотом
Монте-Карло здесь — это просто закон больших чисел в действии. Каждый вызов trial() берёт по одному случайному значению из каждого входного интервала (равномерно, через random.uniform) и прогоняет модель. Накопив 10 000 таких прогонов, мы получаем эмпирическое распределение выхода. Среднее этого распределения оценивает «ожидаемый» доход, а отсортированный список rs позволяет читать перцентили напрямую: элемент с индексом 500 из 10 000 — это 5-й перцентиль, индекс 9500 — 95-й. random.seed(42) фиксирует поток случайных чисел, поэтому вывод воспроизводим до последней цифры.
Важная тонкость: чувствительность (Блок A) и распространение (Блок B) дополняют друг друга. Первый говорит, куда смотреть (сократите отток — и разброс схлопнется сильнее всего), второй — насколько широка итоговая неопределённость с учётом всех входов сразу.
Частые ошибки
- Качать параметр в нереалистичном диапазоне. Если границы
lo/hiвзяты «с потолка», размах в торнадо-анализе ничего не значит. Диапазоны должны отражать реальную неопределённость каждого входа. - Считать чувствительность «по одному» полной картиной. Метод OAT (one-at-a-time) игнорирует взаимодействия параметров — он не заметит, что эффект
rateусиливается при низкомchurn. Для этого и нужен Монте-Карло. - Брать слишком мало прогонов. На 100 итерациях перцентили будут шуметь. 10 000 — разумный минимум для устойчивых хвостов распределения.
- Докладывать только среднее. Среднее без разброса скрывает риск: «21 498» звучит уверенно, но 5-й перцентиль 12 237 предупреждает о плохом сценарии.
- Путать перцентиль со стандартным отклонением. При несимметричном распределении интервал «среднее ± 1.96·стд» и интервал по перцентилям различаются — для перекошенных выходов перцентили честнее.
Итоги
- Анализ чувствительности «по одному параметру» (торнадо-анализ) показывает, какой вход критичнее всего — здесь это отток клиентов (размах 17408).
- Колонка «размах» — прямая мера влияния параметра; сортировка по ней даёт фигуру торнадо.
- Распространение неопределённости через Монте-Карло превращает интервалы входов в распределение выхода.
- Перцентили дают честный доверительный интервал: «доход с 90% уверенностью между 12 и 35 тысячами».
- Два метода дополняют друг друга: один указывает, что измерять точнее, другой — насколько широк итоговый риск.