fix(tradein/estimator): lock honest asking_to_sold_ratio invariant + disambiguate torg bases (#2087 M2/M3) (#2387)
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
This commit is contained in:
parent
9b92b595db
commit
2580158d8c
3 changed files with 56 additions and 1 deletions
|
|
@ -213,6 +213,14 @@ class AggregatedEstimate(BaseModel):
|
|||
expected_sold_range_low_rub: int | None = None
|
||||
expected_sold_range_high_rub: int | None = None
|
||||
expected_sold_per_m2: int | None = None
|
||||
# #2087 M3: этот ratio — asking→sold дисконт ДЛЯ ЭТОГО эстимейта (показанная
|
||||
# asking-медиана median_price_rub vs ожидаемая цена продажи expected_sold_price_rub
|
||||
# по ДКП-корректировке Росреестра, per-rooms/tier коэффициент; #2141 гарантирует
|
||||
# asking_to_sold_ratio == expected_sold_price_rub/median_price_rub байт-в-байт).
|
||||
# НЕ путать с HouseAnalyticsKpi.median_bargain_pct — это торг ВНУТРИ жизни
|
||||
# объявления (start_price→last_price до снятия, house_placement_history по
|
||||
# конкретному дому за 12 мес) — другая база и другой смысл «торга», см. докстринг
|
||||
# HouseAnalyticsKpi.
|
||||
asking_to_sold_ratio: float | None = None # =sold/asking, ~0.72–0.93
|
||||
ratio_basis: str | None = None # 'per_rooms' | 'global_fallback'
|
||||
# ── DaData enrichment (PR Q1) — on-demand для target адреса ──
|
||||
|
|
@ -432,12 +440,23 @@ class RecentSoldEntry(BaseModel):
|
|||
|
||||
|
||||
class HouseAnalyticsKpi(BaseModel):
|
||||
"""Агрегированные KPI по дому(ам) из house_placement_history."""
|
||||
"""Агрегированные KPI по дому(ам) из house_placement_history.
|
||||
|
||||
#2087 M3: median_bargain_pct — торг ВНУТРИ жизни объявления (медиана
|
||||
(start_price-last_price)/start_price×100 по ВСЕМ лотам house_placement_history
|
||||
для дома/домов — не только снятым, без ограничения по давности; 12-мес окно
|
||||
есть только у отдельного recent_sold_rows-запроса, не у этого поля). Другая база
|
||||
и другой смысл «торга», чем AggregatedEstimate.asking_to_sold_ratio (asking-медиана
|
||||
ЭТОГО эстимейта vs ожидаемая ДКП-цена продажи, per-rooms/tier коэффициент) —
|
||||
не суммировать и не подписывать в UI как одну и ту же метрику.
|
||||
"""
|
||||
|
||||
total_lots: int
|
||||
sold_count: int
|
||||
sold_rate_pct: float
|
||||
median_exposure_days: int | None
|
||||
# медиана (start_price-last_price)/start_price×100 по всем лотам дома, без sold-фильтра
|
||||
# и без ограничения по давности (см. докстринг класса)
|
||||
median_bargain_pct: float | None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2681,6 +2681,17 @@ def _price_from_inputs(
|
|||
# expected_sold_price/median_price — дескриптор становится честным, а сам
|
||||
# expected_sold (выкуп) НЕ трогаем: hedonic-uplift остаётся прибит к sale-модели.
|
||||
# Нет сдвига (expected == median×ratio, hedonic OFF) → табличный ratio байт-в-байт.
|
||||
#
|
||||
# #2087 M2 (донат-виджет «−N% к рынку» расходился с показанными ценами):
|
||||
# тот же корень, что #2141 выше — закрывается ЭТИМ recompute'ом, т.к. он
|
||||
# безусловный (выполняется всегда, когда expected_sold_price задан) и стоит
|
||||
# ПОСЛЕ anchor/IMV-blend/quarter-index/corridor-clamp/radius-floor (все они
|
||||
# мутируют median_price ДО этой точки, см. "AFTER all headline mutations"
|
||||
# выше). #2255 segment-multiplier ниже — единственная мутация ПОСЛЕ этого
|
||||
# recompute'а — умножает median_price И expected_sold_price ОДНИМ и тем же
|
||||
# factor, так что их отношение (= сохранённый ratio) инвариантно с точностью
|
||||
# до независимого round() на каждой стороне (см.
|
||||
# test_honest_ratio_invariant_under_segment_multiplier).
|
||||
if expected_sold_price and expected_sold_price > 0:
|
||||
honest_ratio = expected_sold_price / median_price
|
||||
if abs(honest_ratio - asking_to_sold_ratio) > _RATIO_DESCRIPTOR_EPS:
|
||||
|
|
|
|||
|
|
@ -466,3 +466,28 @@ def test_honest_ratio_byte_identical_without_shift(monkeypatch: pytest.MonkeyPat
|
|||
assert pr.asking_to_sold_ratio == 0.90 # байт-в-байт сырой табличный ratio
|
||||
assert pr.expected_sold_price == round(pr.median_price * 0.90)
|
||||
assert abs(pr.asking_to_sold_ratio - pr.expected_sold_price / pr.median_price) < 1e-3
|
||||
|
||||
|
||||
def test_honest_ratio_invariant_under_segment_multiplier(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""#2087 M2 regression: донат-виджет («−N% к рынку») считается фронтом как
|
||||
(asking_to_sold_ratio−1)×100 — рисуется мимо показанных median/expected_sold, если
|
||||
сохранённый ratio разошёлся с фактическим expected_sold_price/median_price. #2255
|
||||
segment-multiplier — ЕДИНСТВЕННАЯ ценовая мутация ПОСЛЕ honest_ratio-пересчёта
|
||||
(#2141): множит median_price И expected_sold_price ОДНИМ и тем же factor, но
|
||||
round() на каждой стороне — независимый. Проверяем, что инвариант держится и
|
||||
здесь (с точностью до шума округления), а не только до мутации."""
|
||||
monkeypatch.setattr(estimator.settings, "estimate_hedonic_correction_enabled", False)
|
||||
monkeypatch.setattr(estimator.settings, "estimate_segment_multiplier_enabled", True)
|
||||
monkeypatch.setattr(
|
||||
estimator.settings,
|
||||
"estimate_segment_multipliers",
|
||||
{"эконом": 1.00, "комфорт": 1.00, "бизнес": 1.08, "элит": 1.06},
|
||||
)
|
||||
# 213_777 ₽/м² falls in the "бизнес" band (160k–220k) → factor 1.08 fires.
|
||||
pr = _call(listings=_lots(213_777.0, n=7), area_m2=53.4, rooms=2, ratio=0.83)
|
||||
|
||||
assert "segment_multiplier" in (pr.sources_used_pre or []) # confirm the mult fired
|
||||
assert pr.expected_sold_price is not None and pr.median_price > 0
|
||||
assert pr.asking_to_sold_ratio == pytest.approx(
|
||||
pr.expected_sold_price / pr.median_price, abs=1e-3
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue