fix(sf): exclude NULL competitors from aggregate calculations (velocity, market_avg, top_sellers) #328
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#328
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/sf-aggregate-exclude-nulls"
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
User design policy: показывать все ЖК на карте/в списке, но aggregates (velocity, средневзвешенная цена, top sellers, coverage) — только non-null (ЖК без mapping в Objective не искажают metrics).
Symptoms before
На участке
66:41:0204016:10:avg_price_per_m2_rub != null(после OBJ-3 PR #324)Fix
backend/app/api/v1/parcels.py(+57 lines):После
compute_velocityдобавлен market_pulse computation block, который фильтруетcompetitor_rowsгдеavg_price_per_m2_rub IS NOT NULL:market_avg_price_per_m2— plain average по priced competitors only (Python-side, без SQL change)top_sellers— top-5 byunits_soldиз priced competitors onlycoverage_pct=priced_count / total * 100avg_velocity_m2— изvelocity_data["monthly_velocity_sqm"](уже non-null by construction вcompute_velocity— только ЖК сobjective_corpus_room_monthdata вносят вклад)3 новых top-level ключа в
/analyzeresponse:market_pulse(dict с velocity / avg price / coverage / top sellers)market_avg_price_per_m2(top-level alias)market_data_coverage_pct(top-level alias)competitorsarray (20 ЖК) не меняется — UI продолжает показывать все.Acceptance
После deploy на участке
66:41:0204016:10:Ожидается:
competitors_total= 20 (без изменений)competitors_with_price≥ 3 (after migration 117 PR #327 — could be 8-10)avg_velocity > 0(не ноль, не overflow)market_avg_price∈ [110000, 220000]coverage_pct≈ priced_count × 5Files
backend/app/api/v1/parcels.py(+57)Part of issue #307 OBJ-3 follow-up + task #69 SF policy fix.
Deep Code Review — APPROVE
Scope: purely additive Python-side post-processing in
parcels.py:2143-2199+ 3 new response keys (market_pulse,market_avg_price_per_m2,market_data_coverage_pct). No existing logic touched.Aggregate math
market_avg_price_per_m2 = sum(prices) / len(prices)— unweighted mean of per-ЖК means (eachavg_price_per_m2_rubalready aROUND(AVG(ol.price_per_m2_rub))perdomrf_obj_idin theobj_pricingCTE). This is "average of averages" — each competitor weighted equally regardless of lot count, which matches the UI intent (market pulse across ЖК, not across lots). Not the Bug #21 shape (no zero-count rows, since NULL is filtered upstream). If product later wantsSUM(price*lots_with_price) / SUM(lots_with_price)— separate enhancement; flag in vault as known trade-off.NULL / zero / divide-by-zero
avg_price_per_m2_rub is not Nonefilter —0correctly preserved (0 is not None).Decimal | NonefromROUND(...::numeric)→float(...)safe.units_soldguardis not None and int(...) > 0— handles LEFT JOIN nulls, prevents zero-sales rows intop_sellers.coverage_pctguarded withif _competitors_total > 0 else 0.0.market_avg_price = Nonewhen no priced competitors — frontend should handle (UI fallback already exists per task #69).Contract / response schema
@router.post("/{cad_num}/analyze")has noresponse_model(line 1050) → raw dict returned, new keys pass through cleanly. No Pydantic schema update needed.npm run codegen) — follow-up PR; additive optional fields won't break compilation.Tests
set(keys) == {...}/.json() == {...}) anywhere intests/api/v1/→ existing analyze tests (test_analyze_competitors_status,test_analyze_parcel_meta,test_analyze_recent_permits,test_custom_pois) won't break.market_pulseshape — acceptable for fast-track Python-side fix, but worth adding in follow-up alongside the frontend codegen PR.Conventions
psycopg2/requests/print()/ hardcoded credsAnyimported, lines ≤100,logger.*used for velocity exceptionNits (non-blocking, ignore or address later)
int(c["units_sold"])called twice (lambda key + filter) — micro_competitors_with_price) — convention typically signals "unused"; cosmeticMerging via squash.