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'ами, которые читает движок):
|
||||
• 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
|
||||
(точная копия _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`), а не покрытие маппинга
|
||||
domrf↔objective. Продьюсера для второго нет и не было (#2464-H). Прод 13.08:
|
||||
медиана 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 / тренды не установлены.
|
||||
• confounded — шок-окно (is_confounded_window, PR2): ряд пересекает структурный
|
||||
разрыв → оценки смещены (НИКОГДА не 'high').
|
||||
|
|
@ -84,7 +86,8 @@ _SERVICE_RU_DEFAULT: str = "Компонент"
|
|||
|
||||
# deal_count: число сделок (продаж) за окно. high — длинная плотная выборка,
|
||||
# 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_LOW: int = 15
|
||||
|
||||
|
|
@ -101,8 +104,14 @@ _ANALOG_COUNT_LOW: int = 2 # < этого (т.е. ≤1 ЖК) → low
|
|||
_DOMRF_COVERAGE_HIGH: float = 0.6
|
||||
_DOMRF_COVERAGE_LOW: float = 0.2
|
||||
|
||||
# history_months: глубина ряда (мес). Зеркало §9.6 _CONF_HIGH_MIN_OBS=24 (≥2 года) /
|
||||
# _MIN_OBS=8 (пол): короткий ряд → тренды/чувствительность не установлены.
|
||||
# history_months: глубина ряда (мес). Пороги созвучны rate_sensitivity:
|
||||
# _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_LOW: int = 12
|
||||
|
||||
|
|
|
|||
|
|
@ -588,8 +588,13 @@ def _timing_overlap(
|
|||
) -> float | None:
|
||||
"""Ось тайминга: временна́я близость окон запуска. PURE.
|
||||
|
||||
exp(−|Δмесяцев| / half_life): одновременный выход → 1.0, расхождение в half_life мес
|
||||
→ 0.5, дальше затухает. Чем ближе наши запуски, тем сильнее пересекаются окна продаж
|
||||
0.5 ** (|Δмесяцев| / half_life): одновременный выход → 1.0, расхождение в half_life
|
||||
мес → ровно 0.5, дальше затухает.
|
||||
|
||||
Формула в докстринге раньше была записана как exp(−Δ/half_life) — она даёт при
|
||||
Δ=half_life не 0.5, а exp(−1) ≈ 0.368, то есть противоречила соседнему же
|
||||
утверждению «→ 0.5». Верен КОД (строка ниже несёт то же пояснение); расходился
|
||||
докстринг (#2464 кластер H). Чем ближе наши запуски, тем сильнее пересекаются окна продаж
|
||||
= выше каннибализация. Любая дата None → None (ось НЕДОСТУПНА — НЕ фабрикуем). PURE.
|
||||
"""
|
||||
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")
|
||||
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]
|
||||
seen = 0
|
||||
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