Приоритизация: RICE, ICE, Kano, MoSCoW, WSJF
Приоритизация — самый профанируемый навык в продуктовой работе. Её обычно преподают как набор аббревиатур: «посчитай RICE, отсортируй, вот бэклог». Это неверно на уровне постановки задачи. Все фреймворки ниже — не способы узнать правильный ответ, а способы сделать спор явным: превратить «мне кажется, это важнее» в конкретное число, за которое можно нести ответственность и которое можно проверить через квартал.
Разберём с первых принципов: какую математическую задачу мы вообще решаем, почему сортировка по «ценности» неправильна, откуда берётся деление на усилия, когда оно доказуемо оптимально, а когда ломается — и что с этим делают в проде.
1. Задача с первых принципов
1.1. Почему приоритизация вообще существует
Три факта, из которых следует всё остальное:
- Поток идей неограничен. Стейкхолдеры, поддержка, продажи, аналитика и сами разработчики генерируют запросы быстрее, чем команда способна их выпускать. Бэклог из 400 задач — это не «плохо ведут бэклог», это нормальное равновесие ненасыщаемого входа.
- Пропускная способность жёстко ограничена и почти не растёт от найма в краткосроке (закон Брукса: добавление людей в опаздывающий проект замедляет его).
- Ценность идей распределена крайне неравномерно. По данным экспериментов Microsoft/Amazon примерно треть протестированных идей улучшает целевую метрику, треть нейтральна, треть вредит (Kohavi, Tang, Xu, «Trustworthy Online Controlled Experiments»), а внутри «работающей» трети распределение эффекта — тяжёлохвостое: несколько идей дают больше, чем все остальные вместе.
Из (1) и (2) следует, что выбирать придётся всегда; из (3) — что цена ошибки выбора велика. Если бы ценность была распределена равномерно, порядок не имел бы значения и приоритизация была бы бессмысленным ритуалом. Она осмысленна ровно потому, что распределение неравномерно.
1.2. Три разные задачи, которые путают
Слово «приоритизация» обозначает три математически разные задачи:
| Задача | Формально | Метод |
|---|---|---|
| Что вообще брать в работу | отбор подмножества при бюджете B |
knapsack, MoSCoW |
| В каком порядке делать отобранное | минимизация суммарных потерь от задержки | WSJF / WSPT |
| Что удалить из бэклога навсегда | фильтрация по стратегии | стратегические критерии, Kano |
Их регулярно решают одним инструментом — и получают мусор. RICE отвечает на первый вопрос, WSJF — на второй, Kano — скорее на третий (какие фичи вообще имеет смысл рассматривать и с каким ожиданием эффекта). Ниже это разделение выдерживается явно.
1.3. Что такое «приоритет» в единицах
Полезная опора: приоритет — это ожидаемая ценность на единицу дефицитного ресурса. Дефицитный ресурс у продуктовой команды — не деньги, а время сильных инженеров. Отсюда общая форма всех скоринговых моделей:
score = E[ценность] / стоимость
Всё остальное — вариации того, как оценивать числитель (охват × эффект × уверенность, или сумма компонент бизнес-ценности) и знаменатель (человеко-недели, story points, «аппетит»).
2. Карта методов
Разница между группами принципиальна:
- скоринг даёт полный порядок и иллюзию объективности;
- классификация даёт грубые классы, но лучше отражает нелинейность ценности;
- экономика потока — единственная группа с доказуемой оптимальностью;
- социальные методы решают не математическую, а политическую задачу — согласие стейкхолдеров.
3. ICE: минимальная модель
ICE придумал Шон Эллис для приоритизации ростовых экспериментов (см. GrowthHackers / Sean Ellis):
ICE = Impact × Confidence × Ease
Каждый компонент — оценка по шкале 1..10:
- Impact — насколько сильно изменится целевая метрика, если гипотеза верна;
- Confidence — насколько мы уверены, что она верна (есть ли данные, а не мнение);
- Ease — насколько легко проверить (обратная стоимость).
Достоинство одно, но важное: ICE считается за 30 секунд и годится для десятков мелких однородных экспериментов, где полноценная оценка дороже самого эксперимента.
Проблемы ICE серьёзны и их надо знать:
- Шкалы не откалиброваны. «Impact 7» у двух людей — разные вещи. Умножение трёх субъективных чисел даёт разброс, который легко перекрывает разницу между задачами.
- Нет охвата. Фича для 100% пользователей и фича для 2% могут получить одинаковый Impact, потому что «для тех, кто ею пользуется, эффект большой».
- Мультипликативность усиливает шум: относительная ошибка произведения складывается из относительных ошибок сомножителей.
- Confidence легко используется как ручка регулировки — сначала выбирают желаемый результат, потом подгоняют Confidence.
ICE применим для гипотез внутри одного типа (например, письма в реактивации). Как только задачи разнородны — нужен RICE.
4. RICE: рабочая лошадка
RICE предложила команда Intercom, чтобы уйти от споров о том, чья фича важнее (Sean McBride, «RICE: Simple prioritization for product managers»):
RICE = (Reach × Impact × Confidence) / Effort
4.1. Компоненты — и как их не испортить
Reach (охват) — количество людей или событий за фиксированный период. Это ключевая дисциплина метода: единица измерения одна для всего бэклога и всегда указывается явно — «пользователей в квартал», «заказов в месяц». Считается из аналитики, а не из головы:
-- Reach для фичи «повторный заказ в один клик»:
-- сколько уникальных пользователей в квартал вообще попадают в этот сценарий
SELECT COUNT(DISTINCT user_id) AS reach_per_quarter
FROM orders
WHERE created_at >= DATE_TRUNC('quarter', CURRENT_DATE) - INTERVAL '3 months'
AND created_at < DATE_TRUNC('quarter', CURRENT_DATE)
AND user_id IN ( -- те, у кого уже был хотя бы один заказ
SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) >= 1
);
Если Reach невозможно посчитать запросом — это сигнал, что вы не понимаете, кому делаете фичу. Подробнее про источники таких чисел — в статье о метриках (https://courses.digitable.life/post/product-management/02-metrics/) и в аналитике (https://courses.digitable.life/post/product-management/08-analytics-and-decisions/).
Impact (эффект) — насколько сильно изменится поведение того, кого фича коснулась. Intercom использует дискретную шкалу, и это правильно: она не даёт спорить о «7 против 8».
| Impact | Значение | Смысл |
|---|---|---|
| Massive | 3 | меняет сценарий целиком |
| High | 2 | заметно лучше |
| Medium | 1 | ощутимо |
| Low | 0.5 | чуть-чуть |
| Minimal | 0.25 | на грани измеримости |
Confidence (уверенность) — 100% / 80% / 50% / 20%. Это не «насколько я верю», а какого качества доказательства стоят за Reach и Impact. Итамар Гилад предлагает привязывать уровень к типу свидетельства — от «мнение» до «результат A/B-теста» (Confidence Meter):
- 100% — прошлый A/B-тест или прямые данные;
- 80% — количественные данные + качественные интервью;
- 50% — интервью или аналогия с конкурентом;
- 20% — «нам кажется» (moonshot);
- ниже 20% — не считать, а сначала идти в discovery (https://courses.digitable.life/post/product-management/01-discovery-and-research/).
Effort (усилия) — человеко-месяцы (или -недели) всей команды: продакт, дизайн, бэкенд, фронтенд, QA, аналитика. Оценивать грубо (0.5 / 1 / 2 / 3 / 5), с участием инженеров.
4.2. Реализация и сложность
from dataclasses import dataclass
from typing import Iterable
IMPACT_SCALE = {"massive": 3.0, "high": 2.0, "medium": 1.0, "low": 0.5, "minimal": 0.25}
CONFIDENCE_SCALE = {"high": 1.0, "medium": 0.8, "low": 0.5, "moonshot": 0.2}
@dataclass(frozen=True)
class Item:
name: str
reach: float # пользователей за квартал — ЕДИНИЦА ОДНА НА ВЕСЬ БЭКЛОГ
impact: str # ключ из IMPACT_SCALE
confidence: str # ключ из CONFIDENCE_SCALE
effort: float # человеко-месяцы всей команды
@property
def score(self) -> float:
if self.effort <= 0:
raise ValueError(f"{self.name}: effort должен быть > 0")
return (self.reach
* IMPACT_SCALE[self.impact]
* CONFIDENCE_SCALE[self.confidence]
/ self.effort)
def rank(items: Iterable[Item]) -> list[Item]:
"""Полный порядок по убыванию RICE. O(n log n) времени, O(n) памяти."""
return sorted(items, key=lambda i: i.score, reverse=True)
backlog = [
Item("Вход по одноразовому коду", reach=42_000, impact="medium", confidence="high", effort=1.5),
Item("Повторный заказ в один клик", reach=12_500, impact="high", confidence="medium", effort=2.0),
Item("Тёмная тема", reach=60_000, impact="minimal", confidence="high", effort=1.0),
Item("Рекомендации на ML", reach=55_000, impact="massive", confidence="moonshot", effort=6.0),
Item("Экспорт в CSV для B2B", reach= 900, impact="massive", confidence="high", effort=0.5),
]
for it in rank(backlog):
print(f"{it.score:9.1f} {it.name}")
Вывод:
28000.0 Вход по одноразовому коду
15000.0 Тёмная тема
10000.0 Повторный заказ в один клик
5500.0 Рекомендации на ML
5400.0 Экспорт в CSV для B2B
Сложность. Скоринг — O(n), сортировка — O(n log n) времени и O(n) памяти.
Вычислительно задача тривиальна; вся сложность — в качестве входных чисел.
Это главное, что стоит запомнить: алгоритм здесь не является узким местом, данные являются.
4.3. Что этот пример уже показал
Посмотрите на результат внимательно — он поучителен:
- «Тёмная тема» с минимальным эффектом вылезла на второе место только за счёт охвата. Это классический артефакт: RICE систематически завышает косметические изменения с большим Reach. Лечение — честная шкала Impact (косметика редко даёт даже 0.25) и проверка: «если мы это выкатим, какая метрика из дашборда сдвинется и на сколько?».
- «Экспорт в CSV» с охватом 900 человек оказался внизу, хотя это могут быть 900 корпоративных клиентов, дающих 60% выручки. RICE не знает о ценности пользователя. Лечение — считать Reach во взвешенных единицах (например, в ARR затронутых аккаунтов) или вести отдельные бэклоги по сегментам.
- «Рекомендации на ML» получили низкий скор из-за Confidence 0.2 — и это правильное поведение модели: сначала дешёвый эксперимент, снижающий неопределённость, потом крупная ставка.
4.4. Неопределённость: RICE как распределение, а не число
Реальные Reach/Impact/Effort — это интервалы, а не точки. Ранжирование по средним теряет информацию: два элемента со скором 5000 и 5200 неразличимы, если разброс каждого ±3000. Честный подход — прогнать Монте-Карло и смотреть на вероятность попасть в топ-k.
import random
import statistics
from collections import Counter
def triangular(lo: float, mode: float, hi: float) -> float:
"""Треугольное распределение — минимальная модель экспертной оценки (PERT-стиль)."""
return random.triangular(lo, hi, mode)
def monte_carlo_top_k(estimates: dict[str, dict], k: int = 3, trials: int = 20_000):
"""
estimates[name] = {"reach": (lo, mode, hi), "impact": (...), "effort": (...), "conf": p}
Возвращает вероятность попадания каждого элемента в топ-k.
Сложность: O(trials * n log n) времени, O(n) памяти.
"""
hits = Counter()
for _ in range(trials):
scored = []
for name, e in estimates.items():
r = triangular(*e["reach"])
i = triangular(*e["impact"])
eff = triangular(*e["effort"])
# Confidence моделируем как вероятность того, что эффект вообще есть
realized = i if random.random() < e["conf"] else 0.0
scored.append((r * realized / eff, name))
scored.sort(reverse=True)
for _, name in scored[:k]:
hits[name] += 1
return {name: hits[name] / trials for name in estimates}
estimates = {
"Вход по коду": {"reach": (30_000, 42_000, 50_000), "impact": (0.5, 1.0, 2.0),
"effort": (1.0, 1.5, 3.0), "conf": 0.9},
"Один клик": {"reach": (8_000, 12_500, 20_000), "impact": (1.0, 2.0, 3.0),
"effort": (1.5, 2.0, 4.0), "conf": 0.7},
"Тёмная тема": {"reach": (50_000, 60_000, 65_000), "impact": (0.1, 0.25, 0.5),
"effort": (0.8, 1.0, 1.5), "conf": 0.95},
"ML-рекомендации":{"reach": (40_000, 55_000, 70_000), "impact": (1.0, 3.0, 5.0),
"effort": (4.0, 6.0, 12.0), "conf": 0.25},
}
for name, p in sorted(monte_carlo_top_k(estimates).items(), key=lambda kv: -kv[1]):
print(f"P(топ-3) = {p:5.1%} {name}")
Что даёт такой прогон на практике:
- элементы, попадающие в топ-k с вероятностью > 80%, — робастный выбор, их берут без спора;
- элементы с вероятностью 40–60% — зона, где решает стратегия, а не арифметика;
- если у ML-рекомендаций
P(топ-3) = 35%, но приconf → 0.6она становится 85%, то самая ценная задача квартала — не фича, а эксперимент, поднимающий confidence. Это способ формально обосновать инвестицию в discovery.
5. Value vs Effort: квадрант как коммуникация
Скор — это число, а число плохо спорится на встрече со стейкхолдерами. Тот же RICE, разложенный на две оси, читается мгновенно:
Как читать:
- Быстрые победы (высокая ценность, малые усилия) — делать сразу, но не залипать: такой квадрант быстро вычерпывается, и команда, живущая только в нём, годами полирует локальный максимум, не двигая продукт стратегически.
- Крупные ставки — 1–2 за квартал, не больше. Требуют явного снижения неопределённости до старта.
- Заполнители — берутся, когда есть простой или как «сахар» для морали команды.
- Неблагодарная работа — дорого и малоценно. Здесь живёт значительная часть техдолга, и именно поэтому чистый value/effort скоринг никогда не даст выделить на него ресурс. Технические инвестиции защищают не скором, а квотой бюджета (см. раздел про MoSCoW).
Ограничение квадранта: он не показывает уверенность. Точка в «крупных ставках» с confidence 20% и точка с confidence 100% выглядят одинаково. Кодируйте уверенность размером или прозрачностью маркера.
6. Модель Кано: ценность нелинейна
RICE неявно предполагает, что ценность аддитивна и линейна: вложил вдвое больше — получил вдвое больше. Нориаки Кано в 1984 году показал, что это неверно (Kano N. et al., «Attractive Quality and Must-Be Quality», Journal of the Japanese Society for Quality Control; хороший современный разбор — Daniel Zacarias, «The Complete Guide to the Kano Model»).
6.1. Пять категорий атрибутов
- Must-be (базовые). Отсутствие вызывает ярость, наличие не даёт никакой радости. Приложение не падает, платёж проходит, пароль восстанавливается. Инвестиции сверх «работает» бессмысленны — кривая насыщается.
- Performance (одномерные). Чем больше, тем лучше, примерно линейно: скорость доставки, время загрузки, размер каталога. Это единственная категория, где RICE-логика честна.
- Attractive (восхищающие). Отсутствие не замечают, наличие вызывает восторг. Именно здесь возникает дифференциация от конкурентов — и именно эту категорию убивает наивный скоринг, потому что Reach у неё поначалу низкий, а Impact неизмерим до запуска.
- Indifferent (безразличные). Пользователю всё равно. Огромная часть бэклога любой зрелой компании живёт здесь — и обнаруживается только опросом.
- Reverse (обратные). Чем больше, тем хуже для части сегмента: обязательная регистрация, «умная» лента вместо хронологической, лишние уведомления.
Дрейф. Категории не постоянны. Камера в телефоне: 2003 — attractive, 2010 — performance, 2020 — must-be. Отсюда практический вывод: продукт, который вкладывается только в must-be, медленно, но верно превращается в commodity.
6.2. Опрос Кано: как измерить, а не угадать
Метод строгий. Про каждую фичу задают два вопроса:
- функциональный: «Как вы отнесётесь, если эта возможность будет?»
- дисфункциональный: «Как вы отнесётесь, если её не будет?»
Оба — с одинаковыми пятью вариантами: нравится / ожидаю / всё равно / могу смириться / не нравится. Пара ответов однозначно даёт категорию по таблице оценок Кано.
LIKE, EXPECT, NEUTRAL, TOLERATE, DISLIKE = range(5)
# Таблица Кано: строки — ответ на функциональный вопрос, столбцы — на дисфункциональный.
# A = attractive, O = one-dimensional (performance), M = must-be,
# I = indifferent, R = reverse, Q = questionable (противоречивый ответ)
KANO_TABLE = [
# like expect neutral tolerate dislike <- дисфункциональный
["Q", "A", "A", "A", "O"], # like
["R", "I", "I", "I", "M"], # expect
["R", "I", "I", "I", "M"], # neutral
["R", "I", "I", "I", "M"], # tolerate
["R", "R", "R", "R", "Q"], # dislike
]
def classify(functional: int, dysfunctional: int) -> str:
return KANO_TABLE[functional][dysfunctional]
def analyze(responses: list[tuple[int, int]]) -> dict:
"""
responses — ответы респондентов по ОДНОЙ фиче.
Возвращает категорию большинства и коэффициенты Better/Worse (Timko).
Сложность: O(n) времени, O(1) памяти.
"""
from collections import Counter
cats = Counter(classify(f, d) for f, d in responses)
valid = sum(v for k, v in cats.items() if k != "Q")
if valid == 0:
return {"category": "Q", "better": 0.0, "worse": 0.0}
a, o, m, i = cats["A"], cats["O"], cats["M"], cats["I"]
better = (a + o) / valid # 0..1: насколько наличие поднимает удовлетворённость
worse = -(o + m) / valid # -1..0: насколько отсутствие её роняет
category = max(("A", a), ("O", o), ("M", m), ("I", i), key=lambda kv: kv[1])[0]
return {"category": category, "better": round(better, 2), "worse": round(worse, 2)}
# Пример: «уведомление о статусе доставки»
print(analyze([(LIKE, DISLIKE)] * 30 + [(EXPECT, DISLIKE)] * 45 + [(NEUTRAL, NEUTRAL)] * 25))
# {'category': 'M', 'better': 0.3, 'worse': -0.75}
Коэффициенты Timko (better / worse) полезнее самой категории: они непрерывны, их можно
класть на диаграмму рассеяния и сравнивать фичи между собой. Фича с worse = -0.9 — это
«не сделаем — потеряем людей», её место в разделе Must у MoSCoW независимо от RICE-скора.
Практические ограничения. Нужно 20–30 валидных ответов на сегмент (иначе шум), формулировки вопросов должны быть на языке пользователя (иначе получите Q), и опрос меряет декларируемое, а не реальное поведение — люди систематически переоценивают свой интерес к новому. Поэтому Kano хорош для фильтрации (что точно не делать) и для понимания типа ценности, а не для точного прогноза эффекта.
7. MoSCoW: не про скоринг, а про обязательства
MoSCoW пришёл из DSDM — метода, построенного вокруг фиксированных сроков и переменного объёма (Agile Business Consortium). Категории:
- Must have — без этого релиз не имеет смысла или незаконен. Тест жёсткий: «что произойдёт, если мы выпустим без этого?» Если ответ не «релиз отменяется» — это не Must.
- Should have — важно, больно без этого, но существует обходной путь.
- Could have — желательно, отбрасывается первым при нехватке времени.
- Won’t have (this time) — явно зафиксированное «не сейчас». Самая недооценённая категория: она превращает бесконечный спор в записанное решение с датой пересмотра.
7.1. Правило квот — то, ради чего метод и нужен
Ключевая практика DSDM, которую почти всегда теряют: Must не должен превышать ~60% бюджета итерации, Should ≈ 20%, Could ≈ 20%. Эти 20% Could — не «приятные мелочи», а буфер против неопределённости оценок. Если всё помечено Must, буфера нет, и любая задержка бьёт по обязательствам.
def check_moscow_budget(items: list[tuple[str, str, float]], capacity: float) -> dict:
"""
items: (название, категория M/S/C/W, оценка в человеко-неделях)
Проверяет соблюдение квот DSDM: Must <= 60%, Should ~20%, Could ~20%.
"""
buckets = {"M": 0.0, "S": 0.0, "C": 0.0, "W": 0.0}
for _, cat, effort in items:
buckets[cat] += effort
committed = buckets["M"] + buckets["S"] + buckets["C"]
report = {
"must_share": round(buckets["M"] / capacity, 2),
"buffer_share": round(buckets["C"] / capacity, 2), # Could = сбрасываемый балласт
"overcommitted": committed > capacity,
}
report["healthy"] = report["must_share"] <= 0.6 and report["buffer_share"] >= 0.15
return report
plan = [
("Приём платежей", "M", 4.0),
("Возвраты", "M", 3.0),
("История заказов", "S", 2.0),
("Тёмная тема", "C", 1.0),
("Реферальная программа","W", 0.0),
]
print(check_moscow_budget(plan, capacity=12.0))
# {'must_share': 0.58, 'buffer_share': 0.08, 'overcommitted': False, 'healthy': False}
Отчёт честно говорит: Must в норме, но буфера мало — при первом же срыве оценки резать будет нечего.
7.2. Где MoSCoW ломается
- Инфляция Must. Каждый стейкхолдер считает своё Must-ом. Лечится только тем, что бюджет Must ограничен явно и виден всем: добавить своё Must можно, вынув чужое.
- Нет порядка внутри категории. Двадцать Must-задач не говорят, с чего начать, — MoSCoW надо дополнять WSJF или RICE внутри класса.
- Не работает без фиксированного срока. Метод создан для timeboxed-поставки; в непрерывном потоке (Kanban) он вырождается в набор ярлыков.
8. WSJF и экономика задержки: единственная модель с доказательством
Все методы выше — эвристики. WSJF — нет: у него есть теорема.
8.1. Cost of Delay
Дон Райнертсен («The Principles of Product Development Flow», 2009, reinertsenassociates.com) сформулировал главную мысль: задержка поставки имеет денежную цену, и она обычно доминирует над стоимостью разработки. Cost of Delay (CoD) — сколько мы теряем за единицу времени, пока фича не поставлена: недополученная выручка, отток, штрафы, потерянное окно рынка.
Если у нас есть набор задач с длительностями d_i и стоимостью задержки c_i (у.е. в неделю),
то суммарные потери при порядке σ равны:
L(σ) = Σ_i c_i × C_i, где C_i — момент завершения задачи i при этом порядке
Правило Смита (WSPT, 1956): сортировка по убыванию c_i / d_i минимизирует L(σ)
для одного исполнителя без прерываний
(Smith W.E., Naval Research Logistics Quarterly).
В SAFe та же формула называется WSJF
(scaledagileframework.com/wsjf):
WSJF = Cost of Delay / Job Size
CoD = User-Business Value + Time Criticality + Risk Reduction & Opportunity Enablement
8.2. Почему это оптимально — доказательство перестановкой
Пусть в оптимальном порядке соседние задачи i (идёт первой) и j. Сравним потери двух вариантов
на общем префиксе времени t:
L(i,j) = c_i(t + d_i) + c_j(t + d_i + d_j)
L(j,i) = c_j(t + d_j) + c_i(t + d_j + d_i)
L(i,j) - L(j,i) = c_j·d_i - c_i·d_j
Порядок i, j не хуже, когда c_j·d_i ≤ c_i·d_j, то есть c_i/d_i ≥ c_j/d_j.
Любая пара соседей, нарушающая это условие, улучшается перестановкой; отношение
транзитивно, значит сортировка по убыванию c/d глобально оптимальна. Это ровно тот же
аргумент обмена, что и в жадных доказательствах у CLRS (гл. 16, «Greedy Algorithms»).
from dataclasses import dataclass
@dataclass(frozen=True)
class Job:
name: str
value: float # user-business value
time_criticality: float
risk_reduction: float
size: float # job size / duration
@property
def cod(self) -> float:
return self.value + self.time_criticality + self.risk_reduction
@property
def wsjf(self) -> float:
return self.cod / self.size
def total_delay_cost(order: list[Job]) -> float:
"""Суммарные потери Σ c_i · C_i при заданном порядке. O(n)."""
t, loss = 0.0, 0.0
for job in order:
t += job.size
loss += job.cod * t
return loss
jobs = [
Job("Миграция биллинга", value=8, time_criticality=3, risk_reduction=8, size=13),
Job("Ускорение поиска", value=5, time_criticality=5, risk_reduction=1, size=3),
Job("Отчёт для аудита", value=2, time_criticality=13, risk_reduction=5, size=2),
Job("Онбординг мобильный",value=8, time_criticality=2, risk_reduction=2, size=5),
]
by_wsjf = sorted(jobs, key=lambda j: j.wsjf, reverse=True)
by_value = sorted(jobs, key=lambda j: j.cod, reverse=True)
print(f"WSJF-порядок: потери = {total_delay_cost(by_wsjf):.0f}")
print(f"По ценности: потери = {total_delay_cost(by_value):.0f}")
# WSJF-порядок: потери = 652
# По ценности: потери = 818
Разница в 20% возникает без изменения объёма работ — только за счёт последовательности. Это и есть ответ на вопрос «зачем вообще делить на размер»: не потому, что «дешёвое приятнее», а потому что деление на размер доказуемо минимизирует потери.
8.3. Оговорки, которые в SAFe обычно не проговаривают
- Шкала Фибоначчи для CoD — порядковая, а не интервальная. Складывать три оценки по Фибоначчи и делить — арифметика над рангами. Теорема Смита требует настоящих денег в числителе. Практический вывод: WSJF надёжно отделяет «сильно выше» от «сильно ниже», но разница между 2.6 и 2.9 — шум.
- CoD редко линеен. У релиза под регуляторный дедлайн CoD равен нулю до даты и огромен после (ступенька); у сезонной фичи — пик и обвал. Для нелинейных профилей правило Смита неоптимально, и задачи с жёсткой датой планируют отдельно, «от дедлайна назад».
- Модель однопоточная. Реальная команда ведёт 3–5 задач параллельно; при большом WIP выигрыш от правильного порядка съедается временем ожидания. Ограничение WIP даёт обычно больше, чем идеальная сортировка.
- Size ≠ Duration. Задача на 2 человеко-недели может идти месяц из-за ожидания смежников. В знаменателе должно стоять календарное время до поставки ценности, а не трудозатраты.
9. Когда сортировки недостаточно
9.1. Ограничение бюджета: это задача о рюкзаке
Сортировка по «плотности ценности» оптимальна для порядка, но не для отбора при фиксированном бюджете. Классический контрпример: бюджет 10 недель, задачи A (ценность 60, 6 недель, плотность 10), B (50, 5, плотность 10), C (45, 5, плотность 9). Жадный по плотности берёт A, потом не влезает B, добирает… ничего (осталось 4) → 60. Оптимум B+C = 95.
def knapsack(items: list[tuple[str, float, int]], capacity: int):
"""
items: (название, ценность, целочисленная стоимость в неделях)
Классическая 0/1-задача о рюкзаке, динамика по бюджету.
Время O(n · capacity), память O(capacity) для значения
(+ O(n · capacity) бит для восстановления набора).
"""
n = len(items)
dp = [0.0] * (capacity + 1)
take = [[False] * (capacity + 1) for _ in range(n)]
for idx, (_, value, cost) in enumerate(items):
for cap in range(capacity, cost - 1, -1): # обратный проход = каждый предмет один раз
candidate = dp[cap - cost] + value
if candidate > dp[cap]:
dp[cap] = candidate
take[idx][cap] = True
chosen, cap = [], capacity
for idx in range(n - 1, -1, -1):
if take[idx][cap]:
chosen.append(items[idx][0])
cap -= items[idx][2]
return dp[capacity], list(reversed(chosen))
print(knapsack([("A", 60, 6), ("B", 50, 5), ("C", 45, 5)], capacity=10))
# (95.0, ['B', 'C'])
Разрыв между жадным и оптимальным решением на реальных бэклогах обычно 5–15% — не катастрофа, но и не ноль. Практический вывод не «внедрите DP в планирование», а более скромный: после сортировки по RICE проверьте, не остаётся ли в конце квартала неиспользуемый хвост ёмкости, который лучше закрыть двумя средними задачами вместо одной крупной.
9.2. Зависимости: приоритет плюс граф
Ни один скоринг не знает, что фича B невозможна без миграции A. Правильная модель — топологическая сортировка с приоритетом (жадный выбор среди готовых к запуску узлов):
import heapq
from collections import defaultdict
def prioritized_topo_order(scores: dict[str, float], deps: dict[str, list[str]]) -> list[str]:
"""
deps[x] = список задач, которые должны быть сделаны ДО x.
Среди доступных всегда берём задачу с максимальным score.
Время O((V + E) log V), память O(V + E).
"""
indeg = {task: 0 for task in scores}
children = defaultdict(list)
for task, prereqs in deps.items():
for p in prereqs:
children[p].append(task)
indeg[task] += 1
heap = [(-scores[t], t) for t, d in indeg.items() if d == 0]
heapq.heapify(heap)
order = []
while heap:
_, task = heapq.heappop(heap)
order.append(task)
for child in children[task]:
indeg[child] -= 1
if indeg[child] == 0:
heapq.heappush(heap, (-scores[child], child))
if len(order) != len(scores):
raise ValueError("В графе зависимостей есть цикл")
return order
Важный продуктовый нюанс: зависимость — это тоже решение, а не факт природы. Прежде чем строить длинную цепочку, спросите, нельзя ли её разорвать (заглушка, ручной процесс, feature flag). Каждая устранённая зависимость сокращает время до обратной связи сильнее, чем любая перестановка приоритетов.
10. Как это выглядит в проде
Ни в одной здоровой компании бэклог не является отсортированным списком RICE-скоров. Реальный процесс — многослойный:
идеи, запросы, баги, техдолг"] --> B{Соответствует
стратегии квартала?} B -- нет --> W["Won't have
с датой пересмотра"] B -- да --> C{Достаточно ли
данных для оценки?} C -- нет --> D["Discovery:
интервью, аналитика,
прототип"] D --> C C -- да --> E["Оценка Reach / Impact /
Confidence / Effort"] E --> F["RICE-скор
+ Монте-Карло на топ-k"] F --> G{Тип по Kano} G -- "must-be с worse < -0.7" --> H["Must have:
в план вне очереди"] G -- "indifferent" --> W G -- "performance / attractive" --> I["Раскладка по квотам:
60% рост / 20% техдолг /
20% must-be и поддержка"] H --> I I --> J["Порядок внутри квартала
по WSJF + зависимости"] J --> K["Обязательства команды
на итерацию"] K --> L["Ретро приоритизации:
сверка прогноза Impact
с фактом"] L -.калибровка оценок.-> E
Ключевые элементы, которых нет ни в одном учебнике по фреймворкам:
Квоты бюджета важнее скоров. Практически все зрелые команды сначала делят ёмкость на корзины («новая ценность / техдолг и надёжность / поддержка и обязательства»), а скоринг применяют внутри корзины. Иначе техдолг и надёжность не выигрывают никогда: их ценность отложена и плохо измеряется, а любой скоринг систематически смещён в сторону измеримого краткосрочного эффекта.
Замкнутая петля калибровки. Через квартал сверяют прогнозный Impact с фактическим (из A/B-теста, см. https://courses.digitable.life/post/product-management/05-mvp-and-experiments/). Команды, которые этого не делают, через год имеют бэклог с абсолютно недостоверными числами. Команды, которые делают, через год умеют оценивать — потому что получают обратную связь.
Защита от игр в оценки. Как только скор влияет на решение, участники начинают им управлять: Reach раздувается, Effort занижается. Это закон Гудхарта в чистом виде. Противоядия: Effort оценивают инженеры (а не автор идеи), Reach считается запросом к данным, Confidence подтверждается ссылкой на артефакт (запись интервью, дашборд, отчёт эксперимента).
Социальные методы для стейкхолдеров. Когда спор политический, а не аналитический, помогает Buy a Feature Люка Хоманна: стейкхолдерам выдают ограниченный бюджет «денег» и предлагают купить фичи, часть из которых дороже индивидуального бюджета (Innovation Games). Дефицит делает компромиссы явными за 40 минут — быстрее, чем любая таблица.
Жизненный цикл элемента бэклога. Полезно явно моделировать состояния, потому что «лежит в бэклоге» — не одно состояние, а пять:
Состояние «Заморожена» — тот самый механизм, который не даёт бэклогу расти бесконечно. Без правила автоматического устаревания бэклог превращается в свалку, где стоимость поиска нужного превышает стоимость его повторного придумывания.
11. Сравнение методов
| Метод | Что оптимизирует | Вход | Сильная сторона | Где ломается |
|---|---|---|---|---|
| ICE | грубый порядок | 3 оценки 1..10 | скорость, десятки гипотез | нет охвата, высокий шум |
| RICE | ценность / усилие | охват из данных | сравнение разнородного, явная неопределённость | занижает стратегические ставки и малые ценные сегменты |
| Value/Effort | коммуникация | 2 оценки | понятен всем за 10 секунд | теряет уверенность и риск |
| Kano | тип ценности | опрос 20–30 чел. | ловит нелинейность и «безразличные» фичи | дорог, меряет декларации |
| MoSCoW | выполнение обязательств к сроку | классы + бюджет | буфер против срыва оценок | инфляция Must, нет порядка внутри класса |
| WSJF | суммарные потери от задержки | CoD и длительность | доказуемая оптимальность | требует денежного CoD и линейности |
| Knapsack | ценность при жёстком бюджете | ценность + стоимость | точный отбор | ценность редко аддитивна |
Практическая комбинация, которая работает: Kano отсеивает indifferent → квоты бюджета делят ёмкость → RICE ранжирует внутри корзины «рост» → MoSCoW фиксирует обязательства итерации → WSJF задаёт порядок внутри итерации. Один метод на всё не натягивается.
12. Типичные ошибки
- Скоринг вместо стратегии. RICE — это способ сравнить варианты внутри выбранного направления. Он никогда не подскажет, на какой рынок идти. Если направления нет, скоринг просто аккуратно оптимизирует движение в никуда — см. https://courses.digitable.life/post/product-management/04-strategy-and-roadmap/.
- Ложная точность. Скор 43.7 против 41.2 при оценках «плюс-минус вдвое» — это шум, выданный за решение. Округляйте до порядка величины, группируйте в 3 корзины.
- Разные единицы Reach в одной таблице. «Пользователей в месяц» рядом с «событий в год» делает всю сортировку бессмысленной. Проверяйте единицы первым делом.
- Effort от автора идеи. Систематическое занижение вдвое. Оценивает команда.
- Confidence как ручка подгонки. Требуйте ссылку на артефакт под каждое значение.
- Отсутствие корзины на техдолг и надёжность. Любая value/effort модель их проигрывает, потому что их ценность — предотвращённый ущерб, а он не наблюдаем. Только квота.
- Игнорирование стоимости владения. В Effort входит только разработка, но у каждой фичи есть постоянная стоимость: поддержка, тесты, документация, рост сложности UI. Фича с Effort 1 месяц и вечным хвостом поддержки часто хуже, чем Effort 2 без него. Здесь же — вопрос о удалении фич: это тоже приоритизация.
- Никогда не смотрят назад. Без сверки прогноза с фактом числа не улучшаются никогда.
- Приоритизация решений вместо возможностей. Тереза Торрес показывает, что сравнивать надо сначала возможности (проблемы пользователей), и только внутри выбранной возможности — решения (Opportunity Solution Tree). Сравнение «чат-бот против экспорта в Excel» бессмысленно: это решения разных проблем.
- Оценка вместо аппетита. В подходе Shape Up от Basecamp вопрос ставится наоборот: не «сколько это займёт», а «сколько мы готовы на это потратить», и решение подгоняется под бюджет (Shape Up). Это радикально снижает инфляцию оценок.
13. Мини-итог
- Приоритизация решает три разные задачи — отбор, порядок и отсев; смешивать их нельзя.
- Общая форма любой скоринговой модели —
E[ценность] / стоимость; различия только в том, как оценивается числитель и знаменатель. - ICE — для быстрых однородных гипотез, RICE — рабочий стандарт для разнородного бэклога; его главный вклад — явный охват из данных и явная уверенность.
- Оценки — распределения, а не числа: Монте-Карло по топ-k отделяет робастные решения от случайных и обосновывает инвестиции в снижение неопределённости.
- Kano напоминает, что ценность нелинейна: must-be насыщается, attractive не измеряется заранее, indifferent надо находить и выбрасывать.
- MoSCoW — про обязательства и бюджетный буфер, а не про важность; квота Must ≤ 60% и 20% сбрасываемого Could — суть метода.
- WSJF — единственная модель с доказательством (правило Смита, аргумент обмена); но требует денежного и линейного CoD и рушится при высоком WIP.
- Скоринг всегда смещён против отложенной ценности — техдолг, надёжность и стратегические ставки защищаются квотами бюджета, а не скорами.
- Числа становятся достоверными только там, где есть замкнутая петля: прогноз → релиз → измерение → калибровка.
Источники
- Sean McBride. RICE: Simple prioritization for product managers — Intercom.
- Donald Reinertsen. The Principles of Product Development Flow, 2009 — reinertsenassociates.com.
- W. E. Smith. Various optimizers for single-stage production, Naval Research Logistics Quarterly, 1956.
- SAFe. Weighted Shortest Job First.
- Daniel Zacarias. The Complete Guide to the Kano Model.
- Agile Business Consortium (DSDM), MoSCoW Prioritisation — agilebusiness.org.
- Teresa Torres. Opportunity Solution Trees.
- Itamar Gilad. The Confidence Meter.
- Ryan Singer. Shape Up — Basecamp.
- Kohavi, Tang, Xu. Trustworthy Online Controlled Experiments, 2020.
- Cormen, Leiserson, Rivest, Stein. Introduction to Algorithms, гл. 16 (жадные алгоритмы) и 15 (динамическое программирование).
Что дальше
Приоритизация отвечает на вопрос «что раньше» внутри уже выбранного направления. Откуда берётся само направление, как оно превращается в план на кварталы и почему роадмап из списка фич — плохая идея, разбираем в следующей статье: