fix(tradein): flag coarse-geo estimates as low-confidence (#693) #707
2 changed files with 102 additions and 1 deletions
|
|
@ -1903,6 +1903,32 @@ async def estimate_quality(
|
||||||
" Оценка ниже коридора реальных сделок Росреестра по улице."
|
" Оценка ниже коридора реальных сделок Росреестра по улице."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── #693: coarse-geo downgrade ──────────────────────────────────────────
|
||||||
|
# Когда DaData дала только грубую точность (settlement/city/region/unknown,
|
||||||
|
# qc_geo >= 2), PostGIS-радиус крутится вокруг центроида НП и собирает
|
||||||
|
# аналоги из случайных частей города → уверенная «medium» вводит в
|
||||||
|
# заблуждение. Помечаем как ориентировочную (low). КОНСЕРВАТИВНО:
|
||||||
|
# 1) есть позитивный сигнал грубости (dadata.qc_geo >= 2);
|
||||||
|
# 2) якорь «тот же дом» НЕ сработал (anchor_tier is None) — иначе оценка
|
||||||
|
# стоит на реальных комплах дома (#691 path) и геоцентроид не важен;
|
||||||
|
# 3) есть радиусное число (median_price > 0) — что квалифицировать.
|
||||||
|
if (
|
||||||
|
dadata is not None
|
||||||
|
and dadata.qc_geo is not None
|
||||||
|
and dadata.qc_geo >= 2
|
||||||
|
and anchor_tier is None
|
||||||
|
and median_price > 0
|
||||||
|
):
|
||||||
|
_coarse_label = {2: "населённого пункта", 3: "города", 4: "региона"}.get(
|
||||||
|
dadata.qc_geo, "населённого пункта"
|
||||||
|
)
|
||||||
|
confidence = "low"
|
||||||
|
explanation = (explanation or "") + (
|
||||||
|
f" Адрес определён лишь до уровня {_coarse_label} — точные координаты "
|
||||||
|
"дома найти не удалось, поэтому оценка ориентировочная (аналоги взяты "
|
||||||
|
"по широкой окрестности)."
|
||||||
|
)
|
||||||
|
|
||||||
# 5. Deals — ДКП-only sales (вторичка) из rosreestr_deals.
|
# 5. Deals — ДКП-only sales (вторичка) из rosreestr_deals.
|
||||||
# Importer фильтрует doc_type='ДКП' (PR-A 2026-05-24), ДДУ застройщиков
|
# Importer фильтрует doc_type='ДКП' (PR-A 2026-05-24), ДДУ застройщиков
|
||||||
# исключены — больше не скёюят median вторички ~110-120 К/м².
|
# исключены — больше не скёюят median вторички ~110-120 К/м².
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,7 @@ def _run_estimate(
|
||||||
ratio_tuple: tuple[float | None, str | None] = (0.92, "per_rooms"),
|
ratio_tuple: tuple[float | None, str | None] = (0.92, "per_rooms"),
|
||||||
payload=None,
|
payload=None,
|
||||||
radius_analogs: list[dict[str, Any]] | None = None,
|
radius_analogs: list[dict[str, Any]] | None = None,
|
||||||
|
dadata_result: Any | None = None,
|
||||||
):
|
):
|
||||||
"""estimate_quality со всеми I/O застабленными; _fetch_anchor_comps форсирован.
|
"""estimate_quality со всеми I/O застабленными; _fetch_anchor_comps форсирован.
|
||||||
|
|
||||||
|
|
@ -396,7 +397,10 @@ def _run_estimate(
|
||||||
with (
|
with (
|
||||||
patch.object(settings, "estimate_same_building_anchor_enabled", flag_enabled),
|
patch.object(settings, "estimate_same_building_anchor_enabled", flag_enabled),
|
||||||
patch("app.services.estimator.geocode", new=AsyncMock(return_value=_make_fake_geo())),
|
patch("app.services.estimator.geocode", new=AsyncMock(return_value=_make_fake_geo())),
|
||||||
patch("app.services.estimator.dadata_clean_address", new=AsyncMock(return_value=None)),
|
patch(
|
||||||
|
"app.services.estimator.dadata_clean_address",
|
||||||
|
new=AsyncMock(return_value=dadata_result),
|
||||||
|
),
|
||||||
patch("app.services.estimator.match_house_readonly", return_value=None),
|
patch("app.services.estimator.match_house_readonly", return_value=None),
|
||||||
patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)),
|
patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)),
|
||||||
patch(
|
patch(
|
||||||
|
|
@ -667,3 +671,74 @@ def test_anchor_floor_weight_neutral_when_data_missing() -> None:
|
||||||
)
|
)
|
||||||
assert a is not None and b is not None
|
assert a is not None and b is not None
|
||||||
assert abs(a["anchor_ppm2"] - b["anchor_ppm2"]) < 1.0
|
assert abs(a["anchor_ppm2"] - b["anchor_ppm2"]) < 1.0
|
||||||
|
|
||||||
|
|
||||||
|
# ── #693: coarse-geo (centroid) confidence downgrade ─────────────────────────
|
||||||
|
|
||||||
|
_APPROX_FLAG = "оценка ориентировочная"
|
||||||
|
|
||||||
|
|
||||||
|
def _make_dadata(qc_geo: int | None):
|
||||||
|
"""Минимальный DadataAddressResult с заданным qc_geo (прочие поля None/пусто)."""
|
||||||
|
from app.services.dadata import DadataAddressResult
|
||||||
|
|
||||||
|
return DadataAddressResult(
|
||||||
|
canonical_address=None,
|
||||||
|
house_cadnum=None,
|
||||||
|
house_fias_id=None,
|
||||||
|
lat=None,
|
||||||
|
lon=None,
|
||||||
|
qc_geo=qc_geo,
|
||||||
|
qc_house=None,
|
||||||
|
kladr_id=None,
|
||||||
|
okato=None,
|
||||||
|
oktmo=None,
|
||||||
|
metro=[],
|
||||||
|
raw={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimate_coarse_geo_no_anchor_downgrades_to_low() -> None:
|
||||||
|
"""#693: qc_geo=3 (город) + НЕТ якоря, но радиусные аналоги есть (median>0) →
|
||||||
|
confidence='low' + флаг «ориентировочной» оценки (центроид НП, не дом)."""
|
||||||
|
est = _run_estimate(
|
||||||
|
anchor_comps=[],
|
||||||
|
anchor_tier=None,
|
||||||
|
dadata_result=_make_dadata(qc_geo=3),
|
||||||
|
)
|
||||||
|
assert est.median_price_rub > 0 # радиусное число есть → было что квалифицировать
|
||||||
|
assert est.confidence == "low"
|
||||||
|
assert _APPROX_FLAG in est.confidence_explanation
|
||||||
|
assert "города" in est.confidence_explanation
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimate_coarse_geo_with_anchor_not_downgraded() -> None:
|
||||||
|
"""#693 protect #691: qc_geo=3 грубый, НО якорь дома сработал (tier=A) →
|
||||||
|
confidence НЕ даунгрейдится (оценка стоит на реальных комплах дома)."""
|
||||||
|
est = _run_estimate(
|
||||||
|
anchor_comps=_SB_COMPS_PREMIUM,
|
||||||
|
anchor_tier="A",
|
||||||
|
dadata_result=_make_dadata(qc_geo=3),
|
||||||
|
)
|
||||||
|
# Якорь сам выставил confidence — не "low" от #693-даунгрейда, и без флага.
|
||||||
|
assert _APPROX_FLAG not in (est.confidence_explanation or "")
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimate_precise_geo_no_downgrade() -> None:
|
||||||
|
"""#693: qc_geo=0 (дом, точно) + нет якоря → НЕТ даунгрейда (точный адрес)."""
|
||||||
|
est = _run_estimate(
|
||||||
|
anchor_comps=[],
|
||||||
|
anchor_tier=None,
|
||||||
|
dadata_result=_make_dadata(qc_geo=0),
|
||||||
|
)
|
||||||
|
assert _APPROX_FLAG not in (est.confidence_explanation or "")
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimate_no_dadata_no_false_downgrade() -> None:
|
||||||
|
"""#693: dadata=None (нет сигнала) → НЕ фабрикуем даунгрейд (no false positive)."""
|
||||||
|
est = _run_estimate(
|
||||||
|
anchor_comps=[],
|
||||||
|
anchor_tier=None,
|
||||||
|
dadata_result=None,
|
||||||
|
)
|
||||||
|
assert _APPROX_FLAG not in (est.confidence_explanation or "")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue