estimate_dedup_analogs_enabled: False → True. Бэктест #1966 OFF vs ON accuracy-идентичен (MAPE 13.89%, coverage 83.33%, bias −3.83%, median width/cv без изменений) — включение меняет только user-visible n_analogs: перестаёт быть раздутым кросс-постингом одного физлота на avito+cian+domklik ×3. Откат — ENV ESTIMATE_DEDUP_ANALOGS_ENABLED=false. Тесты разведены default vs explicit (не ослаблены): - test_estimator_dedup_cross_source_2087: добавлен test_dedup_default_is_on (без monkeypatch — дедуп активен по умолчанию); test_dedup_flag_off_is_noop оставляет ЯВНЫЙ OFF-override. - test_backtest_regression_gate: пиним флаг OFF — гейт это байт-идентичный replay frozen OFF-capture (recorded call-sequence фикстуры без дедупа); с ON _dedup_cross_source триммит listings до quarter_indexes_lookup и control-flow расходится с записью. - test_estimator_n_analogs_priced (autouse) + test_radius_path_n_analogs_unchanged: пиним OFF — инвариант «n_analogs = число priced-аналогов»/radius-passthrough ортогонален дедупу, а синтетические аналоги делят адрес/этаж/площадь и схлопнулись бы как кросс-посты. Refs #2087, #2173
This commit is contained in:
parent
3ad1550363
commit
3505a4e3e5
5 changed files with 61 additions and 7 deletions
|
|
@ -265,9 +265,11 @@ class Settings(BaseSettings):
|
|||
# ТУ ЖЕ цену → нулевой вклад в дисперсию) → cv/коридор НЕ сужаются. Это фикс
|
||||
# ЧЕСТНОСТИ СЧЁТА (n_analogs не раздут ×3 кросс-постами, source_counts по
|
||||
# физлотам), accuracy-нейтральный, а НЕ рычаг сужения cv (рычаг cv→коридор —
|
||||
# estimate_sb_clip_after_weight, уже ON). Default False (байт-идентично) — флаг
|
||||
# готов к монкипатч-бэктесту/canary. ENV: ESTIMATE_DEDUP_ANALOGS_ENABLED.
|
||||
estimate_dedup_analogs_enabled: bool = False
|
||||
# estimate_sb_clip_after_weight, уже ON). Default ON (#2173): бэктест #1966 OFF vs
|
||||
# ON accuracy-идентичен (MAPE 13.89%, coverage 83.33%, bias −3.83%, median width/cv
|
||||
# без изменений), меняется только user-visible n_analogs — перестаёт быть раздутым
|
||||
# кросс-постингом ×3. ENV: ESTIMATE_DEDUP_ANALOGS_ENABLED (=false откатывает).
|
||||
estimate_dedup_analogs_enabled: bool = True
|
||||
|
||||
# ── #1871 P2: split-дома wide-corridor disclosure (default ON, порог 1.2) ──
|
||||
# Tier A (same-building) матчит по address-regex (намеренно НЕ house_id — дом
|
||||
|
|
|
|||
|
|
@ -25,8 +25,11 @@ import math
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||
|
||||
from app.services import estimator
|
||||
from scripts.backtest_estimator import load_fixture, replay_fixture
|
||||
|
||||
_FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
|
@ -73,7 +76,16 @@ def test_fixture_and_baseline_committed() -> None:
|
|||
assert _BASELINE_PATH.exists(), f"frozen baseline missing: {_BASELINE_PATH}"
|
||||
|
||||
|
||||
def test_backtest_regression_gate() -> None:
|
||||
def test_backtest_regression_gate(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# The frozen fixture records the injected-callback control flow captured with
|
||||
# cross-source physical dedup OFF (#2087 H4 was a no-op default at capture time).
|
||||
# Dedup is now default ON (#2173), but this gate is a byte-identical REPLAY of a
|
||||
# frozen OFF capture — with dedup active _dedup_cross_source would trim listings
|
||||
# before quarter_indexes_lookup and the recorded call sequence would diverge. Pin
|
||||
# the flag OFF so the replay follows the captured control flow. (Accuracy-neutral:
|
||||
# backtest #1966 OFF vs ON is identical; dedup only trims user-visible n_analogs,
|
||||
# so the OFF baseline stays the valid spine regression reference.)
|
||||
monkeypatch.setattr(estimator.settings, "estimate_dedup_analogs_enabled", False)
|
||||
fixture = load_fixture(_FIXTURE_PATH)
|
||||
baseline = json.loads(_BASELINE_PATH.read_text(encoding="utf-8"))
|
||||
# Round-trip the replay output through JSON before comparing: the committed
|
||||
|
|
|
|||
|
|
@ -90,11 +90,28 @@ def test_phys_key_area_bucket_tolerates_rounding() -> None:
|
|||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# _dedup_cross_source — флаг OFF (byte-identical)
|
||||
# Флаг: default ON (#2173) vs явный OFF (byte-identical)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_dedup_default_is_on() -> None:
|
||||
# #2173: default флага теперь ON (в проде дедуп активен без ENV-override).
|
||||
# Без monkeypatch settings отражает config-default → дедуп схлопывает
|
||||
# кросс-пост-тройку одного физлота. Явно НЕ монкипатчим — проверяем именно
|
||||
# поведение по умолчанию.
|
||||
assert estimator.settings.estimate_dedup_analogs_enabled is True
|
||||
lots = [
|
||||
_lot(source="avito"),
|
||||
_lot(source="cian"),
|
||||
_lot(source="domklik"),
|
||||
]
|
||||
out = estimator._dedup_cross_source(lots)
|
||||
assert len(out) == 1 # дедуп активен по умолчанию
|
||||
|
||||
|
||||
def test_dedup_flag_off_is_noop(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# OFF-путь проверяем ЯВНЫМ override (не полагаясь на default, который теперь ON):
|
||||
# при флаге OFF проход байт-идентичен — возвращается тот же объект.
|
||||
monkeypatch.setattr(estimator.settings, "estimate_dedup_analogs_enabled", False)
|
||||
lots = [
|
||||
_lot(source="avito"),
|
||||
|
|
|
|||
|
|
@ -26,9 +26,13 @@ from datetime import UTC, datetime
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
# Settings требует DATABASE_URL при инициализации (fail-fast, C-3).
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||||
|
||||
from app.services import estimator
|
||||
|
||||
# Переиспользуем harness _run_estimate / _make_payload из anchor-теста (полный
|
||||
# estimate путь со всеми I/O застабленными). anchor_comps=[] + anchor_tier=None
|
||||
# направляет estimate в radius-aggregation ветку (где живёт фикс #2).
|
||||
|
|
@ -39,6 +43,16 @@ _h = importlib.util.module_from_spec(_spec)
|
|||
_spec.loader.exec_module(_h)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _pin_dedup_off(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Эти тесты проверяют инвариант #2 «n_analogs = число priced-аналогов»,
|
||||
# ортогональный кросс-source физдедупу (#2087 H4, default ON с #2173).
|
||||
# Синтетические аналоги делят один адрес/этаж/площадь → дедуп схлопнул бы
|
||||
# близкие по цене как кросс-посты и исказил счётчик. Пиним флаг OFF, чтобы
|
||||
# мерить именно #2 в изоляции (не полагаясь на прод-default, который теперь ON).
|
||||
monkeypatch.setattr(estimator.settings, "estimate_dedup_analogs_enabled", False)
|
||||
|
||||
|
||||
def _listing(price_per_m2: float | None, area_m2: float = 60.0) -> dict[str, Any]:
|
||||
"""Радиусный аналог; price_per_m2=None моделирует листинг без ₽/м².
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/
|
|||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
|
||||
from app.services import estimator
|
||||
from app.services.estimator import (
|
||||
_compute_same_building_anchor,
|
||||
_geocode_is_coarse,
|
||||
|
|
@ -1045,8 +1047,15 @@ def test_anchor_n_analogs_syncs_to_anchor_count() -> None:
|
|||
assert "по 4 аналогам" in (est.confidence_explanation or "")
|
||||
|
||||
|
||||
def test_radius_path_n_analogs_unchanged() -> None:
|
||||
"""#695 контроль: без якоря n_analogs = радиусный счёт (поведение не меняется)."""
|
||||
def test_radius_path_n_analogs_unchanged(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""#695 контроль: без якоря n_analogs = радиусный счёт (поведение не меняется).
|
||||
|
||||
Проверяет radius-passthrough vs anchor-override — ортогонально кросс-source
|
||||
дедупу (#2087 H4, default ON с #2173). 5 ИДЕНТИЧНЫХ листингов — это ровно
|
||||
патологический кросс-пост, который дедуп схлопнул бы в 1; пиним флаг OFF,
|
||||
чтобы мерить именно #695-инвариант, не полагаясь на прод-default (теперь ON).
|
||||
"""
|
||||
monkeypatch.setattr(estimator.settings, "estimate_dedup_analogs_enabled", False)
|
||||
radius = [_make_listing(price_per_m2=200_000.0) for _ in range(5)]
|
||||
est = _run_estimate(anchor_comps=[], anchor_tier=None, radius_analogs=radius)
|
||||
assert est.n_analogs == 5
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue