feat(analyze): competitor + pipeline lat/lon для слоёв карты (#999 Step 1) #1074
1 changed files with 44 additions and 1 deletions
|
|
@ -399,6 +399,36 @@ PIPELINE_SEVERITY_HIGH_THRESHOLD = 3000 # flats_total >= это → high
|
|||
PIPELINE_TOP_OBJECTS_LIMIT = 10
|
||||
|
||||
|
||||
def _coord_round(value: Any) -> float | None:
|
||||
"""#999 — привести координату (lat/lon) к float, округлённому до 6 dp.
|
||||
|
||||
Источник — PostGIS latitude/longitude (float8/numeric → может прийти Decimal).
|
||||
None/невалидное значение → None (graceful: объект без координат не ломает
|
||||
ответ, фронт просто не рисует маркер). 6 dp ≈ 0.1м точности — достаточно
|
||||
для карты.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return round(float(value), 6)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _competitor_with_coords(row: Any) -> dict[str, Any]:
|
||||
"""#999 (958-B4) — competitor-dict + nullable lat/lon (EPSG:4326).
|
||||
|
||||
Additive shape: сохраняет ВСЕ существующие ключи competitor_rows через
|
||||
{**dict(row)} (distance_m и пр. без изменений), затем перезаписывает lat/lon
|
||||
округлёнными float|None (исходные SQL-алиасы lat/lon приходят сырыми
|
||||
float/Decimal). Никакие текущие поля не удаляются и не меняются.
|
||||
"""
|
||||
out = dict(row)
|
||||
out["lat"] = _coord_round(out.get("lat"))
|
||||
out["lon"] = _coord_round(out.get("lon"))
|
||||
return out
|
||||
|
||||
|
||||
def _aggregate_pipeline(rows: list[Any]) -> dict[str, Any]:
|
||||
"""D4 (#36) — собрать pipeline_24mo aggregate из rows domrf_kn_objects.
|
||||
|
||||
|
|
@ -488,6 +518,11 @@ def _aggregate_pipeline(rows: list[Any]) -> dict[str, Any]:
|
|||
# упадёт в JSON serialization).
|
||||
"ready_dt": ready_dt.isoformat() if ready_dt else None,
|
||||
"distance_m": round(float(distance_m)) if distance_m is not None else None,
|
||||
# #999 (958-B4): lat/lon (EPSG:4326) для Leaflet-слоёв. Источник —
|
||||
# та же geom (latitude/longitude), что и distance_m. Nullable: объект
|
||||
# без координат → None (latest_obj фильтрует, но guard на всякий).
|
||||
"lat": _coord_round(r.get("lat")),
|
||||
"lon": _coord_round(r.get("lon")),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -1580,6 +1615,8 @@ def analyze_parcel(
|
|||
o.district_name,
|
||||
o.site_status,
|
||||
o.ready_dt,
|
||||
o.latitude AS lat,
|
||||
o.longitude AS lon,
|
||||
p.avg_price_per_m2_rub,
|
||||
p.avg_area_pd,
|
||||
p.units_sold,
|
||||
|
|
@ -1636,6 +1673,8 @@ def analyze_parcel(
|
|||
) AS obj_class,
|
||||
flat_count,
|
||||
ready_dt,
|
||||
o.latitude AS lat,
|
||||
o.longitude AS lon,
|
||||
ST_Distance(
|
||||
ST_SetSRID(ST_MakePoint(o.longitude, o.latitude), 4326)::geography,
|
||||
ST_Centroid(ST_GeomFromText(:wkt, 4326))::geography
|
||||
|
|
@ -2648,7 +2687,11 @@ def analyze_parcel(
|
|||
"ekb_center": {"lat": EKB_CENTER_LAT, "lon": EKB_CENTER_LON},
|
||||
"note": "Бонус к score: <5км +3.0, 5-10км +1.5, 10-15км +0.5, >15км 0",
|
||||
},
|
||||
"competitors": [dict(c) for c in competitor_rows],
|
||||
# #999 (958-B4): competitors несут lat/lon (EPSG:4326) для Leaflet-слоёв.
|
||||
# Additive — все существующие поля сохранены через {**dict(c)}; lat/lon
|
||||
# округлены до 6 dp (float|None). latest_obj фильтрует latitude IS NOT NULL,
|
||||
# поэтому здесь координаты обычно заполнены, но _coord_round graceful к None.
|
||||
"competitors": [_competitor_with_coords(c) for c in competitor_rows],
|
||||
# OBJ-3 fix: aggregate market metrics — только non-null competitors.
|
||||
# ЖК без objective_mapping остаются на карте (competitors list),
|
||||
# но исключены из avg/velocity/top_sellers расчётов.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue