Оценка: дедуп склеивает улицы вида «40-летия Комсомола», пороги Tier H — в настройки для бэктеста #3552

Merged
bot-backend merged 6 commits from fix/estimator-analog-quality into main 2026-09-17 09:23:07 +00:00
Collaborator

Пять коммитов плюс merge origin/main. Ни один issue этот PR не закрывает: у обоих приёмка возможна только после деплоя (см. ниже).

Правки по ревью (17.09)

Каждую находку сначала проверил сам.

1. Блокирующая: тесты ставили флаг, который удаляет #3556. Подтверждено, исправлено (коммит 1a9a612a).
Проверка: наложил дифф этого PR на голову #3556 (31c31f8f) в отдельном worktree и прогнал 2087, 3234, гейт бэктеста и test_1970. Результат rc=1, 2 failed, 144 passed:

E       AttributeError: Settings(...) has no attribute 'estimate_dedup_analogs_enabled'
tests/test_estimator_dedup_cross_source_2087.py:963: AttributeError
tests/test_estimator_dedup_cross_source_2087.py:981: AttributeError

Что сделал: убрал 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.
  • Финальный дифф PR на голове #3556 (2087, 3234, гейт, 1970, 2265): 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).
Когда стал проверять на проде, нашёл больше, чем было в ревью. У порядковой улицы три формата, и все три разбирались по-разному:

формат пример (прод) было
тип, номер, имя (cian) улица 2-я Синичкина, 22 ('', '')
номер, имя, тип (domklik, avito) Москва, 2-я Синичкина улица, 22 ('синичкина', '22'), номер потерян
номер, тип, имя (yandex, avito, domklik) 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 объявлений. Прогнал разбор до и после:

  • разбор изменился у 8 713 объявлений: было ('', '') 954, был ложный mkr … 302, был другой токен 7 457 (почти всегда тот же без номера);
  • ни один адрес не потерял токен. Первая версия правки теряла его на «2-й переулок 1 Мая» (11 объявлений). Поймал этим же замером и починил до коммита;
  • номер дома изменился у 3 адресов (11 объявлений), все угловые или экзотика: «Волжский бульвар 95-й квартал, к3» было ('волжский', '95'), стало ('95-й', ''), одинаково у yandex и domklik;
  • групп (улица, дом), где сходятся ≥2 источника: 1305 → 1473;
  • из 81 прежней группы, которая распалась: 60 — это разные порядковые улицы (9-я и 12-я Парковая, 1-я и 4-я Тверская-Ямская), 3 держались на ложном mkr-ключе, 18 — голая и порядковая улица в разных местах («Парковая улица, Долгопрудный» и «3-я Парковая, Москва»; «Ленинский проспект» и «4-й Ленинский переулок»; «улица 1 Мая, Балашиха» и «2-й переулок 1 Мая, Павловский Посад»). Недосклейка из-за правки одна: domklik сам пишет «Волоколамский проезд, 6 к1» без номера;
  • слабые токены: улицы «N-й квартал» дают '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:

источник адрес разбор до
avito ул. 40-летия Комсомола,32Г ('', ''): в дедуп не попадает вообще
yandex …Екатеринбург, улица 40-летия Комсомола, 32А ('', '')
cian р-н Кировский, мкр. ЖБИ, улица 40-летия Комсомола, 8А ('mkr жби', '40')
domklik Москва, 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 больше. Сдвиги метрик:

  • expected_sold MAPE: 15.88 → 15.92;
  • покрытие: 1056 → 1057;
  • medium MAPE: 27.59 → 26.53 (n=30);
  • high MAPE: 19.6 → 19.97 (n=8).

Это шум в обе стороны. Однобуквенная правка baseline не сдвинула.

Приёмка на проде (после деплоя, с 18.09):

  1. Маркер кода в контейнере, отличает старое от нового в обе стороны:
    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');
    • код до этого PR: ('', '') ('синичкина', '22').
  2. Первый пункт acceptance #2291: перезамер методикой #2417 (pre и post на одном наборе групп, код из контейнера). Выписать выживших по классам. Закрывать #2291, если выживших не больше, чем кадастр-конфликтов плюс ~3 %.

#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, что литералы. Поведение прода не меняется.
  • Ручки временные: условие возврата в константы записано в коде (пункт 6 правок).

Приёмка на проде (после деплоя, с 18.09):

  1. Маркер: 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.
  2. Четыре прогона: база, -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).
  3. Дефолт менять отдельным PR, только если MAPE не хуже базы. В том же PR вернуть обе ручки в константы, после чего закрыть #3234.

Тесты

  • tests/test_estimator_dedup_cross_source_2087.py: +26 тестов. Из них 12 на дефис, 12 кейсов однобуквенных порядковых, склейка трёх форматов плюс cian, «9-я / 12-я Парковая» не сливаются.
  • tests/test_3234_tier_h_settings.py: 4 теста.
  • Целевые (2087, 3234, гейт бэктеста, 2265, 1871, display consistency, same_building_anchor): 175 passed, rc=0.
  • Весь сьют МЕРА backend: 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.
  • Связка с #3556 (дифф PR на 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:

  • M0, estimator.py из головы до правки: 12 failed, 91 passed, rc=1
    E       AssertionError: assert ('синичкина', '22') == ('2-я синичкина', '22')
    E       AssertionError: assert ('', '') == ('2-я новосибирская', '5')
    E       AssertionError: assert ('mkr перловка', '1') == ('1-я крестьянская', '33/2')
    E       AssertionError: assert ('1 мая', '13') == ('2-й 1 мая', '13')
    E       AssertionError: assert 2 == 1      # четыре формата не склеились
    E       AssertionError: assert 1 == 2      # 9-я и 12-я Парковая слиплись
    
  • M1, убран только порядковый из регексов (перестановка осталась): 12 failed, 91 passed, rc=1, в том числе assert 4 == 1.
  • M2, убрана только перестановка «номер, тип, имя»: 6 failed, 97 passed, rc=1
    E       AssertionError: assert ('синичкина', '22') == ('2-я синичкина', '22')
    E       AssertionError: assert ('марьиной рощи', '8') == ('12-й марьиной рощи', '8')
    E       AssertionError: assert 2 == 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.
  • Миграций в диффе PR нет. Merge origin/main принёс 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, так что проверять заново.
  • С #3556 сливается без конфликтов в любом порядке, тесты зелёные в связке.

Refs #2291, Refs #3234

🤖 Generated with Claude Code

Пять коммитов плюс merge origin/main. Ни один issue этот PR не закрывает: у обоих приёмка возможна только после деплоя (см. ниже). ## Правки по ревью (17.09) Каждую находку сначала проверил сам. **1. Блокирующая: тесты ставили флаг, который удаляет #3556. Подтверждено, исправлено** (коммит `1a9a612a`). Проверка: наложил дифф этого PR на голову #3556 (`31c31f8f`) в отдельном worktree и прогнал 2087, 3234, гейт бэктеста и test_1970. Результат `rc=1, 2 failed, 144 passed`: ``` E AttributeError: Settings(...) has no attribute 'estimate_dedup_analogs_enabled' tests/test_estimator_dedup_cross_source_2087.py:963: AttributeError tests/test_estimator_dedup_cross_source_2087.py:981: AttributeError ``` Что сделал: убрал `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`. - Финальный дифф PR на голове #3556 (2087, 3234, гейт, 1970, 2265): `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`). Когда стал проверять на проде, нашёл больше, чем было в ревью. У порядковой улицы три формата, и все три разбирались по-разному: | формат | пример (прод) | было | |---|---|---| | тип, номер, имя (cian) | `улица 2-я Синичкина, 22` | `('', '')` | | номер, имя, тип (domklik, avito) | `Москва, 2-я Синичкина улица, 22` | `('синичкина', '22')`, номер потерян | | номер, тип, имя (yandex, avito, domklik) | `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 объявлений. Прогнал разбор до и после: - разбор изменился у 8 713 объявлений: было `('', '')` 954, был ложный `mkr …` 302, был другой токен 7 457 (почти всегда тот же без номера); - **ни один адрес не потерял токен.** Первая версия правки теряла его на «2-й переулок 1 Мая» (11 объявлений). Поймал этим же замером и починил до коммита; - номер дома изменился у 3 адресов (11 объявлений), все угловые или экзотика: «Волжский бульвар 95-й квартал, к3» было `('волжский', '95')`, стало `('95-й', '')`, одинаково у yandex и domklik; - групп (улица, дом), где сходятся ≥2 источника: **1305 → 1473**; - из 81 прежней группы, которая распалась: 60 — это разные порядковые улицы (9-я и 12-я Парковая, 1-я и 4-я Тверская-Ямская), 3 держались на ложном mkr-ключе, 18 — голая и порядковая улица в разных местах («Парковая улица, Долгопрудный» и «3-я Парковая, Москва»; «Ленинский проспект» и «4-й Ленинский переулок»; «улица 1 Мая, Балашиха» и «2-й переулок 1 Мая, Павловский Посад»). Недосклейка из-за правки одна: domklik сам пишет «Волоколамский проезд, 6 к1» без номера; - слабые токены: улицы «N-й квартал» дают `'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: | источник | адрес | разбор до | |---|---|---| | avito | `ул. 40-летия Комсомола,32Г` | `('', '')`: в дедуп не попадает вообще | | yandex | `…Екатеринбург, улица 40-летия Комсомола, 32А` | `('', '')` | | cian | `р-н Кировский, мкр. ЖБИ, улица 40-летия Комсомола, 8А` | `('mkr жби', '40')` | | domklik | `Москва, 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 больше. Сдвиги метрик: - expected_sold MAPE: 15.88 → 15.92; - покрытие: 1056 → 1057; - medium MAPE: 27.59 → 26.53 (n=30); - high MAPE: 19.6 → 19.97 (n=8). Это шум в обе стороны. Однобуквенная правка baseline не сдвинула. **Приёмка на проде (после деплоя, с 18.09):** 1. Маркер кода в контейнере, отличает старое от нового в обе стороны: `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')`; - код до этого PR: `('', '') ('синичкина', '22')`. 2. Первый пункт acceptance #2291: перезамер методикой #2417 (pre и post на одном наборе групп, код из контейнера). Выписать выживших по классам. Закрывать #2291, если выживших не больше, чем кадастр-конфликтов плюс ~3 %. ## #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`, что литералы. Поведение прода не меняется. - Ручки временные: условие возврата в константы записано в коде (пункт 6 правок). **Приёмка на проде (после деплоя, с 18.09):** 1. Маркер: `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. 2. Четыре прогона: база, `-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`). 3. Дефолт менять отдельным PR, только если MAPE не хуже базы. В том же PR вернуть обе ручки в константы, после чего закрыть #3234. ## Тесты - `tests/test_estimator_dedup_cross_source_2087.py`: +26 тестов. Из них 12 на дефис, 12 кейсов однобуквенных порядковых, склейка трёх форматов плюс cian, «9-я / 12-я Парковая» не сливаются. - `tests/test_3234_tier_h_settings.py`: 4 теста. - Целевые (2087, 3234, гейт бэктеста, 2265, 1871, display consistency, same_building_anchor): `175 passed, rc=0`. - Весь сьют МЕРА backend: `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. - Связка с #3556 (дифф PR на `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`: - M0, `estimator.py` из головы до правки: `12 failed, 91 passed, rc=1` ``` E AssertionError: assert ('синичкина', '22') == ('2-я синичкина', '22') E AssertionError: assert ('', '') == ('2-я новосибирская', '5') E AssertionError: assert ('mkr перловка', '1') == ('1-я крестьянская', '33/2') E AssertionError: assert ('1 мая', '13') == ('2-й 1 мая', '13') E AssertionError: assert 2 == 1 # четыре формата не склеились E AssertionError: assert 1 == 2 # 9-я и 12-я Парковая слиплись ``` - M1, убран только порядковый из регексов (перестановка осталась): `12 failed, 91 passed, rc=1`, в том числе `assert 4 == 1`. - M2, убрана только перестановка «номер, тип, имя»: `6 failed, 97 passed, rc=1` ``` E AssertionError: assert ('синичкина', '22') == ('2-я синичкина', '22') E AssertionError: assert ('марьиной рощи', '8') == ('12-й марьиной рощи', '8') E AssertionError: assert 2 == 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**. - Миграций в диффе PR нет. Merge origin/main принёс `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, так что проверять заново. - С #3556 сливается без конфликтов в любом порядке, тесты зелёные в связке. Refs #2291, Refs #3234 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 2 commits 2026-09-17 07:32:30 +00:00
Регексы названия улицы в _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>
feat(mera/estimator): порог и окно этажности Tier H — в настройки, для бэктеста (#3234)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 23s
CI / changes (pull_request) Successful in 24s
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 6m30s
08216e4c43
Tier H («тот же класс дома») отдаётся только при ≥5 комплах, иначе молча уходит в
Tier W без фильтра по классу. Прод, Loki 30 сут (комментарий 12.09): 61 из 86
попыток не доживает, в 31 из них в своей полосе 0 комплов. Какой вариант лучше —
порог 5→4, окно этажей ±30%→±50% или оба — решает только MAPE бэктеста на сделках,
а оба числа были литералами: сравнить варианты без правки кода было нельзя.

estimate_tier_h_min_comps (5) и estimate_tier_h_floors_tol (0.30) в Settings,
_fetch_analogs берёт их оттуда. Дефолты дают те же tf_min/tf_max, что литералы
(сверено для total_floors 1..199), поведение прода не меняется. Бэктест варианта:
docker exec -e ESTIMATE_TIER_H_MIN_COMPS=4 -e ESTIMATE_TIER_H_FLOORS_TOL=0.5 …

Дефолт не меняю: бэктест вариантов не прогнан (нужен деплой этих ручек).

Refs #3234

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Light1YT added 4 commits 2026-09-17 08:54:19 +00:00
Два новых теста класса 4 ставили monkeypatch.setattr(settings,
"estimate_dedup_analogs_enabled", True). PR #3556 (#2378) удаляет это поле из
Settings, и setattr по несуществующему атрибуту падает AttributeError. CI
гоняет голову ветки, а не merge-ref, поэтому второй смерженный PR пришёл бы
зелёным и уронил main. На main флаг и так ON по умолчанию
(test_dedup_default_is_on), так что строки лишние при любом порядке мержа.

Проверено: дифф PR, наложенный на голову #3556 (31c31f8f), до правки
2 failed / 144 passed (AttributeError), после 146 passed. Тесты не стали
пустыми: откат префиксного регекса без флага даёт assert 4 == 1 и 1 == 2.

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>
Ревью #3552: направление расходится с #3556/#1970, где неменявшиеся
estimate_*-настройки переводят в константы движка. Две новые ручки нужны
только для бэктеста вариантов; условие возврата записано рядом с полями,
чтобы его увидел тот, кто будет делать шаг 4 #3234.

Refs #3234

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merge origin/main в fix/estimator-analog-quality
All checks were successful
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 5m24s
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 17s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
3992d7546f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit a672178bcb into main 2026-09-17 09:23:07 +00:00
Sign in to join this conversation.
No reviewers
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#3552
No description provided.