Перейти к основному содержимому

Статистические распределения — числа «как в жизни»

Обычный <gen type="number" value="150..200"/> выдаёт числа равномерно150 так же вероятно, как 175 или 199. Но реальные данные почти никогда так не выглядят: рост людей кучкуется вокруг среднего, зарплаты перекошены (много средних, мало очень больших), а богатство и вовсе с длинным, тяжёлым хвостом. Чтобы сгенерировать правдоподобные числа, добавьте к генератору чисел атрибут distribution.

Как включить

Никакого нового тега — distribution это атрибут на знакомом <gen type="number">:

<gen type="number" distribution="normal" mean="170" sd="10"/>

Как только есть distribution="...", генератор переключается из режима «диапазон» в режим «распределение». Нет этого атрибута — работает в точности как раньше.

Вот четыре колонки, каждая из своего распределения (сид demo, 8 строк):

<env count="8" seed="demo">
<sequence name="Рост"> <gen type="number" distribution="normal" mean="170" sd="10"/></sequence>
<sequence name="Зарплата"><gen type="number" distribution="lognormal" meanlog="10.8" sdlog="0.5"/></sequence>
<sequence name="Пауза"> <gen type="number" distribution="exponential" rate="0.5" decimals="1"/></sequence>
<sequence name="Баланс"> <gen type="number" distribution="pareto" alpha="1.5" xmin="1000"/></sequence>
</env>
./run life.tdc (8 строк)
156 см |  29884 ₽ | 0.4 с | 1329 ₽
175 см |  75675 ₽ | 0.0 с | 2337 ₽
179 см |  35084 ₽ | 1.1 с | 3587 ₽
160 см |  74516 ₽ | 3.2 с | 3130 ₽
178 см |  65256 ₽ | 1.1 с | 3285 ₽
170 см |  39817 ₽ | 0.4 с | 1664 ₽
159 см | 107734 ₽ | 1.9 с | 8781 ₽
182 см |  65121 ₽ | 1.4 с | 2924 ₽

Рост держится около 170, зарплаты перекошены вправо, паузы почти всегда короткие, а баланс не опускается ниже 1000, но иногда «выстреливает».

Примеры вывода иллюстративны

Гистограммы и значения ниже — это то, что выдаёт типичный запуск. Точные числа могут отличаться в зависимости от версии ядра, но неизменной остаётся форма, которую гарантирует каждое распределение. Распределения детерминированы: тот же seed — те же числа.

Равномерно или «как в жизни» — видно на гистограмме

Проблема. Глядя на столбик чисел, не поймёшь, «плоские» они или собраны в кучу. Нарисуем форму: сгенерируем 300 «ростов» двумя способами и разложим их по корзинам. Каждая строка гистограммы — это диапазон, а # показывают, сколько значений в него попало (внизу n — всего значений, min/max — края).

Обычный диапазонvalue="150..200". Числа рассыпаны ровным слоем: рост 150 так же вероятен, как 175 или 199. В жизни так не бывает:

<gen type="number" value="150..200"/>
./run uniform.tdc (300 строк)
150 | ############################# 27
154 | ####################### 21
158 | ############################### 29
163 | ######################## 22
167 | ##################### 19
171 | ######################## 22
175 | ################### 18
179 | ################################## 31
183 | ############################# 27
188 | ##################################### 34
192 | ############## 13
196 | ######################################## 37
n=300  min=150  max=200

То же, но distribution="normal" (центр 175, разброс 8). Появляется «колокол»: большинство ростов у центра, к краям всё реже — как у настоящих людей:

<gen type="number" distribution="normal" mean="175" sd="8"/>
./run normal.tdc (300 строк)
157 | ### 5
161 | ############# 19
165 | ###################### 33
169 | ####################################### 57
172 | ######################################## 59
176 | ########################## 39
180 | ################################## 50
184 | ################# 25
188 | #### 6
192 | ## 3
195 | ## 3
199 | # 1
n=300  min=157  max=203
Ровная гистограмма рядом с колоколом на том же диапазоне
Один и тот же диапазон 150–200, сгенерированный по 20 000 раз каждым способом и посчитанный.
  • обычный диапазон: любой рост примерно так же вероятен, как любой другой
  • тот же диапазон с нормальным распределением: холм вокруг центра

Диапазон значений почти тот же (≈150–200), а форма совершенно разная: у равномерного все столбики примерно одной высоты, у нормального — горка с пиком у 175 и редкими краями. В этом весь смысл — переход от «равномерно (нереалистично)» к «как в жизни».

Все девять форм, каждая сгенерирована с теми атрибутами, что подписаны снизу. Горизонтальная ось обрезана по 99-му перцентилю — иначе длинный хвост сжимает всё видимое в одну первую колонку:

Гистограммы девяти поддерживаемых распределений
Девять прогонов по 8000 значений, посчитанных по корзинам. Вот как эти названия выглядят на самом деле.
  • сгенерированные значения, разложенные по корзинам

normal — «колокол»

Что это. Значения кучкуются вокруг центра и становятся тем реже, чем дальше от него, симметрично в обе стороны. Классическая колоколообразная кривая.

Где встречается. Рост, вес, температура, оценки на экзамене, ошибки измерений — всё, что колеблется вокруг типичного значения.

Параметры. mean — центр (среднее). sd — разброс (стандартное отклонение: насколько широко значения расходятся от центра). Правило: примерно две трети значений попадают в mean ± sd.

<gen type="number" distribution="normal" mean="170" sd="10"/>
./run normal.tdc (300 строк)
147 | #### 6
153 | ################# 28
159 | ################################## 55
164 | ######################################## 65
170 | ################################### 57
176 | #################################### 59
182 | ############# 21
188 | ### 5
193 | ## 3
199 | # 1
n=300  min=147  max=205

→ рост в сантиметрах: большинство между 160 и 180, редко 150 или 190. Симметричный «колокол» с пиком у 170.

Когда брать. Колонка вроде «рост», «вес» или «температура» — любая величина с типичным значением и симметричным разбросом вокруг него.

lognormal — перекос вправо

Что это. Как normal, но «завалено» вправо: очень много небольших значений и длинный хвост крупных. Никогда не бывает отрицательным.

Где встречается. Зарплаты, цены, размеры городов, длительность сессий — там, где «много обычного, мало огромного».

Параметры. meanlog и sdlog — это центр и разброс логарифма значения (так это распределение задают в статистике). На практике: больше meanlog → крупнее типичное значение; больше sdlog → длиннее хвост богатых. Удобный ориентир — типичное значение ≈ e^meanlog (при meanlog=10.8 это ≈ 49 000).

<gen type="number" distribution="lognormal" meanlog="10.8" sdlog="0.5"/>
./run lognormal.tdc (300 строк)
 15633 | ######################################## 114
41886 | ####################################### 110
68140 | ################## 51
94393 | ###### 17
120646 | # 3
146900 | # 1
173153 | # 2
199406 | # 1
225659 |  0
251913 | # 1
n=300  min=15633  max=278166

→ зарплата: толпа около 50 000, немного людей — сильно больше. Пик слева, длинный хвост вправо (до ~278 000).

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

exponential — время между событиями

Что это. Промежутки между случайными событиями: много коротких, изредка длинные. Всегда неотрицательное.

Где встречается. Время между заказами, между кликами, между отказами сервера, между звонками в колл-центр.

Параметры. rate — это λ (лямбда): сколько событий в среднем происходит за единицу времени. Среднее ожидание = 1 / rate. При rate=0.5 события происходят примерно раз в 2 единицы, поэтому типичная пауза — около 2.

<gen type="number" distribution="exponential" rate="0.5"/>
./run exponential.tdc (300 строк)
 0 | ############################ 70
1 | ######################################## 98
2 | ################### 47
3 | ################ 38
4 | ####### 17
5 | ####### 18
6 | # 3
7 | # 3
8 | # 2
9 | # 1
10 | # 2
12 | # 1
n=300  min=0  max=12

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

Когда брать. Любая колонка «сколько времени прошло до…» / «интервал между…».

pareto — «правило 80/20», длинный хвост

Что это. Экстремальный перекос: почти все значения небольшие, но редкие — огромные. Тот самый принцип «20% причин дают 80% следствий».

Где встречается. Богатство (немного людей владеют почти всем), просмотры роликов, размеры файлов, население городов, число подписчиков.

Параметры. xmin — минимально возможное значение (ниже него не бывает). alpha — «толщина хвоста»: чем меньше alpha, тем толще хвост (больше редких гигантов). При alpha около 1–2 хвост очень выражен.

<gen type="number" distribution="pareto" alpha="1.5" xmin="1000"/>
./run pareto.tdc (300 строк)
 1002 | ######################################## 259
4234 | #### 23
7466 | # 8
10698 | # 5
13930 | # 1
17162 |  0
20393 | # 1
23625 | # 2
26857 |  0
30089 | # 1
n=300  min=1002  max=33321

→ «баланс на счёте»: у всех не меньше 1000, но у единиц — в разы больше. Почти всё (259 из 300) в первой корзине, а редкие гиганты уходят до ~33 000 — тот самый «длинный хвост».

Когда брать. Величины, где несколько экземпляров доминируют над всеми остальными: деньги, популярность, размеры.

weibull — время до отказа, надёжность

Что это. Гибкое распределение «времени до события»: в отличие от exponential (где риск постоянен), здесь риск может расти или падать со временем. Форму задаёт один параметр.

Где встречается. Срок службы деталей до поломки, время до отказа оборудования, скорость ветра, размеры частиц.

Параметры. shape (форма): shape < 1 — «детская смертность» (ломается рано или живёт долго); shape = 1 — ведёт себя как exponential (постоянный риск); shape > 1 — износ (чем дольше работает, тем вероятнее откажет). scale — характерный масштаб времени (примерно типичное время до отказа).

<gen type="number" distribution="weibull" shape="1.5" scale="1000"/>
./run weibull.tdc (300 строк)
  30 | ########################## 57
358 | ######################################## 88
687 | ##################### 47
1015 | ######################## 52
1343 | ########## 21
1672 | ########## 23
2000 | ### 6
2328 | # 2
2656 | # 3
2985 | # 1
n=300  min=30  max=3313

→ «часов до отказа»: обычно сотни–тысячи, с износом. При shape=1.5 пик не прижат к нулю (как у exponential), а сдвинут вправо: сначала риск растёт, потом хвост спадает.

Когда брать. Любая «наработка до отказа» / «время до события», где риск не постоянен во времени.

poisson — счётчики событий

Что это. Сколько случайных событий произошло за интервал. Целое число. Значения кучкуются около среднего, но это счётчики (0, 1, 2, 3…), а не непрерывная величина.

Где встречается. Звонки в колл-центр за час, дефекты на партию, письма в день, голы за матч, посетители в минуту.

Параметры. lambda — среднее число событий за интервал. При lambda=4 обычно получается 2–6 событий, изредка 0 или 8+.

<gen type="number" distribution="poisson" lambda="4"/>
./run poisson.tdc (300 строк)
 0 | #### 5
1 | ############ 17
2 | #################################### 50
3 | ####################################### 53
4 | ##################################### 51
5 | ######################################## 55
6 | #################### 28
7 | ############### 21
8 | ########## 14
9 | ### 4
10 | # 2
n=300  min=0  max=10

→ «звонков за час»: 3, 0, 7, 5, 3… Это счётчики (целые), пик у среднего lambda=4, значения расходятся в обе стороны, но никогда не бывают меньше 0.

Ограничение. lambda ограничена сверху числом 700. Для очень больших средних берите normal с mean=lambda и sd ≈ корень из lambda — это стандартное приближение.

Когда брать. Любая колонка «сколько раз случилось за период».

zipf — ранжированные данные, «звёзды и длинный хвост»

Что это. Немного «звёзд» (ранг 1, 2, 3…) встречаются очень часто, а огромный хвост редких — почти никогда. Выдаёт номер ранга (1, 2, … n).

Где встречается. Частота слов в языке, популярность сайтов, города по населению, число подписчиков, продажи товаров.

Параметры. n — сколько всего рангов (например, 100 товаров). s — «крутизна»: больше s → сильнее перекос в сторону первых рангов. При s≈1 это классический закон Ципфа.

<gen type="number" distribution="zipf" n="100" s="1.1"/>
./run zipf.tdc (300 строк)
 1 | ######################################## 174
11 | ########## 42
20 | ###### 27
30 | ### 12
39 | # 4
49 | ### 11
59 | ## 7
68 | ## 8
78 | ## 9
87 | # 6
n=300  min=1  max=97

→ «ранг товара»: чаще всего #1–#5, изредка #58 или #37. Первые ранги забирают почти всё (174 из 300 — это ранги 1–10), остальные 90 позиций — редкий длинный хвост.

Когда брать. Когда нужно смоделировать «топ доминирует» — выбор из пронумерованного списка, где первые позиции берут почти всё.

gamma — суммарное время ожидания

Что это. Обобщение exponential: если exponential — это время до одного события, то gamma — суммарное время до нескольких. Всегда неотрицательное, с перекосом вправо.

Где встречается. Время до накопления N событий, страховые выплаты, объёмы осадков, размеры очередей, время ответа сервиса (сумма этапов).

Параметры. shape (k) — «сколько этапов/событий суммируется» (больше shape → пик симметричнее и правее). scale (θ) — масштаб. Среднее ≈ shape · scale.

<gen type="number" distribution="gamma" shape="2" scale="1000"/>
./run gamma.tdc (300 строк)
  71 | ############################ 53
803 | ######################################## 77
1534 | ############################# 56
2266 | ############################# 55
2997 | ######### 18
3729 | ######### 17
4460 | ###### 11
5192 | #### 8
5923 | # 2
6655 | ## 3
n=300  min=71  max=7386

→ «миллисекунд ожидания»: обычно около 2000 (2·1000), с хвостом. В отличие от exponential, пик не прижат к нулю, а сдвинут вправо — это сумма двух этапов.

Когда брать. Суммарные времена или объёмы, где складывается несколько случайных вкладов.

beta — доли и вероятности (число от 0 до 1)

Что это. Единственное здесь распределение, чьи значения всегда между 0 и 1. Форму (где пик, насколько симметрично) задают два параметра.

Где встречается. Доли, проценты, вероятности, коэффициенты конверсии, рейтинги 0–1, доля брака.

Параметры. alpha и beta «перетягивают» массу к 1 и к 0 соответственно. alpha = beta — симметрично вокруг 0.5; alpha < beta — перекос к нулю (как низкая конверсия); alpha > beta — к единице. Среднее ≈ alpha / (alpha + beta).

<gen type="number" distribution="beta" alpha="2" beta="8" decimals="3"/>

Поскольку значения лежат между 0 и 1, подписи корзин слева округляются до 0 — смотрите на min/max внизу, чтобы увидеть реальный разброс (0.008..0.583):

./run beta.tdc (300 строк)
0 | ######################## 35
0 | ######################################## 59
0 | ################################# 49
0 | ############################### 46
0 | ################################# 48
0 | ############## 21
0 | ############ 18
0 | ######### 14
0 | #### 6
1 | ### 4
n=300  min=0.008  max=0.583

→ «коэффициент конверсии»: обычно 0.05–0.4 (среднее ≈ 0.2). Всё зажато в 0..1, пик у ~0.2, к единице почти ничего не доходит. Значения дробные — задайте decimals, иначе они округлятся до 0/1.

Когда брать. Любая величина-доля в диапазоне 0..1: вероятность, процент, конверсия, рейтинг.

Управление выводом: decimals, min, max

decimals — знаки после запятой

По умолчанию 0 — значение округляется до целого (рост «170», а не «170.4213»). Нужны дроби — задайте явно:

<gen type="number" distribution="exponential" rate="1" decimals="3"/> <!-- например, 0.693 -->

Это важнее всего для beta (чьи значения живут в 0..1) и для любого времени или отношения, где округление до целого «съело» бы детали.

min / max — обрезка по границам

Распределения иногда выдают крайние значения (normal при малом mean может уйти в минус). min / max удерживают значение в границах: всё, что ниже min, прижимается к min, всё, что выше max, — к max.

<gen type="number" distribution="normal" mean="30" sd="20" min="0"/> <!-- возраст не бывает отрицательным -->

Это видно на гистограмме. Без min нормальное с mean=30 sd=20 заходит в отрицательную область (min=-16):

./run age-raw.tdc (300 строк)
-16 | ### 6
-4 | ############### 27
7 | ############################## 52
19 | ################################## 60
30 | ######################################## 70
42 | ############################### 55
53 | ############ 21
65 | ## 4
76 | ## 4
88 | # 1
n=300  min=-16  max=99

С min="0" всё, что было ниже нуля, прижимается к нулю — левый край ровно 0, а нулевая корзина «раздувается» (она вобрала бывшие отрицательные):

./run age-clipped.tdc (300 строк)
 0 | ################################# 46
10 | ################################# 46
20 | ####################################### 53
30 | ######################################## 55
40 | ####################################### 54
50 | ####################### 31
59 | ###### 8
69 | ## 3
79 | ## 3
89 | # 1
n=300  min=0  max=99

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

Колокол и он же, обрезанный по границам
Тот же генератор без границ и с границами. Значения за границей прижимаются к ближайшей, поэтому на краях вырастает всплеск.
  • слева без границ, справа тот же прогон с границами 160 и 180

Чего нельзя совмещать

distribution заменяет обычный числовой режим, поэтому его нельзя ставить на тот же генератор вместе с диапазоном или процентами — TDC об этом честно сообщит:

  • value (диапазон), percent, length, include и exclude — все несовместимы с distribution, ошибка TDC088:
./run bad.tdc
error[TDC088]: <gen type="number" distribution="..."> cannot be combined with "value"
note: A distribution replaces the range/percent. Remove "value", or drop "distribution" to use a range.
  • незнакомое имя распределения или пропущенный обязательный параметр (например, normal без sd) — ошибка TDC089 с подсказкой, что требуется:
./run bad.tdc
error[TDC089]: distribution "normal": "sd" is required and must be a number
note: Distributions: normal (mean, sd), lognormal (meanlog, sdlog), exponential (rate), pareto (alpha, xmin). Optional: decimals, min, max.

Детерминизм и большие объёмы

Как и всё в TDC, распределения детерминированы: тот же seed даёт тот же результат. И они работают на любом объёме — значение каждой строки считается по её номеру, поэтому память не растёт с числом строк (см. Большие объёмы вывода). Миллиард логнормальных «зарплат» — не проблема.

Все девять распределений готовы: normal, lognormal, exponential, pareto, weibull, poisson, zipf, gamma и beta — точные, детерминированные и работающие потоково на любом объёме.

Смотрите также

  • Number — режимы диапазона и строки-из-цифр, которые заменяет этот атрибут.
  • Большие объёмы вывода — почему распределения остаются дешёвыми даже на миллиарде строк.
  • Связанные данные — как удерживать сгенерированные поля согласованными внутри строки.