estimator: положение объекта на рынке (перцентиль внутри когорты) #2899

Closed
opened 2026-08-15 16:04:24 +00:00 by lekss361 · 3 comments
Owner

Макет отчёта показывает бейдж «Верх рынка» — позицию квартиры внутри распределения аналогов. Поля нет ни в AggregatedEstimate, ни в v2/types.ts, никто не считает.

Как лучше сделать

  • Перцентиль ₽/м² объекта внутри той же когорты аналогов, что дала медиану, плюс человеческий лейбл (низ / рынок / верх).
  • Не подменять location_index_pct — тот про локацию против медианы города, это другая величина.
  • Отдавать вместе с n_analogs: перцентиль по 5 объектам — шум, ниже порога не показывать вовсе.
Макет отчёта показывает бейдж «Верх рынка» — позицию квартиры внутри распределения аналогов. Поля нет ни в `AggregatedEstimate`, ни в `v2/types.ts`, никто не считает. ## Как лучше сделать - Перцентиль ₽/м² объекта внутри той же когорты аналогов, что дала медиану, плюс человеческий лейбл (низ / рынок / верх). - **Не подменять** `location_index_pct` — тот про локацию против медианы города, это другая величина. - Отдавать вместе с `n_analogs`: перцентиль по 5 объектам — шум, ниже порога не показывать вовсе.
lekss361 added the
priority/p2
scope/backend
labels 2026-08-15 16:04:24 +00:00
lekss361 added the
enhancement
tradein
labels 2026-08-16 10:25:31 +00:00
Collaborator

PR #2926 смержен и выкачен. Что проверено на проде, а что — нет.

Проверено

Миграция применилась:

column_name        | data_type | is_nullable
market_percentile  | smallint  | YES

Код в контейнере (не «смержен», а именно в работающем образе):
estimator.py — 7 вхождений, api/v1/trade_in.py — 4, schemas/trade_in.py — 1.

Функция считает верно — прогнал прямо в tradein-backend:

MARKET_PERCENTILE_MIN_N = 15
медианный объект  -> 50
дорогой объект    -> 99      ← край зажат, не 100
дешёвый объект    -> 1       ← и не 0
когорта 14 лотов  -> None    ← порог держит

НЕ проверено, и это важно

Ни одна живая оценка через это ещё не прошла. Значение считается только на POST
/estimate, а трафика сейчас нет: за последние 7 суток — 6 оценок, за 24 часа — 0,
все 1086 исторических строк просрочены (still_readable = 0).

То есть сквозной путь «реальная квартира → когорта → перцентиль в ответе» пока
подтверждён только тестами и проверкой функции в контейнере. Критерий, по которому это
можно будет закрыть окончательно:

SELECT count(*) FILTER (WHERE market_percentile IS NOT NULL) AS with_pct,
       count(*)                                              AS total
  FROM trade_in_estimates WHERE created_at > '2026-08-19';

Ожидание по замеру покрытия: примерно каждая третья оценка получит число (n≥15 было у
35.8% исторических), остальные — None. Если окажется, что не заполняется вообще ни у
одной — значит когорта на живом пути меньше, чем в историческом хвосте, и порог надо
пересматривать замером, а не на глаз.

UI

Бейджа нет намеренно. Поле в ответе есть; где он стоит, как называется и что показывать
при None — продуктовое решение. Скажи, если нужен, — сделаю отдельным PR.

PR #2926 смержен и выкачен. Что проверено на проде, а что — нет. ## Проверено **Миграция применилась:** ``` column_name | data_type | is_nullable market_percentile | smallint | YES ``` **Код в контейнере** (не «смержен», а именно в работающем образе): `estimator.py` — 7 вхождений, `api/v1/trade_in.py` — 4, `schemas/trade_in.py` — 1. **Функция считает верно** — прогнал прямо в `tradein-backend`: ``` MARKET_PERCENTILE_MIN_N = 15 медианный объект -> 50 дорогой объект -> 99 ← край зажат, не 100 дешёвый объект -> 1 ← и не 0 когорта 14 лотов -> None ← порог держит ``` ## НЕ проверено, и это важно **Ни одна живая оценка через это ещё не прошла.** Значение считается только на POST `/estimate`, а трафика сейчас нет: за последние 7 суток — 6 оценок, за 24 часа — 0, все 1086 исторических строк просрочены (`still_readable = 0`). То есть сквозной путь «реальная квартира → когорта → перцентиль в ответе» пока подтверждён только тестами и проверкой функции в контейнере. Критерий, по которому это можно будет закрыть окончательно: ```sql SELECT count(*) FILTER (WHERE market_percentile IS NOT NULL) AS with_pct, count(*) AS total FROM trade_in_estimates WHERE created_at > '2026-08-19'; ``` Ожидание по замеру покрытия: примерно **каждая третья** оценка получит число (n≥15 было у 35.8% исторических), остальные — `None`. Если окажется, что не заполняется вообще ни у одной — значит когорта на живом пути меньше, чем в историческом хвосте, и порог надо пересматривать замером, а не на глаз. ## UI Бейджа нет намеренно. Поле в ответе есть; где он стоит, как называется и что показывать при `None` — продуктовое решение. Скажи, если нужен, — сделаю отдельным PR.
Collaborator

Критерий из комментария 19.08 выполнен живым трафиком; фронт-часть — в PR #3024

Бэкенд: перцентиль заполняется на боевых оценках

trade_in_estimates с 19.08 (прод, 21.08 11:5x UTC):

21.08 08:20  pct=69   n_analogs=16   (praktika)
21.08 09:31  pct=57   n_analogs=23   (admin)
21.08 11:09  pct=NULL n_analogs=11   (praktika)   ← когорта < MARKET_PERCENTILE_MIN_N=15

Две из трёх получили число, третья честно NULL ровно по порогу — то, что ожидалось («примерно каждая третья… остальные None»). Сквозной путь «реальная квартира → когорта → перцентиль в ответе» подтверждён не тестом, а строками таблицы.

Чего не хватало: фронт

Поле не было ни в types/trade-in.ts, ни в v2-маппере — «бейдж из макета» не рисовался нигде, хотя бэкенд отдавал число с 19.08. PR #3024:

  • AggregatedEstimate.market_percentile?: number | null;
  • карточка «РЕКОМЕНДОВАННАЯ ЦЕНА» получает ту же спокойную плашку, что у «к цене объявления»: лейбл по терцилям (≤33 Низ рынка / В рынке / ≥67 Верх рынка) и подпись «69-й перцентиль среди 16 аналогов» — число рядом, чтобы лейбл не читался точнее, чем он есть; при null плашки нет (не «В рынке» по умолчанию);
  • в ResultPanel плашка вынесена в DeltaPill и рисуется и под гистограммой — раньше JSX плашки жил только в ветке без баров, и delta карточки 1 терялся бы молча;
  • vitest: лейблы/границы/null/контроль карточки 2/склонение; на main красный по значению.

После деплоя проверю /trade-in/v2 на оценке с n ≥ 15 (плашка есть) и n < 15 (нет) и приложу скриншот — тогда можно закрывать.

## Критерий из комментария 19.08 выполнен живым трафиком; фронт-часть — в PR #3024 ### Бэкенд: перцентиль заполняется на боевых оценках `trade_in_estimates` с 19.08 (прод, 21.08 11:5x UTC): ``` 21.08 08:20 pct=69 n_analogs=16 (praktika) 21.08 09:31 pct=57 n_analogs=23 (admin) 21.08 11:09 pct=NULL n_analogs=11 (praktika) ← когорта < MARKET_PERCENTILE_MIN_N=15 ``` Две из трёх получили число, третья честно `NULL` ровно по порогу — то, что ожидалось («примерно каждая третья… остальные None»). Сквозной путь «реальная квартира → когорта → перцентиль в ответе» подтверждён не тестом, а строками таблицы. ### Чего не хватало: фронт Поле не было ни в `types/trade-in.ts`, ни в v2-маппере — «бейдж из макета» не рисовался нигде, хотя бэкенд отдавал число с 19.08. PR #3024: - `AggregatedEstimate.market_percentile?: number | null`; - карточка «РЕКОМЕНДОВАННАЯ ЦЕНА» получает ту же спокойную плашку, что у «к цене объявления»: лейбл по терцилям (`≤33` Низ рынка / В рынке / `≥67` Верх рынка) и подпись «69-й перцентиль среди 16 аналогов» — число рядом, чтобы лейбл не читался точнее, чем он есть; при `null` плашки нет (не «В рынке» по умолчанию); - в `ResultPanel` плашка вынесена в `DeltaPill` и рисуется и под гистограммой — раньше JSX плашки жил только в ветке без баров, и delta карточки 1 терялся бы молча; - vitest: лейблы/границы/null/контроль карточки 2/склонение; на main красный по значению. После деплоя проверю `/trade-in/v2` на оценке с `n ≥ 15` (плашка есть) и `n < 15` (нет) и приложу скриншот — тогда можно закрывать.
Collaborator

Закрываю: бэкенд подтверждён живыми оценками, фронт выкачен (PR #30244ae14055, на проде образ e31c5372 ⊇ него)

API на проде (GET /api/v1/trade-in/estimate/{id} изнутри tradein-backend, 21.08 13:5x UTC):

38f7ce67…  n_analogs=16  market_percentile=69   confidence=high
967e7639…  n_analogs=23  market_percentile=57   confidence=medium
ec2fd824…  n_analogs=11  market_percentile=None (когорта < 15)

Фронт в работающем контейнере: tradein-frontend (image revision e31c5372), в /app/.next/static/chunks/app/v2/page-*.js есть строка «перцентиль среди» — код плашки в сборке. Маппер покрыт vitest: 69/n=16 → «Верх рынка» · «69-й перцентиль среди 16 аналогов», границы терцилей, null → плашки нет, delta карточки 2 не тронут.

Чего не делал: скриншот страницы. /trade-in/v2 за формой входа, а пароль за пользователя я не ввожу. Кто залогинен — откройте /trade-in/v2?id=38f7ce67-1fed-4107-81cb-88dc943eb178: под гистограммой карточки «РЕКОМЕНДОВАННАЯ ЦЕНА» плашка «Верх рынка · 69-й перцентиль среди 16 аналогов»; на ?id=ec2fd824-3a91-4abd-9763-bbc8e6e7fc1e плашки быть не должно. Если увидите иное — переоткройте, это будет дефект рендера, не данных.

## Закрываю: бэкенд подтверждён живыми оценками, фронт выкачен (PR #3024 → `4ae14055`, на проде образ `e31c5372` ⊇ него) **API на проде** (GET `/api/v1/trade-in/estimate/{id}` изнутри `tradein-backend`, 21.08 13:5x UTC): ``` 38f7ce67… n_analogs=16 market_percentile=69 confidence=high 967e7639… n_analogs=23 market_percentile=57 confidence=medium ec2fd824… n_analogs=11 market_percentile=None (когорта < 15) ``` **Фронт в работающем контейнере**: `tradein-frontend` (image revision `e31c5372`), в `/app/.next/static/chunks/app/v2/page-*.js` есть строка «перцентиль среди» — код плашки в сборке. Маппер покрыт vitest: `69/n=16 → «Верх рынка» · «69-й перцентиль среди 16 аналогов»`, границы терцилей, `null → плашки нет`, delta карточки 2 не тронут. **Чего не делал:** скриншот страницы. `/trade-in/v2` за формой входа, а пароль за пользователя я не ввожу. Кто залогинен — откройте `/trade-in/v2?id=38f7ce67-1fed-4107-81cb-88dc943eb178`: под гистограммой карточки «РЕКОМЕНДОВАННАЯ ЦЕНА» плашка «Верх рынка · 69-й перцентиль среди 16 аналогов»; на `?id=ec2fd824-3a91-4abd-9763-bbc8e6e7fc1e` плашки быть не должно. Если увидите иное — переоткройте, это будет дефект рендера, не данных.
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#2899
No description provided.