feat(site-finder): D2 velocity-score (#34 sub-PR 1/2) #146
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
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#146
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/34-velocity-score-backend"
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?
Summary
#34 D2 Velocity-score — sub-PR 1 of 2 (backend service).
Closes audit gap:
sale_graph15876 rows не использовались.Service:
backend/app/services/site_finder/velocity.pycompute_velocity(db, parcel_geom_wkt, radius_km=3.0, obj_class=None)→VelocityResult:Schema corrections vs spec
comm_nameregion_cdlatitude/longitude+ ST_MakePointarea_sq(м²) primary,realised*45fallbackTests
102/102 pass.
Sub-PR 2 (next)
Frontend MarketTab block.
Vault
code/modules/Module_Velocity_Service.mdCloses #144
Refs: #34
Bot review (SHA
62836d9, CI ✅)Закрывает audit gap (15876 строк
sale_graphнеиспользуемых). Sub-PR 1/2 — backend service inert до UI.Algorithm
5-шаговый pipeline корректно реализован:
DISTINCT ON (obj_id) ORDER BY snapshot_date DESC NULLS LAST— NULL-safeobj_idза окноmonths_window=6с filtertype='apartments'PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY velocity)+HAVING COUNT(DISTINCT report_month) >= 3(stable signal only)Score formula:
min(1.0, monthly_velocity / (n_with_sales × ekb_median × 2))— saturated at 2× median per competitor. Sensible cap.Schema awareness
PR body документирует разрыв spec ↔ actual:
obj_name→comm_nameregion_code→region_cdST_MakePoint(longitude, latitude)area_sqprimary +realised*45fallbackAuthor грепнул, не доверился спеке. Это правильный подход.
Safety
✅ Все DB queries обёрнуты try/except → return None — graceful degradation для bad data
✅
parcels.pyintegration с outer try/except → analyze_parcel не крашится если velocity падает✅
@dataclass(frozen=True)— immutable result, no accidental mutation✅ Fallback median константа (4500 м²/мес) если в БД нет данных за window
Tests (10 unit + 102/102 общих)
Coverage всех edge cases: no competitors / no sales / healthy → high / few comps → low / medium threshold / median fallback / score cap=1.0 / total_sqm=0 → None / as_dict structure / sample top-5 sorted.
Минор (не блокеры — про future iterations)
realised*45heuristic: 45 м² для конверсии contract count → м² еслиarea_sqNULL. Studios 25-30 м², premium 80-100 м² — может смещать total для ЖК с пустым area_sq. Для агрегированного regional score приемлемо.SRID assumption:
ST_GeomFromText(:parcel_wkt, 4326)не верифицирует input SRID. Если caller передаст МСК-66 WKT (SRID 100000), distances ломаются. Сейчас parcels.py передаёт стандартный WGS84geom_wkt. Желателен docstring assertion / runtime check в follow-up.region_cd = 66hardcoded — ЕКБ-only. Расширение на другие регионы — параметр в follow-up.approve merge