fix(tradein): тест ловит копию константы, а не equality; честный комментарий про вклад свежести (#2660)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m44s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m44s
По ревью PR #2664. 1. test_freshness_window_is_the_estimator_constant_not_a_copy проверял `lc.LISTINGS_FRESH_DAYS is estimator.LISTINGS_FRESH_DAYS` — CPython кэширует малые int, поэтому скопированный литерал `LISTINGS_FRESH_DAYS = 14` тест бы ПРОШЁЛ, хотя докстринг обещает ловить ровно это. Прошлая фальсификация срабатывала лишь потому, что откат удалял имя целиком (AttributeError). Теперь проверяем исходник через inspect.getsource — фальсифицировано подстановкой копии литерала вместо импорта: тест краснеет. 2. Комментарий в location_index.py приписывал свежести чужую заслугу. Прод-разложение: из −14.8% сдвига городской медианы −14.7 п.п. даёт сегментный гард и лишь −0.18 п.п. свежесть. Для этой метрики свежесть — не коррекция смещения, а страховка на будущее, оплаченная третью пула (3 504 вторичных строки, из них 2 724 живые) и ростом дисперсии: на центре ЕКБ n 423 → 86, индекс гуляет по выбору окна на 12-14 п.п. Размен верный, но он должен быть написан как размен. Там же задокументирован новый режим отказа: свежесть связала витрину со здоровьем сбора — встанет скрейпинг на 14 дней, и insufficient_data прилетит всем пользователям разом. Учитывая, что #2574 это месяц молчаливой поломки сбора, сценарий не гипотетический. Окно свежести не меняю — вопрос вынесен отдельно. Refs #2660
This commit is contained in:
parent
837ad8cfd4
commit
d173163025
2 changed files with 46 additions and 12 deletions
|
|
@ -168,16 +168,39 @@ def _pct_deviation(local_median_ppm2: float, city_median_ppm2: float) -> float:
|
|||
# price_per_m2 BETWEEN sanity-границы — не бизнес-калибровка, а защита от битых строк
|
||||
# (см. _PRICE_PER_M2_SANITY_MIN/MAX выше).
|
||||
#
|
||||
# #2660 свежесть + сегмент — оба предиката ЗЕРКАЛЯТ _COMMON_WHERE эстиматора:
|
||||
# - scraped_at > NOW() - LISTINGS_FRESH_DAYS: `is_active` на проде не означает
|
||||
# «живо» — деактиватор протухших работает не на всех источниках, и в пуле
|
||||
# висят объявления, не виденные месяцами. Прод-замер (пул location_index,
|
||||
# bbox ЕКБ): 30 222 активных строк, свежих 11 453; медиана 172 984 ₽/м² по
|
||||
# всему пулу против 163 363 по свежим. Порог тот же, что у эстиматора
|
||||
# (LISTINGS_FRESH_DAYS) — витрина обязана смотреть на тот же пул, что и цена.
|
||||
# - listing_segment guard (#1186): новостройки — девелоперский прайс, он
|
||||
# завышает медиану и локальную, и городскую (прод: 19 003 новостройки из
|
||||
# 30 222; медиана вторички 147 632 ₽/м²). NULL = legacy вторичка до м.011.
|
||||
# #2660 свежесть + сегмент — оба предиката ЗЕРКАЛЯТ _COMMON_WHERE эстиматора.
|
||||
# Вклад у них РАЗНЫЙ, и не тот, на который легко подумать. Прод-разложение
|
||||
# (2026-08-05, пул location_index — bbox ЕКБ + sanity ₽/м² + geo_precision):
|
||||
#
|
||||
# было (только is_active) 30 222 строк 172 984 ₽/м²
|
||||
# + только свежесть 11 453 строк 163 363 ₽/м²
|
||||
# + только сегмент 11 219 строк 147 632 ₽/м²
|
||||
# стало (оба) 7 715 строк 147 368 ₽/м²
|
||||
#
|
||||
# - listing_segment guard (#1186) — ЭТО и есть исправление смещения: из −14.8%
|
||||
# сдвига городской медианы он даёт −14.7 п.п. Девелоперский прайс новостроек
|
||||
# завышал и локальную, и городскую медиану. NULL = legacy вторичка до м.011.
|
||||
# Мертвецы, кстати, живут почти целиком тут же: из 18 769 протухших строк
|
||||
# пула 15 265 — новостройки, и гард выносит их заодно.
|
||||
# - scraped_at > NOW() - LISTINGS_FRESH_DAYS — даёт ПОВЕРХ сегмента всего
|
||||
# −0.18 п.п. Для ЭТОЙ метрики он не коррекция смещения, а СТРАХОВКА на
|
||||
# будущее (пул совпадает с пулом цены; если завтра протухнет вторичка —
|
||||
# виджет не соврёт), и страховка не бесплатная: выбрасывает 3 504 вторичных
|
||||
# строки, из которых 2 724 — живые объявления, отскрейпленные 15-30 дней
|
||||
# назад. Пул −31%, шум растёт: на центре ЕКБ (r=800) n падает 423 → 86, а
|
||||
# сам индекс гуляет по выбору окна на 12-14 п.п. (7д +75.7% / 14д +77.0% /
|
||||
# 21д +79.1% / 30д +64.7%) — при n=86 это в пределах шума выборки медианы.
|
||||
# Размен «меньше смещения ↔ больше дисперсии» сделан осознанно: старое число
|
||||
# было предвзятым, новое — шумным, но честным. Окно менять здесь НЕ надо,
|
||||
# LISTINGS_FRESH_DAYS живёт в estimator.py (см. импорт выше).
|
||||
#
|
||||
# НОВЫЙ РЕЖИМ ОТКАЗА (знать обязательно): свежесть связала витрину со здоровьем
|
||||
# СБОРА. Встанет скрейпинг на LISTINGS_FRESH_DAYS — городская выборка не наберёт
|
||||
# MIN_SAMPLE_SIZE, и "insufficient_data" прилетит ВСЕМ пользователям разом; до
|
||||
# этой правки виджет продолжал бы показывать устаревшее число. Учитывая, что
|
||||
# #2574 — ровно месяц молчаливой поломки сбора, сценарий не гипотетический.
|
||||
# Деградация честная (прочерк, а не выдуманное число), но она теперь массовая.
|
||||
#
|
||||
# Порог MIN_SAMPLE_SIZE после сужения пула набирается реже, но лестница радиусов
|
||||
# упирается в отказ редко — прод-симуляция на 246 реальных точках оценок:
|
||||
# insufficient_data 0 → 1 точка (0.4%), 800м хватает 241 точке из 246.
|
||||
|
|
|
|||
|
|
@ -162,10 +162,21 @@ def test_median_queries_exclude_novostroyki() -> None:
|
|||
|
||||
def test_freshness_window_is_the_estimator_constant_not_a_copy() -> None:
|
||||
"""Второе определение свежести разъедется с эстиматором при первой перекалибровке —
|
||||
витрина начнёт показывать пул, отличный от того, на котором считается цена."""
|
||||
витрина начнёт показывать пул, отличный от того, на котором считается цена.
|
||||
|
||||
Проверяем ИСХОДНИК, а не равенство значений: CPython кэширует малые int (−5…256),
|
||||
поэтому `lc.LISTINGS_FRESH_DAYS is estimator.LISTINGS_FRESH_DAYS` проходит и для
|
||||
скопированного литерала `LISTINGS_FRESH_DAYS = 14` — то есть стережёт не то, что
|
||||
обещает. Отсутствие присваивания в модуле = константа может быть только импортом.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
from app.services import estimator
|
||||
|
||||
assert lc.LISTINGS_FRESH_DAYS is estimator.LISTINGS_FRESH_DAYS
|
||||
assert "LISTINGS_FRESH_DAYS =" not in inspect.getsource(
|
||||
lc
|
||||
), "константа скопирована в location_index — она должна ИМПОРТИРОВАТЬСЯ из estimator"
|
||||
assert lc.LISTINGS_FRESH_DAYS == estimator.LISTINGS_FRESH_DAYS
|
||||
|
||||
|
||||
def test_both_median_queries_bind_the_same_freshness_window() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue