витрина: строка без улицы больше не открывает страницу и игру #3291
2 changed files with 71 additions and 15 deletions
|
|
@ -30,8 +30,8 @@
|
|||
самой сделки. Отбраковываем только то, чего в данных НЕТ (нет прогноза, нет
|
||||
квартала, нет площади) — «число некрасивое» причиной не является.
|
||||
|
||||
Полнота — сколько из полей, которые видит посетитель (район, этаж, этажность),
|
||||
у строки заполнено. Свежесть — порядок квартала сделки.
|
||||
Полнота — сколько из полей, которые видит посетитель (район, этаж, этажность,
|
||||
схема улицы), у строки заполнено. Свежесть — порядок квартала сделки.
|
||||
|
||||
ЧЕСТНОСТЬ ВИТРИНЫ (нарушение любого пункта = витрина врёт)
|
||||
----------------------------------------------------------
|
||||
|
|
@ -69,7 +69,10 @@
|
|||
нет ни координат окна, ни констант проекции: точку дома по ней нельзя
|
||||
поставить даже случайно. Название сматчилось с OSM у 550 из 654 названий —
|
||||
92.3% сделок; остальным `street_scheme` = NULL, и это штатно: фронт
|
||||
показывает район. Наличие схемы НА ОТБОР НЕ ВЛИЯЕТ (см. `_schemes_for`).
|
||||
показывает район. Наличие схемы ВХОДИТ В ПОЛНОТУ (см. `completeness`):
|
||||
строка, которой нечем нарисовать карту, полнее строки с картой быть не
|
||||
может. Это признак «поле заполнено», как район и этаж, а не величина
|
||||
ошибки, — правило отбора выше не нарушено.
|
||||
* СЧЁТЧИКИ ЕДУТ НА ФРОНТ, А НЕ ТОЛЬКО В ЛОГ. «Мы показываем 20 отличных
|
||||
строк» неотличимо от «столько и было», пока рядом не написано, сколько
|
||||
сделок рассмотрено и сколько годных строк не поместилось. Поэтому итог
|
||||
|
|
@ -97,7 +100,7 @@ from typing import Any
|
|||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.services.street_scheme import build_street_scheme, load_street_index
|
||||
from app.services.street_scheme import StreetIndex, build_street_scheme, load_street_index
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -158,15 +161,26 @@ class ShowcaseRow:
|
|||
# None штатно: у части сделок координаты нет, подставлять туда нечего.
|
||||
lat: float | None = None
|
||||
lon: float | None = None
|
||||
# Есть ли чем нарисовать схему улицы (название сделки нашлось в OSM).
|
||||
# Саму схему строим только для показанных строк — см. `_schemes_for`.
|
||||
has_street: bool = False
|
||||
|
||||
|
||||
def completeness(row: ShowcaseRow) -> int:
|
||||
"""Сколько ВИДИМЫХ посетителю полей заполнено (0..3).
|
||||
"""Сколько ВИДИМЫХ посетителю полей заполнено (0..4).
|
||||
|
||||
Считаем район/этаж/этажность: комнаты и площадь есть у всех кандидатов по
|
||||
построению выборки, поэтому в оценке полноты они бесполезны.
|
||||
Считаем район/этаж/этажность и схему улицы: комнаты и площадь есть у всех
|
||||
кандидатов по построению выборки, поэтому в оценке полноты они бесполезны.
|
||||
|
||||
СХЕМА УЛИЦЫ — ТАКОЕ ЖЕ ВИДИМОЕ ПОЛЕ, как район. Строка без улицы рисует на
|
||||
фронте полигон РАЙОНА вместо схемы улиц, то есть этого поля у неё просто
|
||||
нет, и полнее строки с картой она быть не может. Это признак наличия
|
||||
данных, а не величина ошибки: строка с большим отклонением, но со схемой,
|
||||
показывается как есть.
|
||||
"""
|
||||
return sum(x is not None for x in (row.district, row.floor, row.total_floors))
|
||||
return int(row.has_street) + sum(
|
||||
x is not None for x in (row.district, row.floor, row.total_floors)
|
||||
)
|
||||
|
||||
|
||||
def _sort_key(row: ShowcaseRow) -> tuple[int, date, int]:
|
||||
|
|
@ -197,6 +211,7 @@ def build_row(
|
|||
n_analogs: int,
|
||||
lat: float | None = None,
|
||||
lon: float | None = None,
|
||||
has_street: bool = False,
|
||||
) -> ShowcaseRow | None:
|
||||
"""Кандидат → строка витрины, либо None если считать не из чего.
|
||||
|
||||
|
|
@ -251,6 +266,7 @@ def build_row(
|
|||
n_analogs=n_analogs,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
has_street=has_street,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -320,19 +336,27 @@ _INSERT_SQL = text(
|
|||
)
|
||||
|
||||
|
||||
def _schemes_for(db: Session, chosen: list[ShowcaseRow], addresses: dict[int, str | None]) -> dict:
|
||||
def _schemes_for(
|
||||
db: Session,
|
||||
index: StreetIndex,
|
||||
chosen: list[ShowcaseRow],
|
||||
addresses: dict[int, str | None],
|
||||
) -> dict:
|
||||
"""Схемы улиц ТОЛЬКО для показанных строк: id сделки → схема.
|
||||
|
||||
Считаем после отбора, а не до: схема — это два пространственных запроса на
|
||||
сделку, и на двухстах кандидатах ради двадцати показанных это четыреста
|
||||
лишних запросов в чужую базу.
|
||||
|
||||
НА ОТБОР НАЛИЧИЕ СХЕМЫ НЕ ВЛИЯЕТ — и это то же правило, что запрещает отбор
|
||||
по величине ошибки. Ранжируй мы строки со сматчившейся улицей выше, витрина
|
||||
показывала бы не работу оценщика, а те 92% адресов, которые удобно легли на
|
||||
OSM. Схема — украшение строки, а не причина её показать.
|
||||
НА ОТБОР ВЛИЯЕТ НЕ ЭТОТ ШАГ, А ПОЛНОТА (`completeness`), куда наличие улицы
|
||||
входит наравне с районом и этажом. До 2026-08-31 здесь было записано
|
||||
обратное — «схема на отбор не влияет», — и первой строкой витрины и первым
|
||||
раундом игры стояла единственная из двадцати сделка БЕЗ улицы: у неё вместо
|
||||
схемы улиц рисовался полигон района. Отсутствие видимого поля не может
|
||||
делать строку самой полной. Запрет отбора по ВЕЛИЧИНЕ ОШИБКИ этим не
|
||||
затронут: строка с большим отклонением, но со схемой, стоит в витрине как
|
||||
есть.
|
||||
"""
|
||||
index = load_street_index(db)
|
||||
out = {}
|
||||
for row in chosen:
|
||||
scheme = build_street_scheme(db, index, addresses.get(row.deal_id))
|
||||
|
|
@ -377,6 +401,10 @@ def refresh_landing_showcase_deals(
|
|||
logger.info("витрина: загружено %d ДКП-сделок (city=%s, since=%s)", len(deals), city, since)
|
||||
|
||||
districts = _fetch_districts(db, [d.id for d in deals])
|
||||
# Индекс улиц нужен ДО отбора: сматчился ли адрес с OSM — это признак
|
||||
# полноты строки. Поиск по индексу идёт в памяти, запрос ровно один на
|
||||
# прогон, дорогие пространственные запросы остались в `_schemes_for`.
|
||||
street_index = load_street_index(db)
|
||||
|
||||
candidates: list[ShowcaseRow] = []
|
||||
n_priced = 0
|
||||
|
|
@ -408,6 +436,7 @@ def refresh_landing_showcase_deals(
|
|||
# тип этого не поймает: обе величины float.
|
||||
lat=deal.lat,
|
||||
lon=deal.lon,
|
||||
has_street=street_index.lookup(deal.address) is not None,
|
||||
)
|
||||
if row is None:
|
||||
n_incomplete += 1
|
||||
|
|
@ -415,7 +444,7 @@ def refresh_landing_showcase_deals(
|
|||
candidates.append(row)
|
||||
|
||||
chosen = select_rows(candidates, limit)
|
||||
schemes = _schemes_for(db, chosen, {d.id: d.address for d in deals})
|
||||
schemes = _schemes_for(db, street_index, chosen, {d.id: d.address for d in deals})
|
||||
|
||||
db.execute(_DELETE_SQL)
|
||||
db.execute(_DELETE_RUNS_SQL)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ def _row(
|
|||
total_floors: int | None = 9,
|
||||
deal_date: date = date(2026, 1, 1),
|
||||
err_pct: float = 10.0,
|
||||
has_street: bool = True,
|
||||
) -> ShowcaseRow:
|
||||
return ShowcaseRow(
|
||||
deal_id=deal_id,
|
||||
|
|
@ -45,6 +46,7 @@ def _row(
|
|||
fact_rub=5_500_000,
|
||||
err_pct=err_pct,
|
||||
n_analogs=40,
|
||||
has_street=has_street,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -72,6 +74,31 @@ def test_selection_prefers_fresher_quarter_at_equal_completeness() -> None:
|
|||
assert [r.deal_id for r in select_rows([older, fresher], limit=1)] == [2]
|
||||
|
||||
|
||||
def test_selection_prefers_row_with_street_scheme() -> None:
|
||||
"""Строка без улицы не может быть самой полной: ей нечем нарисовать карту.
|
||||
|
||||
Так дефект и выглядел в проде: первой строкой витрины и первым раундом игры
|
||||
«Проверьте себя» стояла единственная из двадцати сделка БЕЗ улицы, и вместо
|
||||
схемы улиц у неё рисовался полигон РАЙОНА.
|
||||
|
||||
Всё остальное здесь одинаково, а id подобраны так, что без учёта улицы
|
||||
выиграл бы разрыв ключа по id. Ломать так: убрать `row.has_street` из
|
||||
`completeness` — тест покраснеет ПО ЗНАЧЕНИЮ, порядком [9, 8].
|
||||
|
||||
Вторая сторона проверки — строка без улицы ОСТАЁТСЯ в витрине: она не
|
||||
первая, но и не выброшена. Прятать промахи по-прежнему нельзя.
|
||||
"""
|
||||
no_street = _row(9, has_street=False, err_pct=75.7)
|
||||
with_street = _row(8, err_pct=3.0)
|
||||
|
||||
chosen = select_rows([no_street, with_street], limit=2)
|
||||
|
||||
assert [r.deal_id for r in chosen] == [8, 9], (
|
||||
"строка, которой нечем нарисовать карту, оказалась полнее строки со "
|
||||
"схемой улиц — витрина открывается полигоном района"
|
||||
)
|
||||
|
||||
|
||||
def test_selection_is_deterministic_on_full_ties() -> None:
|
||||
"""Полные совпадения ключа разводятся id — иначе витрина «мерцает»."""
|
||||
rows = [_row(7), _row(9), _row(8)]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue