Оценка: дедуп склеивает улицы вида «40-летия Комсомола», пороги Tier H — в настройки для бэктеста #3552
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3552
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/estimator-analog-quality"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Пять коммитов плюс merge origin/main. Ни один issue этот PR не закрывает: у обоих приёмка возможна только после деплоя (см. ниже).
Правки по ревью (17.09)
Каждую находку сначала проверил сам.
1. Блокирующая: тесты ставили флаг, который удаляет #3556. Подтверждено, исправлено (коммит
1a9a612a).Проверка: наложил дифф этого PR на голову #3556 (
31c31f8f) в отдельном worktree и прогнал 2087, 3234, гейт бэктеста и test_1970. Результатrc=1, 2 failed, 144 passed:Что сделал: убрал
monkeypatch.setattr(..., "estimate_dedup_analogs_enabled", True)изtest_dedup_collapses_hyphen_numbered_street_four_sourcesиtest_dedup_hyphen_numbered_street_different_houses_stay_distinct. На main флаг и так ON по умолчанию, это проверяетtest_dedup_default_is_on.146 passed, rc=0.164 passed, rc=0.git merge-treeдвух голов проходит без конфликтов.rc=1:assert 4 == 1(четыре источника не склеились) иassert 1 == 2(два дома cian слиплись).2. Про
Closes: ревьюер прав, менять нечего. #2291 и #3234 остаются открытыми, приёмка у обоих после деплоя.3. #2291 (б): однобуквенные порядковые «2-я Новосибирская», «3-й Конный». Подтверждено, исправлено (коммит
45017d25).Когда стал проверять на проде, нашёл больше, чем было в ревью. У порядковой улицы три формата, и все три разбирались по-разному:
улица 2-я Синичкина, 22('', '')Москва, 2-я Синичкина улица, 22('синичкина', '22'), номер потерян2-я ул. Синичкина,22('синичкина', '22'), номер потерянПотерянный номер даёт не только непопадание в дедуп, но и ложную склейку. «Москва, 9-я Парковая улица, 5» и «Москва, 12-я Парковая улица, 5» получали один ключ
('парковая', '5'). Это разные дома одного района, а дедуп идёт внутри радиуса.Что сделал:
(?:\d+-[а-яё]\s+)?перед (нумерованным) именем;_DEDUP_ORDINAL_BEFORE_TYPE_RE).Многобуквенный путь («40-летия», «22-го») не тронут.
Замер на живых данных (tradein-postgres, только SELECT, 17.09). Взял все 12 848 разных пар (source, address) с шаблоном «цифра, дефис или пробел, буква», это 29 959 объявлений. Прогнал разбор до и после:
('', '')954, был ложныйmkr …302, был другой токен 7 457 (почти всегда тот же без номера);('волжский', '95'), стало('95-й', ''), одинаково у yandex и domklik;'4-й'(55 объявлений), потому что «квартал» стоит в шум-словах. У всех источников токен одинаковый, так что для дедупа это не мешает.Гейт бэктеста зелёный, baseline не менялся: фикстура по Екатеринбургу, а порядковые улицы почти все в Москве и области.
4. #2291 (а): «8-Марта» против «8 Марта». Проверил, не правлю. Дефект реальный, но на проде его нет. Среди тех же 12 848 адресов нашёл 51 ключ нумерованной улицы («цифра + имя»), и ни у одного нет двух написаний сразу, через дефис и через пробел. Нормализация дефиса в пробел поменяла бы уже работающие токены «40-летия комсомола» ради нуля случаев. Вернуться к этому, если перезамер после деплоя покажет такие пары.
5. #2291: мусорный токен «12-квартал». Согласен с ревьюером, это экзотика. Не трогаю.
6. #3234: ручки должны вернуться в константы. Согласен (коммит
cbf0f6de). Условие записал в комментарий рядом с полями вconfig.py: после решения по бэктесту (шаг 4 #3234) вернуть их в константы движка, как #1970 сделал с остальными. Это условие закрытия #3234.7. Уборка. Свои временные worktree (
wt-3552-on-3556,wt-3552v2-on-3556) удалил.scratchpad/wt-comboсоздал ревьюер, его не трогал: чтобы удалить, нуженgit worktree remove --force, а мне--forceзапрещён.#2291, класс 4: нумерованные улицы через дефис не попадали в дедуп
Что было. Регексы названия улицы в
_parse_street_house(_DEDUP_STREET_NAME_RE,_DEDUP_STREET_NAME_SUFFIX_RE) пускали ведущую цифру только через пробел («8 Марта»), а через дефис нет. Прогон кода из origin/main:ул. 40-летия Комсомола,32Г('', ''): в дедуп не попадает вообще…Екатеринбург, улица 40-летия Комсомола, 32А('', '')р-н Кировский, мкр. ЖБИ, улица 40-летия Комсомола, 8А('mkr жби', '40')Москва, 60-летия Октября проспект, 18 к3('октября', '18/3')У cian хуже, чем просто пропуск: все дома улицы получают один ключ и дом «40». Поэтому две разные квартиры в домах 8А и 35 с одинаковыми этажом, площадью и ценой сливались в одну.
Что сделано.
(?:\d+\s+)?→(?:\d+(?:\s+|-))?в обоих регексах. Однобуквенные порядковые описаны выше, в пункте 3 правок по ревью.Замер на живых данных (17.09, 5311 пар source/address, 13 449 активных объявлений). Разбор изменился у 730 объявлений. Все 14 новых токенов оказались настоящими улицами: 40-летия комсомола (70), 40-летия октября (64), 3-го интернационала (52), 800-летия москвы (34) и так далее.
Baseline бэктеста перегенерирован в первом коммите. Правка разбора меняет состав пула, поэтому гейт покраснел:
metrics.calibration.high.mape_pct: 19.97 != baseline 19.6. Построчно на тех же 1600 сделках изменились 99: у 48 ошибка стала меньше, у 51 больше. Сдвиги метрик:Это шум в обе стороны. Однобуквенная правка baseline не сдвинула.
Приёмка на проде (после деплоя, с 18.09):
docker exec tradein-backend python -c "from app.services.estimator import _parse_street_house as p; print(p('ул. 40-летия Комсомола,32Г'), p('2-я ул. Синичкина,22'))"('40-летия комсомола', '32г') ('2-я синичкина', '22');('', '') ('синичкина', '22').#3234: порог и окно этажности Tier H вынесены в настройки, дефолты прежние
Что было. Tier H в
_fetch_analogs(год ±15, этажность ×0.7…×1.3) отдаётся наружу только приlen(tier_h) >= 5, иначе молча уходит в Tier W. По Loki за 30 суток так случилось в 61 попытке из 86, и в 31 из них в своей полосе нет ни одного компла. Оба числа были литералами, поэтому прогнать варианты без правки кода было нельзя.Что сделано.
Settingsдобавленыestimate_tier_h_min_comps: int = 5иestimate_tier_h_floors_tol: float = 0.30.tf_min/tf_max, что литералы. Поведение прода не меняется.Приёмка на проде (после деплоя, с 18.09):
docker exec tradein-backend python -c "from app.core.config import settings as s; print(s.estimate_tier_h_min_comps, s.estimate_tier_h_floors_tol)". Новый образ печатает5 0.3, старый падает с AttributeError.-e ESTIMATE_TIER_H_MIN_COMPS=4,-e ESTIMATE_TIER_H_FLOORS_TOL=0.5и оба сразу. Команда каждого:docker exec [-e …] tradein-backend python -m scripts.backtest_estimator --city 'Екатеринбург' --sample 1500 --since 2025-06-01 --resolve-house-id --json 2>&1. Сравнить MAPE и число строкfallthrough to W(они идут в stderr, отсюда2>&1).Тесты
tests/test_estimator_dedup_cross_source_2087.py: +26 тестов. Из них 12 на дефис, 12 кейсов однобуквенных порядковых, склейка трёх форматов плюс cian, «9-я / 12-я Парковая» не сливаются.tests/test_3234_tier_h_settings.py: 4 теста.175 passed, rc=0.DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheproviderдал 6255 passed, 42 skipped, rc=0.uv run ruff check app tests: rc=0.ruff format --checkпо 4 изменённым файлам: rc=0.31c31f8f):164 passed, rc=0.Фальсификация
Каждый раз портил копию исходника, после прогона восстанавливал,
diff -qбез отличий.Флаг (правка 1). Прогон с monkeypatch на голове #3556 дал
2 failed, 144 passed, текст выше. Без monkeypatch тот же прогон146 passed. Без флага откат префиксного регекса даёт2 failed:assert 4 == 1,assert 1 == 2.Однобуквенные порядковые (правка 3), файл
test_estimator_dedup_cross_source_2087.py:estimator.pyиз головы до правки:12 failed, 91 passed, rc=112 failed, 91 passed, rc=1, в том числеassert 4 == 1.6 failed, 97 passed, rc=1Каждую половину правки ловит свой тест.
Дефис (первый коммит). Откат
estimator.pyна origin/main даёт7 failed:assert ('mkr жби', '40') == ('40-летия комсомола', '32г');assert 4 == 1;assert 1 == 2.Tier H. Возврат литералов
>= 5,* 0.7,* 1.3даёт2 failed:assert 'W' == 'H'иassert (6, 12) == (4, 14).Деплой
tradein-mvp/backend, то есть один образ. Пересоздаются tradein-backend, tradein-scraper, tradein-tgbot.308_domclick_msk_disable_until_incremental_save.sql, но она уже в main и к этому PR не относится.SELECT count(*) FROM scrape_runs WHERE status='running'должен дать 0, иначеcompose up -dубьёт идущие обходы. На 17.09 08:54 UTC было 0, в 06:12 было 3, так что проверять заново.Refs #2291, Refs #3234
🤖 Generated with Claude Code
Регексы названия улицы в _parse_street_house пускали ведущую цифру только через пробел («8 Марта»), а через дефис нет. «40-летия Комсомола», «22-го Партсъезда», «3-го Интернационала» разбирались так: - avito «ул. 40-летия Комсомола,32Г» и yandex — ('', ''), дедупа нет вовсе; - cian «мкр. ЖБИ, улица 40-летия Комсомола, 8А» — ('mkr жби', '40'): все дома улицы получали один ключ и дом «40», разные квартиры с одинаковыми этажом/площадью/ценой сливались в одну; - domklik «60-летия Октября проспект» — ('октября', …). Фикс: (?:\d+\s+)? → (?:\d+(?:\s+|-))? в _DEDUP_STREET_NAME_RE и _DEDUP_STREET_NAME_SUFFIX_RE. На 5310 живых адресах прода с «цифра-буква» меняется разбор у 730 объявлений, все новые токены — реальные нумерованные улицы (14 штук, от «40-летия комсомола» до «2-ая новосибирская»). Бэктест-гейт: baseline перегенерирован. В реплее дедуп включён (как на захвате), поэтому правка разбора сдвигает пул: изменились 99 из 1600 сделок, у 48 ошибка меньше, у 51 больше. expected_sold MAPE 15.88→15.92, покрытие вилки 1056→1057, headline не сдвинулся. Refs #2291 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Что было. После «40-летия Комсомола» остались однобуквенные порядковые: регексы требовали после «N-» минимум две буквы. Одна улица в трёх форматах разбиралась по-разному: cian «улица 2-я Синичкина» давал ('', ''), avito/yandex «2-я ул. Синичкина» и domklik «2-я Синичкина улица» теряли номер ('синичкина'). Потеря номера — это ложная склейка: «9-я Парковая, 5» и «12-я Парковая, 5» получали один ключ ('парковая', '5'). Что сделано. В обоих регексах названия улицы добавлен необязательный порядковый «N-x » перед (нумерованным) именем; формат «номер тип имя» переставляется в «тип номер имя» до разбора. Номер дома с дефисом не тронут. Замер на проде (tradein-postgres, SELECT, 17.09): 12 848 разных адресов с шаблоном «цифра-дефис/пробел-буква», 29 959 объявлений. Разбор изменился у 8 713 объявлений; ни один адрес не потерял токен. Межисточниковых групп (улица, дом) 1305 -> 1473. Из 81 прежней группы, которая распалась, 60 — разные порядковые улицы, 3 — ложный mkr-ключ, 18 — голая и порядковая улица в разных городах (одна — источник сам потерял номер). Гейт бэктеста без изменений baseline. Refs #2291 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>