Системное мышление Моделирование и его пределы: что модель не покажет
0%

Моделирование и его пределы: что модель не покажет

Моделирование и его пределы: что модель не покажет

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

Возьмите любую схему с двумя петлями — усиливающей и уравновешивающей. Что произойдёт с системой? Ответ зависит от того, какая петля сильнее и когда именно она включается, а этого на схеме нет. Стрелки со знаками задают только направления влияний. Поведение — рост, колебание, выход на плато, перелёт и обвал — определяется числами: коэффициентами, начальными уровнями запасов, длиной задержек. Схема из двадцати узлов совместима с десятками качественно разных траекторий, и вы не отличите их, пока не посчитаете.

Отсюда практический разлом темы. Слева — «системное мышление» как способ красиво нарисовать проблему на доске: такой артефакт нельзя опровергнуть и, значит, нельзя на нём ничего построить. Справа — модель как исполняемый объект с уравнениями, единицами измерения, параметрами, прогоном и набором тестов, который она может провалить. Глава про переход от левого к правому и про то, где правое всё равно упирается в стену.

Модель полезна не когда она правдоподобна, а когда она делает предсказание, которое можно проверить дешевле, чем стоит ошибка. Всё остальное — способ структурировать разговор; это тоже ценно, но это другая ценность и другие обещания.

Четыре разных вопроса, которые называют одним словом «модель»

Прежде чем строить, ответьте: на какой вопрос модель должна ответить. Требования к проверке у этих вопросов разные, и большинство споров о моделях — спор людей, решающих разные задачи.

Назначение Формулировка результата Что нужно, чтобы поверить Реалистичность в инженерии
Объяснить прошлое «Очередь выросла не от нагрузки, а от падения ёмкости» Воспроизведение известной истории плюс отсутствие более простого объяснения Высокая: данные есть
Сравнить варианты (ранг) «Увеличить пул соединений полезнее, чем добавить реплику» Устойчивость ранга к разумному разбросу параметров Высокая: ранг устойчивее чисел
Предсказать величину «Через 6 недель очередь дойдёт до 40 тысяч» Проверка вне обучающей выборки, интервал, история попаданий Низкая для организационных систем, средняя для технических
Договориться о языке «Мы согласны, что дежурство — это запас, а не поток» Ничего: это соглашение, а не утверждение о мире Всегда достижима, но не даёт предсказаний

Разница между второй и третьей строкой — центральная. Ранг вариантов часто устойчив к тому, что параметры известны с точностью «плюс-минус два раза», а конкретное число — почти никогда. Ниже мы это измерим, а не постулируем. И правило, экономящее кварталы: сначала запишите назначение, потом стройте. Модель для ранжирования не превращается в модель прогноза от того, что кто-то попросил «числа на квартал».

Лестница строгости: от разговора к эксперименту

Моделирование — не «построить симуляцию». Это лестница, где каждая ступень дороже предыдущей и отвечает на более узкий вопрос. Ошибка номер один — прыгать сразу на четвёртую ступень, минуя третью, которая часто уже закрывает вопрос.

Ступень Отвечает на вопрос Не отвечает
Диаграмма «какие петли вообще есть» «какая победит и когда»
Оценка на салфетке «порядок величины, есть ли запас» «динамика во времени»
Динамическая модель «форма траектории, эффект политики» «точные числа, хвосты»
Дискретно-событийная «p99, переполнение буферов, редкие сочетания» «поведение людей»
Статистическая на данных «что коррелирует и как сильно» «что будет при вмешательстве»
Эксперимент «что реально произойдёт» «почему»

Третья ступень недооценена катастрофически: закон Литтла $L = \lambda W$ и формула отклика очереди $1/(1-\rho)$ решают, по опыту, больше половины споров о ёмкости — без единой строчки кода (запасы и потоки, нагрузочное тестирование). А седьмая ступень — не «модель», а её конкурент: если эксперимент дешевле модели и достаточно безопасен, делайте эксперимент. Модель нужна там, где эксперимент невозможен (нельзя месяц не нанимать ради проверки гипотезы о найме), необратим (нельзя «попробовать» переписать сервис) или слишком медленный (эффект техдолга виден за год, а решать надо в понедельник).

Кейс: техдолг и скорость поставки

Возьмём спор, идущий в каждой команде: сколько времени отдавать на уборку кода вместо фич. Продакт говорит «сейчас важнее фичи», техлид — «через полгода мы встанем». Оба правы внутри своего горизонта, и на словах спор нерешаем.

Две петли: усиливающая (долг → меньше мощности → меньше уборки → больше долга) и уравновешивающая (уборка снимает долг). Что победит — из схемы не видно. Это и есть момент, где диаграмма исчерпана.

Уравнения и размерности

Первый обязательный шаг — единицы измерения. Каждая стрелка обязана стать уравнением, где размерности сходятся; половина неработающих моделей ломается здесь.

мощность       [чел·нед/нед] = разработчики [чел] × продуктивность [нед/нед] × (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 % розыгрышей. Ответ на вопрос «убирать ли долг» буквально зависит от того, где проведена граница по времени (границы системы).

Формулировка, которая переживёт проверку: «при горизонте от года структура даёт устойчивый ответ — доля на уборку строго больше нуля; конкретная величина модели неизвестна и подбирается эмпирически».

Калибровка и почему совпадение с историей почти ничего не значит

Дальше — самая опасная часть. Модель подогнали под историю, графики легли один в другой, все довольны. Проблема в том, что хорошая подгонка — необходимое, но крайне слабое условие.

Эквифинальность: разные структуры, одна история

Эквифинальность — разные структуры дают одно и то же наблюдаемое поведение; в статистике то же явление называют проблемой идентифицируемости. Практический смысл: данных истории может физически не хватать, чтобы отличить две модели, дающие противоположные советы.

Проверим численно. Сгенерируем «правду», которой наблюдатель не знает: инциденты порождаются и долгом, и трафиком. Затем подгоним две однопричинные структуры и сравним их прогнозы на вмешательство.

"""Эквифинальность: две несовместимые структуры одинаково хорошо описывают одну историю."""
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
  1. Обе модели отлично описывают историю. $R^2$ равен 0.94 и 0.93 — по обычным меркам «модель хорошо объясняет данные».
  2. Прогнозы расходятся качественно. A обещает, что инциденты исчезнут; B — что вырастут на треть. Между этими ответами лежат разные бюджеты, найм и дорожная карта.
  3. Модель с лучшей подгонкой предсказывает хуже. У A выше $R^2$ и ниже RMSE — и ошибается она сильнее. Качество подгонки не ранжирует модели по качеству предсказания при вмешательстве.
  4. Ни одна не права: правда — смесь, и ни одна однопричинная структура её не содержит. Это типовая ситуация, а не сконструированный курьёз.
  5. Диапазон калибровки нарушен. Долг при подгонке менялся в коридоре 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): коэффициенты, оценённые при одной политике, меняются при смене политики, потому что участники подстраивают поведение. Прямой перенос: скорость команды, измеренная до введения нормы по скорости, не сохраняется после её введения.

Доработкой модели это не лечится: изменение возникает вне модели, в участниках. Защита одна — не превращать выходы модели в целевые показатели и разделять метрики наблюдения и метрики оценки (метрики).

Когда моделировать не надо

Полезный навык — вовремя не строить модель. Признаки, при которых моделирование — потеря времени:

  1. Эксперимент дешевле. Можно за день снять нагрузочный тест — снимайте. Модель ёмкости, которую можно измерить, — ритуал.
  2. Решение обратимо и дёшево или горизонт решения короче времени сборки модели. Тратить неделю на модель для решения, которое откатывается за час, — плохой размен.
  3. Спор идёт о ценностях, а не о механизме. «Надёжность важнее срока» — предпочтение, оно не проверяется прогоном. Модель здесь работает как риторическое оружие, и это худший её режим.
  4. Нет ни одной измеримой переменной. Если все узлы схемы — «мотивация», «культура», «доверие», и ни для одного нет прокси-показателя, получится неопровергаемая конструкция.

Обратные признаки — когда модель окупается: эксперимент невозможен или необратим; эффект отложен на месяцы; спорящие расходятся в механизме, а не в ценностях; цена ошибки заметно выше стоимости моделирования; есть хотя бы две-три измеримые переменные с историей.

Как встроить моделирование в инженерную работу

  • Двадцатиминутная модель перед спором. Прежде чем обсуждать «нужна ли реплика», посчитайте на салфетке: приток, ёмкость, запас, время разгребания. Часть споров исчезает на этом шаге. И предсказание в тикете: до эксперимента запишите число и интервал — «ожидаю 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 неделях ранг переворачивается — горизонт есть часть границы системы.
  • Модель не покажет: то, что за границей; хвосты, если в ней нет дисперсии; поведение за порогом смены режима; реакцию людей на саму модель; точный момент и амплитуду.

Источники

Что дальше

Инструменты трека собраны: границы, петли, запасы, задержки, пороги, архетипы, точки воздействия, диаграммы и модели с их пределами. Осталось прогнать их через реальные инженерные ситуации целиком — от первого симптома до проверяемого вывода.

Разборы: инцидент, техдолг, найм и очередь как системы

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов