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

Генератор timeseries

Когда нужен. Когда значения должны двигаться как настоящий сигнал во времени — продажи по дням, показания датчика, веб-трафик. Реальные ряды — это не плоский шум и не одно распределение: это слои — общий тренд (рост или спад), повторяющаяся сезонность (недельная, годовая) и случайный шум сверху. Генератор timeseries собирает значение строки именно так:

значение(i) = base + trend·i + amplitude·sin(2π·i / period) + noise·случайное

где i — номер строки (ось времени), считается с нуля: первая строка (i = 0) равна ровно base.

Выводы ниже иллюстративны: точные цифры зависят от версии ядра и seed, но важна именно форма — тренд, волна, дрожание.

Один и тот же генератор четыре раза, с добавлением одного атрибута на каждом шаге — по 120 строк на панель.
  • Aтолько base: ровная линия
  • Bдобавлен trend: линия пошла вверх
  • Cдобавлены period и amplitude: поверх тренда легла волна
  • Dдобавлен noise: волна перестала быть идеальной

Зачем это, а не просто случайные числа

Обычный генератор number выдаёт белый шум — значения скачут вокруг среднего без всякой памяти. Вот number с нормальным распределением, центрированным на 1000:

<sequence name="Шум"><gen type="number" distribution="normal" mean="1000" sd="120"/></sequence>
./run noise.tdc
День 1     841
День 2     1341
День 3     1047
День 4     1010
День 5     1086
День 6     1077
День 7     862
День 8     1072
День 9     1114
День 10    782
День 11    979
День 12    1014

Ни роста, ни спада, ни повтора — каждый день всё топчется около 1000. На реальные метрики это не похоже: у продаж есть тренд (бизнес растёт), есть недельный ритм (в выходные иначе, чем в будни), и уже поверх этого — случайные отклонения. Именно эти три слоя и добавляет timeseries.

Атрибуты

<gen type="timeseries" base="1000" trend="20" period="7" amplitude="150" noise="30"/>
АтрибутЧто задаёт
baseСтартовый уровень (по умолчанию 0)
trendНаклон: на сколько растёт значение каждый шаг
periodПериод сезонной волны (в строках), напр. 7 — неделя; можно несколько: 7,365
amplitudeВысота сезонной волны — по одной на каждый period
peak_atНа какой строке волна пикует (по умолчанию — на четверти периода) — по одной на каждый period
noiseСила случайного шума (стандартное отклонение)
noise_correlationНасколько шум одной строки переходит в следующую, -1..1 (по умолчанию 0)
decimalsЗнаков после запятой (по умолчанию 0 — целое)

Любой слой необязателен. Разделы ниже разбирают их по одному — что каждый делает и когда его брать.

base — стартовый уровень

base фиксирует значение самой первой строки (i = 0) и уровень, от которого отсчитывается всё остальное. Сам по себе — без trend, волны и noise — это просто прямая линия.

<gen type="timeseries" base="500"/>
./run base.tdc
500
500
500
500
500

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

trend — направление

trend — это наклон: каждая строка добавляет trend к предыдущей. Положительный — растёт, отрицательный — падает. С одними лишь base + trend получается мёртвая прямая.

<gen type="timeseries" base="1000" trend="20"/>
./run trend.tdc
1000
1020
1040
1060
1080

Используйте для роста или спада, который хочется видеть явно, — число подписчиков, прибавляющее 20 в день, батарея, теряющая фиксированный заряд за цикл.

period и amplitude — сезонная волна

Эти два работают в паре, и по отдельности ни один ничего не делает. period — сколько строк занимает один полный цикл (7 — недельный ритм, 365 — годовой); amplitude — насколько волна отклоняется вверх и вниз от линии тренда. Вместе они кладут повторяющуюся волну sin на то, что дают base + trend.

<gen type="timeseries" base="1000" trend="20" period="7" amplitude="150"/>
./run season.tdc
1000
1137
1186
1125
1015
954
1003

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

peak_at — на какой строке волна выше всего

period и amplitude говорят, какой длины волна и как далеко она качается. Они не говорят, когда она пикует, и ответ по умолчанию застаёт врасплох: волна стартует с середины размаха и идёт вверх, поэтому пик приходится на четверть периода.

На двенадцати месячных строках это строка 3 — апрель. Конфиг писался ради «летом теплее», а апрель — это не оно:

<gen type="timeseries" base="15" amplitude="10" period="12" decimals="1"/>
./run temp.tdc — самая высокая строка четвёртая
15.0
20.0
23.7
25.0
23.7
20.0
15.0
10.0
6.3
5.0
6.3
10.0

peak_at называет строку напрямую. Строки считаются с нуля, поэтому июль — это строка 6:

<gen type="timeseries" base="15" amplitude="10" period="12" peak_at="6" decimals="1"/>
./run temp.tdc — самая высокая строка седьмая
5.0
6.3
10.0
15.0
20.0
23.7
25.0
23.7
20.0
15.0
10.0
6.3

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

Это строка, а не угол. period уже считается в строках — значит и peak_at тоже: 182 из 365 это первое июля, и такое число берут из календаря, а не из радиан. Значение за пределами периода заворачивается, поэтому peak_at="18" при period="12" — это то же самое, что 6; дробь допустима, когда пик приходится между строками.

peak_at="0" стоит знать отдельно: он заставляет волну начаться с максимума — форма, которую простой синус не даст ни при какой амплитуде.

Если вы потянулись к phase

Это название из обработки сигналов, и в TDC его нет. peak_at делает ту же работу в той единице, которой пользуется весь остальной генератор. Написанный phase= — ошибка, которая это и говорит.

peak_at требует period: у волны сначала должна появиться длина, и только потом — высшая точка. Без неё это TDC253.

noise — шероховатость реального мира

noise — это стандартное отклонение случайного колебания, добавляемого к каждой строке. Это разница между учебниковой кривой и настоящим замером.

<gen type="timeseries" base="1000" trend="20" period="7" amplitude="150" noise="30"/>
./run noise-layer.tdc
1048
1152
1210
1093
1017
978
991

Сравните с чистой волной выше: форма та же, но каждая точка слегка дрожит (1000 → 985). Прибавьте шума для шумного датчика, убавьте — для гладкого агрегата. Дрожание воспроизводимо — см. Детали.

decimals — дробные значения

По умолчанию вывод округляется до целого. decimals оставляет столько знаков после запятой — для температур, цен или любой измеренной величины.

<gen type="timeseries" base="20" trend="0.5" noise="0.3" decimals="1"/>
./run decimals.tdc
20.5
20.6
21.2
21.2
22.0

Собираем ряд по слоям

Лучший способ прочувствовать генератор — включать слои по одному. Ниже три колонки <sequence> бегут по одним и тем же «дням»: тренд (только trend), +сезон (добавили period + amplitude) и +шум (добавили noise).

<sequence name="A"><gen type="timeseries" base="1000" trend="20"/></sequence>
<sequence name="B"><gen type="timeseries" base="1000" trend="20" period="7" amplitude="150"/></sequence>
<sequence name="C"><gen type="timeseries" base="1000" trend="20" period="7" amplitude="150" noise="30"/></sequence>
...
<data>День ${{День}} тренд=${{A}} +сезон=${{B}} +шум=${{C}}</data>
./run series.tdc
День   тренд   +сезон   +шум
01     1000     1000     985
02     1020     1137     1087
03     1040     1186     1192
04     1060     1125     1107
05     1080     1015     936
06     1100     954      966
07     1120     1003     1031
08     1140     1140     1087
09     1160     1277     1311
10     1180     1326     1347
11     1200     1265     1261
12     1220     1155     1126

Читаем по колонкам:

  • тренд — мёртвая прямая: +20 каждый день, 1000, 1020, 1040 …. Направление есть, но живого сигнала нет.
  • +сезон — на прямую легла недельная волна (period="7"). Внутри каждой недели значение поднимается к пику и опускается ко дну: пики приходятся на дни 3 и 10 (1186 → 1326), провал — на день 6 (954). Пики и провалы повторяются через 7 строк, и каждый следующий выше предыдущего ровно на trend · period = 20 · 7 = 140 — волна едет вверх по тренду.
  • +шум — тот же рисунок, но слегка дрожит (1000 → 985), как у настоящих замеров.

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

Несколько сезонов сразу

В жизни у ряда редко бывает один сезон. Магазин берёт больше по субботам и больше в декабре; энергосеть живёт суточным циклом и годовым. Напишите оба: period, amplitude и peak_at принимают списки через запятую, по одной записи на волну, и волны складываются.

<gen type="timeseries" base="1000" trend="2"
period="7,365" amplitude="120,400" peak_at="5,182" decimals="0"/>

Это недельная волна высотой 120 с пиком на 5-й день и годовая высотой 400 с пиком на 182-й — в одной колонке.

./run shop.tdc (count=16, seed=shop)
day 0: 573
day 1: 494
day 2: 496
day 3: 580
day 4: 684
day 5: 732
day 6: 689
day 7: 591
day 8: 512
day 9: 515
day 10: 600
day 11: 705
day 12: 753
day 13: 712
day 14: 614
day 15: 536

Недельные пики видно невооружённым глазом — дни 5, 12 и 19, — и каждая неделя чуть выше предыдущей, потому что годовая волна ещё поднимается к 182-му дню.

Три списка описывают одни и те же волны, позиция в позицию, поэтому они обязаны совпадать по длине: под period="7,365" нужны две амплитуды. Единственное сокращение — одна amplitude на все волны, если они одной высоты. Всё остальное движок не станет угадывать (TDC304).

Шум, который помнит — noise_correlation

Обычный noise независим: каждую строку трясёт саму по себе, и высокое значение ничего не говорит о следующем. Настоящая погрешность измерения так себя ведёт редко. Датчик, ушедший в тепло, ещё какое-то время держится тепла; очередь, которая начала копиться, продолжает копиться. Код, проверенный только на независимом шуме, ни разу не встречал того случая, на котором действительно сломается.

noise_correlation говорит, сколько шума одной строки переходит в следующую: 0 — тот самый независимый шум, что этот генератор выдавал всегда, 0.9 — сильно связанный. Это модель AR(1), если название вам знакомо; если нет — читайте как «насколько шум липкий».

<sequence name="White"> <gen type="timeseries" base="20" noise="2" decimals="1"/></sequence>
<sequence name="Drifty"> <gen type="timeseries" base="20" noise="2" noise_correlation="0.9" decimals="1"/></sequence>
./run sensor.tdc (count=16, seed=sensor)
independent 17.8   correlated 22.4
independent 18.5   correlated 21.0
independent 20.8   correlated 21.8
independent 21.1   correlated 20.3
independent 19.3   correlated 22.6
independent 18.4   correlated 23.9
independent 20.4   correlated 23.2
independent 18.7   correlated 21.5
independent 21.6   correlated 20.9
independent 23.1   correlated 20.7
independent 20.0   correlated 20.6
independent 23.2   correlated 20.2
independent 22.4   correlated 18.9
independent 20.0   correlated 18.7
independent 18.5   correlated 18.4
independent 18.5   correlated 18.5

Левая колонка случайно скачет вокруг 20. Правая бредёт: поднялась до 23.9, потом за десяток строк сползла к 18.4. Разброс у обеих одинаковый — корреляция меняет то, как шум движется, а не то, какой он большой.

Мелким шрифтом

  • Нужен noise=. Иначе корреляция чего? Такое отклоняется, а не игнорируется (TDC305).
  • Между −1 и 1, и не 1: на единице ряд уйдёт и не вернётся — это уже случайное блуждание, а не шум.
  • Отрицательное значение чередует — каждая строка отталкивается от предыдущей; так выглядят перерегулированные контуры управления.
  • Оба движка, любой объём. Коррелированное значение по-прежнему считается по номеру строки, поэтому потоковый прогон на миллиард строк даёт тот же файл, что и прогон в памяти, а память не растёт. Помнит последние 64 строки: достаточно далеко, чтобы вес всего, что старше, был пренебрежимо мал, и достаточно близко, чтобы это стоило около 20 наносекунд на строку.

Детали

  • Детерминированно: тот же seed даёт тот же ряд. Шум тоже воспроизводим — он считается по номеру строки, а не «броском монеты» на лету.
  • Любой объём, оба движка: значение считается по номеру строки, поэтому память не растёт (см. Большие выгрузки). Миллиард точек — не проблема.
  • Ось времени — это номер строки. Она естественно сочетается с increment (счётчик дней) или колонкой date рядом, так что каждое значение несёт реальную дату.

См. также

  • Number — одиночные случайные значения со статистическими распределениями.
  • Pattern — когда форму нельзя описать через тренд + сезон.
  • Counters / Date — индекс дня или реальная дата, чтобы поставить рядом с рядом.