Множители доверительного интервала по регионам (#3540)
All checks were successful
Deploy Trade-In / changes (push) Successful in 17s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m45s
Deploy Trade-In / build-backend (push) Successful in 1m10s
Deploy Trade-In / deploy (push) Successful in 6m59s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m44s
All checks were successful
Deploy Trade-In / changes (push) Successful in 17s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m45s
Deploy Trade-In / build-backend (push) Successful in 1m10s
Deploy Trade-In / deploy (push) Successful in 6m59s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m44s
This commit is contained in:
parent
c63c48eea2
commit
1c039b358c
8 changed files with 265 additions and 44 deletions
|
|
@ -436,6 +436,24 @@ class Settings(BaseSettings):
|
|||
# (проверено: 80.0% coverage на тех же 2366).
|
||||
estimate_pi_low_mult: float = 0.649 # empirical p10 of sold/expected_sold (#1966, n=2366)
|
||||
estimate_pi_high_mult: float = 1.392 # empirical p90 of sold/expected_sold (#1966, n=2366)
|
||||
# ── региональные множители PI (замер 2026-09-16) ──────────────────────────
|
||||
# Скаляры выше сняты ТОЛЬКО на Екатеринбурге (обл.66, #1966) и молча
|
||||
# применялись ко всем регионам — прогон `--engine full --sample 2000
|
||||
# --since 2025-06-01 --spread scattered --seed 42 --resolve-house-id
|
||||
# --pi-report` (гедоническая коррекция #2002 уже выключена в main) показал,
|
||||
# что достигнутое покрытие «80%-й» вилки уезжает по региону:
|
||||
# регион n p10 p50 p90 факт. покрытие
|
||||
# 77 1969 0.448 1.043 1.440 71.2%
|
||||
# 50 1632 0.703 1.117 1.500 74.8%
|
||||
# 66 1360 0.649 1.143 1.584 66.2%
|
||||
# Воспроизведено на независимой выборке (seed 777): 77 → 0.453/1.040/1.455,
|
||||
# 50 → 0.701/1.115/1.514 — числа устойчивы, это не шум сэмпла.
|
||||
# ВАЖНО: p50 (медиана actual_sold/expected_sold) > 1 во ВСЕХ трёх регионах —
|
||||
# точка expected_sold систематически занижена на 4-14%. Эта правка чинит
|
||||
# только ШИРИНУ/покрытие вилки по региону, а НЕ смещение точки. Калибровка
|
||||
# asking→sold на более коротком (свежем) окне — отдельная незакрытая задача.
|
||||
estimate_pi_low_mult_by_region: dict[int, float] = {77: 0.448, 50: 0.703, 66: 0.649}
|
||||
estimate_pi_high_mult_by_region: dict[int, float] = {77: 1.440, 50: 1.500, 66: 1.584}
|
||||
# ── #2002: hedonic year+area correction на точку expected_sold ─────────────
|
||||
# Диагноз: estimator систематически промахивается по эре дома + размеру —
|
||||
# недооценивает новостройки, плохо держит крупные лоты. Held-out fit (n=2366
|
||||
|
|
|
|||
|
|
@ -3311,6 +3311,42 @@ def _analog_word_dative(n: int) -> str:
|
|||
return "аналогу" if n == 1 else "аналогам"
|
||||
|
||||
|
||||
def pi_multipliers_for_region(region_code: int | None) -> tuple[float, float]:
|
||||
"""#pi-by-region: (low_mult, high_mult) калиброванного ~80% prediction interval для региона.
|
||||
|
||||
Скаляры `settings.estimate_pi_low_mult`/`estimate_pi_high_mult` (#1966) сняты
|
||||
ТОЛЬКО на Екатеринбурге (обл.66) и молча применялись ко всем регионам. Замер
|
||||
2026-09-16 (`--engine full --sample 2000 --since 2025-06-01 --spread scattered
|
||||
--seed 42 --resolve-house-id --pi-report`) показал достигнутое покрытие 66.2%
|
||||
(обл.66) / 71.2% (77, Москва) / 74.8% (50, Мособласть) вместо заявленных ~80% —
|
||||
те же множители тупее регионального рынка. Регион вне карты (или None) →
|
||||
скаляры-фолбэк + WARNING: ни падения, ни молчаливой подстановки чужого
|
||||
региона (см. sber_region_series_name выше — тот же паттерн).
|
||||
"""
|
||||
low = (
|
||||
settings.estimate_pi_low_mult_by_region.get(region_code)
|
||||
if region_code is not None
|
||||
else None
|
||||
)
|
||||
high = (
|
||||
settings.estimate_pi_high_mult_by_region.get(region_code)
|
||||
if region_code is not None
|
||||
else None
|
||||
)
|
||||
if low is not None and high is not None:
|
||||
return low, high
|
||||
logger.warning(
|
||||
"PI multipliers: region_code=%r нет в estimate_pi_*_mult_by_region — беру "
|
||||
"скаляры-фолбэк low=%.3f/high=%.3f (откалиброваны на обл.66). Если по "
|
||||
"региону пошли сделки — сними p10/p90 и заведи в карте, иначе покрытие "
|
||||
"вилки будет хуже заявленных ~80%%",
|
||||
region_code,
|
||||
settings.estimate_pi_low_mult,
|
||||
settings.estimate_pi_high_mult,
|
||||
)
|
||||
return settings.estimate_pi_low_mult, settings.estimate_pi_high_mult
|
||||
|
||||
|
||||
def _price_from_inputs(
|
||||
*,
|
||||
listings: list[dict],
|
||||
|
|
@ -3344,6 +3380,11 @@ def _price_from_inputs(
|
|||
# объяснение называло тот радиус, по которому реально искали. None →
|
||||
# прежнее поведение (FALLBACK_RADIUS_M) для оффлайн-вызывающих (бэктест).
|
||||
search_radius_m: int | None = None,
|
||||
# Региональные множители PI (#pi-by-region) — тот же target_region_code, что уже
|
||||
# скоупит ratio_resolver/ДКП-коридор в estimate_quality (#3051 PR-A). None →
|
||||
# pi_multipliers_for_region фолбэкается на скаляры (прежнее поведение
|
||||
# байт-в-байт для оффлайн-вызывающих, у которых региона нет).
|
||||
region_code: int | None = None,
|
||||
) -> PricingResult:
|
||||
"""Deterministic pricing orchestration — pure, synchronous, zero I/O.
|
||||
|
||||
|
|
@ -4102,8 +4143,11 @@ def _price_from_inputs(
|
|||
# ~80% honest coverage. Старый IQR-производный band (asking-IQR × ratio)
|
||||
# покрывал лишь ~55% реальных продаж при заявленном «диапазоне оценки».
|
||||
# Множители low<1<high (дефолты) гарантируют low ≤ point ≤ high.
|
||||
expected_sold_range_low = round(expected_sold_price * settings.estimate_pi_low_mult)
|
||||
expected_sold_range_high = round(expected_sold_price * settings.estimate_pi_high_mult)
|
||||
# #pi-by-region: множители региональные (pi_multipliers_for_region) — те же
|
||||
# скаляры на всех регионах давали 66.2%/71.2%/74.8% вместо ~80%.
|
||||
_pi_low_mult, _pi_high_mult = pi_multipliers_for_region(region_code)
|
||||
expected_sold_range_low = round(expected_sold_price * _pi_low_mult)
|
||||
expected_sold_range_high = round(expected_sold_price * _pi_high_mult)
|
||||
else:
|
||||
# legacy: диапазон производный от IQR аналогов (asking-IQR × ratio).
|
||||
expected_sold_range_low = round(range_low * effective_ratio)
|
||||
|
|
@ -5160,6 +5204,7 @@ async def estimate_quality(
|
|||
geo=geo,
|
||||
dadata_qc_geo=dadata.qc_geo if dadata else None,
|
||||
search_radius_m=search_radius_m,
|
||||
region_code=target_region_code,
|
||||
)
|
||||
|
||||
# Unpack pricing result
|
||||
|
|
|
|||
|
|
@ -2230,6 +2230,13 @@ def _predict_full_spine(
|
|||
return res
|
||||
|
||||
pr = m._price_from_inputs(
|
||||
# #pi-by-region: множители вилки теперь региональные, и живой прогон обязан
|
||||
# считать ТЕ ЖЕ, что прод — иначе харнесс молча мерил бы старые скаляры и
|
||||
# показывал покрытие, которого в проде нет. Регион берём из уже посчитанного
|
||||
# target_region_code (строка выше), он зеркалит резолв estimate_quality.
|
||||
# Замороженный реплей фикстуры (replay_fixture) сюда НЕ ходит и региона не
|
||||
# передаёт — его путь остаётся байт-в-байт прежним, регресс-гейт не двигается.
|
||||
region_code=target_region_code,
|
||||
listings=listings,
|
||||
area_m2=deal.area_m2,
|
||||
rooms=deal.rooms,
|
||||
|
|
|
|||
|
|
@ -273,14 +273,13 @@ def test_expected_sold_applied_when_ratio_present(monkeypatch: pytest.MonkeyPatc
|
|||
assert est.expected_sold_per_m2 == round(est.median_price_per_m2 * ratio)
|
||||
# #1966: expected_sold range is now a calibrated ~80% PI around the POINT
|
||||
# (point × [p10, p90] of sold/expected_sold), not the old asking-IQR × ratio band.
|
||||
from app.core.config import settings
|
||||
# #3xxx: multipliers are regional now — resolve via pi_multipliers_for_region
|
||||
# (region 66, EKB fixture geo), not the raw scalar (66's high != scalar high).
|
||||
from app.services.estimator import pi_multipliers_for_region
|
||||
|
||||
assert est.expected_sold_range_low_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_low_mult
|
||||
)
|
||||
assert est.expected_sold_range_high_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_high_mult
|
||||
)
|
||||
_low_mult, _high_mult = pi_multipliers_for_region(66)
|
||||
assert est.expected_sold_range_low_rub == round(est.expected_sold_price_rub * _low_mult)
|
||||
assert est.expected_sold_range_high_rub == round(est.expected_sold_price_rub * _high_mult)
|
||||
assert (
|
||||
est.expected_sold_range_low_rub
|
||||
<= est.expected_sold_price_rub
|
||||
|
|
@ -407,14 +406,12 @@ def test_expected_sold_fires_on_anchor_only_no_radius_comps(
|
|||
assert est.expected_sold_price_rub == round(est.median_price_rub * ratio)
|
||||
assert est.expected_sold_per_m2 == round(est.median_price_per_m2 * ratio)
|
||||
# #1966: calibrated ~80% PI around the expected_sold point (not asking-IQR × ratio).
|
||||
from app.core.config import settings
|
||||
# #3xxx: multipliers are regional now — resolve via pi_multipliers_for_region.
|
||||
from app.services.estimator import pi_multipliers_for_region
|
||||
|
||||
assert est.expected_sold_range_low_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_low_mult
|
||||
)
|
||||
assert est.expected_sold_range_high_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_high_mult
|
||||
)
|
||||
_low_mult, _high_mult = pi_multipliers_for_region(66)
|
||||
assert est.expected_sold_range_low_rub == round(est.expected_sold_price_rub * _low_mult)
|
||||
assert est.expected_sold_range_high_rub == round(est.expected_sold_price_rub * _high_mult)
|
||||
|
||||
|
||||
def test_expected_sold_none_when_median_price_zero() -> None:
|
||||
|
|
|
|||
|
|
@ -143,18 +143,16 @@ def test_expected_sold_clamped_to_headline_when_ratio_above_1(
|
|||
)
|
||||
# #1966: expected_sold range is now a calibrated ~80% PI around the point
|
||||
# (point × [p10, p90] of sold/expected_sold), so it is NO LONGER bounded by the
|
||||
# asking-IQR band — the high arm (point × 1.392) legitimately exceeds range_high.
|
||||
# asking-IQR band — the high arm legitimately exceeds range_high.
|
||||
# The invariant we keep is the calibrated band itself + ordering low ≤ point ≤ high.
|
||||
from app.core.config import settings
|
||||
# #3xxx: multipliers are regional now — resolve via pi_multipliers_for_region.
|
||||
from app.services.estimator import pi_multipliers_for_region
|
||||
|
||||
_low_mult, _high_mult = pi_multipliers_for_region(66)
|
||||
assert est.expected_sold_range_high_rub is not None
|
||||
assert est.expected_sold_range_high_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_high_mult
|
||||
)
|
||||
assert est.expected_sold_range_high_rub == round(est.expected_sold_price_rub * _high_mult)
|
||||
assert est.expected_sold_range_low_rub is not None
|
||||
assert est.expected_sold_range_low_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_low_mult
|
||||
)
|
||||
assert est.expected_sold_range_low_rub == round(est.expected_sold_price_rub * _low_mult)
|
||||
assert (
|
||||
est.expected_sold_range_low_rub
|
||||
<= est.expected_sold_price_rub
|
||||
|
|
@ -174,14 +172,12 @@ def test_expected_sold_not_clamped_when_ratio_below_1() -> None:
|
|||
assert est.expected_sold_price_rub == round(est.median_price_rub * ratio)
|
||||
assert est.expected_sold_per_m2 == round(est.median_price_per_m2 * ratio)
|
||||
# #1966: calibrated ~80% PI around the expected_sold point (not asking-IQR × ratio).
|
||||
from app.core.config import settings
|
||||
# #3xxx: multipliers are regional now — resolve via pi_multipliers_for_region.
|
||||
from app.services.estimator import pi_multipliers_for_region
|
||||
|
||||
assert est.expected_sold_range_low_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_low_mult
|
||||
)
|
||||
assert est.expected_sold_range_high_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_high_mult
|
||||
)
|
||||
_low_mult, _high_mult = pi_multipliers_for_region(66)
|
||||
assert est.expected_sold_range_low_rub == round(est.expected_sold_price_rub * _low_mult)
|
||||
assert est.expected_sold_range_high_rub == round(est.expected_sold_price_rub * _high_mult)
|
||||
assert est.expected_sold_price_rub < est.median_price_rub
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -519,14 +519,12 @@ def test_expected_sold_consistent_with_blended_median() -> None:
|
|||
assert est.expected_sold_price_rub == round(est.median_price_rub * ratio)
|
||||
assert est.expected_sold_per_m2 == round(est.median_price_per_m2 * ratio)
|
||||
# #1966: expected_sold range — калиброванный ~80% PI вокруг точки, не asking-IQR × ratio.
|
||||
from app.core.config import settings
|
||||
# #3xxx: multipliers are regional now — resolve via pi_multipliers_for_region.
|
||||
from app.services.estimator import pi_multipliers_for_region
|
||||
|
||||
assert est.expected_sold_range_high_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_high_mult
|
||||
)
|
||||
assert est.expected_sold_range_low_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_low_mult
|
||||
)
|
||||
_low_mult, _high_mult = pi_multipliers_for_region(66)
|
||||
assert est.expected_sold_range_high_rub == round(est.expected_sold_price_rub * _high_mult)
|
||||
assert est.expected_sold_range_low_rub == round(est.expected_sold_price_rub * _low_mult)
|
||||
# Sanity: sold < asking (ratio<1), но НЕ абсурдная «скидка» от stale 6М-базы.
|
||||
assert est.expected_sold_price_rub == round(12_000_000 * ratio)
|
||||
|
||||
|
|
|
|||
161
tradein-mvp/backend/tests/test_estimator_pi_by_region.py
Normal file
161
tradein-mvp/backend/tests/test_estimator_pi_by_region.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"""Regional prediction-interval multipliers (#1966 follow-up, 2026-09-16).
|
||||
|
||||
`estimate_pi_low_mult`/`estimate_pi_high_mult` (#1966) were calibrated ONLY on
|
||||
Yekaterinburg (region 66, n=2366) and applied blindly to every region. A prod
|
||||
backtest (`--engine full --sample 2000 --since 2025-06-01 --spread scattered
|
||||
--seed 42 --resolve-house-id --pi-report`, hedonic correction OFF) showed the
|
||||
achieved coverage of the "~80%" band drifting by region: 66.2% (66), 71.2%
|
||||
(77, Moscow), 74.8% (50, Moscow oblast) instead of the claimed ~80%.
|
||||
`pi_multipliers_for_region` resolves per-region p10/p90 multipliers with a
|
||||
scalar fallback for regions outside the map — mirrors `sber_region_series_name`.
|
||||
|
||||
NOTE: importing app.services.estimator pulls app.core.config.Settings which
|
||||
requires DATABASE_URL. Set it BEFORE importing app modules.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||
|
||||
import logging
|
||||
|
||||
from app.core.config import settings
|
||||
from app.services import estimator
|
||||
from app.services.estimator import pi_multipliers_for_region
|
||||
from app.services.geocoder import GeocodeResult
|
||||
|
||||
# ── helpers (mirror tests/test_estimator_range_floor.py) ──────────────────────
|
||||
|
||||
|
||||
def _geo() -> GeocodeResult:
|
||||
return GeocodeResult(
|
||||
lat=56.838,
|
||||
lon=60.597,
|
||||
full_address="ул. Тестовая, 1",
|
||||
provider="nominatim",
|
||||
confidence="approximate",
|
||||
)
|
||||
|
||||
|
||||
def _lot(ppm2: float, address: str = "ул. Тестовая, 1", source: str = "avito") -> dict:
|
||||
return {"price_per_m2": ppm2, "address": address, "source": source}
|
||||
|
||||
|
||||
def _lots(ppm2: float, n: int = 7) -> list[dict]:
|
||||
return [_lot(ppm2, address=f"ул. Тестовая, {i + 1}") for i in range(n)]
|
||||
|
||||
|
||||
def _call(
|
||||
*,
|
||||
listings: list[dict],
|
||||
ratio: float,
|
||||
region_code: int | None,
|
||||
area_m2: float = 50.0,
|
||||
rooms: int | None = 2,
|
||||
) -> estimator.PricingResult:
|
||||
def ratio_resolver(appm2: float | None) -> tuple[float | None, str | None]:
|
||||
return ratio, "per_rooms"
|
||||
|
||||
return estimator._price_from_inputs(
|
||||
listings=listings,
|
||||
area_m2=area_m2,
|
||||
rooms=rooms,
|
||||
repair_state=None,
|
||||
floor=5,
|
||||
total_floors=10,
|
||||
target_year=None,
|
||||
analog_tier="W",
|
||||
fallback_used=False,
|
||||
area_widened=False,
|
||||
anchor_comps=[],
|
||||
anchor_tier_fetched=None,
|
||||
dkp_raw=None,
|
||||
imv_anchor=None,
|
||||
imv_eval=None,
|
||||
yandex_val_present=False,
|
||||
cian_val_present=False,
|
||||
ratio_resolver=ratio_resolver,
|
||||
quarter_index_lookup=lambda q: None,
|
||||
quarter_indexes_lookup=lambda qs: {},
|
||||
target_house_cadnum=None,
|
||||
dadata_coarse=False,
|
||||
geo=_geo(),
|
||||
dadata_qc_geo=None,
|
||||
region_code=region_code,
|
||||
)
|
||||
|
||||
|
||||
# ── 1. Resolver: regional maps differ from Yekaterinburg (region 66) ──────────
|
||||
|
||||
|
||||
def test_moscow_and_moscow_oblast_multipliers_differ_from_ekb() -> None:
|
||||
ekb_low, ekb_high = pi_multipliers_for_region(66)
|
||||
msk_low, msk_high = pi_multipliers_for_region(77)
|
||||
mo_low, mo_high = pi_multipliers_for_region(50)
|
||||
|
||||
assert (msk_low, msk_high) != (ekb_low, ekb_high)
|
||||
assert (mo_low, mo_high) != (ekb_low, ekb_high)
|
||||
# Sanity against the measured 2026-09-16 backtest table.
|
||||
assert msk_low == 0.448
|
||||
assert msk_high == 1.440
|
||||
assert mo_low == 0.703
|
||||
assert mo_high == 1.500
|
||||
assert ekb_low == 0.649
|
||||
assert ekb_high == 1.584
|
||||
|
||||
|
||||
# ── 2. Region outside the map (or None) falls back to the scalars ─────────────
|
||||
|
||||
|
||||
def test_unknown_region_falls_back_to_scalars(caplog) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger=estimator.logger.name):
|
||||
low, high = pi_multipliers_for_region(12345)
|
||||
assert (low, high) == (settings.estimate_pi_low_mult, settings.estimate_pi_high_mult)
|
||||
assert any("PI multipliers" in r.getMessage() for r in caplog.records)
|
||||
|
||||
|
||||
def test_none_region_falls_back_to_scalars() -> None:
|
||||
low, high = pi_multipliers_for_region(None)
|
||||
assert (low, high) == (settings.estimate_pi_low_mult, settings.estimate_pi_high_mult)
|
||||
|
||||
|
||||
# ── 3. Invariant from #1966: low < 1 < point ⇒ low ≤ point ≤ high ─────────────
|
||||
|
||||
|
||||
def test_low_below_one_high_above_one_for_every_mapped_region() -> None:
|
||||
all_regions = set(settings.estimate_pi_low_mult_by_region) | set(
|
||||
settings.estimate_pi_high_mult_by_region
|
||||
)
|
||||
assert all_regions, "regional PI map must not be empty"
|
||||
for region_code in all_regions:
|
||||
low, high = pi_multipliers_for_region(region_code)
|
||||
assert low < 1 < high, f"region {region_code}: low={low} high={high}"
|
||||
# Scalar fallback carries the same invariant.
|
||||
assert settings.estimate_pi_low_mult < 1 < settings.estimate_pi_high_mult
|
||||
|
||||
|
||||
# ── 4. Integration: _price_from_inputs uses the REGIONAL multipliers ──────────
|
||||
|
||||
|
||||
def test_price_from_inputs_uses_regional_multipliers_for_moscow() -> None:
|
||||
ratio = 0.9
|
||||
est = _call(listings=_lots(200_000.0), ratio=ratio, region_code=77)
|
||||
low_mult, high_mult = pi_multipliers_for_region(77)
|
||||
assert est.expected_sold_price is not None
|
||||
assert est.expected_sold_range_low == round(est.expected_sold_price * low_mult)
|
||||
assert est.expected_sold_range_high == round(est.expected_sold_price * high_mult)
|
||||
# Same call with region 66 must give a DIFFERENT high arm (regions diverge).
|
||||
est_ekb = _call(listings=_lots(200_000.0), ratio=ratio, region_code=66)
|
||||
assert est_ekb.expected_sold_range_high != est.expected_sold_range_high
|
||||
|
||||
|
||||
def test_price_from_inputs_region_none_falls_back_to_scalars() -> None:
|
||||
ratio = 0.9
|
||||
est = _call(listings=_lots(200_000.0), ratio=ratio, region_code=None)
|
||||
assert est.expected_sold_price is not None
|
||||
assert est.expected_sold_range_low == round(
|
||||
est.expected_sold_price * settings.estimate_pi_low_mult
|
||||
)
|
||||
assert est.expected_sold_range_high == round(
|
||||
est.expected_sold_price * settings.estimate_pi_high_mult
|
||||
)
|
||||
|
|
@ -568,13 +568,12 @@ def test_estimate_expected_sold_distinct_after_anchor() -> None:
|
|||
assert abs(est.expected_sold_per_m2 - round(est.median_price_per_m2 * ratio)) <= 1
|
||||
# #1966: expected_sold range — калиброванный ~80% PI вокруг точки (point × [p10,p90]
|
||||
# sold/expected_sold), не asking-IQR × ratio.
|
||||
# #3xxx: multipliers are regional now — resolve via pi_multipliers_for_region.
|
||||
from app.services.estimator import pi_multipliers_for_region
|
||||
|
||||
assert est.expected_sold_range_high_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_high_mult
|
||||
)
|
||||
assert est.expected_sold_range_low_rub == round(
|
||||
est.expected_sold_price_rub * settings.estimate_pi_low_mult
|
||||
)
|
||||
_low_mult, _high_mult = pi_multipliers_for_region(66)
|
||||
assert est.expected_sold_range_high_rub == round(est.expected_sold_price_rub * _high_mult)
|
||||
assert est.expected_sold_range_low_rub == round(est.expected_sold_price_rub * _low_mult)
|
||||
# Distinct: sold строго ниже asking (ratio < 1) — фикс двойных идентичных чисел.
|
||||
assert est.expected_sold_price_rub < est.median_price_rub
|
||||
assert est.expected_sold_per_m2 < est.median_price_per_m2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue