fix(generator): правки по двум линиям ревью плана состава

- Зачем:
  - ревью нашло два места, где код и документы говорили неправду, и
    несколько мест, где имена или комментарии вводили в заблуждение.
- Что:
  - обещание докстроки `seeds.py` подкреплено тестом: адрес в дереве
    даёт тот же подпоток, что цепочка `spawn`.
  - в тесте гарантии пар убран сторож-тавтология, вместо него проверка,
    что заказы назначены с двух разных кук.
  - `Cohort.visitors_on` — «кто пришёл в день D» спрашивается у когорты,
    а не собирается снаружи из четырёх её массивов.
  - имена: `CLIENT_ID_LIMIT`, `_RETURN_*_CUMULATIVE`, `first_of_day`,
    `WEEKLY_PROFILE_PERCENT` — профиль недели один на весь мир, по нему
    же пойдёт трафик дня-функции (#39).
  - спека: в дерево зёрен внесена ветвь предыстории; окно активности —
    от первого визита человека, общее на обе куки (иначе загляд назад
    ленивой формы удваивается); оценка накопленной аудитории больше не
    спорит с измерением.
  - CONTEXT.md: «подпоток» и «конфигурация мира».
- Проверка:
  - make test (296), make lint, make typecheck; мутации перепроверены
    после переноса среза дня в `Cohort`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 12:33:27 +03:00
co-authored by Claude Opus 5
parent abac6afe18
commit 2502f906a8
8 changed files with 98 additions and 52 deletions
+10
View File
@@ -20,6 +20,16 @@ _Избегать_: состояние мира
Способ спросить состав мира: функция зерна, выдающая его по дням — Способ спросить состав мира: функция зерна, выдающая его по дням —
когорту новых кук, их возвраты, назначенные заказы двухкуковых пар. когорту новых кук, их возвраты, назначенные заказы двухкуковых пар.
**Подпоток**:
Ветвь дерева случайности генератора: своё зерно у состава мира, у каждого
дня и у каждого компонента дня. Подпоток задан позицией в дереве, а не
порядком вычислений.
**Конфигурация мира**:
Модуль чистых данных со всеми числами мира: приток, профили возвратов, доли
покупателей, D0. Правка модуля — смена мира. Модуль-близнец контракта схемы:
там колонки, здесь числа.
**Когорта дня**: **Когорта дня**:
Люди, впервые пришедшие в мир в один день, со всеми их куками и визитами. Люди, впервые пришедшие в мир в один день, со всеми их куками и визитами.
Единица плана состава: когорта — функция зерна и номера дня. Единица плана состава: когорта — функция зерна и номера дня.
+27 -20
View File
@@ -66,9 +66,12 @@
дня D — функция (зерно, D), её случайность — подпоток состава мира, дня D — функция (зерно, D), её случайность — подпоток состава мира,
ветвящийся по номеру дня (позиция в дереве — раздел 2), отдельный от ветвящийся по номеру дня (позиция в дереве — раздел 2), отдельный от
подпотока дня-функции. Аудитория дня — когорта D плюс возвраты когорт подпотока дня-функции. Аудитория дня — когорта D плюс возвраты когорт
последних дней: окно активности куки — хвост возвратов (константа последних дней: окно активности — хвост возвратов (константа мира —
мира — раздел 9), отсчитанный от её первого визита; за краем окна кука раздел 9), отсчитанный от первого визита человека и общий на обе его
не возвращается, а профиль возвратов затухает к краю, поэтому обрыв в куки (уточнение при исполнении #38, 2026-08-02: окно от рождения
каждой куки растянуло бы жизнь когорты вдвое, а с ней и загляд назад,
которым ленивая форма и держится). За краем окна кука не
возвращается, а профиль возвратов затухает к краю, поэтому обрыв в
данных не виден. Стоимость дня не зависит от прожитого — день 500 данных не виден. Стоимость дня не зависит от прожитого — день 500
стоит как день 5, горизонт не является входом. Счётчики состава стоит как день 5, горизонт не является входом. Счётчики состава
считаются прогоном плана по дням, без генерации событий. считаются прогоном плана по дням, без генерации событий.
@@ -84,7 +87,8 @@
зазора — по тому же затухающему профилю, что и возвраты (уточнение при зазора — по тому же затухающему профилю, что и возвраты (уточнение при
исполнении #38, 2026-08-02): куку человек заводит, пока ещё ходит. исполнении #38, 2026-08-02): куку человек заводит, пока ещё ходит.
Равные шансы по всему окну означали бы вторую куку у давно ушедшего Равные шансы по всему окну означали бы вторую куку у давно ушедшего
человека — и вчетверо меньше пар, реализованных внутри снимка. Каждой паре план назначает дни гарантированных заказов: по человека — и вчетверо меньше пар, реализованных внутри снимка.
Каждой паре план назначает дни гарантированных заказов: по
одному на куку, из дней визитов этой куки, на оси от D0 и позже; одному на куку, из дней визитов этой куки, на оси от D0 и позже;
человеку предыстории, чьё окно активности таких дней не оставляет, человеку предыстории, чьё окно активности таких дней не оставляет,
пара не назначается. День-функция обязана назначенные заказы пара не назначается. День-функция обязана назначенные заказы
@@ -161,7 +165,10 @@
любой платформе — баг генератора, а не допуск. любой платформе — баг генератора, а не допуск.
- **Раздача зерна — иерархией подпотоков.** Корневое зерно → подпоток - **Раздача зерна — иерархией подпотоков.** Корневое зерно → подпоток
состава мира, ветвящийся по номеру дня на когорты плана (состав состава мира, ветвящийся по номеру дня на когорты плана (состав
спрашивается по дням — раздел 1; уточнение при исполнении #38); спрашивается по дням — раздел 1; уточнение при исполнении #38).
Дни предыстории отрицательны, а позиция в дереве — неотрицательное
целое, поэтому у предыстории своя ветвь состава, отдельная от оси
(уточнение при исполнении #38, 2026-08-02);
(зерно, день) → подпоток дня → именованные подпотоки компонентов: трафик, (зерно, день) → подпоток дня → именованные подпотоки компонентов: трафик,
торговые события, расхождения, опоздания — в фиксированном порядке. торговые события, расхождения, опоздания — в фиксированном порядке.
По построению: параллельный прогон равен последовательному; продление По построению: параллельный прогон равен последовательному; продление
@@ -403,30 +410,30 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
возвращающиеся — в среднем 3–4 возврата, профиль убывающий: почти все возвращающиеся — в среднем 3–4 возврата, профиль убывающий: почти все
в первые 7–10 дней, тонкий хвост поздних возвратов и повторных в первые 7–10 дней, тонкий хвост поздних возвратов и повторных
покупок — до края окна (цикл повторной покупки магазина — месяцы). покупок — до края окна (цикл повторной покупки магазина — месяцы).
Хвост возвратов — окно активности куки от первого визита — и глубина Хвост возвратов — окно активности человека от его первого визита,
предыстории: 90 дней (решение владельца 2026-08-02: дольше квартала общее на обе его куки, — и глубина предыстории: 90 дней (решение
стенд никто не гоняет, а заказы старых посетителей продолжаются весь владельца 2026-08-02: дольше квартала стенд никто не гоняет, а заказы
прогон; плата — чуть меньше пар, полностью реализованных внутри старых посетителей продолжаются весь прогон; плата — чуть меньше пар,
14-дневного снимка). Покупатели считаются людьми, полностью реализованных внутри 14-дневного снимка). Покупатели
не куками; доля покупателей — 5% людей когорты, а людей в когорте считаются людьми, не куками; доля покупателей — 5% людей когорты, а
почти столько же, сколько кук: вторые куки пар добавляют меньше людей в когорте почти столько же, сколько кук: вторые куки пар
процента. Доля — число плана, а не торгового тикета: без него не добавляют меньше процента. Доля — число плана, а не торгового тикета: без него не
отобрать двухкуковые пары; #40 наследует его, не переоткрывая. отобрать двухкуковые пары; #40 наследует его, не переоткрывая.
Сходимость с разделом 5: средняя кука активна ≈1,9 дня → дневная Сходимость с разделом 5: средняя кука активна ≈1,9 дня → дневная
аудитория ≈7 100 — середина вилки 6–8 тыс.; уникумов за 14 дней аудитория ≈7 100 — середина вилки 6–8 тыс.; уникумов за 14 дней
снимка ≈60 тыс. — ≈53 тыс. новыми куками плюс возвраты предыстории снимка — под 70 тыс.: новые куки плюс возвраты предыстории (точные
(не путать с ~50 тыс. событий одного дня) — рост `uniq(ClientID)` с числа — в измерении ниже; не путать с ~50 тыс. событий одного дня) —
горизонтом виден сразу; новых покупателей ~190 в день → конверсия рост `uniq(ClientID)` с горизонтом виден сразу; новых покупателей
~2% на сессию. ~190 в день → конверсия ~2% на сессию.
- **D0 = 2026-06-01, понедельник** — решение владельца при нарезке - **D0 = 2026-06-01, понедельник** — решение владельца при нарезке
(2026-08-01). Дата недавняя, чтобы данные первые месяцы выглядели (2026-08-01). Дата недавняя, чтобы данные первые месяцы выглядели
свежими; привязки к реальному календарю у констант мира всё равно нет. свежими; привязки к реальному календарю у констант мира всё равно нет.
- **Измерено на собранном плане** (#38, канонический мир, 14 дней): - **Измерено на собранном плане** (#38, канонический мир, 14 дней):
приток 3 828 кук в день в среднем, дневная аудитория 6 235–7 124 — приток 3 828 кук в день в среднем, дневная аудитория 6 235–7 124 —
обе величины в вилках раздела 5. Накопленная аудитория за снимок — обе величины в вилках раздела 5. Накопленная аудитория за снимок —
68 тыс. кук: 53,6 тыс. новыми плюс 14,5 тыс. возвратами предыстории, 68 тыс. кук: 53,6 тыс. новыми плюс 14,5 тыс. возвратами предыстории
а не ≈60 тыс., как оценивалось выше, — возвраты предыстории при (при нарезке возвраты предыстории оценили вдвое скромнее — отсюда
оценке посчитали вдвое скромнее. Пар, у которых оба назначенных ходившая раньше оценка ≈60 тыс.). Пар, у которых оба назначенных
заказа попали внутрь 14 дней, — 170; остальные пары горизонт заказа попали внутрь 14 дней, — 170; остальные пары горизонт
переживают, их вторая кука приходит позже. Цифры пересчитываются переживают, их вторая кука приходит позже. Цифры пересчитываются
прогоном плана, событий для них не нужно. прогоном плана, событий для них не нужно.
+32 -21
View File
@@ -30,13 +30,13 @@ from clickstream_generator import world
from clickstream_generator.seeds import cohort_stream from clickstream_generator.seeds import cohort_stream
# Куки живут числами ниже 2^53: выше JSON округляет — тот же довод, что у # Куки живут числами ниже 2^53: выше JSON округляет — тот же довод, что у
# `WatchID` в контракте схемы. # `WatchID` в контракте схемы. Граница не достигается: 2^53 сам уже за ней.
MAX_CLIENT_ID = 2**53 CLIENT_ID_LIMIT = 2**53
# Кумулятивные веса: выбор по ним — целочисленный, бросок попадает в чью-то # Кумулятивные веса: выбор по ним — целочисленный, бросок попадает в чью-то
# долю общего веса. # долю общего веса.
_RETURN_COUNTS = np.cumsum(world.RETURN_COUNT_WEIGHTS) _RETURN_COUNT_CUMULATIVE = np.cumsum(world.RETURN_COUNT_WEIGHTS)
_RETURN_DELAYS = np.cumsum(world.RETURN_DELAY_WEIGHTS) _RETURN_DELAY_CUMULATIVE = np.cumsum(world.RETURN_DELAY_WEIGHTS)
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -75,6 +75,18 @@ class Cohort:
"""Сколько пар получили назначенные заказы.""" """Сколько пар получили назначенные заказы."""
return len(self.pair_cookies) return len(self.pair_cookies)
def visitors_on(
self, day: int
) -> tuple[NDArray[np.uint64], NDArray[np.bool_], NDArray[np.bool_]]:
"""Кто из когорты пришёл в день `day`: куки, покупатели, заказы пар.
Как визиты и пары уложены в массивы, знает только когорта: снаружи
спрашивают день и получают три ряда одной длины.
"""
here = self.visit_cookie[self.visit_day == day]
ordering = self.pair_cookies[self.pair_order_days == day]
return self.client_id[here], self.buyer[here], np.isin(here, ordering)
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class DayAudience: class DayAudience:
@@ -119,12 +131,12 @@ def cohort(seed: int, day: int) -> Cohort:
] ]
cookies = people + paired.size cookies = people + paired.size
client_id = rng.integers(1, MAX_CLIENT_ID, cookies, dtype=np.uint64) client_id = rng.integers(1, CLIENT_ID_LIMIT, cookies, dtype=np.uint64)
birth_day = np.full(cookies, day, dtype=np.int64) birth_day = np.full(cookies, day, dtype=np.int64)
# Вторая кука рождается, пока человек ещё ходит: тем же затухающим # Вторая кука рождается, пока человек ещё ходит: тем же затухающим
# профилем, что и возвраты, — обычно через дни, изредка через месяцы. # профилем, что и возвраты, — обычно через дни, изредка через месяцы.
# Фиксированного зазора нет, иначе пары в данных узнавались бы по нему. # Фиксированного зазора нет, иначе пары в данных узнавались бы по нему.
birth_day[people:] += 1 + _pick(rng, _RETURN_DELAYS, paired.size) birth_day[people:] += 1 + _pick(rng, _RETURN_DELAY_CUMULATIVE, paired.size)
visit_cookie, visit_day = _visits(rng, birth_day, day + world.RETURN_TAIL_DAYS) visit_cookie, visit_day = _visits(rng, birth_day, day + world.RETURN_TAIL_DAYS)
pair_cookies, pair_order_days = _assign_orders( pair_cookies, pair_order_days = _assign_orders(
@@ -156,12 +168,10 @@ def audience(seed: int, day: int) -> DayAudience:
client_id, buyer, assigned = [], [], [] client_id, buyer, assigned = [], [], []
# Предыстория ровно такой глубины, чтобы окна хватило и первому дню оси. # Предыстория ровно такой глубины, чтобы окна хватило и первому дню оси.
for born in range(day - world.RETURN_TAIL_DAYS, day + 1): for born in range(day - world.RETURN_TAIL_DAYS, day + 1):
born_cohort = cohort(seed, born) came, bought, ordered = cohort(seed, born).visitors_on(day)
here = born_cohort.visit_cookie[born_cohort.visit_day == day] client_id.append(came)
client_id.append(born_cohort.client_id[here]) buyer.append(bought)
buyer.append(born_cohort.buyer[here]) assigned.append(ordered)
ordering = born_cohort.pair_cookies[born_cohort.pair_order_days == day]
assigned.append(np.isin(here, ordering))
return DayAudience( return DayAudience(
day=day, day=day,
client_id=np.concatenate(client_id), client_id=np.concatenate(client_id),
@@ -177,7 +187,7 @@ def counters(seed: int, days: int) -> PlanCounters:
seen: list[NDArray[np.uint64]] = [] seen: list[NDArray[np.uint64]] = []
pairs = 0 pairs = 0
# Дальше горизонта когорты не заглядывают, ближе предыстории — не живут. # Ниже — вся предыстория: её когорты ещё возвращаются в горизонт.
for born in range(-world.RETURN_TAIL_DAYS, days): for born in range(-world.RETURN_TAIL_DAYS, days):
born_cohort = cohort(seed, born) born_cohort = cohort(seed, born)
inside = (born_cohort.visit_day >= 0) & (born_cohort.visit_day < days) inside = (born_cohort.visit_day >= 0) & (born_cohort.visit_day < days)
@@ -200,7 +210,7 @@ def counters(seed: int, days: int) -> PlanCounters:
def _influx(rng: np.random.Generator, day: int) -> int: def _influx(rng: np.random.Generator, day: int) -> int:
"""Сколько новых людей приходит в этот день: число мира по недельной волне.""" """Сколько новых людей приходит в этот день: число мира по недельной волне."""
base = world.DAILY_INFLUX * world.WEEKLY_INFLUX_PERCENT[day % 7] // 100 base = world.DAILY_INFLUX * world.WEEKLY_PROFILE_PERCENT[day % 7] // 100
spread = base * world.INFLUX_JITTER_PERCENT // 100 spread = base * world.INFLUX_JITTER_PERCENT // 100
return base + int(rng.integers(-spread, spread + 1)) return base + int(rng.integers(-spread, spread + 1))
@@ -212,10 +222,10 @@ def _visits(
cookies = birth_day.size cookies = birth_day.size
returns = np.zeros(cookies, dtype=np.int64) returns = np.zeros(cookies, dtype=np.int64)
returning = rng.integers(0, 100, cookies) >= world.ONE_SHOT_PERCENT returning = rng.integers(0, 100, cookies) >= world.ONE_SHOT_PERCENT
returns[returning] = 1 + _pick(rng, _RETURN_COUNTS, int(returning.sum())) returns[returning] = 1 + _pick(rng, _RETURN_COUNT_CUMULATIVE, int(returning.sum()))
owner = np.repeat(np.arange(cookies, dtype=np.int64), returns) owner = np.repeat(np.arange(cookies, dtype=np.int64), returns)
delay = 1 + _pick(rng, _RETURN_DELAYS, owner.size) delay = 1 + _pick(rng, _RETURN_DELAY_CUMULATIVE, owner.size)
cookie = np.concatenate((np.arange(cookies, dtype=np.int64), owner)) cookie = np.concatenate((np.arange(cookies, dtype=np.int64), owner))
when = np.concatenate((birth_day, birth_day[owner] + delay)) when = np.concatenate((birth_day, birth_day[owner] + delay))
@@ -226,9 +236,9 @@ def _visits(
cookie, when = cookie[order], when[order] cookie, when = cookie[order], when[order]
# Два возврата в один день — один визит: день у куки бывает только один. # Два возврата в один день — один визит: день у куки бывает только один.
once = np.ones(cookie.size, dtype=bool) first_of_day = np.ones(cookie.size, dtype=bool)
once[1:] = (cookie[1:] != cookie[:-1]) | (when[1:] != when[:-1]) first_of_day[1:] = (cookie[1:] != cookie[:-1]) | (when[1:] != when[:-1])
return cookie[once], when[once] return cookie[first_of_day], when[first_of_day]
def _assign_orders( def _assign_orders(
@@ -247,8 +257,9 @@ def _assign_orders(
""" """
on_axis = visit_day >= 0 on_axis = visit_day >= 0
counts = np.bincount(visit_cookie[on_axis], minlength=cookies) counts = np.bincount(visit_cookie[on_axis], minlength=cookies)
# Визиты куки идут подряд и по возрастанию дня, поэтому дни от D0 — хвост # Визиты отсортированы по куке, а внутри куки — по дню, и дни от D0 идут
# её блока: до конца блока ровно `counts` визитов. # последними. Значит, дни на оси у куки — хвост её блока: от конца блока
# назад ровно `counts` визитов.
first_on_axis = np.searchsorted(visit_cookie, np.arange(cookies), "right") - counts first_on_axis = np.searchsorted(visit_cookie, np.arange(cookies), "right") - counts
assigned = np.all(counts[pair_cookies] > 0, axis=1) assigned = np.all(counts[pair_cookies] > 0, axis=1)
+1 -1
View File
@@ -25,7 +25,7 @@ from enum import IntEnum
import numpy as np import numpy as np
# Канонический зерно эталонного мира — константа репозитория; манифест хранит # Каноническое зерно эталонного мира — константа репозитория; манифест хранит
# его в паспорте мира. Свои зёрна менти крутит без гарантий манифеста. # его в паспорте мира. Свои зёрна менти крутит без гарантий манифеста.
CANONICAL_SEED = 20260601 CANONICAL_SEED = 20260601
+7 -5
View File
@@ -7,7 +7,8 @@
Числа решены спекой и связаны между собой; связки сторожат тесты Числа решены спекой и связаны между собой; связки сторожат тесты
`test_world.py`, чтобы правка одного числа не рассыпала вывод соседнего. `test_world.py`, чтобы правка одного числа не рассыпала вывод соседнего.
Здесь только чистые данные как в контракте схемы, никакой логики. Здесь только числа как в контракте схемы, никакого поведения; длинные
таблицы записаны коротко, но остаются таблицами.
""" """
from datetime import date from datetime import date
@@ -23,10 +24,11 @@ ORIGIN = date(2026, 6, 1)
# добавляют к нему меньше процента. # добавляют к нему меньше процента.
DAILY_INFLUX = 3_800 DAILY_INFLUX = 3_800
# Недельная волна притока, проценты от среднего: понедельник … воскресенье. # Недельная волна мира, проценты от среднего: понедельник … воскресенье.
# Модулируется тем же профилем, что трафик, — иначе доля новичков скакала бы # Профиль один на весь мир: по нему идёт приток, по нему же пойдёт трафик
# по дням недели. В сумме ровно 700: за неделю средний день остаётся средним. # дня-функции — иначе доля новичков скакала бы по дням недели. В сумме ровно
WEEKLY_INFLUX_PERCENT = (105, 108, 107, 105, 95, 88, 92) # 700: за неделю средний день остаётся средним.
WEEKLY_PROFILE_PERCENT = (105, 108, 107, 105, 95, 88, 92)
# Разброс притока изо дня в день, проценты: ровный приток выдал бы себя в # Разброс притока изо дня в день, проценты: ровный приток выдал бы себя в
# первом же графике по дням. # первом же графике по дням.
+3 -3
View File
@@ -141,7 +141,7 @@ def test_every_pair_orders_from_both_cookies_on_the_axis(day: int):
for cookies, days in zip( for cookies, days in zip(
cohort.pair_cookies.tolist(), cohort.pair_order_days.tolist(), strict=True cohort.pair_cookies.tolist(), cohort.pair_order_days.tolist(), strict=True
): ):
assert len(set(days)) == len(days) or cookies[0] != cookies[1] assert cookies[0] != cookies[1], "заказы пары — с двух разных кук"
for cookie, order_day in zip(cookies, days, strict=True): for cookie, order_day in zip(cookies, days, strict=True):
assert order_day >= 0 assert order_day >= 0
assert (cookie, order_day) in visits assert (cookie, order_day) in visits
@@ -168,8 +168,8 @@ def test_the_influx_breathes_with_the_week():
counters = plan.counters(CANONICAL_SEED, SNAPSHOT_DAYS) counters = plan.counters(CANONICAL_SEED, SNAPSHOT_DAYS)
weekdays = [size for day, size in enumerate(counters.new_cookies) if day % 7 < 5] weekdays = [size for day, size in enumerate(counters.new_cookies) if day % 7 < 5]
weekend = [size for day, size in enumerate(counters.new_cookies) if day % 7 >= 5] weekend = [size for day, size in enumerate(counters.new_cookies) if day % 7 >= 5]
expected = mean(world.WEEKLY_INFLUX_PERCENT[5:]) / mean( expected = mean(world.WEEKLY_PROFILE_PERCENT[5:]) / mean(
world.WEEKLY_INFLUX_PERCENT[:5] world.WEEKLY_PROFILE_PERCENT[:5]
) )
assert abs(mean(weekend) / mean(weekdays) - expected) < 0.03 assert abs(mean(weekend) / mean(weekdays) - expected) < 0.03
+16
View File
@@ -25,6 +25,22 @@ def test_canonical_seed_is_a_repository_constant():
assert isinstance(CANONICAL_SEED, int) assert isinstance(CANONICAL_SEED, int)
def test_addressing_a_subtree_equals_spawning_down_to_it():
"""Свойство numpy, на котором стоит вся раздача зерна.
Потомок определяется парой (зерно, позиция в дереве): выписанный руками
`spawn_key` даёт тот же подпоток, что цепочка `spawn`. Иначе результат
зависел бы от порядка вычислений, и «параллельно равно последовательно»
не выполнялось бы.
"""
chained = np.random.SeedSequence(CANONICAL_SEED).spawn(1)[0].spawn(4)[3]
addressed = np.random.SeedSequence(CANONICAL_SEED, spawn_key=(0, 3))
assert chained.spawn_key == addressed.spawn_key
assert first_draws(np.random.Generator(np.random.PCG64(chained))) == first_draws(
np.random.Generator(np.random.PCG64(addressed))
)
def test_stream_is_a_position_in_the_tree_not_an_order_of_calls(): def test_stream_is_a_position_in_the_tree_not_an_order_of_calls():
straight = first_draws(cohort_stream(CANONICAL_SEED, 5)) straight = first_draws(cohort_stream(CANONICAL_SEED, 5))
cohort_stream(CANONICAL_SEED, 0) cohort_stream(CANONICAL_SEED, 0)
+2 -2
View File
@@ -21,8 +21,8 @@ def test_origin_is_a_monday():
def test_weekly_profile_covers_a_week_and_averages_to_one(): def test_weekly_profile_covers_a_week_and_averages_to_one():
assert len(world.WEEKLY_INFLUX_PERCENT) == 7 assert len(world.WEEKLY_PROFILE_PERCENT) == 7
assert sum(world.WEEKLY_INFLUX_PERCENT) == 700 assert sum(world.WEEKLY_PROFILE_PERCENT) == 700
def test_returning_share_matches_the_spec(): def test_returning_share_matches_the_spec():