Локальный индекс считает медианы по своему региону, а статус точек интереса не врёт #3517

Merged
lekss361 merged 2 commits from fix/msk-location-index-region into main 2026-09-13 11:14:45 +00:00
Owner

Два дефекта, из-за которых Москву нельзя было запускать.

Медианы считались без фильтра по региону. Оба SQL отбирали листинги только по прямоугольнику продуктового ядра и радиусу, колонки региона в файле не было ни разу. Границы регионов не прямоугольны: в московское ядро попадают Химки, Реутов, Котельники, Люберцы. Замер на проде 13.09: внутри ядра Москвы лежал 16 801 областной лот с медианой 251 295 ₽/м² и 82 719 московских с 458 208, из-за примеси городская медиана падала до 422 222, то есть на 7,9%. Починить по имени города нельзя — у областных строк город пустой.

Фильтр строгий: region_code = :region_code, без поблажки для строк без региона. Такая поблажка была бы лазейкой ровно того класса, который правка и закрывает. Для Свердловской области это статистически ничто: под полным набором фильтров в ядре 15 858 строк, из них без региона 3, медиана сдвигается со 151 101 на 151 103 ₽/м².

Статус точек интереса врал. Проверка смотрела на count(*) по всей таблице — 4 850 строк, все екатеринбургские. Для Москвы таблица не пуста, значит статус молча становился «ок» при пустом блоке «что рядом», хотя внутри московского ядра ноль точек. Теперь проверка считает точки в прямоугольнике ядра того региона, которому принадлежит адрес, и честно отдаёт «нет данных». Терминология не расширялась.

Загрузка московских точек интереса сюда не входит — это отдельная работа с Overpass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs

Два дефекта, из-за которых Москву нельзя было запускать. **Медианы считались без фильтра по региону.** Оба SQL отбирали листинги только по прямоугольнику продуктового ядра и радиусу, колонки региона в файле не было ни разу. Границы регионов не прямоугольны: в московское ядро попадают Химки, Реутов, Котельники, Люберцы. Замер на проде 13.09: внутри ядра Москвы лежал 16 801 областной лот с медианой 251 295 ₽/м² и 82 719 московских с 458 208, из-за примеси городская медиана падала до 422 222, то есть на 7,9%. Починить по имени города нельзя — у областных строк город пустой. Фильтр строгий: `region_code = :region_code`, без поблажки для строк без региона. Такая поблажка была бы лазейкой ровно того класса, который правка и закрывает. Для Свердловской области это статистически ничто: под полным набором фильтров в ядре 15 858 строк, из них без региона 3, медиана сдвигается со 151 101 на 151 103 ₽/м². **Статус точек интереса врал.** Проверка смотрела на `count(*)` по всей таблице — 4 850 строк, все екатеринбургские. Для Москвы таблица не пуста, значит статус молча становился «ок» при пустом блоке «что рядом», хотя внутри московского ядра ноль точек. Теперь проверка считает точки в прямоугольнике ядра того региона, которому принадлежит адрес, и честно отдаёт «нет данных». Терминология не расширялась. Загрузка московских точек интереса сюда не входит — это отдельная работа с Overpass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs
lekss361 added 2 commits 2026-09-13 10:47:16 +00:00
Дефект 1: bbox продукт-ядра Москвы (55.55-55.95/37.30-37.90) — прямоугольник,
не административная граница, и захватывает соседние Химки/Реутов/Котельники/
Люберцы (region_code=50). Обе SQL-медианы (_MEDIAN_PPM2_LOCAL_SQL/CITYWIDE_SQL)
фильтровали листинги только по bbox, без region_code — прод-замер 13.09.2026:
городская медиана Москвы получалась заниженной на 7.9% (422 222 вместо 458 208
₽/м²) из-за примеси 16 801 подмосковного лота. У областных строк city IS NULL,
поэтому фильтровать по имени города нельзя — добавлен bind-параметр
region_code, заполняемый из уже определённого для точки Region.code.
Для 66 сохранено byte-for-byte: `OR region_code IS NULL` оставляет в пуле
некоторое количество legacy-листингов без региональной метки — ровно то же
множество строк, что и старый запрос без фильтра вовсе (проверено на проде:
внутри bbox 66 других region_code, кроме 66 и NULL, не существует).

Дефект 2: гейт _fetch_nearby_poi решал poi_status по count(*) ПО ВСЕЙ
osm_poi_ekb_local (4 850 екатеринбургских строк, всегда > 0) — для Москвы
это давало poi_status="ok" с пустым списком «что рядом», хотя внутри
московского продукт-ядра POI нет вообще. Гейт теперь считает count в
bbox_product_core региона точки; при 0 — тот же статус "unavailable",
что и раньше (терминология не расширяется, trade_in.py трактует его как
«нет данных» без изменений).

Тесты: tests/services/test_location_index.py — добавлены проверки bind-
параметра region_code в обеих median-SQL, региональной изоляции gate'а POI
(bbox запроса совпадает с bbox_product_core переданного региона), резолва
московской точки в region 77, и end-to-end сценария для Москвы (region_code
долетает в запрос, poi_status="unavailable"). 43/43 pytest зелёные,
ruff check + ruff format --check чисто.
fix(tradein): убрать OR region_code IS NULL из median-SQL location_index
All checks were successful
CI Trade-In / changes (pull_request) Successful in 16s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 / changes (pull_request) Successful in 19s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m33s
3fae2b4ed8
Строгое region_code = CAST(:region_code AS integer), без OR-ветки на NULL.

Второй прод-замер (13.09.2026, полный набор фильтров модуля — is_active,
price sanity, geo_precision, свежесть 14 дней, сегмент, bbox ядра 66):
15 858 строк проходят фильтры, из них 3 с NULL region_code. Медиана с ними
151 101 ₽/м², без них 151 103 — сдвиг 2 рубля на 151 тысяче (0.001%),
для региона 66 статистически ничто.

Мягкий OR NULL был бы лазейкой того же класса, что и сам дефект: в
listings 16 930 NULL-region строк Avito, 24 из них уже лежат в bbox
Москвы (сегодня не проходят по свежести, но могут завтра) — OR NULL
тихо открыл бы им дорогу обратно в московскую медиану тем же путём,
каким туда раньше попадала Московская область.

Тесты: test_median_queries_do_not_admit_null_region_code (обе median-SQL
не содержат "OR region_code" / "region_code IS NULL"), комментарий перед
SQL переписан под новое решение с этими числами.
lekss361 merged commit 76a2963edc into main 2026-09-13 11:14:45 +00:00
lekss361 deleted branch fix/msk-location-index-region 2026-09-13 11:14:45 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3517
No description provided.