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
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:
parent
249182b678
commit
c9ba595abe
4 changed files with 24 additions and 23 deletions
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue