tradein: TVF street_sales_vs_listings фильтрует сделки по d.rooms — та же синтетика, что снята в #3256; /sales-vs-listings отдаёт пустое пересечение #3451

Closed
opened 2026-09-11 21:53:46 +00:00 by bot-backend · 1 comment
Collaborator

Выделено из ревью PR #3445 (#3256).

Факт. data/sql/211_sales_vs_listings_segment_guard.sql:89AND d.rooms = p_rooms, а trade_in.py:2543 отдаёт туда комнатность клиента. Но deals.rooms — синтетика из площади (321 559 из 321 560 строк: rooms == area_bucket(area_m2), max(rooms)=4), то есть предикат работает как второй фильтр по площади и спорит с полосой, которую TVF считает сама. Ровно та патология, что снята в #3256 на четырёх сделочных сайтах.

Ключ асимметричный — копипастой не чинится. Строкой ниже (:113) стоит l.rooms = p_rooms, и он законен: у объявлений комнатность настоящая. То есть нужно снять предикат только со сделочной стороны, оставив листинговую.

Последствие в продукте. После #3256 экран расходится сам с собой: KPI «Сделки» (из /street-deals, ключ снят) у ~30 % клиентов становится непустым, а строки пар ДКП↔объявление под ним (из /sales-vs-listings, ключ на месте) остаются пустыми. Экран не ломается (знаменатель убран в #3320), но выглядит как потеря данных. Те же 180 клиентов, у которых предикат вырезал сделочную выборку целиком, здесь по-прежнему видят пусто.

Чинить: миграцией — новая версия TVF без d.rooms = p_rooms, l.rooms не трогать. Тест по значению: клиент с 5-6 комнатами (для которого сделочная сторона пуста всегда) получает непустое пересечение; клиент с типичной комнатностью — прежний результат.

Приёмка: на тех же 1179 реальных запросах доля непустых ответов /sales-vs-listings растёт до уровня /street-deals; расхождение между KPI и таблицей на экране исчезает.

Refs #3256, PR #3445, #3320.

Выделено из ревью PR #3445 (#3256). **Факт.** `data/sql/211_sales_vs_listings_segment_guard.sql:89` — `AND d.rooms = p_rooms`, а `trade_in.py:2543` отдаёт туда комнатность клиента. Но `deals.rooms` — синтетика из площади (321 559 из 321 560 строк: `rooms == area_bucket(area_m2)`, `max(rooms)=4`), то есть предикат работает как второй фильтр по площади и спорит с полосой, которую TVF считает сама. Ровно та патология, что снята в #3256 на четырёх сделочных сайтах. **Ключ асимметричный — копипастой не чинится.** Строкой ниже (`:113`) стоит `l.rooms = p_rooms`, и он **законен**: у объявлений комнатность настоящая. То есть нужно снять предикат только со сделочной стороны, оставив листинговую. **Последствие в продукте.** После #3256 экран расходится сам с собой: KPI «Сделки» (из `/street-deals`, ключ снят) у ~30 % клиентов становится непустым, а строки пар ДКП↔объявление под ним (из `/sales-vs-listings`, ключ на месте) остаются пустыми. Экран не ломается (знаменатель убран в #3320), но выглядит как потеря данных. Те же 180 клиентов, у которых предикат вырезал сделочную выборку целиком, здесь по-прежнему видят пусто. **Чинить:** миграцией — новая версия TVF без `d.rooms = p_rooms`, `l.rooms` не трогать. Тест по значению: клиент с 5-6 комнатами (для которого сделочная сторона пуста всегда) получает непустое пересечение; клиент с типичной комнатностью — прежний результат. **Приёмка:** на тех же 1179 реальных запросах доля непустых ответов `/sales-vs-listings` растёт до уровня `/street-deals`; расхождение между KPI и таблицей на экране исчезает. Refs #3256, PR #3445, #3320.
Author
Collaborator

Приёмка на проде — 2026-09-12

Миграция 300 доехала (проверял не статусами джоб, а фактами в боевой БД):

_schema_migrations LIKE '300_%'                          = 1
d.rooms = p_rooms в теле ЖИВОЙ функции (pg_proc.prosrc)  = 0
l.rooms = p_rooms там же                                 = 1
перегрузок street_sales_vs_listings в pg_proc            = 1   (ловушка #2627 не наступила)

Живой вызов на проде — тот же клиент, что в замере был пустым (Белинского, 90 м², 2к):

SELECT count(*), count(listing_id), median(discount_pct), array_agg(DISTINCT deal_rooms)
FROM street_sales_vs_listings('%Белинского%', 90.0, 2, 180, 0.15, 24, 'Екатеринбург');
было стало
сделок в выдаче 0 46
из них с подобранным объявлением 0 22
медианный торг не показывался −22.39 %
комнатности сделок в выдаче {3, 4}

Последняя строка — это и есть честность правки: клиент спрашивал про 2 комнаты, а deal_rooms показывает 3 и 4, потому что у сделок Росреестра это бакет площади, а не комнатность (90 м² → бакет 4, 62–85 м² → бакет 3). Раньше предикат d.rooms = 2 выбрасывал обе эти группы целиком — то есть всю полосу ±15 % вокруг 90 м².

Приёмка по исходной формулировке

«Доля непустых ответов растёт до уровня /street-deals» — измерено офлайн на тех же 1160 реальных запросах: 84.4 % → 94.2 %, выборка не сократилась ни у кого (0 из 954). Показываемое число тоже замерено, по замечанию ревью: медианный торг показан 231 → 269 клиентам, сама медиана −7.38 % → −7.63 % (сдвиг 0.25 п.п. в сторону скидки, а не в плюс, как опасалось ревью). Один ключ (Ясная, 65 м², 2к) перешёл из «показано» в «погашено гейтом правдоподобия» — назван прямо.

Оговорка, которую уношу дальше: прогноз в самой issue был «те же 180 клиентов», измерено 94. Прогноз брался по коридору эстиматора с другими period/tolerance; в код и в PR положено измеренное.

PR #3461, merged. Остаток по теме — #3234 (подбор аналогов), там свой замер.

## Приёмка на проде — 2026-09-12 Миграция 300 доехала (проверял не статусами джоб, а фактами в боевой БД): ``` _schema_migrations LIKE '300_%' = 1 d.rooms = p_rooms в теле ЖИВОЙ функции (pg_proc.prosrc) = 0 l.rooms = p_rooms там же = 1 перегрузок street_sales_vs_listings в pg_proc = 1 (ловушка #2627 не наступила) ``` Живой вызов на проде — тот же клиент, что в замере был пустым (`Белинского, 90 м², 2к`): ```sql SELECT count(*), count(listing_id), median(discount_pct), array_agg(DISTINCT deal_rooms) FROM street_sales_vs_listings('%Белинского%', 90.0, 2, 180, 0.15, 24, 'Екатеринбург'); ``` | | было | стало | |---|---|---| | сделок в выдаче | **0** | **46** | | из них с подобранным объявлением | 0 | **22** | | медианный торг | не показывался | **−22.39 %** | | комнатности сделок в выдаче | — | `{3, 4}` | Последняя строка — это и есть честность правки: клиент спрашивал про 2 комнаты, а `deal_rooms` показывает 3 и 4, потому что у сделок Росреестра это **бакет площади**, а не комнатность (90 м² → бакет 4, 62–85 м² → бакет 3). Раньше предикат `d.rooms = 2` выбрасывал обе эти группы целиком — то есть всю полосу ±15 % вокруг 90 м². ## Приёмка по исходной формулировке «Доля непустых ответов растёт до уровня `/street-deals`» — измерено офлайн на тех же 1160 реальных запросах: **84.4 % → 94.2 %**, выборка не сократилась ни у кого (0 из 954). Показываемое число тоже замерено, по замечанию ревью: медианный торг показан 231 → **269** клиентам, сама медиана −7.38 % → −7.63 % (сдвиг 0.25 п.п. в сторону скидки, а не в плюс, как опасалось ревью). Один ключ (`Ясная, 65 м², 2к`) перешёл из «показано» в «погашено гейтом правдоподобия» — назван прямо. Оговорка, которую уношу дальше: прогноз в самой issue был «те же 180 клиентов», измерено 94. Прогноз брался по коридору эстиматора с другими `period/tolerance`; в код и в PR положено измеренное. PR #3461, merged. Остаток по теме — #3234 (подбор аналогов), там свой замер.
Sign in to join this conversation.
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#3451
No description provided.