tradein: бэкфилл городских меток — ТОЛЬКО yandex/cian, наивный гео-критерий снесёт ~640 корректных меток Авито #2628

Closed
opened 2026-08-02 12:03:44 +00:00 by lekss361 · 1 comment
Owner

Поток перекрыт в PR #2626 (гео-проверка городской метки при записи), но накопленное не самозалечивается и требует отдельной чистки. У этой чистки есть неочевидное ограничение, из-за которого наивная реализация сделает хуже.

Почему само не рассосётся

save_listings при конфликте делает city = COALESCE(EXCLUDED.city, listings.city) (base.py:589) и city = COALESCE(:city, city) (base.py:701). То есть NULL от новой защиты не перетирает уже записанный неверный город. 106 активных строк Верхней Пышмы будут переживать каждый повторный обход со своей ошибочной меткой бесконечно.

Проверено после деплоя #2626: 758 строк всего / 118 активных — без изменений, как и ожидалось.

Главное ограничение: нельзя применять гео-критерий ко всем источникам

Надёжность координат разная по источникам, и защита при записи включается ровно там, где координаты правдивые.

Авито не отдаёт координаты в фазе выдачиlat=lon=None у каждой карточки (providers/avito/serp.py, три явных места), а save_listings вызывается ДО фазы деталей. Поэтому защита для Авито инертна по построению.

И это хорошо, потому что координаты Авито в базе систематически неверны. Медиана расстояния от якоря города до его же объявлений:

город источник n p50 max за радиусом
Каменск-Уральский avito 222 93,5 км 93,8 209
Нижний Тагил avito 506 118,0 км 125,3 387
Серов avito 30 301,4 км 307,6 25
Каменск-Уральский yandex 129 3,2 км 8,8 0
Нижний Тагил yandex 87 4,0 км 9,5 0
Верхняя Пышма yandex 119 14,9 км 19,2 106

Гигантские медианы Авито — это в точности расстояние до Екатеринбурга: строкам проставлен центроид ЕКБ (известная утечка геокодера на адресах без улицы; у всех 21 строки Пышмы координаты идентичны). Но адрес при этом называет свой город: Каменск 214 из 222, Тагил 389 из 506, Серов 30 из 30, Пышма 24 из 26.

То есть у Авито врут координаты, а метка города верна. Наивный бэкфилл по гео-критерию снесёт ~640 корректных меток и сделает данные хуже, чем сейчас.

Как делать правильно

  1. Ограничить бэкфилл источниками с провайдерскими координатамиyandex и cian. Там точность правила подтверждена: 113 корректных срабатываний из 115.
  2. Либо сверять с текстом адреса: если адрес называет город метки — метку не трогать независимо от координат. Это покрыло бы и Авито, но требует парсинга топонимов, а он в проекте уже есть (_names_non_ekb_city).
  3. Учесть два известных ложных срабатывания правила (оба единичные, оба активные): Верхняя Пышма, улица Орджоникидзе, 1 на 14,4 км и Первоуральск, Береговая улица, 7А на 28,1 км — Первоуральский городской округ географически больше города, и 15 км тесноваты для округа, хотя верны для города.

Масштаб чистки

118 активных строк / 758 всего получили бы NULL. Из них по yandex/cian — 115, и это тот безопасный набор, с которого стоит начать. Три строки Серова по Авито — ложные срабатывания, их трогать нельзя.

Денежный эффект — небольшой и в правильную сторону

asking_to_sold_ratio.py:124 гейтит AND (city IS NULL OR city ILIKE :asking_city), то есть NULL считается «своим». Обнуление неверной метки НЕ убирает лот из выборки Пышмы (было: попадал по имени, станет: по NULL). Реальный эффект один: 106 настоящих екатеринбургских лотов вернутся в екатеринбургскую выборку, откуда их выбрасывала неверная метка. Магнитуда менее процента от ~11,5 тыс.

Побочно: комментарии в коде объясняют механизм неверно

В пяти с лишним местах (докстринги base.py, pipeline.py, комментарии тестов) написано, что утечка идёт через radius_m=25000 у яндексовых развёрток. Но providers/yandex/serp.py:655 прямо говорит: lat/lon/radius_m ignored (uses rgid, city-scoped). Реальный вектор — rgid (166366), а radius_m инертен. Фикс координатный и работает независимо от механизма, но объяснение введёт в заблуждение следующего инженера. Стоит поправить при ближайшем касании.

Связано: #2626 (перекрытие потока), #2583, #2617 (money-path через listings.city).

Поток перекрыт в PR #2626 (гео-проверка городской метки при записи), но накопленное **не самозалечивается** и требует отдельной чистки. У этой чистки есть неочевидное ограничение, из-за которого наивная реализация сделает хуже. ## Почему само не рассосётся `save_listings` при конфликте делает `city = COALESCE(EXCLUDED.city, listings.city)` (`base.py:589`) и `city = COALESCE(:city, city)` (`base.py:701`). То есть `NULL` от новой защиты **не перетирает** уже записанный неверный город. 106 активных строк Верхней Пышмы будут переживать каждый повторный обход со своей ошибочной меткой бесконечно. Проверено после деплоя #2626: 758 строк всего / 118 активных — без изменений, как и ожидалось. ## ⛔ Главное ограничение: нельзя применять гео-критерий ко всем источникам Надёжность координат разная по источникам, и защита при записи включается ровно там, где координаты правдивые. **Авито не отдаёт координаты в фазе выдачи** — `lat=lon=None` у каждой карточки (`providers/avito/serp.py`, три явных места), а `save_listings` вызывается ДО фазы деталей. Поэтому защита для Авито инертна по построению. И это хорошо, потому что координаты Авито в базе систематически неверны. Медиана расстояния от якоря города до его же объявлений: | город | источник | n | p50 | max | за радиусом | |---|---|---|---|---|---| | Каменск-Уральский | avito | 222 | **93,5 км** | 93,8 | 209 | | Нижний Тагил | avito | 506 | **118,0 км** | 125,3 | 387 | | Серов | avito | 30 | **301,4 км** | 307,6 | 25 | | Каменск-Уральский | yandex | 129 | 3,2 км | 8,8 | 0 | | Нижний Тагил | yandex | 87 | 4,0 км | 9,5 | 0 | | Верхняя Пышма | yandex | 119 | 14,9 км | 19,2 | 106 | Гигантские медианы Авито — это в точности расстояние до Екатеринбурга: строкам проставлен центроид ЕКБ (известная утечка геокодера на адресах без улицы; у всех 21 строки Пышмы координаты идентичны). **Но адрес при этом называет свой город**: Каменск 214 из 222, Тагил 389 из 506, Серов **30 из 30**, Пышма 24 из 26. То есть у Авито **врут координаты, а метка города верна**. Наивный бэкфилл по гео-критерию снесёт ~640 корректных меток и сделает данные хуже, чем сейчас. ## Как делать правильно 1. **Ограничить бэкфилл источниками с провайдерскими координатами** — `yandex` и `cian`. Там точность правила подтверждена: 113 корректных срабатываний из 115. 2. **Либо** сверять с текстом адреса: если адрес называет город метки — метку не трогать независимо от координат. Это покрыло бы и Авито, но требует парсинга топонимов, а он в проекте уже есть (`_names_non_ekb_city`). 3. **Учесть два известных ложных срабатывания** правила (оба единичные, оба активные): `Верхняя Пышма, улица Орджоникидзе, 1` на 14,4 км и `Первоуральск, Береговая улица, 7А` на 28,1 км — Первоуральский городской округ географически больше города, и 15 км тесноваты для округа, хотя верны для города. ## Масштаб чистки 118 активных строк / 758 всего получили бы `NULL`. Из них по yandex/cian — 115, и это тот безопасный набор, с которого стоит начать. Три строки Серова по Авито — ложные срабатывания, их трогать нельзя. ## Денежный эффект — небольшой и в правильную сторону `asking_to_sold_ratio.py:124` гейтит `AND (city IS NULL OR city ILIKE :asking_city)`, то есть `NULL` считается «своим». Обнуление неверной метки НЕ убирает лот из выборки Пышмы (было: попадал по имени, станет: по `NULL`). Реальный эффект один: 106 настоящих екатеринбургских лотов вернутся в екатеринбургскую выборку, откуда их выбрасывала неверная метка. Магнитуда менее процента от ~11,5 тыс. ## Побочно: комментарии в коде объясняют механизм неверно В пяти с лишним местах (докстринги `base.py`, `pipeline.py`, комментарии тестов) написано, что утечка идёт через `radius_m=25000` у яндексовых развёрток. Но `providers/yandex/serp.py:655` прямо говорит: `lat/lon/radius_m ignored (uses rgid, city-scoped)`. Реальный вектор — rgid (166366), а `radius_m` инертен. Фикс координатный и работает независимо от механизма, но объяснение введёт в заблуждение следующего инженера. Стоит поправить при ближайшем касании. Связано: #2626 (перекрытие потока), #2583, #2617 (money-path через `listings.city`).
Collaborator

Сделано в PR #2646 (merged, миграция 207 авто-применена на прод, verified):

  • Чистка строго yandex/cian, двойной критерий (haversine 1:1 с write-time guard'ом И «адрес не называет город метки» — issue п.1+п.2 вместе): Авито не тронут, оба известных false-positive исключены.
  • Прод-результат: 53 строки обнулено (Верхняя Пышма 45, Первоуральск 8, все yandex). Меньше оценки ~115: у ~60 строк адрес называет свой город — им доверяем (ровно тот принцип, что защищает Авито), плюс часть листингов деактивировалась с 02.08. Post-verify: повторный dry-run = 0 (идемпотентность подтверждена).
  • Побочное из issue: 6 комментариев с неверным объяснением вектора («radius_m=25000») поправлены на реальный (rgid, city-scoped).

Серая зона осталась осознанно: строки за радиусом, чей адрес называет город метки, — по логике issue им доверяем; если захочется дочистить и их, это отдельное решение (риск = avito-класс). Неактивные строки (605) не тронуты — money-path фильтрует is_active (проверено ревью). Закрываю.

Сделано в PR #2646 (merged, миграция 207 авто-применена на прод, verified): - Чистка строго yandex/cian, **двойной** критерий (haversine 1:1 с write-time guard'ом И «адрес не называет город метки» — issue п.1+п.2 вместе): Авито не тронут, оба известных false-positive исключены. - Прод-результат: **53 строки** обнулено (Верхняя Пышма 45, Первоуральск 8, все yandex). Меньше оценки ~115: у ~60 строк адрес называет свой город — им доверяем (ровно тот принцип, что защищает Авито), плюс часть листингов деактивировалась с 02.08. Post-verify: повторный dry-run = 0 (идемпотентность подтверждена). - Побочное из issue: 6 комментариев с неверным объяснением вектора («radius_m=25000») поправлены на реальный (rgid, city-scoped). Серая зона осталась осознанно: строки за радиусом, чей адрес называет город метки, — по логике issue им доверяем; если захочется дочистить и их, это отдельное решение (риск = avito-класс). Неактивные строки (605) не тронуты — money-path фильтрует is_active (проверено ревью). Закрываю.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#2628
No description provided.