refactor(forecasting): rename commercial_share_pct → commercial_sell_through_pct (#1635)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Successful in 1m40s
CI / openapi-codegen-check (push) Successful in 1m38s
CI / backend-tests (pull_request) Successful in 9m51s
CI / changes (push) Successful in 7s
CI / frontend-tests (pull_request) Successful in 53s
CI / openapi-codegen-check (pull_request) Successful in 3m10s
CI / backend-tests (push) Successful in 9m52s

Внутренний recommendation→product_scoring контракт-ключ был мислейблом: величина —
темп распродажи нежилого (sell-through, прокси ликвидности/спроса), а НЕ доля нежилого
в объёме застройки. Переименован ключ + исправлены reason/docstring/комментарии у
потребителя _score_commercial. Числовая логика не изменена. Ключ внутренний (нет
frontend/schema/openapi-потребителей) → rename контракт-безопасен. pytest 171 passed.

Closes #1635
This commit is contained in:
Light1YT 2026-06-17 13:32:03 +05:00
parent 249182b678
commit c9ba595abe
4 changed files with 24 additions and 23 deletions

View file

@ -174,9 +174,10 @@ _MORTG_MAX_X_PCT: float = 25.0
# дифференциация). Overlay недоступен/сбой → None (НЕ 0 — нет данных ≠ нет ниш).
_DIFF_TARGET_USP: float = 3.0
# commercial: commercial_share_pct ∈ [0,100] (§10.4 реализованная доля нежилого) → /100.
# Сигнал ДЕГРАДИРОВАН по дизайну (objective ~ жильё): overlay.commercial.available=False
# → скор None (НЕ 0). available=True → share/100 как прокси силы спроса на коммерцию.
# commercial: commercial_sell_through_pct ∈ [0,100] (§10.4 темп распродажи нежилого,
# sell-through, прокси спроса) → /100. Сигнал ДЕГРАДИРОВАН по дизайну (objective ~ жильё):
# overlay.commercial.available=False → скор None (НЕ 0). available=True → sell_through/100
# как прокси силы спроса на коммерцию.
# confidence (МЕТА-скор): маппинг confidence-меток вкладывающих сервисов в [0,1] →
# усредняем с долей доступных скоров (data-quality). high=1.0 / medium=0.6 / low=0.25 —
@ -463,21 +464,24 @@ def _score_differentiation(
def _score_commercial(
commercial: dict[str, Any] | None,
) -> tuple[float | None, Confidence, str]:
"""commercial ← recommendation §10.4 сигнал → share_pct/100. ДЕГРАДИРУЕТ в None. PURE.
"""commercial ← recommendation §10.4 сигнал → sell_through_pct/100. ДЕГРАДИРУЕТ в None. PURE.
overlay.commercial.available=True commercial_share_pct/100 (прокси спроса на
нежилое). available=False / None (objective ~ жильё, тонко) None (НЕ 0-как-заглушка).
confidence наследуется из сигнала (или 'low').
overlay.commercial.available=True commercial_sell_through_pct/100 (темп распродажи
нежилого, sell-through, прокси спроса). available=False / None (objective ~ жильё, тонко)
None (НЕ 0-как-заглушка). confidence наследуется из сигнала (или 'low').
"""
if not commercial or not commercial.get("available"):
return None, "low", "Коммерческий сигнал §10.4 недоступен (нет данных по нежилому)."
share_pct = commercial.get("commercial_share_pct")
if share_pct is None:
return None, "low", "Коммерческий сигнал §10.4 недоступен (доля не измерена)."
sell_through_pct = commercial.get("commercial_sell_through_pct")
if sell_through_pct is None:
return None, "low", "Коммерческий сигнал §10.4 недоступен (темп распродажи не измерен)."
raw_conf = commercial.get("confidence", "low")
conf: Confidence = raw_conf if raw_conf in ("high", "medium", "low") else "low"
value = _clamp01(float(share_pct) / 100.0)
reason = f"Реализованная доля коммерции ~{round(float(share_pct), 1)}% (§10.4, прокси спроса)."
value = _clamp01(float(sell_through_pct) / 100.0)
reason = (
f"Темп распродажи нежилого ~{round(float(sell_through_pct), 1)}% (§10.4, прокси "
"ликвидности/спроса на нежилое; НЕ доля нежилого в объёме застройки)."
)
return value, conf, reason

View file

@ -774,12 +774,9 @@ def _commercial_signal(
return {
"available": True,
"premise_kind": _COMMERCIAL_PREMISE_KIND,
# NB #1635: ключ `commercial_share_pct` — МИСЛЕЙБЛ (величина = темп
# распродажи sell_through, а не доля нежилого в объёме). Это контракт-ключ,
# читаемый product_scoring._score_commercial (product_scoring.py:474, чей
# reason на :480 повторяет тот же мислейбл) — переименование в
# commercial_sell_through_pct cross-file, координировать с product_scoring.py.
"commercial_share_pct": sell_through_pct,
# #1635: ключ = ТЕМП РАСПРОДАЖИ нежилого (sell_through, прокси
# ликвидности/спроса), НЕ доля нежилого в объёме застройки.
"commercial_sell_through_pct": sell_through_pct,
"n_lots": n_lots,
"confidence": confidence,
"reason": {

View file

@ -409,7 +409,7 @@ class TestScoreDifferentiation:
class TestScoreCommercial:
def test_available_share_scaled(self) -> None:
signal = {"available": True, "commercial_share_pct": 40.0, "confidence": "medium"}
signal = {"available": True, "commercial_sell_through_pct": 40.0, "confidence": "medium"}
value, conf, _r = _score_commercial(signal)
assert value == pytest.approx(0.4)
assert conf == "medium"
@ -417,7 +417,7 @@ class TestScoreCommercial:
def test_in_range(self) -> None:
for share in (0.0, 25.0, 100.0):
value, _c, _r = _score_commercial(
{"available": True, "commercial_share_pct": share, "confidence": "low"}
{"available": True, "commercial_sell_through_pct": share, "confidence": "low"}
)
assert value is not None
assert 0.0 <= value <= 1.0
@ -439,7 +439,7 @@ class TestScoreCommercial:
def test_bad_confidence_falls_back_low(self) -> None:
value, conf, _r = _score_commercial(
{"available": True, "commercial_share_pct": 10.0, "confidence": "garbage"}
{"available": True, "commercial_sell_through_pct": 10.0, "confidence": "garbage"}
)
assert value is not None
assert conf == "low"
@ -728,7 +728,7 @@ def _patch_all(
overlay: Any = None,
) -> Any:
"""Контекст-менеджер: патчит ВСЕ 8 backing-сервисов их return_value (или дефолтом)."""
commercial = {"available": True, "commercial_share_pct": 30.0, "confidence": "medium"}
commercial = {"available": True, "commercial_sell_through_pct": 30.0, "confidence": "medium"}
forecast_rv = [forecast if forecast is not None else _forecast_stub(0.5)]
competitors_rv = (
competitors if competitors is not None else _competitors_response_stub([0.4, 0.2])

View file

@ -744,7 +744,7 @@ class TestCommercialSignal:
out = _commercial_signal(MagicMock(), "Ленинский", 12)
assert out is not None
assert out["available"] is True
assert out["commercial_share_pct"] == 42.5
assert out["commercial_sell_through_pct"] == 42.5
assert out["n_lots"] == 120
assert out["confidence"] == "medium"
assert out["reason"]["advisory"] is True