Variance analysis в инженерии: 5 причин, почему план уехал от факта
Открываете отчёт Plan-vs-Actual за Q3. План по инженерке — $1.8M. Факт — $2.34M. Расхождение — +30%. CFO хочет объяснение к пятнице.
Учебник советует: «разбирайтесь с любой строкой, где |факт − план| > 10%». На этом большинство разборов и заканчивается — и здесь же ломаются. У 30%-ного гэпа в инженерном бюджете минимум 5 разных причин. У каждой — своя сигнатура в данных. Если не декомпозировать variance, рискуете уволить PM-а, когда настоящий виновник — внеплановый раунд повышений зарплат в августе.
Variance analysis по CIMA разбирает расхождение как дерево: rate × volume × mix. С инженерным бюджетом сложнее — труд не однородный товар. Ниже версия, которая работает на реальных деньгах разработческих команд.
{/* truncate */}
Реальная разбивка: scope creep — крупнейшая статья, K seasonality — самая незаметная.
Хватит смотреть на разрыв. Начните называть причину.
Deloitte CFO Insights регулярно отмечают инженерку как категорию с худшей точностью прогноза. Причины две: cost-of-labor сезонит, и скоуп подвижен. 30% мимо по аренде — ошибка биллинга. 30% мимо по разработке — норма, если только вы не можете расписать каждый процент по причинам.
По ~40 mid-size B2B-командам, которые мы наблюдаем через модуль Plan-vs-Actual в PanDev Metrics, квартальное расхождение > 15% всегда раскладывается на пять повторяющихся причин. Частоты ниже — это как часто причина была главным контрибьютором перерасхода квартала.
Таблица 1 — 5 причин variance в инженерном бюджете
| # | Причина | Сигнатура в данных | Где смотреть в PanDev | Частота как #1 | Средняя доля |
|---|---|---|---|---|---|
| 1 | Ретро-пересчёт ставок | Часовая ставка прошлых месяцев меняется задним числом | Audit log loaded rate; история OverheadCoefficientFullRecalcCronJob | 8% | 10–15% |
| 2 | Сезонный скачок K | Та же FTE и часы, но cost-per-hour лезет вверх в отпускные месяцы | График OverheadCoefficient per-month | 28% | 20–30% |
| 3 | Scope creep | Новые issue_keys появляются после старта спринта; задача удвоилась | Дельта issue-key в mv_activity_total_user_issue_daily | 35% | 30–45% |
| 4 | Capacity overrun (FTE > 90%) | Инженеры стабильно в красной зоне; следом — найм | Красные счётчики в employee-utilization-widget | 16% | 15–25% |
| 5 | Внеплановые отсутствия | Больничные / экстренные отпуска закрыты подрядчиками по более высокой ставке | Override-ы в CustomEmployeeWorkingTime | 13% | 5–10% |
Два вывода. Scope creep лидирует по частоте — что большинство CFO и так подозревают. Сюрприз — что K spike самая незаметная причина: её не видно в headcount-отчёте, потому что никого не нанимали и никто не уволился, а cost-per-hour тихонько вырос с 0.39 до 0.46 между июнем и августом.
Причина #1 — Ретро-пересчёт ставок (8%, сигнатура: ставки задним числом)
В августе подписан review зарплат с эффективной датой 1 июня. Внезапно loaded hourly rate за июнь и июль другие, чем были на прошлой неделе. Отчёты, запущенные в июле, выглядели аккуратно. Отчёты в сентябре показывают $80K дыру в Q2.
Это сценарий запуска нашего OverheadCoefficientFullRecalcCronJob — когда историческая ставка меняется, все зависимые финансовые метрики пересчитываются с нуля. Сигнатура однозначная: один и тот же запрос за один и тот же период даёт разные числа в двух прогонах. Если в variance-отчёте «двигается» прошлое — это ваша причина.
Лечение не аналитическое, а операционное: закрывать прошедшие периоды после month-end close, выделять ретро-повышения отдельной строкой и прямо называть пересчёт в комментарии к variance. Не прятать в шуме.
Подробнее о том, как это распространяется по cost stack, — в нашей статье retroactive rate change handling (следующая в серии).
Причина #2 — Сезонный скачок K (28%, сигнатура: cost вверх, часы на месте)
Overhead coefficient K — отношение оплачиваемого времени к закодированному — не константа. Лето его убивает. Декабрьские праздники в части рынков — тоже. Наурыз в СНГ — тоже. Математика разобрана в статье per-month K seasonality, и сигнатура у этой причины самая чистая: та же FTE, тот же объём задач, тот же темп поставки — но cost-per-feature растёт на 15–20%.
В разборе ниже K вырос с 0.39 в Q2 до 0.46 в Q3. Те же инженеры. Те же проекты. Зарплатный фонд почти не сдвинулся. Но каждый закодированный час теперь несёт на 18% больше overhead. На плане в $1.8M это уже примерно $130K — четверть всего variance из-за одного коэффициента.
Большинство планировочных инструментов не моделируют K помесячно. Excel-шаблоны используют годовое среднее — поэтому лето стабильно ломает бюджет, и никто не понимает, почему.
Причина #3 — Scope creep (35%, сигнатура: issue keys приходят с опозданием)
Очевидная причина, но обычно её меряют неправильно. Сравнивают plan story points vs. delivered story points. Правильное сравнение — запланированные issue keys vs. issue keys, которые когда-либо касались спринта. Наша materialised view mv_activity_total_user_issue_daily хранит ежедневный набор «трогавшихся» ключей — можно реплеить, какие задачи появились на 3-й день спринта, который скоупили на 0-й.
Два паттерна scope creep:
- Bolt-on: новая фича, добавленная после kickoff. Видимая. Защитимая.
- Stealth growth: уже запланированная фича, у которой acceptance criteria разрослись по ходу спринта. Невидимая ни в одном трекере без версионирования требований.
В наших данных stealth growth добавляет около 220 dev-hours на типичный план «1 квартал, 2 фичи». При loaded rate $100/час это $22K на фичу, $44K за квартал. В worked example ниже две фичи поглотили 220 незапланированных dev-hours, что добавило $220K к расхождению.
DORA State of DevOps пишет, что элитные команды быстрее не на задаче — у них меньше variance в cycle time. Та же логика и в бюджете: важна стабильность, а не пиковая скорость.
Причина #4 — FTE > 90% (16%, сигнатура: всплеск красной зоны)
Когда инженеры квартал сидят с утилизацией > 90%, через 90 дней происходят две вещи: люди уходят, и менеджеры начинают паниковать с наймом. Оба эффекта попадают в variance двумя кварталами позже, не сейчас. Лидирующий индикатор — счётчик красной зоны в employee-utilization-widget: инженеры выше 90% на протяжении 4+ недель подряд.
В наших данных на каждого инженера в красной зоне 6+ недель приходится 38% вероятности backfill или net-new hire в течение 90 дней. Один внеплановый найм на 40-персонной команде — это 1–2% годового бюджета. 4 человека в красной зоне → 2 найма → ~$90K непланового кост в Q3.
Честная оговорка: это лидирующий индикатор, а не объяснение текущего квартала. Когда он попадает в variance, ущерб уже состоялся. Виджет — еженедельно. Variance-отчёт — ежеквартально.
Причина #5 — Внеплановые отсутствия (13%, сигнатура: кластер working-time override-ов)
Один сломал ногу. Двое ушли в декрет на одной неделе. Команда закрывает дыру подрядчиком за $150/час против планового loaded rate $90/час. Каждый override в CustomEmployeeWorkingTime оставляет след — если кластеризовать их по месяцу, leave gaps выглядят совсем не как плановый отпуск.
Самая мелкая из пяти причин, но её стоит выделять, потому что её часто записывают в scope creep. Различающий признак: scope creep растит набор issue-keys, leave gap растит долю контракторских часов.
Worked example — декомпозиция 30% variance в Q3
Команда 40 человек, план Q3 $1.8M, факт $2.34M. Variance: $540K (30%). Атрибуция:
Таблица 2 — Атрибуция variance в примерном квартале
| Причина | Механика | Variance ($) | Доля от $540K | Сигнал детекции |
|---|---|---|---|---|
| Scope creep | 2 фичи поглотили 220 незапланированных dev-hours | $220,000 | 41% | Issue-key дельта в mv_activity_total_user_issue_daily |
| K seasonality | K вырос 0.39 → 0.46 (июл–сен) | $130,000 | 24% | Per-month K chart: скачок > 10% MoM |
| FTE > 90% → найм | 4 инженера в красной зоне → 2 найма | $90,000 | 17% | Виджет утилизации: счётчик красной зоны |
| Ретро-ставка | Августовские повышения с 1 июня | $80,000 | 15% | Дельта в audit log loaded rate |
| Внеплановые отсутствия | 1 контрактор подменил $150/час | $20,000 | 4% | Override-ы в CustomEmployeeWorkingTime |
| Итого | $540,000 | 100% |
Этот разбор меняет три вещи:
- Разговор с CFO перестаёт быть «инженерка перетратила» и становится «scope creep — 41%, K seasonality — 24%». Разные проблемы, разные владельцы, разные решения.
- Самая незаметная строка — $130K из-за K — никогда бы не всплыла в headcount-обзоре. Те же люди, те же часы, больше денег.
- Инстинктивное желание CFO «сократить штат» лечит причину #4 (15% variance), оставляя нетронутой более крупную причину #3 (41%).
Endpoint POST /departments/{id}/finance/projects в PanDev возвращает percentageChange по проектам — это первый срез: отсортированный список, куда смотреть в первую очередь. Декомпозиция выше — то, что вы делаете после того, как выбрали самый «двигающийся» проект.
Как вшить variance-алерты в месячный цикл
Variance analysis — post-hoc анализ. Правильное время — раз в месяц, а не раз в квартал. К моменту, когда Q3-обзор выпадает в начале октября, данные уже устарели на 90 дней, а люди, которые могли вмешаться в июле, занимаются другими пожарами. Настройте email-алерты variance раз в месяц, чтобы проблемы всплывали внутри квартала, а не после.
Чек-лист на закрытие месяца:
- Закрыть ставки прошлого месяца. Ретро-правки только с пометкой в комментарии.
- Посчитать K за закрытый месяц; сравнить с trailing 12-month средним. Флаг при отклонении > 5%.
- Снять issue-key дельту по активным спринтам. Флаг, если спринт «распух» по issue-keys больше чем на 15% после kickoff.
- Посчитать инженеров в красной зоне (> 90%) за месяц. Флаг при росте.
- Список всех override-ов в
CustomEmployeeWorkingTimeза месяц. Сумма часов по контракторской ставке.
Весь цикл занимает меньше часа, если он инструментирован. По сравнению с квартальным post-mortem это разница между управлением и отчётностью.
Когда этот фреймворк не подходит
- Меньше 10 инженеров: шум по каждой причине шире самой variance. Считайте суммарно, не по причинам.
- Чисто проектный шоп / агентская модель: client billing и есть variance — scope creep становится событием выручки, а не затрат. Другая математика.
- Тяжёлая contractor-mix (> 40%): причины #4 и #5 доминируют по построению; таблица перевешивается в сторону leave / overrun.
- Первый квартал на новом трекере: ещё нет истории issue-keys, чтобы чисто детектить scope creep. Подождите полный квартал данных, прежде чем доверять атрибуции.
По теме
- Bottom-up конструирование инженерного бюджета — input-сторона: как построить план, с которым потом сравнивать факт.
- Per-Month K: сезонность и реальная стоимость инженерки — математика причины #2.
- Capacity planning в инженерке: математика, а не вайбы — математика причины #4.
30% variance — это информация, а не провал. Провал — это закрыть квартал, не назвав, какая из пяти причин потратила ваши деньги.
