refactor(tradein): консолидировать corridor/street-deals city-scope на _resolve_target_city (п.3)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Successful in 51s
CI / changes (pull_request) Successful in 7s
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
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Successful in 51s
CI / changes (pull_request) Successful in 7s
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
#2488 (_resolve_target_city + LOWER(d.city)=:target_city) смёржен НА ВЕРХ моего #2489 (substring :address ILIKE '%'||d.city||'%') → в _fetch_dkp_corridor образовался двойной city-фильтр (benign, но избыточный + латентный ё/е edge в substring). - estimator._fetch_dkp_corridor: убран мой substring-фильтр, оставлен #2488 _resolve_target_city (словарь ~30 городов обл.66 вкл. ЕКБ + все sweep-города; city=None → фильтр не применяется — прежнее #2488-поведение). Убран unused :address bind. - api/v1/trade_in.get_street_deals: substring заменён на тот же _resolve_target_city паттерн (консистентность; #2488 не трогал street-deals). city_filter — литерал, значение bind-параметром (не инъекция). Live-verified: Ленина 2к ЕКБ → median 118052, 1 город (байт-в-байт как substring). Regression-гейт байт-зелёный (коридор заморожен в фикстуре), 53 теста, ruff clean.
This commit is contained in:
parent
51473fc841
commit
d31b1e01ee
2 changed files with 23 additions and 20 deletions
|
|
@ -1638,7 +1638,12 @@ def get_street_deals(
|
||||||
После PR-A (#549) таблица deals содержит только ДКП (ДДУ-первичка отфильтрована
|
После PR-A (#549) таблица deals содержит только ДКП (ДДУ-первичка отфильтрована
|
||||||
в import-rosreestr.sh).
|
в import-rosreestr.sh).
|
||||||
"""
|
"""
|
||||||
from app.services.estimator import _deal_to_analog, _percentile, extract_street_name
|
from app.services.estimator import (
|
||||||
|
_deal_to_analog,
|
||||||
|
_percentile,
|
||||||
|
_resolve_target_city,
|
||||||
|
extract_street_name,
|
||||||
|
)
|
||||||
|
|
||||||
now = datetime.now(tz=UTC)
|
now = datetime.now(tz=UTC)
|
||||||
# #1381: отображаемое окно должно совпадать с SQL-фильтром ниже, который
|
# #1381: отображаемое окно должно совпадать с SQL-фильтром ниже, который
|
||||||
|
|
@ -1673,21 +1678,25 @@ def get_street_deals(
|
||||||
area_min = area_m2 * (1.0 - area_tolerance)
|
area_min = area_m2 * (1.0 - area_tolerance)
|
||||||
area_max = area_m2 * (1.0 + area_tolerance)
|
area_max = area_m2 * (1.0 + area_tolerance)
|
||||||
|
|
||||||
|
# #C1 city-scope (п.3, консистентно с estimator._fetch_dkp_corridor): резолвим
|
||||||
|
# город целевого адреса через _resolve_target_city (словарь ~30 городов обл.66
|
||||||
|
# вкл. ЕКБ + sweep-города) и фильтруем сделки по этому городу — одноимённые улицы
|
||||||
|
# др. городов не контаминируют витрину. None (адрес вне словаря) → фильтр не
|
||||||
|
# применяется. `city_filter` — литерал (не user-input), значение идёт bind-параметром.
|
||||||
|
target_city = _resolve_target_city(address)
|
||||||
|
city_filter = "AND LOWER(city) = CAST(:target_city AS text)" if target_city else ""
|
||||||
|
|
||||||
rows = (
|
rows = (
|
||||||
db.execute(
|
db.execute(
|
||||||
text(
|
text(
|
||||||
"""
|
f"""
|
||||||
SELECT address, area_m2, rooms, floor, total_floors,
|
SELECT address, area_m2, rooms, floor, total_floors,
|
||||||
price_rub, price_per_m2, deal_date, source
|
price_rub, price_per_m2, deal_date, source
|
||||||
FROM deals
|
FROM deals
|
||||||
WHERE source = 'rosreestr'
|
WHERE source = 'rosreestr'
|
||||||
AND address ILIKE :street_pattern
|
AND address ILIKE :street_pattern
|
||||||
AND address ~* :street_regex
|
AND address ~* :street_regex
|
||||||
-- #C1 city-scope (см. estimator._fetch_dkp_corridor): сделка в городе
|
{city_filter}
|
||||||
-- целевого адреса — одноимённые улицы др. городов обл.66 (миграция 177)
|
|
||||||
-- не контаминируют витрину. Матчим канонический city как подстроку адреса.
|
|
||||||
AND city IS NOT NULL
|
|
||||||
AND :address ILIKE '%' || city || '%'
|
|
||||||
AND rooms = CAST(:rooms AS integer)
|
AND rooms = CAST(:rooms AS integer)
|
||||||
AND area_m2 BETWEEN :area_min AND :area_max
|
AND area_m2 BETWEEN :area_min AND :area_max
|
||||||
AND deal_date > NOW() - (CAST(:period_months AS integer) || ' months')::interval
|
AND deal_date > NOW() - (CAST(:period_months AS integer) || ' months')::interval
|
||||||
|
|
@ -1698,7 +1707,7 @@ def get_street_deals(
|
||||||
{
|
{
|
||||||
"street_pattern": "%" + street_name + "%",
|
"street_pattern": "%" + street_name + "%",
|
||||||
"street_regex": r"\m" + street_name + r"\M",
|
"street_regex": r"\m" + street_name + r"\M",
|
||||||
"address": address,
|
"target_city": target_city.lower() if target_city else None,
|
||||||
"rooms": rooms,
|
"rooms": rooms,
|
||||||
"area_min": area_min,
|
"area_min": area_min,
|
||||||
"area_max": area_max,
|
"area_max": area_max,
|
||||||
|
|
|
||||||
|
|
@ -1363,9 +1363,12 @@ def _fetch_dkp_corridor(
|
||||||
return None
|
return None
|
||||||
area_min = area * (1.0 - area_tolerance)
|
area_min = area * (1.0 - area_tolerance)
|
||||||
area_max = area * (1.0 + area_tolerance)
|
area_max = area * (1.0 + area_tolerance)
|
||||||
# (oblast C2): city приходит уже lower-cased (_resolve_target_city) — deals.city в БД
|
# #C1/#C2 city-scope (консолидировано, п.3 dedup #2488↔#2489): сделка обязана
|
||||||
# хранится с исходным регистром («Нижний Тагил»), поэтому сравниваем через
|
# быть в ГОРОДЕ целевого адреса. city приходит из _resolve_target_city (lower-case,
|
||||||
# LOWER(d.city), а не byte-equal.
|
# словарь ~30 крупных городов обл.66 вкл. ЕКБ + все sweep-города). deals.city в БД
|
||||||
|
# с исходным регистром → сравниваем через LOWER(d.city). city=None (адрес вне
|
||||||
|
# словаря, редкий мелкий н.п.) → фильтр не применяется (прежнее #2488-поведение).
|
||||||
|
# Прежний #2489 substring-фильтр УБРАН — был двойным с этим (benign, но ё/е edge).
|
||||||
city_filter_sql = "AND LOWER(d.city) = CAST(:target_city AS text)" if city else ""
|
city_filter_sql = "AND LOWER(d.city) = CAST(:target_city AS text)" if city else ""
|
||||||
try:
|
try:
|
||||||
rows = (
|
rows = (
|
||||||
|
|
@ -1378,14 +1381,6 @@ def _fetch_dkp_corridor(
|
||||||
WHERE d.source = 'rosreestr'
|
WHERE d.source = 'rosreestr'
|
||||||
AND d.address ILIKE :street_pattern
|
AND d.address ILIKE :street_pattern
|
||||||
AND d.address ~* :street_regex
|
AND d.address ~* :street_regex
|
||||||
-- #C1 city-scope: сделка должна быть в ГОРОДЕ целевого адреса.
|
|
||||||
-- Миграция 177 залила ДКП по всей обл.66 (deals.city, 368 городов) —
|
|
||||||
-- без этого одноимённые улицы др. городов (Ленина/Мира/…)
|
|
||||||
-- контаминируют коридор (median занижался ~−46% для ЕКБ). Матчим
|
|
||||||
-- канонический deals.city как подстроку целевого адреса (адрес
|
|
||||||
-- всегда содержит город: «…г. Екатеринбург…»). self-normalizing.
|
|
||||||
AND d.city IS NOT NULL
|
|
||||||
AND :address ILIKE '%' || d.city || '%'
|
|
||||||
AND d.rooms = CAST(:rooms AS integer)
|
AND d.rooms = CAST(:rooms AS integer)
|
||||||
AND d.area_m2 BETWEEN :area_min AND :area_max
|
AND d.area_m2 BETWEEN :area_min AND :area_max
|
||||||
AND d.deal_date > NOW()
|
AND d.deal_date > NOW()
|
||||||
|
|
@ -1404,7 +1399,6 @@ def _fetch_dkp_corridor(
|
||||||
{
|
{
|
||||||
"street_pattern": "%" + street_name + "%",
|
"street_pattern": "%" + street_name + "%",
|
||||||
"street_regex": r"\m" + street_name + r"\M",
|
"street_regex": r"\m" + street_name + r"\M",
|
||||||
"address": address,
|
|
||||||
"rooms": rooms,
|
"rooms": rooms,
|
||||||
"area_min": area_min,
|
"area_min": area_min,
|
||||||
"area_max": area_max,
|
"area_max": area_max,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue