diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index 457746a4..e1809cbc 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -376,6 +376,10 @@ GEOTECH_BY_REGION: dict[int, dict[str, Any]] = { } +# SF-20 — success_recommendation confidence thresholds +SUCCESS_REC_MIN_DEALS = 15 # ниже → не показываем (пусто) +SUCCESS_REC_STRONG_DEALS = 30 # ≥ → data_confidence='strong'; 15-29 → 'weak' + # D4 (#36) — pipeline 24mo constants. Размещены в одном месте для тюнинга # и аудита; severity пороги матчатся с acceptance #36. PIPELINE_RADIUS_M = 5000 @@ -1800,6 +1804,8 @@ def analyze_parcel( logger.warning("zoning query failed for %s: %s", cad_num, e) # 10c) Success recommendation — топ квартирография по district из v_bucket_success_score + # SF-20: адаптивный порог — MIN_DEALS=15 (показываем), STRONG=30 (confidence='strong'). + # Если n_deals всех строк < STRONG → data_confidence='weak' + UI badge «слабые данные». success_recommendation: dict[str, Any] | None = None if district_row: try: @@ -1819,9 +1825,14 @@ def analyze_parcel( .mappings() .all() ) - if success_rows: + # Фильтруем строки ниже абсолютного минимума (< SUCCESS_REC_MIN_DEALS) + valid_rows = [r for r in success_rows if int(r["n_deals"]) >= SUCCESS_REC_MIN_DEALS] + if valid_rows: + max_deals = max(int(r["n_deals"]) for r in valid_rows) + data_confidence = "strong" if max_deals >= SUCCESS_REC_STRONG_DEALS else "weak" success_recommendation = { "district": district_row["district_name"], + "data_confidence": data_confidence, "ranking": [ { "bucket": r["bucket"], @@ -1834,12 +1845,12 @@ def analyze_parcel( round(float(r["avg_area_m2"]), 1) if r["avg_area_m2"] else None ), } - for r in success_rows + for r in valid_rows ], - "top_bucket": dict(success_rows[0]) if success_rows else None, + "top_bucket": dict(valid_rows[0]), "note": ( "Топ комнатность по 'успешности' = z-scores: velocity×0.5 + price×0.3 " - "- area×0.2. Min 30 сделок в группе за 24 мес. " + "- area×0.2. Min 15 сделок в группе за 24 мес. " "Используй для квартирографии проекта." ), } diff --git a/data/sql/86_v_bucket_success_score.sql b/data/sql/86_v_bucket_success_score.sql index 2d9aaded..ea5a2567 100644 --- a/data/sql/86_v_bucket_success_score.sql +++ b/data/sql/86_v_bucket_success_score.sql @@ -53,7 +53,7 @@ bucket_aggs AS ( AND o.region_cd = 66 AND f.snapshot_date > CURRENT_DATE - INTERVAL '24 months' GROUP BY 1, 2, 3 - HAVING COUNT(*) >= 30 -- статистически значимая выборка + HAVING COUNT(*) >= 15 -- мин 15: weak-confidence (15-29), strong (≥30) ), z_scores AS ( @@ -96,7 +96,7 @@ ORDER BY district_name, obj_class, success_score DESC; COMMENT ON VIEW v_bucket_success_score IS 'Success-driven bucket ranking per (district, class). ' 'Z-scores: velocity (+) приоритет, price (+) приоритет, area (-) приоритет. ' - 'Min 30 сделок в группе. ' + 'Min 15 сделок в группе (15-29 = weak confidence, ≥30 = strong). ' 'Используется recommend_mix для смещения рекомендации (issue #25).'; COMMIT; diff --git a/frontend/src/components/site-finder/SuccessRecommendationBlock.tsx b/frontend/src/components/site-finder/SuccessRecommendationBlock.tsx index 1314bda3..df952f47 100644 --- a/frontend/src/components/site-finder/SuccessRecommendationBlock.tsx +++ b/frontend/src/components/site-finder/SuccessRecommendationBlock.tsx @@ -10,15 +10,15 @@ interface Props { } function successColor(score: number): string { - if (score > 0.5) return "#16a34a"; - if (score >= 0) return "#d97706"; - return "#6b7280"; + if (score > 0.5) return "var(--success)"; + if (score >= 0) return "var(--warn)"; + return "var(--fg-tertiary)"; } function successBg(score: number): string { - if (score > 0.5) return "#dcfce7"; - if (score >= 0) return "#fef3c7"; - return "#f3f4f6"; + if (score > 0.5) return "var(--success-soft)"; + if (score >= 0) return "var(--warn-soft)"; + return "var(--bg-card-alt)"; } function formatPrice(v: number | null): string { @@ -43,7 +43,7 @@ function SuccessBadge({ bucket }: { bucket: SuccessRankingBucket }) { borderRadius: 6, padding: "2px 8px", background: bg, - border: `1px solid ${color}44`, + border: "1px solid var(--border-soft)", }} >