fix(tradein): run same-building anchor without radius analogs (#691) (#704)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 42s
Deploy Trade-In / test (push) Successful in 22s
Deploy Trade-In / deploy (push) Successful in 34s
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 42s
Deploy Trade-In / test (push) Successful in 22s
Deploy Trade-In / deploy (push) Successful in 34s
Co-authored-by: bot-backend <bot-backend@gendsgn.local> Co-committed-by: bot-backend <bot-backend@gendsgn.local>
This commit is contained in:
parent
10626c21ae
commit
f04728297b
2 changed files with 42 additions and 4 deletions
|
|
@ -1645,11 +1645,18 @@ async def estimate_quality(
|
|||
# больше не размывается массовой застройкой). За флагом; OFF ⇒ точно старое
|
||||
# поведение. listing_segment для Tier C берём из самого частого среди аналогов.
|
||||
anchor_tier: str | None = None
|
||||
# #691: гейт НЕ требует радиусных аналогов (listings_clean) / median_price>0.
|
||||
# На проде геокод часто = None → ST_DWithin не находит радиусные комплы →
|
||||
# median=0, и same-building якорь скипался, ХОТЯ комплы того же дома есть
|
||||
# (_fetch_anchor_comps резолвит дом по payload.address без гео — Ткачёва 13,
|
||||
# Сакко 99). Запускаем по resolved-таргету (area + raw-адрес/geo); если комплов
|
||||
# реально нет — _compute_same_building_anchor вернёт None и мутации не будет
|
||||
# (поведение идентично старому). Новый гейт ⊇ старого: при непустом radius
|
||||
# адрес всегда есть, так что ранее-якорившиеся кейсы якорятся по-прежнему.
|
||||
if (
|
||||
settings.estimate_same_building_anchor_enabled
|
||||
and listings_clean
|
||||
and median_price > 0
|
||||
and payload.area_m2
|
||||
and (payload.address or (geo is not None and geo.full_address))
|
||||
):
|
||||
seg_counts: dict[str, int] = {}
|
||||
for lot in listings_clean:
|
||||
|
|
|
|||
|
|
@ -377,13 +377,20 @@ def _run_estimate(
|
|||
flag_enabled: bool = True,
|
||||
ratio_tuple: tuple[float | None, str | None] = (0.92, "per_rooms"),
|
||||
payload=None,
|
||||
radius_analogs: list[dict[str, Any]] | None = None,
|
||||
):
|
||||
"""estimate_quality со всеми I/O застабленными; _fetch_anchor_comps форсирован."""
|
||||
"""estimate_quality со всеми I/O застабленными; _fetch_anchor_comps форсирован.
|
||||
|
||||
radius_analogs=[] моделирует прод-сценарий #691: геокод дал lat/lon, но
|
||||
ST_DWithin не нашёл радиусных аналогов (median=0) — якорь обязан сработать
|
||||
по same-building комплам независимо.
|
||||
"""
|
||||
from app.core.config import settings
|
||||
from app.services.estimator import estimate_quality
|
||||
|
||||
db = MagicMock()
|
||||
pl = payload or _make_payload()
|
||||
radius = list(_RADIUS_ANALOGS if radius_analogs is None else radius_analogs)
|
||||
|
||||
async def _run():
|
||||
with (
|
||||
|
|
@ -394,7 +401,7 @@ def _run_estimate(
|
|||
patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)),
|
||||
patch(
|
||||
"app.services.estimator._fetch_analogs",
|
||||
return_value=(list(_RADIUS_ANALOGS), False, "W"),
|
||||
return_value=(radius, False, "W"),
|
||||
),
|
||||
patch("app.services.estimator._fetch_deals", return_value=[]),
|
||||
patch("app.services.estimator._fetch_dkp_corridor", return_value=None),
|
||||
|
|
@ -434,6 +441,30 @@ def test_estimate_premium_lift_real_in_range() -> None:
|
|||
assert est.range_high_rub >= int(real * 0.9)
|
||||
|
||||
|
||||
def test_estimate_anchor_fires_without_radius_analogs() -> None:
|
||||
"""#691: радиусных аналогов НЕТ (median=0, прод-кейс провала ST_DWithin), но
|
||||
same-building комплы есть → якорь обязан сработать и дать median>0.
|
||||
До фикса гейт требовал listings_clean+median>0 → якорь скипался."""
|
||||
est = _run_estimate(
|
||||
anchor_comps=_SB_COMPS_PREMIUM,
|
||||
anchor_tier="A",
|
||||
radius_analogs=[],
|
||||
)
|
||||
# Якорь дал ненулевой headline несмотря на пустой радиус.
|
||||
assert est.median_price_rub > 0
|
||||
assert est.median_price_per_m2 >= 450_000
|
||||
# range валиден (low ≤ point ≤ high), не вырожден в 0.
|
||||
assert 0 < est.range_low_rub <= est.median_price_rub <= est.range_high_rub
|
||||
|
||||
|
||||
def test_estimate_no_anchor_no_radius_stays_insufficient() -> None:
|
||||
"""#691 контроль: ни радиуса, ни same-building комплов → median остаётся 0
|
||||
(insufficient), фикс НЕ фабрикует число из воздуха."""
|
||||
est = _run_estimate(anchor_comps=[], anchor_tier=None, radius_analogs=[])
|
||||
assert est.median_price_rub == 0
|
||||
assert est.median_price_per_m2 == 0
|
||||
|
||||
|
||||
def test_estimate_economy_no_regression() -> None:
|
||||
"""(b) Эконом-комплы ~112k → guardrail не раздувает, headline ≈ комплов."""
|
||||
eco_comps = [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue