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
|
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]:
|
def _aggregate_pipeline(rows: list[Any]) -> dict[str, Any]:
|
||||||
"""D4 (#36) — собрать pipeline_24mo aggregate из rows domrf_kn_objects.
|
"""D4 (#36) — собрать pipeline_24mo aggregate из rows domrf_kn_objects.
|
||||||
|
|
||||||
|
|
@ -488,6 +518,11 @@ def _aggregate_pipeline(rows: list[Any]) -> dict[str, Any]:
|
||||||
# упадёт в JSON serialization).
|
# упадёт в JSON serialization).
|
||||||
"ready_dt": ready_dt.isoformat() if ready_dt else None,
|
"ready_dt": ready_dt.isoformat() if ready_dt else None,
|
||||||
"distance_m": round(float(distance_m)) if distance_m is not None 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.district_name,
|
||||||
o.site_status,
|
o.site_status,
|
||||||
o.ready_dt,
|
o.ready_dt,
|
||||||
|
o.latitude AS lat,
|
||||||
|
o.longitude AS lon,
|
||||||
p.avg_price_per_m2_rub,
|
p.avg_price_per_m2_rub,
|
||||||
p.avg_area_pd,
|
p.avg_area_pd,
|
||||||
p.units_sold,
|
p.units_sold,
|
||||||
|
|
@ -1636,6 +1673,8 @@ def analyze_parcel(
|
||||||
) AS obj_class,
|
) AS obj_class,
|
||||||
flat_count,
|
flat_count,
|
||||||
ready_dt,
|
ready_dt,
|
||||||
|
o.latitude AS lat,
|
||||||
|
o.longitude AS lon,
|
||||||
ST_Distance(
|
ST_Distance(
|
||||||
ST_SetSRID(ST_MakePoint(o.longitude, o.latitude), 4326)::geography,
|
ST_SetSRID(ST_MakePoint(o.longitude, o.latitude), 4326)::geography,
|
||||||
ST_Centroid(ST_GeomFromText(:wkt, 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},
|
"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",
|
"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.
|
# OBJ-3 fix: aggregate market metrics — только non-null competitors.
|
||||||
# ЖК без objective_mapping остаются на карте (competitors list),
|
# ЖК без objective_mapping остаются на карте (competitors list),
|
||||||
# но исключены из avg/velocity/top_sellers расчётов.
|
# но исключены из avg/velocity/top_sellers расчётов.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue