ПТИЦА: гео-радиусная цена участка считается по лотам проектов ближних ЖК, а не по чужим лотам под устаревшим complex_id (#3583) #3591
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3591
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/3583-radius-price-complex"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
#3583: гео-радиусная цена участка брала лоты чужих ЖК
Что было
analyze_parcel→geo_radius_price(медиана ₽/м² новостроек в 3 км; идёт вfinancial_estimateиdistrict.median_price_basis, если нет квартальной MV) отбирал лоты так:complexesв радиусе →objective_lots WHERE complex_id IN (...). Та же колонка, что в #2962: проставлена один раз миграцией 76, UPSERT70_parse_objective_raw.pyпереписываетproject_nameи не трогаетcomplex_id. Прод 17.09: из 303 677 строк сcomplex_idу 236 354 проект чужой (цифра из #3582, не перемерял).Улики (прод, только чтение,
BEGIN READ ONLY— сам прод отбилCREATE TEMP TABLEв этой транзакции)cad_quarters_geom → cad_buildings → cad_parcels_geom+ST_Centroid, что вanalyze_parcel, SQL — дословный текст изorigin/main. Последний сохранённый прогон после загрузки Объектива 15.09:analysis_runsдля66:41:0702048:27от 16.09 хранитgeo_radius_price = (130552.097266025, 13638, 18), пересчёт старым SQL даёт то же самое до знака.Что сделано
complex → проектчерезcomplex_sources (source='objective')(350 строк, строго 1:1), лоты поproject_name.objective_lots.complex_idбольше не читается.complexes.canonical_name, потому что объекта ДОМ.РФ тут нет. Из 350 связей отвергнуто 6, проверил по координатам и адресам лотов:Итог: 4 чужих проекта в 4.5–11.7 км отсекаются, 2 своих теряются. Разбивка изменений «без сверки → со сверкой» по участкам: md5-выборка — 66 только из-за неверных связей, 30 только из-за верных, 36 из-за обеих, необъяснённых 0. Реальные участки: 13 / 4 / 11 / 0.
3. Скорость. Простая замена фильтра на
JOIN ... ON project_nameчитала все снапшоты проектов (150 400 строк), и сортировка уходила на диск (external merge, 13 МБ): 690 мс против 67 мс у старого запроса. Поэтому дедуп физлота перенесён вLATERALпо проекту сpremise_kind = 'квартира'. Так работаетobjective_lots_physflat_covering_v2_idx(Index Only Scan, без сортировки), и так же отбирают квартиры соседние читатели (market_metrics,best_layouts,_SOLD_COUNT_SQL). EXPLAIN ANALYZE на проде, по 3 прогона:Фильтр по
квартирана медиану не влияет: апартаменты (11 260 строк) есть только у 6 связанных проектов (Re:Volution Towers ×2, Башня «Исеть», Проспект Мира. Компаунд, Свобода Residence, Тактика), и у всех complex без координат, в радиус они не попадают. На 570 участках все три варианта (JOIN, LATERAL, LATERAL + квартира) совпали по(median, n, n_complexes)у 570 из 570.4. SQL вынесен в
_GEO_RADIUS_PRICE_SQL(как_COMPETITORS_SQL), чтобы тест гонял настоящий текст. Мок вtest_analyze_market_price.pyищет"AS n_complexes"+"objective_lots", оба на месте.v_complex_full.objective_lots_n(пункт 2 issue): не трогал, столбец никто не читает. grep поbackend/app,tradein-mvp,frontend:v_complex_fullчитается только вanalytics_queries.py:286(district_name,cad_quarter) и:3052(canonical_name,cad_quarter,latitude,longitude,cad_buildings_n). На проде зависимых view нет (pg_depend: 0 строк). Правка view — это миграция, её делает database-expert, если столбец когда-то начнут читать.Замер «сколько прежних результатов изменится» (прод 17.09, старый SQL против закоммиченного текста константы, радиус 3 км)
Наборы: real — все 70 участков из
analysis_runs; md5 — 500 участков изcad_parcels_geom,ORDER BY md5(cad_num) LIMIT 500. Опубликовано = гейтn ≥ 10и≥ 2 ЖК.Гейт перевернулся у одного участка:
66:41:0105033:19(126 219, 1388 лотов, 2 ЖК) → (115 000, 1305, 1). Второй ЖК там был Графит с лотами «Гранита» из ЖБИ, так что медиана честно не публикуется.Примеры (real):
66:41:0401048:109137 743 → 191 449;66:41:0304011:1124133 665 → 177 583;66:41:0610029:83127 963 → 96 747 (1 ЖК, не публикуется ни до, ни после);66:41:0702048:27130 552 → 138 880.Сохранённые расчёты, которые на это опирались (
analysis_runs, их не переписываю): медиана опубликована в 1435 прогонах по 18 участкам,financial_estimate.price_source = objective_geo_radius— в 381 прогоне (2 участка, последний 10.08),district.median_price_basis— в 380 (4 участка). С 17.08 прогонов 14 по 3 участкам.Стабильность замера: старый SQL в двух проходах совпал у 570 из 570, закоммиченный текст совпал с первым проходом нового у 570 из 570.
Тест и фальсификация
backend/tests/api/v1/test_3583_geo_radius_price_project_bridge.pyгоняет_GEO_RADIUS_PRICE_SQLна временных таблицах (образец —test_2962_competitors_gapfill_bridge.py). Подcomplex_idближнего ЖК лежат 3 лота чужого «Малахита» (его complex в 10 км), у ближнего VEER PARK неверная связь на «Clever Park», «СтудияПарк» проверяет сверку без пробела, у одного лота два снапшота. Ожидание по значению:{'median': 100000.0, 'n': 5.0, 'n_complexes': 2.0}. Режимы как у #2962: в CI (PostGIS уже поднят после #3582) пропуск выключен, вне CI и без базы пропуск объявлен вskip_allowlist.txt(проверено:1 skipped, rc=0).Фальсификация: исправленный
parcels.pyскопирован в сторону, в файл по очереди подложены мутанты, после каждого прогона копия возвращена и сверенаdiff -q.git stashне использовался. Константа в мутантах остаётся, меняется только тело SQL, поэтому красный здесь значит «неверное значение», а не ImportError.Прогоны (локально,
postgis/postgis:16-3.4,CI=true TESTING=1, голова7c52622a)tests/api/v1/test_3583_geo_radius_price_project_bridge.py: 1 passed, rc=0.tests/api/v1/test_analyze_market_price.py+ новый тест без базы: 8 passed, 1 skipped (объявлен), rc=0.backend/tests: 5112 passed, 38 skipped, 11 deselected, 4 failed, rc=1 (код взят у самого pytest). Все 4 падения вtests/ops/test_2203_backup_trailer_grep_dashdash.py:mktemp: unrecognized option '--suffix=.sql.gz', BSD mktemp на macOS, то же, что в #3582.НЕУЧТЁННЫЙ ПРОПУСКв логе нет.uv run ruff check app tests: All checks passed.ruff format --checkпо изменённым файлам: ok. pre-commit: passed.Что не сделано
objective_lots.complex_idостаётся устаревшей: правка UPSERT и backfill ~3.2 млн строк — это запись на прод, решает владелец (как в #3582).complex_sources: 4 неверные и 2 верные, но непрошедшие сверку, не исправлял. Верные («Новая Ботаника-2», «Теплые кварталы (BAZA)») можно вернуть, только поправив данные связи или нормализацию имени. Второе разведёт сверку с #2962, поэтому не делал.data/sql/80_complexes_backfill_nulls.sql— разовая миграция, на ответы не влияет.Приёмка на проде (после деплоя, с 18.09.2026 и до следующей загрузки Объектива, ожидается 22.09 ~03:00 UTC; после загрузки ожидаемые числа ниже устаревают, и замер надо перезапускать)
docker exec gendesign-backend-1 grep -c _GEO_RADIUS_PRICE_SQL /app/app/api/v1/parcels.py→2. Вmainэтой строки нет (0), так что маркер отличает версии.POST /api/v1/parcels/66:41:0702048:27/analyze→geo_radius_price=median 138880.0, lots_count 13911, complexes_count 16(сейчас 130552.10 / 13638 / 18). Или только чтением: первая записьanalysis_runsдля этого участка после деплоя.Issue не закрываю: пункты 1–2 закрыты кодом и проверкой, но приёмка на проде (п. 1–3 выше) после деплоя ещё не сделана.
Refs #3583, #2962
🤖 Generated with Claude Code