Сентябрь 2026 AI-аналитик 10 мин

Как устроен слой метрик для AI-аналитика

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


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

Файл маленький: сто семь строк, три метрики. Дальше — как он устроен, кто его читает и что из этого следует.

О чём дальше

  1. Как устроен файл — цепочка строка · ключ · решение
  2. Пять слов, которые встречаются постоянно
  3. Два исполнителя — сборщик и модель, и кто из них что читает
  4. Что будет, если повторить через неделю — два измерения из шести
  5. Что означают решения — виды выручки, комментарии, три дыры
  6. Как файл живёт — три коммита, две строки из прогонов
  7. Что происходит, когда метрик становится много — симуляция на 24
  8. Чего нет в каноне и чего нет у меня
  9. Границы и с чего начать у себя

Как устроен файл

Сто семь строк. Но не всякая строка что-то объявляет, и не всякое объявление влияет на ответ. Отсюда три ступени, и дальше вся статья про них.

Строка. Всего их 107.

Ключ — строка вида имя: значение, где что-то объявлено. Таких 36. Остальные 71 — комментарии и пустые.

Решение — ключ, который меняет ответ агента. Таких 15. Остальные 21 ключ — названия метрик, их описания и структура файла.

СТРОКИ · КЛЮЧИ · РЕШЕНИЯ107строк в файле36из них ключи — строки вида имя: значение15из ключей — решения, которые меняют ответ55строк комментариев — ничего не объявляют,а объясняют, что будет при другом выбореНа пятнадцать решений — пятьдесят пять строк объяснений.
Строки, ключи, решения

Вот как это выглядит на одной метрике — я взял самую короткую из трёх и пометил каждую строку:

ОДНА МЕТРИКА ЦЕЛИКОМ · 16 СТРОКreturn_rate — самая короткая из трёхСТРУКТУРА  return_rate:СЛУЖЕБНОЕ    label: Доля возвратов    description: >      Какая часть проданного вернулась. Решения: считать      строками или штуками и обрезать ли окно.РЕШЕНИЕ    window_days: 90    grain: line                # line | unitКОММЕНТАРИЙ    # Возврат приходит через 3-21 день. Заказы последних    # 21 дня перед курсором ещё не дозрели.    # 21 — считать только по дозревшим когортам.    # 0  — считать по всему окну и получить заниженную    #      цифру, растущую задним числом.РЕШЕНИЕ    maturity_days: 21СЛУЖЕБНОЕ    tables: [fct_returns, fct_order_items]3 решенияменяют ответ5 комментариевобъясняют, что будет при другом выборе3 служебныхназвание, описание, список таблицплюс имя метрики, две строки описания и три пустых
Одна метрика целиком, с разметкой строк

И рядом с этой цепочкой — 55 строк комментариев. Они ничего не объявляют и потому в неё не входят. Но привязаны они именно к решениям: объясняют, что будет при другом выборе.

На пятнадцать решений — пятьдесят пять строк объяснений.

Где всё это лежит

Файл состоит из трёх частей:

частьстрокиз них комментариеврешений
шапка129
as_of833
metrics874312
всего1075515

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

as_of отвечает на вопрос «на какой момент мы смотрим на данные». Три решения, действуют на все метрики разом.

as_of:
  source: cursor
  filter_on: received_at
  timezone: UTC

metrics отвечает на вопрос «что именно считаем». Три метрики, у каждой свой набор:

метрикастрокиз них комментариеврешенийслужебных полей
active_customers281533
revenue412363
return_rate16533
всего8543129

До восьмидесяти семи не хватает двух строк: заголовка metrics: и пустой строки под ним.

Выручка вдвое сложнее соседей и по числу решений, и по объёму объяснений. Двадцать три строки комментариев на шесть решений — больше, чем у двух других метрик вместе.

Как сходятся два счёта

Выше файл разложен дважды: по частям и по цепочке строка — ключ — решение. Это два разреза одних и тех же ста семи строк, и вот как они пересекаются:

частькомментариевключейпустыхвсего
шапка91212
as_of3418
metrics43311387
всего553616107

Тридцать шесть ключей набираются как 1 + 4 + 31.

А внутри этих тридцати шести решения занимают меньше половины:

ключей
решения15
служебные поля: название, описание, список таблиц9
структура: version, заголовки двух блоков, имена трёх метрик6
продолжения многострочных описаний6
всего36

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

Чего в файле нет ни на одном уровне — колонок. Ни одного места, где сказано, что физически суммировать и по какому выражению. Список таблиц есть, а как из них получить число — нет.

Пять слов, которые дальше встречаются постоянно

Слой метрик — вот этот файл. Записано, как считается каждая метрика.

Сборщик — код, который читает ключи по именам и собирает из них SQL-запрос.

Модель — она получает файл целиком, вместе с комментариями, и пишет SQL сама.

Харнесс — всё, что вокруг модели: что ей показать, как запустить, как разобрать её ответ. Сборщик — часть харнесса.


Два исполнителя

ДВА ИСПОЛНИТЕЛЯ ОДНОГО ФАЙЛАmetrics.yml108 строк27 ключей55 комментариевуходит целикомСборщикrender.pyчитает 10 ключей из 15комментарии не видитМодельпишет SQL самачитает весь текст,включая 55 комментариев4.7%чисел проекта95.3%чисел проектаОДИН ФАЙЛ · РАЗНЫЕ ЧИТАТЕЛИ · РАЗНЫЙ ОХВАТСборщик берёт значения по имени ключа. Модель читает файл как тексти не различает, где ключ, а где проза.
Два исполнителя одного файла

Сборщик. Обычная функция: читает ключи по имени и подставляет значения в шаблон запроса. Комментарии для него — символ решётки и всё, что после. Из пятнадцати решений файла он читает десять.

Модель. Слой уходит ей в промпт целиком:

def layer_metrics() -> str:
    return (ROOT / "metrics" / "metrics.yml").read_text().strip()

Ни фильтра, ни выборки по метрике, ни выбрасывания комментариев. Список таблиц агенту отбирают, а слой отдают всегда целиком: агент на любом вопросе видит все три метрики и все пятьдесят пять комментариев, включая те, что к вопросу отношения не имеют. SQL при этом пишет сам.

Пропорция в моём проекте: сборкой получено 4.7% чисел, чтением — 95.3%.

ЧТО ВИДИТ КАЖДЫЙ ИСПОЛНИТЕЛЬСборщиквидит только ключиguests: includetables: [fct_order_items, fct_returns]2 строкииз двенадцатиМодельвидит весь текст# include — гостевые заказы наравне с остальными.# exclude — только заказы известных клиентов.## Решение появилось после разбора: продакт попросил# «выручку без гостей», а такого объекта в слое# не было вовсе. Агент собрал комбинацию сам —# непоследовательно: гостей отфильтровал в числителе# и не отфильтровал в вычитаемом.## ВАЖНО: решение действует на ОБЕ стороны формулы.guests: includetables: [fct_order_items, fct_returns]12 строкиз двенадцатиОДИН И ТОТ ЖЕ КУСОК ФАЙЛАДля сборщика решётка и всё после неё — пустое место. Для модели это часть инструкции.
Что видит каждый исполнитель

Кто из них читает какую строку

В файле есть ключ revenue.guests с самым длинным комментарием: двенадцать строк, история появления и приписка капсом о том, что решение действует на обе стороны формулы.

Кусок кода, который собирает запрос для выручки, этот ключ не читает. Ни разу. Я проверил не чтением кода, а прогоном: поменял значение на exclude, потом на третий вариант, потом написал в него заведомую бессмыслицу — собранный запрос совпал побайтово во всех случаях. Шесть разных состояний файла, один и тот же хеш SQL.

Для сборщика этот ключ всё равно что не существует — дальше я называю такие строки мёртвыми. Для модели он не мёртв: когда я снял строку вместе с комментарием, агент начал давать два варианта вместо одного, и в SQL появились четыре фильтра, по два на каждую сторону формулы. Он прочитал двенадцать строк объяснения и применил решение к обеим сторонам сразу.

Вот все пятнадцать и кто их читает:

решениесборщикещё кто-тоитог
as_of.sourceнетнетне читает никто
as_of.filter_onнетпечатается в сообщениина ответ не влияет
as_of.timezoneнетнетне читает никто
active_customers.window_daysдаработает
active_customers.activityдаработает
active_customers.guestsдаработает
revenue.window_daysдаработает
revenue.statusesдаработает
revenue.basisдаработает
revenue.returns_attributionдаработает
revenue.compare_toнетрасчёт цены развилкисборщик не исполняет
revenue.guestsнетрасчёт цены развилкисборщик не исполняет
return_rate.window_daysдаработает
return_rate.grainдаработает
return_rate.maturity_daysдаработает

Десять из пятнадцати доходят до запроса. Три не читает никто, ещё два читает не сборщик, а вспомогательный код.

Мёртвых строк в файле оказалось три вида, и они разные:

ТРИ ВИДА МЁРТВОЙ СТРОКИСБОРЩИКМОДЕЛЬЧЕЛОВЕКrevenue.guestsдвенадцать строк объяснениянетдамёртв для сборщика, работает через чтениеas_of.filter_onподписывает числонетнетдане влияет ни на что, но печатается рядом с ответомac.guests · третий вариантcount_each_orderнетбыл дажил только в комментарии — сняли, и он исчез«ЖИВАЯ СТРОКА» — ВОПРОС, ДЛЯ КОГО
Три вида мёртвой строки

Что будет, если повторить через неделю

Различаются они не только охватом.

ЧТО БУДЕТ, ЕСЛИ ПОВТОРИТЬ ЧЕРЕЗ НЕДЕЛЮСборщикшесть состояний файлаshashashashashashaодин и тот же SQL, байт в байтМодельшесть измерений недельной давностивоспроизвелось два из шестиИ база уезжает сама: за двое суток ответ на нетронутом файле сталнеотличим от плеча со снятым решением.ДАТА РЯДОМ С ЧИСЛОМ · БАЗА В ТОМ ЖЕ ПРОГОНЕ · МЕРИТЬ ПАРОЙ
Что будет, если повторить через неделю

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

С моделью иначе, и разброс не сводится к повторам одного прогона.

Первого сентября снятие строки revenue.guests меняло ответ: два варианта вместо одного, симметричный фильтр. Восьмого сентября на том же файле, том же промпте и неизменённом коде — один вариант трижды, и гостей в SQL нет вовсе.

Я перепроверил остальные пять развилок, по которым были измерения недельной давности. Воспроизвелись два из шести.

Шесть развилок, которые я снимал первого сентября, и что вышло восьмого:

что снимал1 сентября8 сентябрявоспроизвелось
привязка возврата1 вариант → 2то же самоеда
что считать активностью1 вариант → 2, триждыто же самоеда
строками или штукамибыло 1, стало 3было 2, стало 3нет
до возвратов или послеразошлись по этой осиразошлись по длине окнанет
гости в выручке1 вариант → 2, трижды1 вариант триждынет
гости в клиентах2 варианта, разные числа2 варианта, числа одинаковыенет

Развалились они по-разному, и это интереснее самой доли.

Одна потеряла альтернативу, которая жила только в комментарии. У решения про гостей в метрике клиентов было три варианта, третий — считать каждый гостевой заказ отдельным человеком. Первого сентября агент его предлагал. Восьмого перестал: единственное место, где этот вариант описан, — снятый комментарий, и без него он не восстанавливается.

Другая сохранила форму и сменила содержание. У решения про базу расчёта число вариантов совпало точь-в-точь, а расходиться они стали по длине окна вместо gross/net. Если бы я считал по числу вариантов, вердикт был бы «воспроизвелось».

И третье, что выяснилось попутно. Чтобы понять, подействовало ли снятие строки, ответ на изменённом файле сравнивают с ответом на нетронутом. Второй и есть точка отсчёта.

Так вот, она не стоит на месте. По одному из вопросов ответ на нетронутом файле давал один вариант первого сентября, два третьего, снова смесь четвёртого. За двое суток точка отсчёта стала неотличима от изменённого файла, хотя файл никто не трогал.

Три правила, если будете мерить своё:

  1. У измерения на агенте есть дата. Писать её рядом с числом.
  2. Ответ на нетронутом файле снимается в том же запуске, что и на изменённом. Сравнивать с записями недельной давности нельзя.
  3. Мерить надо парой: по какой оси разошлись варианты и в скольких повторах она поднялась. Ни одна мера по отдельности не работает — я убедился в этом дважды, и оба раза в разные стороны.

Что означают решения

Пятнадцать решений — это по объявлению. По исполнению меньше: три ключа as_of не решения, а зафиксированные соглашения, они описывают то, что зашито в обработке данных.

ИЗ ЧЕГО СОСТОИТ ОДНО РЕШЕНИЕКЛЮЧguestsсборщик читает по имениЗНАЧЕНИЕincludeсборщик подставляет в шаблонАЛЬТЕРНАТИВАexcludeсборщик не видит · модель читаетОБОСНОВАНИЕ«выручка есть выручка, платил еёчеловек с аккаунтом или без»сборщик не видит · модель читаетПРОИСХОЖДЕНИЕприехало из прогонасборщик не видит · модель читаетСборщику достаются две строки из пяти. Остальные три существуюттолько для модели — и работают, пока она их читает.
Из чего состоит одно решение

У восьми решений из пятнадцати рядом записаны альтернативы:

    # completed_order — активность это завершённый заказ.
    # any_order — отменённый заказ тоже активность: человек приходил.
    activity: completed_order

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

Виды выручки. Одна метрика, шесть решений, и каждое даёт своё число.

решениевыбраноальтернативачто меняется
гостиincludeexclude13 141 142.28 → 11 479 710.42 · 09-01, не воспроизвелось 09-08
привязка возвратаreturn_dateorder_date13 141 142.28 → 13 402 161.46 · 09-01, воспроизвелось 09-08
базаnet_of_returnsgrossне измерено
статусы[completed]+ cancelledне измерено
окно90 днейкалендарный месяцизмерено иначе

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

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

Раздел выше показал, почему это не документация: единственная альтернатива, описанная только в комментарии, исчезла из ответов вместе с ним.

ЧТО ОБЪЯВЛЕНО, А ЧТО НЕТactive_customersrevenuereturn_ratewindow_days909090activitycompleted_orderguestsexcludeincludestatuses[completed]basisnet_of_returnsreturns_attributionreturn_datecompare_toyear_over_yeargrainlinematurity_days2127 клеток · заполнено 12красным — дыры: решение объявлено у соседей и пропущено здесь
Что объявлено, а что нет

Три дыры. У доли возвратов нет решения про гостей — она не различает гостевые заказы вовсе, при том что у двух соседних метрик это решение объявлено и с разными значениями. У выручки нет дозревания, хотя она вычитает возвраты. Окно наблюдения стоит у всех трёх метрик голым значением — самое влиятельное решение файла и единственное, которое я ни разу не проверил снятием.

Отдельно, чтобы не путать: пара 353.40 против 360.18, расхождение 1.9% — это не цена развилки про гостей. Там оба варианта гостей исключают, а расходится симметричность применения: фильтр поставлен в продажах и не поставлен в вычитаемых возвратах. Цена развилки — другая пара и 12.64%.


Как файл живёт

Три коммита за восемь дней, один человек, ревью не было.

Большинство решений объявлено заранее. Но два приехали из прогонов, и это записано прямо в файле:

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

Второе — форма сравнения. Строки не хватило посреди опыта, и я дописал её, не останавливая прогон.

Разбираться пришлось долго. Сначала казалось, что дописанная строка испортила прогон. Потом выяснилось, что она вообще ничего не исполняет: ни одна ветка сборщика этот ключ не читает. И ось, за которую он отвечает, не разошлась ни разу — агент сравнивал с прошлым годом и до того, как строка появилась.

Испорчено было другое. Разбор шёл по прогону, где ключа ещё не было, а цена развилки считалась по слою, где он уже был. Числа посчитаны верно, но относятся к разным состояниям файла — и сошлись в отчёте так, будто к одному.

Восстановить хронологию не вышло: время коммита не совпадает со временем правки, а контрольной суммы файла в записях тогда не было. Отсюда правило: у каждого прогона должна быть записана ревизия слоя. Без него через неделю нельзя сказать, к какому состоянию файла относится число. У меня 895 чисел из 897 такой привязки не имеют, и это ограничение всех выводов, а не оговорка к одному.

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


Что происходит, когда метрик становится много

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

ЧТО РАСТЁТ, КОГДА МЕТРИК СТАНОВИТСЯ БОЛЬШЕметрик324×8разных решений918×2строк файла108448×4Комментарии дублируются37 уникальных146 повторов — 79%Заполненность матрицы падает44% · 3 метрики23% · 24 метрики
Что растёт, когда метрик становится больше

Две сущности растут по-разному. as_of не вырос вовсе: свойство мира одно на любое число метрик. metrics вырос в восемь раз.

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

А комментарии растут вместе с употреблениями. Уникального текста тридцать семь строк, всего — сто восемьдесят три. Семьдесят девять процентов комментариев оказались дублями. Объяснение про гостей записано одиннадцать раз, слово в слово.

Проблема не в размере файла, а в том, что при уточнении формулировки её надо поправить в одиннадцати местах.

И заполненность падает. У меня три метрики и девять решений: двадцать семь клеток, заполнено двенадцать. В симуляции двадцать четыре метрики и восемнадцать решений: четыреста тридцать две клетки, заполнена сто одна. С сорока четырёх процентов до двадцати трёх.

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

Напрашивается третий раздел — словарь решений, где альтернатива и объяснение лежат один раз, а метрика называет только выбранное значение. Файл сжимается примерно на треть, править надо в одном месте.

Я этого не делаю, и причина в разделе про двух исполнителей. Вынос объяснения из метрики убирает его оттуда, где его читает модель. А единственная альтернатива, описанная только в комментарии, исчезла из ответов ровно тогда, когда комментарий сняли.


Чего нет в каноне и чего нет у меня

Возьмите любое решение из таблицы выше и поищите ему поле в спецификации семантического слоя.

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

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

Управление метриками, к слову, прямо советует документировать допущения. Но советует людям — чтобы не спорили на совещании. Куда положить допущение так, чтобы его прочитал исполнитель, не говорит никто.

Симметрично про мой файл. Ни сущностей, ни измерений, ни агрегаций. Посчитать по нему физически нельзя: там нет ни одного места, где сказано, как получить число. Это выясняет агент сам, читая схему данных.


Границы

Три метрики, пятнадцать решений, шесть таблиц. Слой заведомо помещается в контекст целиком — что будет на трёхстах метриках, я не мерил.

95.3% чисел получены чтением, а не сборкой. Значит выводы этой статьи — про слой как текст в промпте. Про слой как конфигурацию сборки известно мало: тринадцать ключей из пятнадцати я ни разу не менял, и теперь понятно, что не проверил их сразу по двум осям.

Два измерения из шести не воспроизвелись через неделю. Все числа здесь — с датами, и это не педантизм.

И восемь из десяти чисел получены на одном синтетическом стенде.


С чего начать у себя

Не строить слой. Взять одну метрику, которую считают чаще всего, и выписать по ней решения, которые уже принимались.

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

У меня на это ушло сто семь строк и три вечера.


Приложение: файл целиком

Сто семь строк, как есть.

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

# Семантический слой. Написан руками и читается построчно — в этом весь смысл.
#
# Каждая строка ниже — решение, а не настройка. Метрика без владельца этих
# решений не метрика, а число, о происхождении которого никто не спорил,
# потому что никто его не формулировал.
#
# Проверка понимания: поменяй ОДНУ строку, не трогая данные, и посмотри,
# как переворачивается ответ. Обе цифры — в NOTES.md.

version: 1

# Общая отсечка. Действует на все метрики без исключения.
as_of:
  source: cursor          # момент из state/cursor.txt, он же world_cursor
  filter_on: received_at  # НЕ бизнес-дата: событие, доехавшее после курсора,
                          # не видно, даже если случилось до него. Иначе мир
                          # всеведущ и прошлое переписывается задним числом.
  timezone: UTC           # шесть магазинов пишут в локальной шкале;
                          # приведение делает staging, см. NOTES.md

metrics:

  active_customers:
    label: Активные клиенты
    description: >
      Сколько покупателей были активны в окне. Три решения, каждое меняет
      ответ: длина окна, что считать активностью, что делать с гостями.
    window_days: 90

    # completed_order — активность это завершённый заказ.
    # any_order — отменённый заказ тоже активность: человек приходил.
    activity: completed_order

    # exclude          — гостевые заказы не считаем вовсе. Нижняя граница:
    #                    отвечает на «сколько ИЗВЕСТНЫХ нам клиентов».
    # count_each_order — каждый гостевой заказ считаем отдельным человеком.
    #                    Верхняя граница: один и тот же человек с тремя
    #                    гостевыми заказами превращается в трёх.
    # ignore_guests_exist — то же, что exclude, но честно называется:
    #                    гостей нет в знаменателе НИ ОДНОЙ метрики о клиентах.
    guests: exclude

    # Сущности, без которых метрику не посчитать. Список нужен не агенту,
    # а ОТБОРУ КОНТЕКСТА: он раньше подбирал таблицы по словам вопроса,
    # и в вопросе про выручку слова «возврат» нет — fct_returns не
    # попадала в промпт, агент честно докладывал, что возвратов в схеме
    # нет, и считал gross вместо net_of_returns. Определение метрики
    # знает, что ей нужно; вопрос — не знает.
    tables: [dim_customers, fct_order_items]

  revenue:
    label: Выручка
    description: >
      Сумма продаж в окне. Решения: какие статусы считаем, вычитаем ли
      возвраты и по какой дате их относим.
    window_days: 90
    statuses: [completed]      # добавь 'cancelled' — получишь спрос, а не выручку

    # gross          — до возвратов.
    # net_of_returns — за вычетом возвратов.
    basis: net_of_returns

    # return_date — возврат вычитается из того окна, КОГДА он случился.
    #               Операционный взгляд: месяц закрыт и больше не меняется.
    # order_date  — возврат вычитается из окна, где лежит ЗАКАЗ.
    #               Взгляд на качество продаж: март пересчитывается ещё три
    #               недели после марта.
    returns_attribution: return_date

    # previous_period — сравнивать окно с предыдущим таким же окном.
    # year_over_year  — сравнивать окно с тем же окном год назад.
    compare_to: year_over_year

    # include — гостевые заказы считаются наравне с остальными: выручка
    #           есть выручка, платил её человек с аккаунтом или без.
    # exclude — только заказы известных клиентов.
    #
    # Решение появилось после разбора: продакт попросил «выручку без
    # гостей», а такого объекта в этом слое не было вовсе. guests стояло
    # только у метрик о КЛИЕНТАХ, и агент собрал комбинацию сам —
    # непоследовательно: гостей отфильтровал в числителе и не
    # отфильтровал в вычитаемом. Развилка, которую никто не объявил,
    # опаснее незакрытой: спросить о ней было нельзя.
    #
    # ВАЖНО: решение действует на ОБЕ стороны формулы — и на продажи,
    # и на вычитаемые возвраты. Иначе получится тот самый чек 353.40
    # вместо 360.18.
    guests: include

    tables: [fct_order_items, fct_returns]

  return_rate:
    label: Доля возвратов
    description: >
      Какая часть проданного вернулась. Решения: считать строками или
      штуками и обрезать ли окно на дозревание.
    window_days: 90
    grain: line                # line | unit

    # Возврат приходит через 3-21 день. Заказы последних 21 дня перед
    # курсором ещё не дозрели: часть их возвратов не случилась.
    # 21 — считать только по дозревшим когортам.
    # 0  — считать по всему окну и получить заниженную цифру, которая
    #      растёт задним числом при каждом сдвиге курсора.
    maturity_days: 21

    tables: [fct_returns, fct_order_items]

Если интересно обсудить — напишите в Telegram.

← Все статьи