docs(ptica): три комментария, утверждавших не то, что делает код (#2464 кластер H) (#2943)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 2m4s
Deploy / build-worker (push) Successful in 3m9s
Deploy / deploy (push) Successful in 1m27s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 2m4s
Deploy / build-worker (push) Successful in 3m9s
Deploy / deploy (push) Successful in 1m27s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
This commit is contained in:
parent
8eeb35cee5
commit
43c71a006d
4 changed files with 71 additions and 8 deletions
|
|
@ -24,7 +24,8 @@ High/Medium/Low + RU-причину, которая ЯВНО НАЗЫВАЕТ,
|
||||||
и причина это ПРОГОВАРИВАЕТ. Честность важнее оптимистичной метки.
|
и причина это ПРОГОВАРИВАЕТ. Честность важнее оптимистичной метки.
|
||||||
|
|
||||||
ПОРОГИ (align с per-service gate'ами, которые читает движок):
|
ПОРОГИ (align с per-service gate'ами, которые читает движок):
|
||||||
• deal_count — зеркало market_metrics._confidence (n_lots/n_sold) + §9.6 _MIN_OBS:
|
• deal_count — зеркало market_metrics._confidence (n_lots/n_sold) + порог
|
||||||
|
rate_sensitivity._MIN_OBS:
|
||||||
мало сделок → скоростные метрики статистически ненадёжны.
|
мало сделок → скоростные метрики статистически ненадёжны.
|
||||||
• analog_count (ЖК-аналоги, = market_metrics.obj_count) — high≥3 / medium≥2 / 1 → low
|
• analog_count (ЖК-аналоги, = market_metrics.obj_count) — high≥3 / medium≥2 / 1 → low
|
||||||
(точная копия _CONF_HIGH_MIN_OBJ=3 / _CONF_MEDIUM_MIN_OBJ=2; «1 ЖК» — ТЗ §15-пример).
|
(точная копия _CONF_HIGH_MIN_OBJ=3 / _CONF_MEDIUM_MIN_OBJ=2; «1 ЖК» — ТЗ §15-пример).
|
||||||
|
|
@ -32,7 +33,8 @@ High/Medium/Low + RU-причину, которая ЯВНО НАЗЫВАЕТ,
|
||||||
из Objective (`analyze.market_data_coverage_pct`), а не покрытие маппинга
|
из Objective (`analyze.market_data_coverage_pct`), а не покрытие маппинга
|
||||||
domrf↔objective. Продьюсера для второго нет и не было (#2464-H). Прод 13.08:
|
domrf↔objective. Продьюсера для второго нет и не было (#2464-H). Прод 13.08:
|
||||||
медиана 40%, среднее 31.7%. Низкое покрытие → рынок и конкуренция оценены хуже.
|
медиана 40%, среднее 31.7%. Низкое покрытие → рынок и конкуренция оценены хуже.
|
||||||
• history_months — зеркало §9.6 _CONF_HIGH_MIN_OBS=24 (≥2 года) / _MIN_OBS=8: короткий
|
• history_months — созвучно rate_sensitivity._CONF_HIGH_MIN_OBS=24 (≥2 года) /
|
||||||
|
_MIN_OBS=8 (НЕ §9.6: там свой _MIN_OBS=30, см. комментарий у констант): короткий
|
||||||
ряд → связь rate↔sales / тренды не установлены.
|
ряд → связь rate↔sales / тренды не установлены.
|
||||||
• confounded — шок-окно (is_confounded_window, PR2): ряд пересекает структурный
|
• confounded — шок-окно (is_confounded_window, PR2): ряд пересекает структурный
|
||||||
разрыв → оценки смещены (НИКОГДА не 'high').
|
разрыв → оценки смещены (НИКОГДА не 'high').
|
||||||
|
|
@ -84,7 +86,8 @@ _SERVICE_RU_DEFAULT: str = "Компонент"
|
||||||
|
|
||||||
# deal_count: число сделок (продаж) за окно. high — длинная плотная выборка,
|
# deal_count: число сделок (продаж) за окно. high — длинная плотная выборка,
|
||||||
# medium — рабочий минимум, low — статистически ненадёжно (зеркало духа
|
# medium — рабочий минимум, low — статистически ненадёжно (зеркало духа
|
||||||
# market_metrics: n_sold>0 обязателен; §9.6 _MIN_OBS=8 — пол для регрессии).
|
# market_metrics: n_sold>0 обязателен; rate_sensitivity._MIN_OBS=8 — пол для оценки
|
||||||
|
# чувствительности. НЕ §9.6: у регрессии §9.6 порог свой, _MIN_OBS=30.)
|
||||||
_DEAL_COUNT_HIGH: int = 50
|
_DEAL_COUNT_HIGH: int = 50
|
||||||
_DEAL_COUNT_LOW: int = 15
|
_DEAL_COUNT_LOW: int = 15
|
||||||
|
|
||||||
|
|
@ -101,8 +104,14 @@ _ANALOG_COUNT_LOW: int = 2 # < этого (т.е. ≤1 ЖК) → low
|
||||||
_DOMRF_COVERAGE_HIGH: float = 0.6
|
_DOMRF_COVERAGE_HIGH: float = 0.6
|
||||||
_DOMRF_COVERAGE_LOW: float = 0.2
|
_DOMRF_COVERAGE_LOW: float = 0.2
|
||||||
|
|
||||||
# history_months: глубина ряда (мес). Зеркало §9.6 _CONF_HIGH_MIN_OBS=24 (≥2 года) /
|
# history_months: глубина ряда (мес). Пороги созвучны rate_sensitivity:
|
||||||
# _MIN_OBS=8 (пол): короткий ряд → тренды/чувствительность не установлены.
|
# _CONF_HIGH_MIN_OBS=24 (≥2 года Δln-наблюдений) и _MIN_OBS=8 (пол, ниже которого
|
||||||
|
# чувствительность не считаем). Короткий ряд → тренды/чувствительность не установлены.
|
||||||
|
#
|
||||||
|
# #2464 кластер H: раньше обе константы приписывались «§9.6». Это неверный адрес —
|
||||||
|
# §9.6 (forecasting/regression.py) держит СВОЙ _MIN_OBS=30 (gate-порог для claim) и
|
||||||
|
# _MIN_FIT_OBS=8 (можно ли вообще фитить). Совпадение цифры 8 в двух модулях и сбило
|
||||||
|
# ссылку. Значения 24/8 верны, неверна была атрибуция.
|
||||||
_HISTORY_MONTHS_HIGH: int = 24
|
_HISTORY_MONTHS_HIGH: int = 24
|
||||||
_HISTORY_MONTHS_LOW: int = 12
|
_HISTORY_MONTHS_LOW: int = 12
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -588,8 +588,13 @@ def _timing_overlap(
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""Ось тайминга: временна́я близость окон запуска. PURE.
|
"""Ось тайминга: временна́я близость окон запуска. PURE.
|
||||||
|
|
||||||
exp(−|Δмесяцев| / half_life): одновременный выход → 1.0, расхождение в half_life мес
|
0.5 ** (|Δмесяцев| / half_life): одновременный выход → 1.0, расхождение в half_life
|
||||||
→ 0.5, дальше затухает. Чем ближе наши запуски, тем сильнее пересекаются окна продаж
|
мес → ровно 0.5, дальше затухает.
|
||||||
|
|
||||||
|
Формула в докстринге раньше была записана как exp(−Δ/half_life) — она даёт при
|
||||||
|
Δ=half_life не 0.5, а exp(−1) ≈ 0.368, то есть противоречила соседнему же
|
||||||
|
утверждению «→ 0.5». Верен КОД (строка ниже несёт то же пояснение); расходился
|
||||||
|
докстринг (#2464 кластер H). Чем ближе наши запуски, тем сильнее пересекаются окна продаж
|
||||||
= выше каннибализация. Любая дата None → None (ось НЕДОСТУПНА — НЕ фабрикуем). PURE.
|
= выше каннибализация. Любая дата None → None (ось НЕДОСТУПНА — НЕ фабрикуем). PURE.
|
||||||
"""
|
"""
|
||||||
if candidate_month is None or own_month is None:
|
if candidate_month is None or own_month is None:
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,11 @@ def _extract_plan_from_next_data(html: str) -> str | None:
|
||||||
page_props = blob.get("props", {}).get("pageProps")
|
page_props = blob.get("props", {}).get("pageProps")
|
||||||
root: Any = page_props if isinstance(page_props, dict) else blob
|
root: Any = page_props if isinstance(page_props, dict) else blob
|
||||||
|
|
||||||
# BFS по вложенному dict/list; ключ+значение проверяем на plan-hint.
|
# DFS по вложенному dict/list (stack.pop() — LIFO); ключ+значение проверяем на
|
||||||
|
# plan-hint. Раньше здесь стояло «BFS» — неверно, и это не косметика: функция
|
||||||
|
# возвращает ПЕРВОЕ найденное совпадение, а при упоре в cap (20 000 узлов) обход
|
||||||
|
# успевает посмотреть разные подмножества дерева. То есть порядок влияет и на то,
|
||||||
|
# какой план найдётся, и на то, найдётся ли (#2464 кластер H).
|
||||||
stack: list[Any] = [root]
|
stack: list[Any] = [root]
|
||||||
seen = 0
|
seen = 0
|
||||||
while stack and seen < 20_000: # cap: защита от патологически глубокого JSON
|
while stack and seen < 20_000: # cap: защита от патологически глубокого JSON
|
||||||
|
|
|
||||||
45
backend/tests/services/test_2464h_timing_overlap_formula.py
Normal file
45
backend/tests/services/test_2464h_timing_overlap_formula.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""#2464 кластер H: докстринг `_timing_overlap` обещал не ту формулу.
|
||||||
|
|
||||||
|
Было записано `exp(−|Δмесяцев| / half_life)` и тут же — «расхождение в half_life
|
||||||
|
мес → 0.5». Эти два утверждения несовместимы: `exp(−1) ≈ 0.368`, а не 0.5.
|
||||||
|
|
||||||
|
Код при этом верен — он считает `0.5 ** (Δ/half_life)` и даёт ровно 0.5 на
|
||||||
|
половинном периоде. Расходился докстринг.
|
||||||
|
|
||||||
|
Тест закрепляет СВОЙСТВО, а не текст комментария: на Δ = half_life значение равно
|
||||||
|
0.5, и оно НЕ равно exp(−1). Второе утверждение существенно — без него тест прошёл
|
||||||
|
бы и на экспоненциальной формуле, если бы кто-то «починил» код по старому
|
||||||
|
докстрингу.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.services.forecasting.special_indices import _timing_overlap
|
||||||
|
|
||||||
|
|
||||||
|
def test_half_life_gives_exactly_one_half() -> None:
|
||||||
|
same = date(2026, 1, 1)
|
||||||
|
later = date(2026, 7, 1) # ровно 6 месяцев
|
||||||
|
|
||||||
|
value = _timing_overlap(later, same, half_life_months=6.0)
|
||||||
|
|
||||||
|
assert value is not None
|
||||||
|
assert abs(value - 0.5) < 1e-9, "на половинном периоде значение обязано быть 0.5"
|
||||||
|
assert abs(value - math.exp(-1)) > 0.1, (
|
||||||
|
"значение совпало с exp(−1) — значит формула экспоненциальная, "
|
||||||
|
"а докстринг обещает половинное затухание"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_simultaneous_launch_is_one() -> None:
|
||||||
|
same = date(2026, 1, 1)
|
||||||
|
assert _timing_overlap(same, same, half_life_months=6.0) == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_date_returns_none_not_a_number() -> None:
|
||||||
|
"""Контроль: недоступную ось не фабрикуем (обещание того же докстринга)."""
|
||||||
|
assert _timing_overlap(None, date(2026, 1, 1)) is None
|
||||||
|
assert _timing_overlap(date(2026, 1, 1), None) is None
|
||||||
Loading…
Add table
Reference in a new issue