docs(generator): путеводитель по генератору и живой проход make demo
- Зачем:
- спека отвечает «почему», а жанра «как пройти устройство по коду»
не было (#48); черновик в generator/README.md ждал выверки.
- Что:
- новый набор docs/architecture/generator/: индекс с целевой картиной
и mermaid-схемой плюс шесть поддокументов по темам тикета.
- демо-модуль и цель make demo: канонический D0, все стадии цепочки
на одном экране; тест-сторож проверяет стадии без прибитых чисел.
- раздел «Как это работает» в generator/README.md сжат до ссылки
на путеводитель, черновая врезка снята.
- Проверка:
- из generator/: make lint, make typecheck, make test (430 passed),
make demo; два слепых ревью и узкая перепроверка — находки закрыты.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
33aef73d0e
commit
0c466c082e
11 files changed
+392
-26
No files matched your search
+6
-1
@@ -1,4 +1,4 @@
|
||||
.PHONY: lint typecheck test docs inventory
|
||||
.PHONY: lint typecheck test demo docs inventory
|
||||
|
||||
# --- Проверки ---
|
||||
|
||||
@@ -12,6 +12,11 @@ typecheck:
|
||||
test:
|
||||
uv run pytest
|
||||
|
||||
# --- Пример из путеводителя ---
|
||||
|
||||
demo:
|
||||
uv run python -m clickstream_generator.demo
|
||||
|
||||
# --- Сборка из кода ---
|
||||
|
||||
docs:
|
||||
|
||||
+6
-25
@@ -13,31 +13,11 @@
|
||||
|
||||
## Как это работает
|
||||
|
||||
> Раздел черновой. По сути он верен, но на понятность читателем со стороны не
|
||||
> выверен: пишет его тот, кто держит устройство генератора в голове, а это
|
||||
> худший судья понятности. Выверка — вместе с путеводителем, тикет #48.
|
||||
|
||||
Цепочка одна и всегда та же: корневое зерно → подпотоки по позиции в дереве
|
||||
→ когорта дня (кто впервые пришёл, сколько раз вернётся, чей паспорт какой)
|
||||
→ аудитория дня, то есть когорта плюс возвраты когорт окна → визиты этих кук
|
||||
→ строки событий. Мир при этом ленив: когорта считается по требованию, а до
|
||||
D0 живёт предыстория, поэтому любой день собирается сам по себе — прожитая
|
||||
история для него не нужна и на него не влияет.
|
||||
|
||||
Главное правило для того, кто придёт следующим: **внутри подпотока порядок
|
||||
бросков — часть контракта.** Броски раздаёт один генератор подряд, и k-й
|
||||
бросок достаётся тому, кто спросил k-м. Приписать новый бросок в конец
|
||||
функции безопасно: у прежних он ничего не отнимает. Вставить в середину —
|
||||
значит сдвинуть все броски после него, а с ними и весь мир: события того же
|
||||
дня станут другими, счётчики канонического мира разойдутся с описью, и
|
||||
поймается это не ошибкой, а красным чеком. Ровно поэтому паспорта кук в
|
||||
`plan.cohort` брошены последними, а личность человека приписана уже за ними:
|
||||
куки, пары и паспорта остались теми же до байта.
|
||||
|
||||
Случайность целочисленная — только диапазоны и выбор по целым весам: готовые
|
||||
распределения numpy расходятся между версиями и архитектурами, а обещано
|
||||
побайтовое совпадение (спека, раздел 2). Отсюда `weights.py` вместо
|
||||
`rng.choice` с вероятностями.
|
||||
Корневое зерно ветвится на подпотоки, из них складываются когорты, аудитория
|
||||
дня, визиты, строки событий и заказы. План поднимает состав мира по требованию,
|
||||
а день-функция проживает один день без накопленного состояния. Код этой
|
||||
цепочки, ее схема и запуск D0 собраны в
|
||||
[путеводителе](../docs/architecture/generator/README.md).
|
||||
|
||||
## Что где лежит
|
||||
|
||||
@@ -104,6 +84,7 @@ D0 живёт предыстория, поэтому любой день соб
|
||||
- `make test` — тесты генератора;
|
||||
- `make lint` — ruff: проверка и формат;
|
||||
- `make typecheck` — ty: проверка типов;
|
||||
- `make demo` — пройти стадии канонического D0 на одном экране;
|
||||
- `make docs` — пересобрать описание выгрузки;
|
||||
- `make inventory` — пересобрать опись мира.
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Короткий проход по цепочке генератора на каноническом дне D0."""
|
||||
|
||||
from collections import Counter
|
||||
|
||||
import numpy as np
|
||||
|
||||
from clickstream_generator import day, plan, seeds, world
|
||||
|
||||
DAY = 0
|
||||
|
||||
|
||||
def _number(value: int) -> str:
|
||||
"""Разделяет разряды числа для чтения с экрана."""
|
||||
return f"{value:,}".replace(",", " ")
|
||||
|
||||
|
||||
def render() -> str:
|
||||
"""Собирает D0 и показывает его стадии без числового эталона."""
|
||||
seed = seeds.CANONICAL_SEED
|
||||
cohort = plan.cohort(seed, DAY)
|
||||
audience = plan.audience(seed, DAY)
|
||||
events = day.stream(seed, DAY)
|
||||
|
||||
_, rows_per_visit = np.unique(events.columns["VisitID"], return_counts=True)
|
||||
event_types = Counter(str(value) for value in events.columns["EventType"])
|
||||
smallest_visit = int(rows_per_visit.min())
|
||||
largest_visit = int(rows_per_visit.max())
|
||||
|
||||
return "\n".join(
|
||||
(
|
||||
f"Зерно: {seed}",
|
||||
"Подпотоки D0: состав мира → когорта; день → трафик, торговля, заказы",
|
||||
(
|
||||
f"Когорта D0: людей — {_number(cohort.people)}; "
|
||||
f"кук — {_number(len(cohort.client_id))}"
|
||||
),
|
||||
(
|
||||
f"Аудитория D0: кук — {_number(len(audience.client_id))}; "
|
||||
f"когорты D-{world.RETURN_TAIL_DAYS}...D0"
|
||||
),
|
||||
(
|
||||
f"Визиты по VisitID: групп — {_number(len(rows_per_visit))}; "
|
||||
f"строк в группе — {smallest_visit}–{largest_visit}"
|
||||
),
|
||||
(
|
||||
f"Строки событий: {_number(len(events))} "
|
||||
f"(pageview — {_number(event_types['pageview'])}; "
|
||||
f"add_to_cart — {_number(event_types['add_to_cart'])}; "
|
||||
f"purchase — {_number(event_types['purchase'])}); "
|
||||
f"заказов — {_number(len(events.orders))}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Печатает проход по D0."""
|
||||
print(render())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Запускаемый путеводитель остается коротким и проходит всю цепочку."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_demo_runs_and_shows_the_whole_chain() -> None:
|
||||
"""Сторожит команду и стадии, а числа мира оставляет описи."""
|
||||
finished = subprocess.run(
|
||||
["make", "--silent", "demo"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
cwd=Path(__file__).parents[1],
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert finished.returncode == 0, finished.stderr
|
||||
lines = finished.stdout.splitlines()
|
||||
assert [line.partition(":")[0] for line in lines] == [
|
||||
"Зерно",
|
||||
"Подпотоки D0",
|
||||
"Когорта D0",
|
||||
"Аудитория D0",
|
||||
"Визиты по VisitID",
|
||||
"Строки событий",
|
||||
]
|
||||
assert "когорты D-" in lines[3] and "...D0" in lines[3]
|
||||
assert "строк в группе" in lines[4]
|
||||
assert all(
|
||||
label in lines[5]
|
||||
for label in ("pageview", "add_to_cart", "purchase", "заказов")
|
||||
)
|
||||
Reference in new issue
Block a user