tradein/ПТИЦА: мост gap-fill по objective_lots.complex_id тянет чужие ЖК — скорость и цена раздуты вчетверо у 185 конкурентов #2962

Open
opened 2026-08-20 09:07:03 +00:00 by bot-backend · 0 comments
Collaborator

Что не так

objective_lots.complex_id используется как «этот ЖК» в двух живых местах competitors.py:

  • velocity gap-fill (_COMPETITORS_SQL, CTE mapped) — берёт все project_name ближайшего complex_id;
  • ценовой fallback (_OBJECTIVE_PRICE_FALLBACK_SQL) — берёт медиану price_per_m2_rub по всем лотам того же complex_id.

Комментарий в коде формулирует допущение прямо:

у комплекса может быть несколько корпус-project_name → velocity легитимно суммируется по ним, но НЕ по нескольким комплексам

Допущение не выполняется. Замер по проду 20.08.2026:

всего значений complex_id                     350
project_name на один complex_id, в среднем   28.7
максимум                                      156
complex_id ровно с одним проектом               1
лотов без complex_id                    2 567 496 из 2 871 173  (89%)

Что лежит под одним комплексом

complexes.canonical_name = 'ЖК Мичуринский', лоты по complex_id:

Мичуринский           1036   ← свой
Малахит                 94
Нова парк               88
Космонавтов 11          88
Новая Ботаника-2        83
Амундсен                80
Атлас Ривер             77
Солнечный (Форум-групп) 75
Цветной бульвар         71
на Ясной                64
…всего 119 разных project_name

Это не корпуса одного ЖК, а другие ЖК.

Цена вопроса

По всем 185 конкурентам, которых достаёт gap-fill:

лотов подтягивается на конкурента, в среднем 1470
из них его собственных (совпадение по имени) 432
раздутие 4.0×
максимум чужих проектов под одним комплексом 144

То есть у этих 185 конкурентов скорость продаж суммируется по выборке, ~70 % которой принадлежит другим ЖК, а «медианная цена м²» считается по той же смеси.

Явный objective_complex_mapping (308 объектов) этим не задет — там мост идёт по project_name и остаётся корректным.

Как это расползается

Схема повторяется: находят «пробел в покрытии» и закрывают его копированием того же моста.

  • #1615 — «ценовой fallback не покрывает gap-fill» → мост скопировали в цену;
  • пункт эпика #2464 про _SOLD_COUNT_SQL:553 — «flats_sold не считается для gap-fill конкурентов» → предлагает скопировать мост в третий раз.

Я начал делать третью копию и остановился на замере: правка давала +185 конкурентов с flats_sold, но значения были раздуты вчетверо (пример: 2776 «проданных квартир» у объекта, где своих физлотов ~1000). Ветку не выпускаю — чинить надо мост, а не тиражировать его.

Побочно на этом же замере поймал: при общем DISTINCT ON поверх обеих веток менялись счётчики у 88 конкурентов, к gap-fill отношения не имеющих. Если мост когда-нибудь починят, дедуп физлотов надо делать в каждой ветке ОТДЕЛЬНО.

Направление починки (не делаю без решения)

Мост должен связывать конкурента с ЕГО проектом, а не с мешком проектов:

  1. матчить nearest_cx сразу до project_name (гео + толерантное имя, как сейчас, но результат — имя проекта, а не complex_id);
  2. либо чинить сам objective_lots.complex_id в ETL, если он задуман как ЖК-ссылка;
  3. либо расширять явный objective_complex_mapping — он корректен, но покрывает 308 из 1556.

Любой вариант меняет живые показатели у 185 конкурентов, поэтому нужен приёмочный замер до и после: доля конкурентов с velocity/price, их распределение и сравнение с явным маппингом как эталоном.

Критерий приёмки

Для gap-fill конкурентов доля «своих» лотов (совпадение по имени проекта) — не ниже 90 % против нынешних 29 %.

## Что не так `objective_lots.complex_id` используется как «этот ЖК» в двух живых местах `competitors.py`: - **velocity gap-fill** (`_COMPETITORS_SQL`, CTE `mapped`) — берёт все `project_name` ближайшего `complex_id`; - **ценовой fallback** (`_OBJECTIVE_PRICE_FALLBACK_SQL`) — берёт медиану `price_per_m2_rub` по всем лотам того же `complex_id`. Комментарий в коде формулирует допущение прямо: > у комплекса может быть несколько корпус-project_name → velocity легитимно суммируется по ним, но НЕ по нескольким комплексам **Допущение не выполняется.** Замер по проду 20.08.2026: ``` всего значений complex_id 350 project_name на один complex_id, в среднем 28.7 максимум 156 complex_id ровно с одним проектом 1 лотов без complex_id 2 567 496 из 2 871 173 (89%) ``` ## Что лежит под одним комплексом `complexes.canonical_name = 'ЖК Мичуринский'`, лоты по `complex_id`: ``` Мичуринский 1036 ← свой Малахит 94 Нова парк 88 Космонавтов 11 88 Новая Ботаника-2 83 Амундсен 80 Атлас Ривер 77 Солнечный (Форум-групп) 75 Цветной бульвар 71 на Ясной 64 …всего 119 разных project_name ``` Это не корпуса одного ЖК, а другие ЖК. ## Цена вопроса По всем 185 конкурентам, которых достаёт gap-fill: | | | |---|---| | лотов подтягивается на конкурента, в среднем | **1470** | | из них его собственных (совпадение по имени) | **432** | | **раздутие** | **4.0×** | | максимум чужих проектов под одним комплексом | 144 | То есть у этих 185 конкурентов скорость продаж суммируется по выборке, ~70 % которой принадлежит другим ЖК, а «медианная цена м²» считается по той же смеси. Явный `objective_complex_mapping` (308 объектов) этим не задет — там мост идёт по `project_name` и остаётся корректным. ## Как это расползается Схема повторяется: находят «пробел в покрытии» и закрывают его копированием того же моста. - #1615 — «ценовой fallback не покрывает gap-fill» → мост скопировали в цену; - пункт эпика #2464 про `_SOLD_COUNT_SQL:553` — «flats_sold не считается для gap-fill конкурентов» → предлагает скопировать мост в третий раз. Я начал делать третью копию и **остановился на замере**: правка давала +185 конкурентов с `flats_sold`, но значения были раздуты вчетверо (пример: 2776 «проданных квартир» у объекта, где своих физлотов ~1000). Ветку не выпускаю — чинить надо мост, а не тиражировать его. Побочно на этом же замере поймал: при общем `DISTINCT ON` поверх обеих веток менялись счётчики у 88 конкурентов, к gap-fill отношения не имеющих. Если мост когда-нибудь починят, дедуп физлотов надо делать в каждой ветке ОТДЕЛЬНО. ## Направление починки (не делаю без решения) Мост должен связывать конкурента с ЕГО проектом, а не с мешком проектов: 1. матчить `nearest_cx` сразу до `project_name` (гео + толерантное имя, как сейчас, но результат — имя проекта, а не `complex_id`); 2. либо чинить сам `objective_lots.complex_id` в ETL, если он задуман как ЖК-ссылка; 3. либо расширять явный `objective_complex_mapping` — он корректен, но покрывает 308 из 1556. Любой вариант меняет живые показатели у 185 конкурентов, поэтому нужен приёмочный замер до и после: доля конкурентов с velocity/price, их распределение и сравнение с явным маппингом как эталоном. ## Критерий приёмки Для gap-fill конкурентов доля «своих» лотов (совпадение по имени проекта) — не ниже 90 % против нынешних 29 %.
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#2962
No description provided.