Моделирование и его пределы: что модель не покажет
Предыдущая глава научила рисовать диаграммы причинных связей. Диаграмма — дешёвый и честный инструмент: она фиксирует гипотезу о структуре и делает её обсуждаемой. Но у неё есть свойство, о котором в системной литературе говорят реже, чем следовало бы: диаграмма почти ничего не предсказывает.
Возьмите любую схему с двумя петлями — усиливающей и уравновешивающей. Что произойдёт с системой? Ответ зависит от того, какая петля сильнее и когда именно она включается, а этого на схеме нет. Стрелки со знаками задают только направления влияний. Поведение — рост, колебание, выход на плато, перелёт и обвал — определяется числами: коэффициентами, начальными уровнями запасов, длиной задержек. Схема из двадцати узлов совместима с десятками качественно разных траекторий, и вы не отличите их, пока не посчитаете.
Отсюда практический разлом темы. Слева — «системное мышление» как способ красиво нарисовать проблему на доске: такой артефакт нельзя опровергнуть и, значит, нельзя на нём ничего построить. Справа — модель как исполняемый объект с уравнениями, единицами измерения, параметрами, прогоном и набором тестов, который она может провалить. Глава про переход от левого к правому и про то, где правое всё равно упирается в стену.
Модель полезна не когда она правдоподобна, а когда она делает предсказание, которое можно проверить дешевле, чем стоит ошибка. Всё остальное — способ структурировать разговор; это тоже ценно, но это другая ценность и другие обещания.
Четыре разных вопроса, которые называют одним словом «модель»
Прежде чем строить, ответьте: на какой вопрос модель должна ответить. Требования к проверке у этих вопросов разные, и большинство споров о моделях — спор людей, решающих разные задачи.
| Назначение | Формулировка результата | Что нужно, чтобы поверить | Реалистичность в инженерии |
|---|---|---|---|
| Объяснить прошлое | «Очередь выросла не от нагрузки, а от падения ёмкости» | Воспроизведение известной истории плюс отсутствие более простого объяснения | Высокая: данные есть |
| Сравнить варианты (ранг) | «Увеличить пул соединений полезнее, чем добавить реплику» | Устойчивость ранга к разумному разбросу параметров | Высокая: ранг устойчивее чисел |
| Предсказать величину | «Через 6 недель очередь дойдёт до 40 тысяч» | Проверка вне обучающей выборки, интервал, история попаданий | Низкая для организационных систем, средняя для технических |
| Договориться о языке | «Мы согласны, что дежурство — это запас, а не поток» | Ничего: это соглашение, а не утверждение о мире | Всегда достижима, но не даёт предсказаний |
Разница между второй и третьей строкой — центральная. Ранг вариантов часто устойчив к тому, что параметры известны с точностью «плюс-минус два раза», а конкретное число — почти никогда. Ниже мы это измерим, а не постулируем. И правило, экономящее кварталы: сначала запишите назначение, потом стройте. Модель для ранжирования не превращается в модель прогноза от того, что кто-то попросил «числа на квартал».
Лестница строгости: от разговора к эксперименту
Моделирование — не «построить симуляцию». Это лестница, где каждая ступень дороже предыдущей и отвечает на более узкий вопрос. Ошибка номер один — прыгать сразу на четвёртую ступень, минуя третью, которая часто уже закрывает вопрос.
«мне кажется, дело в ретраях»"] --> L2["2. Причинная диаграмма
узлы, знаки, петли"] --> L3["3. Оценка на салфетке
закон Литтла, Амдал, алгебра"] L3 --> L4["4. Динамическая модель
запасы, потоки, задержки, прогон"] --> L5["5. Дискретно-событийная
очереди с распределениями, хвосты"] --> L7["7. Эксперимент на системе
нагрузочный тест, chaos, A/B"] L3 -.->|"часто здесь можно остановиться"| L7 L6["6. Статистическая модель на данных
регрессия, временные ряды"] -.->|"калибровка и проверка"| L4 L7 -->|"опровержение возвращает наверх"| L2
| Ступень | Отвечает на вопрос | Не отвечает |
|---|---|---|
| Диаграмма | «какие петли вообще есть» | «какая победит и когда» |
| Оценка на салфетке | «порядок величины, есть ли запас» | «динамика во времени» |
| Динамическая модель | «форма траектории, эффект политики» | «точные числа, хвосты» |
| Дискретно-событийная | «p99, переполнение буферов, редкие сочетания» | «поведение людей» |
| Статистическая на данных | «что коррелирует и как сильно» | «что будет при вмешательстве» |
| Эксперимент | «что реально произойдёт» | «почему» |
Третья ступень недооценена катастрофически: закон Литтла $L = \lambda W$ и формула отклика очереди $1/(1-\rho)$ решают, по опыту, больше половины споров о ёмкости — без единой строчки кода (запасы и потоки, нагрузочное тестирование). А седьмая ступень — не «модель», а её конкурент: если эксперимент дешевле модели и достаточно безопасен, делайте эксперимент. Модель нужна там, где эксперимент невозможен (нельзя месяц не нанимать ради проверки гипотезы о найме), необратим (нельзя «попробовать» переписать сервис) или слишком медленный (эффект техдолга виден за год, а решать надо в понедельник).
Кейс: техдолг и скорость поставки
Возьмём спор, идущий в каждой команде: сколько времени отдавать на уборку кода вместо фич. Продакт говорит «сейчас важнее фичи», техлид — «через полгода мы встанем». Оба правы внутри своего горизонта, и на словах спор нерешаем.
(политика, 0…1)"] -->|"+"| DEL["Темп поставки фич
фич-единиц в неделю"] F -->|"−"| CLEAN["Темп уборки
долг-единиц в неделю"] DEL -->|"+"| DEBT["ЗАПАС: технический долг
долг-единиц"] CLEAN -->|"−"| DEBT DEBT -->|"− R1: долг съедает мощность"| EFF["Эффективность разработки
множитель 0…1"] EFF -->|"+"| DEL DEBT -->|"+"| INC["Доля мощности
на тушение инцидентов"] INC -->|"−"| CAP["Доступная мощность
человеко-недель в неделю"] CAP -->|"+"| DEL CAP -->|"+ B1: уборка снимает долг"| CLEAN
Две петли: усиливающая (долг → меньше мощности → меньше уборки → больше долга) и уравновешивающая (уборка снимает долг). Что победит — из схемы не видно. Это и есть момент, где диаграмма исчерпана.
Уравнения и размерности
Первый обязательный шаг — единицы измерения. Каждая стрелка обязана стать уравнением, где размерности сходятся; половина неработающих моделей ломается здесь.
мощность [чел·нед/нед] = разработчики [чел] × продуктивность [нед/нед] × (1 − доля_тушения)
темп_поставки [фич/нед] = мощность × доля_фич × эффективность(долг)
темп_уборки [долг/нед] = мощность × (1 − доля_фич) × скорость_уборки [долг/(чел·нед)]
долг(t+dt) [долг] = долг(t) + (долг_на_фичу [долг/фич] × темп_поставки − темп_уборки) × dt
Проверка: долг/фич × фич/нед × нед = долг — сходится. Если бы не сошлось, дальше идти нельзя: это не «мелкая неточность», а признак того, что в голове склеены две разные величины. Две нелинейности взяты не из воздуха, но и не из данных — это явные допущения, и помечать их надо именно так: эффективность(долг) = 1 / (1 + долг / долг_ref) — насыщающееся трение, первые единицы долга почти не мешают, дальше отдача падает гиперболически; доля_тушения = min(0.8, k × долг) — линейный рост аварийной работы с физическим потолком, потому что команда не может тушить больше, чем у неё есть времени.
Код
"""Модель «техдолг — скорость поставки»: один запас, два потока, две нелинейности."""
from dataclasses import dataclass
@dataclass
class Params:
devs: float = 8.0 # человек
prod: float = 1.0 # чел·нед на человека в неделю
to_features: float = 0.8 # доля времени на фичи — единственная политика
debt_per_feature: float = 0.6 # долг-единиц на фич-единицу
cleanup_rate: float = 1.2 # долг-единиц, снимаемых человеко-неделей уборки
debt_ref: float = 40.0 # долг, при котором эффективность падает вдвое
incident_k: float = 0.004 # доля мощности на тушение, на долг-единицу
max_firefight: float = 0.8 # физический потолок аварийной работы
def step(debt: float, p: Params, dt: float) -> tuple[float, float]:
"""Один шаг интегрирования: возвращает (новый долг, выпущено фич за шаг)."""
firefight = min(p.max_firefight, p.incident_k * debt)
capacity = p.devs * p.prod * (1.0 - firefight)
efficiency = 1.0 / (1.0 + debt / p.debt_ref)
delivered = capacity * p.to_features * efficiency * dt
cleanup = capacity * (1.0 - p.to_features) * p.cleanup_rate * dt
return max(0.0, debt + p.debt_per_feature * delivered - cleanup), delivered
def run(weeks: int, p: Params, debt0: float = 20.0, dt: float = 0.25):
"""История (неделя, долг, накопленный выпуск). O(weeks/dt) времени и памяти."""
debt, shipped, hist = debt0, 0.0, []
for i in range(int(weeks / dt)):
debt, d = step(debt, p, dt)
shipped += d
hist.append(((i + 1) * dt, debt, shipped))
return hist
Прогон по пяти политикам:
доля на фичи | долг через 52 нед | выпущено за 52 нед | выпущено за 12 нед
1.0 | 95.7 | 126.1 | 45.2
0.9 | 65.7 | 143.6 | 44.7
0.8 | 34.5 | 171.2 | 44.3
0.7 | 7.5 | 217.3 | 44.0
0.6 | 0.0 | 235.7 | 44.1
Результат стоит прочитать медленно. На горизонте 12 недель все политики неразличимы — 44–45 фич-единиц, разброс меньше 3 %. На горизонте 52 недель «всё на фичи» проигрывает «70 % на фичи» почти вдвое. Спор продакта и техлида — не спор о фактах, а спор о горизонте, и модель показывает это численно; заодно понятно, почему спор не решается опытом: за квартал разницы действительно не видно. Стоимость прогона — O(T/dt) по времени и O(1) по памяти без хранения истории, для n связанных запасов O(n · T/dt). Вычисления здесь никогда не проблема; проблема — доверие к результату.
Тесты модели: как её ломать, пока это дёшево
Модель, на которую сослались в решении, а до этого не пытались сломать, — источник ложной уверенности хуже, чем отсутствие модели. Набор проверок сформулирован давно: Форрестер и Сенге (1980), глава 21 у Стермана (2000). Ниже — применимые к инженерным моделям, от дешёвых к дорогим.
Экстремальные условия
Тест размерностей — самый дешёвый и самый пропускаемый; особенно опасны «коэффициенты подгонки» без размерности: если в модели есть константа, для которой вы не можете назвать единицы, вы не понимаете, что она делает. Следующий по стоимости — тест экстремальных условий.
Главный тест на структурную осмысленность и лучший по соотношению «польза / усилие». При экстремальных значениях входов модель обязана вести себя так, как повела бы себя реальная система: ноль людей — ничего не производится; вся мощность на уборку — долг падает и упирается в ноль, а не уходит в минус; огромный долг — поставка стремится к нулю, но не становится отрицательной.
"""Тесты модели, которые обязаны падать, если структура неверна."""
def test_extremes():
h = run(52, Params(to_features=0.0), debt0=100.0) # вся мощность на уборку
assert h[-1][1] == 0.0 and min(r[1] for r in h) >= 0.0 # долг падает и не пробивает ноль
assert h[-1][2] == 0.0 # фич при этом не выпускается
h = run(52, Params(devs=0.0), debt0=50.0) # нет людей — не происходит ничего
assert h[-1][1] == 50.0 and h[-1][2] == 0.0
h = run(52, Params(to_features=1.0), debt0=10_000.0) # гигантский долг
assert 0.0 < h[-1][2] < 1.0 # поставка → 0, но не отрицательна
for f in (i / 20 for i in range(21)): # ни при какой политике
assert min(r[1] for r in run(52, Params(to_features=f), debt0=5.0)) >= 0.0
def test_integration_error():
"""Результат не должен заметно зависеть от шага интегрирования."""
ref = run(52, Params(), dt=0.0625)[-1][2]
for dt in (1.0, 0.5, 0.25):
assert abs(run(52, Params(), dt=dt)[-1][2] - ref) / ref < 0.005
Эти тесты ловят реальные дефекты. Уберите из step два ограничителя — потолок min(0.8, ...) на долю тушения и пол max(0.0, ...) на запас — и модель в нормальном режиме останется идентичной (долг 34.5, выпуск 171.2 — до последнего знака), а в тестах покажет вот что:
| Условие | Модель с ограничителями | Модель без них |
|---|---|---|
to_features=0, старт с долга 100 |
долг → 0.0 | долг → −844.3 |
| старт с долга 400, всё на фичи | выпущено 0.3 фич-единицы | выпущено −22.0 фич-единицы |
Отрицательный технический долг и отрицательный выпуск фич — не «неточность», а доказательство того, что структура неверна за пределами узкого коридора, в котором её случайно проверяли. Главное здесь: сверка с историей этого не показала бы вообще, потому что в историческом коридоре обе версии совпадают.
Ошибка интегрирования
Численный артефакт, который легко принять за поведение системы. Проверка: уменьшите шаг вдвое и посмотрите, изменился ли ответ.
Шаг dt, недели |
1.0 | 0.5 | 0.25 | 0.125 | 0.0625 |
|---|---|---|---|---|---|
| Долг через 52 нед | 34.56 | 34.51 | 34.48 | 34.47 | 34.47 |
| Выпущено за 52 нед | 171.35 | 171.26 | 171.21 | 171.18 | 171.17 |
Расхождение между самым грубым и самым точным шагом — 0.1 %, модель численно устойчива. Если бы ответ менялся в разы, «поведение» было бы свойством метода Эйлера, а не системы (численные методы).
Чувствительность — и почему её три вида
Стерман различает три уровня, и путаница между ними порождает большинство неверных выводов: числовая — насколько меняются значения (меняются почти всегда и сильно); поведенческая — меняется ли форма траектории (был плавный выход на плато, стало колебание); политическая — переворачивается ли ранг решений. Нужен обычно только третий. Разыграем 500 наборов параметров, где каждый из четырёх неизвестных коэффициентов взят из диапазона «плюс-минус два-три раза» от базового, и посмотрим, какая политика побеждает.
import random
def policy_sensitivity(weeks: int, trials: int = 500) -> dict:
"""Как часто каждая политика оказывается лучшей при разбросе параметров."""
random.seed(1)
wins: dict[float, int] = {}
for _ in range(trials):
draw = dict(debt_per_feature=random.uniform(0.3, 1.0), # все четыре коэффициента
cleanup_rate=random.uniform(0.6, 2.0), # известны с точностью
debt_ref=random.uniform(20.0, 80.0), # «плюс-минус два-три раза»
incident_k=random.uniform(0.001, 0.008))
shipped = {f: run(weeks, Params(to_features=f, **draw))[-1][2]
for f in (1.0, 0.9, 0.8, 0.7, 0.6, 0.5)}
best = max(shipped, key=shipped.get)
wins[best] = wins.get(best, 0) + 1
return wins
горизонт 52 недели: горизонт 12 недель:
f = 1.0: 1 раз ( 0 %) f = 1.0: 282 раза (56 %)
f = 0.8: 33 раза ( 7 %) f = 0.8: 3 раза ( 1 %)
f = 0.7: 175 раз (35 %) f = 0.7: 78 раз (16 %)
f = 0.6: 207 раз (41 %) f = 0.6: 118 раз (24 %)
f = 0.5: 84 раза (17 %) f = 0.5: 19 раз ( 4 %)
Читаем честно:
- Что модель показывает. На годовом горизонте «сто процентов времени на фичи» оказывается лучшей ровно в одном случае из пятисот. Вывод устойчив к неопределённости параметров в два-три раза — значит, он про структуру, а не про подгонку. Это сильное утверждение, его можно нести в разговор.
- Что модель не показывает. Что оптимум равен 0.65. Оптимальная доля скачет от 0.5 до 0.8 в зависимости от параметров, которых мы не знаем. Любое конкретное число здесь — иллюзия точности.
- Что модель показывает неожиданно. На горизонте 12 недель ранг переворачивается: «всё на фичи» выигрывает в 56 % розыгрышей. Ответ на вопрос «убирать ли долг» буквально зависит от того, где проведена граница по времени (границы системы).
Формулировка, которая переживёт проверку: «при горизонте от года структура даёт устойчивый ответ — доля на уборку строго больше нуля; конкретная величина модели неизвестна и подбирается эмпирически».
Калибровка и почему совпадение с историей почти ничего не значит
Дальше — самая опасная часть. Модель подогнали под историю, графики легли один в другой, все довольны. Проблема в том, что хорошая подгонка — необходимое, но крайне слабое условие.
Эквифинальность: разные структуры, одна история
Эквифинальность — разные структуры дают одно и то же наблюдаемое поведение; в статистике то же явление называют проблемой идентифицируемости. Практический смысл: данных истории может физически не хватать, чтобы отличить две модели, дающие противоположные советы.
20 недель наблюдений,
R² 0.94 и 0.93"] H --> Q{"Заморозить половину
продуктовой работы?"} Q -->|"ответ по A"| QA["инциденты упадут до нуля"] Q -->|"ответ по B"| QB["инциденты продолжат расти"]
Проверим численно. Сгенерируем «правду», которой наблюдатель не знает: инциденты порождаются и долгом, и трафиком. Затем подгоним две однопричинные структуры и сравним их прогнозы на вмешательство.
"""Эквифинальность: две несовместимые структуры одинаково хорошо описывают одну историю."""
import random
WEEKS, HORIZON = 20, 12
def traffic_path(weeks: int, growth: float = 0.03) -> list[float]:
"""Наблюдаемый внешний драйвер: трафик растёт на 3 % в неделю."""
return [(1 + growth) ** (t + 1) for t in range(weeks)]
def ols(x: list[float], y: list[float]) -> tuple[float, float]:
"""Подгонка y ≈ a + b·x. O(n) времени, O(1) памяти."""
n = len(x)
mx, my = sum(x) / n, sum(y) / n
b = sum((xi - mx) * (yi - my) for xi, yi in zip(x, y)) / sum((xi - mx) ** 2 for xi in x)
return my - b * mx, b
random.seed(7) # числа ниже воспроизводятся точно
debt_hist = [r[1] for r in run(WEEKS, Params(to_features=0.8)) if r[0] % 1 == 0]
traf_hist = traffic_path(WEEKS)
# «Правда»: вклад дают обе причины сразу. Наблюдателю виден только результат.
observed = [2.0 + 0.10 * d + 6.0 * tr + random.gauss(0, 0.5)
for d, tr in zip(debt_hist, traf_hist)]
aA, bA = ols(debt_hist, observed) # A: инциденты объясняются техдолгом
aB, bB = ols(traf_hist, observed) # B: инциденты объясняются трафиком
# Вмешательство: 12 недель половина времени уходит на уборку. Трафик растёт как рос.
tail_debt = [r[1] for r in run(HORIZON, Params(to_features=0.5), debt0=debt_hist[-1])
if r[0] % 1 == 0]
tail_traf = traffic_path(WEEKS + HORIZON)[WEEKS:]
predA = [max(0.0, aA + bA * d) for d in tail_debt]
predB = [max(0.0, aB + bB * t) for t in tail_traf]
truth = [2.0 + 0.10 * d + 6.0 * tr for d, tr in zip(tail_debt, tail_traf)]
A (долг) : y = -3.30 + 0.644·x | R² = 0.940 | RMSE = 0.381 | x при калибровке: 20.6…28.3
B (трафик): y = 3.91 + 6.370·x | R² = 0.927 | RMSE = 0.420 | x при калибровке: 1.0…1.8
неделя | A говорит | B говорит | что произойдёт на самом деле
21 | 13.0 | 15.8 | 15.7
24 | 7.3 | 16.9 | 15.8
28 | 0.1 | 18.5 | 16.3
32 | 0.0 | 20.3 | 17.5
- Обе модели отлично описывают историю. $R^2$ равен 0.94 и 0.93 — по обычным меркам «модель хорошо объясняет данные».
- Прогнозы расходятся качественно. A обещает, что инциденты исчезнут; B — что вырастут на треть. Между этими ответами лежат разные бюджеты, найм и дорожная карта.
- Модель с лучшей подгонкой предсказывает хуже. У A выше $R^2$ и ниже RMSE — и ошибается она сильнее. Качество подгонки не ранжирует модели по качеству предсказания при вмешательстве.
- Ни одна не права: правда — смесь, и ни одна однопричинная структура её не содержит. Это типовая ситуация, а не сконструированный курьёз.
- Диапазон калибровки нарушен. Долг при подгонке менялся в коридоре 20.6–28.3, а в прогнозе A применяется при долге около нуля; линейная связь там ничем не подтверждена и мгновенно даёт бессмыслицу — без
max(0.0, ...)модель A предсказывает отрицательное число инцидентов.
Что делать практически:
- Отложенная проверка обязательна. Калибруйте на первых 70 % истории, проверяйте на последних 30 %, которых модель не видела. Это стандарт из машинного обучения — переобучение и регуляризация, оценка моделей; во временных рядах разбиение делается только по времени, никогда случайно (временные ряды). И считайте параметры: модель с восемью подгоняемыми коэффициентами на двадцати точках объяснит что угодно, поэтому отношение «параметров к точкам» — первое, что надо спросить у автора модели.
- Различайте модели вмешательством, а не подгонкой. Единственный способ отделить A от B — изменить один вход и посмотреть: заморозьте продуктовую работу в одной команде на месяц, при экзогенной причине инциденты не изменятся. Это дешевле квартала спора, и это же защищает от подмены корреляции причиной (причинные и статистические ошибки).
Чего модель не покажет никогда
Предыдущие разделы — про модели, которые можно починить. Этот — про пределы, которые чинить нечем; их можно только знать и обходить.
Всё, что вынесено за границу
Тривиально по формулировке и смертельно на практике: модель ничего не знает про то, что вы объявили внешним входом. Если трафик задан как экзогенный рост 3 % в неделю, модель никогда не предскажет маркетинговую кампанию с плюс 40 % за сутки. Формальная проверка называется тестом на адекватность границ: перенесите подозрительную величину внутрь модели и посмотрите, изменились ли выводы; если изменились — граница выбрана неверно (границы системы). Сюда же — отказы из взаимодействия трёх компонентов, два из которых агрегированы в один узел: в модели они не воспроизводятся в принципе, нужны инъекция отказов и свойство-ориентированные тесты (тестирование распределённых систем).
Хвосты: модель средних не про p99
Самый частый и самый дорогой разрыв в инженерии. Непрерывная модель запасов и потоков оперирует средними темпами; у неё нет переменной, отвечающей за разброс, — значит, хвост распределения из неё принципиально не выводится. Загрузка $\rho = 0.8$: приток 0.8 заявки в секунду, мощность 1.0. Модель баланса потоков говорит: приток меньше мощности, очередь пуста, ожидание ноль. Теория массового обслуживания для той же нагрузки даёт среднюю длину очереди $\rho^2/(1-\rho) = 3.2$ и хвост $P(W > t) = \rho e^{-(\mu - \lambda)t}$, откуда $t_{99} = \ln(100\rho)/(\mu - \lambda) = 21.9$ секунды.
"""Одна и та же нагрузка: детерминированная модель против модели с дисперсией."""
import random
LAM, MU = 0.8, 1.0 # заявок/с и обслуживаний/с, загрузка 0.8
def simulate(n: int, seed: int) -> list[float]:
"""M/M/1 в лоб: время ожидания в очереди для n заявок. O(n) времени и памяти."""
rnd = random.Random(seed)
t_arrival = t_free = 0.0
waits = []
for _ in range(n):
t_arrival += rnd.expovariate(LAM)
start = max(t_arrival, t_free)
waits.append(start - t_arrival)
t_free = start + rnd.expovariate(MU)
return waits
def pct(xs: list[float], q: float) -> float:
return sorted(xs)[min(len(xs) - 1, int(q * len(xs)))]
детерминированная модель: очередь 0, ожидание 0.0 с
теория M/M/1: среднее 4.0 с, p99 = 21.9 с
n = 2 000 | среднее по 5 прогонам 2.80…3.71 с | p99 по 5 прогонам 14.3…21.0 с
n = 20 000 | среднее по 5 прогонам 3.33…4.24 с | p99 по 5 прогонам 18.3…22.4 с
n = 200 000 | среднее по 5 прогонам 3.81…4.06 с | p99 по 5 прогонам 20.3…22.5 с
- Ноль, 4 секунды и 22 секунды — три ответа на один вопрос при одной и той же нагрузке. Разница не в данных, а в том, содержит модель дисперсию или нет. Если решение про SLO — берите дискретно-событийную модель или нагрузочный тест, а не поток средних (нагрузочное тестирование).
- Хвост сходится медленнее среднего. На прогоне в 2000 заявок оценка p99 гуляет от 14 до 21 секунды — ошибка до 35 %, тогда как среднее уже близко. У самой симуляции есть погрешность, и её надо считать, а не игнорировать (вероятность и статистика).
Смену режима, точный момент и амплитуду
Параметры оценены в одном режиме; в другом действуют другие уравнения. Кэш с 99 % попаданий и кэш с 90 % — две разные системы, а не одна с другим числом, потому что база начинает работать в другом режиме (нелинейность и пороги). Никакая калибровка на «нормальных» данных не даёт информации о поведении за порогом — там просто нет наблюдений. Следствие: всегда указывайте коридор, в котором модель проверялась, прямо рядом с выводом. «Модель проверена при загрузке 0.3–0.7; при 0.9 её выводы неприменимы» — это не оговорка, а часть результата.
Даже когда режим угадан верно («будет колебание с нарастанием»), время и амплитуда предсказываются заметно хуже: небольшая ошибка в длине задержки сдвигает фазу, небольшая ошибка в коэффициенте меняет амплитуду, а для систем с сильной чувствительностью к начальным условиям это фундаментально (хаос и динамические системы). Отсюда правило формулировок: «через 4–8 недель очередь начнёт расти быстрее линейного» — допустимо; «6 апреля очередь достигнет 40 тысяч» — почти всегда неправда, даже если механизм угадан верно.
Реакцию людей на существование модели
Технические системы не читают отчёты о себе; организационные — читают, и это делает часть моделей самоопровергающимися.
- Закон Гудхарта (в формулировке Мэрилин Стратерн): когда мера становится целью, она перестаёт быть хорошей мерой. Модель, предсказавшая «инцидентов станет 4 в неделю», превращает 4 в цель — и инциденты начинают переклассифицировать в «деградации».
- Закон Кэмпбелла (1976): чем сильнее количественный показатель используется для решений, тем сильнее он искажается и тем сильнее искажает процесс.
- Критика Лукаса (1976): коэффициенты, оценённые при одной политике, меняются при смене политики, потому что участники подстраивают поведение. Прямой перенос: скорость команды, измеренная до введения нормы по скорости, не сохраняется после её введения.
Доработкой модели это не лечится: изменение возникает вне модели, в участниках. Защита одна — не превращать выходы модели в целевые показатели и разделять метрики наблюдения и метрики оценки (метрики).
Когда моделировать не надо
Полезный навык — вовремя не строить модель. Признаки, при которых моделирование — потеря времени:
- Эксперимент дешевле. Можно за день снять нагрузочный тест — снимайте. Модель ёмкости, которую можно измерить, — ритуал.
- Решение обратимо и дёшево или горизонт решения короче времени сборки модели. Тратить неделю на модель для решения, которое откатывается за час, — плохой размен.
- Спор идёт о ценностях, а не о механизме. «Надёжность важнее срока» — предпочтение, оно не проверяется прогоном. Модель здесь работает как риторическое оружие, и это худший её режим.
- Нет ни одной измеримой переменной. Если все узлы схемы — «мотивация», «культура», «доверие», и ни для одного нет прокси-показателя, получится неопровергаемая конструкция.
Обратные признаки — когда модель окупается: эксперимент невозможен или необратим; эффект отложен на месяцы; спорящие расходятся в механизме, а не в ценностях; цена ошибки заметно выше стоимости моделирования; есть хотя бы две-три измеримые переменные с историей.
Как встроить моделирование в инженерную работу
- Двадцатиминутная модель перед спором. Прежде чем обсуждать «нужна ли реплика», посчитайте на салфетке: приток, ёмкость, запас, время разгребания. Часть споров исчезает на этом шаге. И предсказание в тикете: до эксперимента запишите число и интервал — «ожидаю p99 в диапазоне 180–260 мс». Это превращает изменение в проверку модели и калибрует команду (научное и инженерное рассуждение).
- Журнал прогнозов. Дата, прогноз, интервал, факт. Через полгода видно, кто в каких вопросах калиброван, а кто уверенно ошибается, — единственный известный способ отличить экспертизу от красноречия. Сама модель при этом живёт в репозитории: скрипт на сто строк рядом с кодом сервиса, с тестами из раздела выше в CI. Модель, лежащая в чьей-то таблице, устаревает молча.
- Срок годности и оговорки. У модели должна быть дата пересмотра, а у вывода — коридор применимости: «при допущениях A, B, C и в коридоре загрузки 0.3–0.7 модель даёт X». Формулировка «модель показала X» непроверяема и потому бесполезна.
Типовые ошибки
| Ошибка | Как выглядит | Чем лечится |
|---|---|---|
| Модель без назначения | «давайте построим модель нашей системы» | записать вопрос и критерий успеха до начала |
| Карта вместо модели | схема на сорок узлов, ни одного уравнения | три узла с числами полезнее сорока со стрелками |
| Подгонка на всей истории | «совпало идеально» | отложенное окно, разбиение только по времени |
| Коэффициент без размерности | магическая константа 1.3 «для реализма» | тест размерностей, отказ от безымянных множителей |
| Проверка только в норме | тесты гоняют штатный режим | тест экстремальных условий обязателен |
| Точное число вместо ранга | «оптимум 0.65», «будет 40 тысяч» | политическая чувствительность, интервал и коридор применимости |
| Модель как аргумент в споре о ценностях | «модель доказала, что надёжность важнее» | разделять механизм и предпочтение |
| Модель, которую нельзя опровергнуть | любой исход объясняется постфактум | заранее записать, какое наблюдение её убьёт |
Честно про доказательную базу
Внутри системной динамики спор о том, что считать проверкой, идёт с семидесятых. Форрестер и Сенге (1980) предложили набор тестов, ставший каноном; Ясемин Барлас в «Formal Aspects of Model Validity and Validation in System Dynamics» (System Dynamics Review, 1996) сформулировал ключевое различие: главный предмет проверки здесь — структура, а не подгонка выходов, поэтому обычных статистических критериев недостаточно. Это разумно и одновременно опасно: критерий «структура выглядит правдоподобно» гораздо мягче критерия «прогноз сбылся».
Спор о том, легитимна ли диаграмма без симуляции. Джеффри Койл в 2000 году защищал качественное моделирование как самостоятельный результат; Джек Хомер и Роджелио Олива ответили статьёй «Maps and Models in System Dynamics: A Response to Coyle» (System Dynamics Review, 2001), показав, что без прогона нельзя утверждать даже качественное поведение системы из нескольких петель — интуиция ошибается систематически. Вывод для инженера: диаграмма — гипотеза, не результат. Про пределы валидации вообще — статья Наоми Орескес, Кристин Шрейдер-Фрешетт и Кеннета Белица «Verification, Validation, and Confirmation of Numerical Models in the Earth Sciences» (Science, 1994) — лучший текст на эту тему. Тезис: численную модель открытой системы нельзя верифицировать или валидировать в строгом смысле; можно только подтверждать на ограниченном наборе наблюдений, и подтверждение никогда не доказывает истинность структуры — совпадение с данными может возникать из компенсирующих ошибок. Это аргумент не против моделирования, а против слова «валидирована».
Про сложность. В прогнозировании накоплен устойчивый результат: усложнение модели редко улучшает точность вне выборки. Соревнования M-competitions Спироса Макридакиса (с 1982 года) раз за разом показывали конкурентоспособность простых методов; Кестен Грин и Скотт Армстронг в обзоре «Simple Versus Complex Forecasting: The Evidence» (Journal of Business Research, 2015) собрали сравнения, где усложнение в среднем ухудшало точность. Переносить это на системную динамику один в один нельзя — задачи разные, — но как поправка к соблазну «добавим ещё десять переменных» результат работает.
Про большие модели социальных систем — работы Форрестера, «Пределы роста» и полувековой спор о калибровке World3 — разобрано в главах про петли и про запасы и потоки. Короткая версия: главная претензия критиков была методологической — параметры не оценены по данным, поэтому модель не проходит проверку вне выборки, а ретроспективные сверки агрегатов слабо отличают верную структуру от компенсирующих ошибок.
Что известно про пользу обучения на моделях — слабее, чем хотелось бы. Эксперименты Джона Стермана и Дениса Диля с управленческими «тренажёрами» (1995) устойчиво показывают, что люди плохо справляются с задачами, где есть накопление и задержка, — результат воспроизводимый. А вот «после тренировки на симуляторе люди принимают лучшие решения в реальной работе» — гипотеза со смешанными данными, и подавать её как факт не стоит. Общий фон сформулировал Джордж Бокс в «Science and Statistics» (1976): все модели неверны, некоторые полезны. Полная мысль важнее усечённой цитаты — задача не в поиске «истинной» модели, а в поиске экономного описания, достаточно хорошего для конкретной цели, и в постоянной готовности его сломать.
Мини-итог
- Диаграмма фиксирует гипотезу о структуре; поведение определяется числами, и без прогона его нельзя утверждать даже качественно.
- Первое действие — записать назначение: объяснить прошлое, сравнить варианты, предсказать величину или договориться о языке. Требования к проверке у них разные.
- Ступень «оценка на салфетке» закрывает больше споров, чем любая симуляция; ступень «эксперимент» — конкурент модели, а не её продолжение. Вывод всегда снабжается коридором применимости и допущениями: «модель показала X» — не результат.
- Тесты по возрастанию стоимости: размерности → экстремальные условия → шаг интегрирования → отложенное окно → чувствительность. Экстремальные условия ловят дефекты, которых сверка с историей не видит: без ограничителей наша модель показывала отрицательный техдолг, оставаясь идеальной в штатном коридоре.
- Хорошая подгонка почти ничего не доказывает: две несовместимые структуры дали $R^2$ 0.94 и 0.93 на одной истории и противоположные прогнозы на вмешательство, причём лучше подогнанная ошиблась сильнее.
- Из модели надёжно извлекается ранг вариантов, а не число: «всё на фичи» проиграло в 499 случаях из 500 при разбросе параметров в два-три раза, но оптимальная доля гуляла от 0.5 до 0.8. При этом на 12 неделях ранг переворачивается — горизонт есть часть границы системы.
- Модель не покажет: то, что за границей; хвосты, если в ней нет дисперсии; поведение за порогом смены режима; реакцию людей на саму модель; точный момент и амплитуду.
Источники
- J. Forrester, P. Senge. Tests for Building Confidence in System Dynamics Models. TIMS Studies in the Management Sciences 14, 1980 — канонический список проверок. Развитие темы — J. Sterman. Business Dynamics. McGraw-Hill, 2000, глава 21 «Truth and Beauty: Validation and Model Testing».
- Y. Barlas. Formal Aspects of Model Validity and Validation in System Dynamics. System Dynamics Review 12(3), 1996.
- J. Homer, R. Oliva. Maps and Models in System Dynamics: A Response to Coyle. System Dynamics Review 17(4), 2001.
- N. Oreskes, K. Shrader-Frechette, K. Belitz. Verification, Validation, and Confirmation of Numerical Models in the Earth Sciences. Science 263, 1994.
- G. Box. Science and Statistics. JASA 71(356), 1976. K. Green, J. Armstrong. Simple Versus Complex Forecasting: The Evidence. Journal of Business Research 68(8), 2015; S. Makridakis, M. Hibon. The M3-Competition. International Journal of Forecasting 16(4), 2000.
- R. Lucas. Econometric Policy Evaluation: A Critique. Carnegie-Rochester Conference Series on Public Policy 1, 1976; D. Campbell. Assessing the Impact of Planned Social Change, 1976; M. Strathern. «Improving Ratings»: Audit in the British University System. European Review 5(3), 1997.
- E. Diehl, J. Sterman. Effects of Feedback Complexity on Dynamic Decision Making. Organizational Behavior and Human Decision Processes 62(2), 1995.
- D. Meadows. Leverage Points, 1999; System Dynamics Society — архивы System Dynamics Review и разборы моделей.
Что дальше
Инструменты трека собраны: границы, петли, запасы, задержки, пороги, архетипы, точки воздействия, диаграммы и модели с их пределами. Осталось прогнать их через реальные инженерные ситуации целиком — от первого симптома до проверяемого вывода.