fix(sf): exclude NULL competitors from aggregate calculations (velocity, market_avg, top_sellers)
ЖК без маппинга в Objective (Желдорипотека, Эфес и пр.) остаются в competitor list для карты, но исключаются из aggregate-метрик. Добавлен блок market_pulse в ответ /analyze: - market_avg_price_per_m2: AVG(avg_price_per_m2_rub) по non-null competitors - avg_velocity_m2: monthly_velocity_sqm из compute_velocity (уже non-null by design) - market_data_coverage_pct / coverage_pct: priced_count / total * 100 - top_sellers: топ-5 ЖК по units_sold среди priced - competitors_total / competitors_with_price: для UI badge competitor list (20 ЖК) не изменён.
This commit is contained in:
parent
0c4d7f1ffb
commit
630a5bbd77
1 changed files with 57 additions and 0 deletions
|
|
@ -2143,6 +2143,57 @@ def analyze_parcel(
|
||||||
except Exception as _ve:
|
except Exception as _ve:
|
||||||
logger.warning("velocity compute failed for %s: %s", cad_num, _ve)
|
logger.warning("velocity compute failed for %s: %s", cad_num, _ve)
|
||||||
|
|
||||||
|
# OBJ-3 aggregate fix: market_pulse — агрегаты ТОЛЬКО по ЖК с ненулевыми ценами.
|
||||||
|
# Конкуренты без маппинга в Objective (NULL avg_price_per_m2_rub) остаются
|
||||||
|
# в competitor list для карты, но исключаются из расчётов рыночных метрик.
|
||||||
|
_competitors_with_price = [c for c in competitor_rows if c["avg_price_per_m2_rub"] is not None]
|
||||||
|
_competitors_total = len(competitor_rows)
|
||||||
|
_competitors_priced = len(_competitors_with_price)
|
||||||
|
if _competitors_with_price:
|
||||||
|
_prices = [float(c["avg_price_per_m2_rub"]) for c in _competitors_with_price]
|
||||||
|
_market_avg_price = round(sum(_prices) / len(_prices))
|
||||||
|
# top_sellers: ЖК с ненулевыми units_sold, топ-5 по объёму
|
||||||
|
_with_sales = [
|
||||||
|
c
|
||||||
|
for c in _competitors_with_price
|
||||||
|
if c["units_sold"] is not None and int(c["units_sold"]) > 0
|
||||||
|
]
|
||||||
|
_top_sellers = sorted(
|
||||||
|
_with_sales,
|
||||||
|
key=lambda c: int(c["units_sold"]),
|
||||||
|
reverse=True,
|
||||||
|
)[:5]
|
||||||
|
_top_sellers_list = [
|
||||||
|
{
|
||||||
|
"obj_id": c["obj_id"],
|
||||||
|
"comm_name": c["comm_name"],
|
||||||
|
"dev_name": c["dev_name"],
|
||||||
|
"units_sold": int(c["units_sold"]),
|
||||||
|
"avg_price_per_m2_rub": int(c["avg_price_per_m2_rub"]),
|
||||||
|
}
|
||||||
|
for c in _top_sellers
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
_market_avg_price = None
|
||||||
|
_top_sellers_list = []
|
||||||
|
|
||||||
|
_coverage_pct = (
|
||||||
|
round(_competitors_priced * 100.0 / _competitors_total, 1)
|
||||||
|
if _competitors_total > 0
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
# avg_velocity_m2 — берём из velocity_data если есть; это уже только по
|
||||||
|
# ЖК с objective_corpus_room_month данными (non-null by construction).
|
||||||
|
_avg_velocity = velocity_data["monthly_velocity_sqm"] if velocity_data else None
|
||||||
|
market_pulse: dict[str, Any] = {
|
||||||
|
"avg_velocity_m2": _avg_velocity,
|
||||||
|
"market_avg_price_per_m2": _market_avg_price,
|
||||||
|
"competitors_total": _competitors_total,
|
||||||
|
"competitors_with_price": _competitors_priced,
|
||||||
|
"coverage_pct": _coverage_pct,
|
||||||
|
"top_sellers": _top_sellers_list,
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"cad_num": cad_num,
|
"cad_num": cad_num,
|
||||||
"source": source,
|
"source": source,
|
||||||
|
|
@ -2170,6 +2221,12 @@ def analyze_parcel(
|
||||||
"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],
|
"competitors": [dict(c) for c in competitor_rows],
|
||||||
|
# OBJ-3 fix: aggregate market metrics — только non-null competitors.
|
||||||
|
# ЖК без objective_mapping остаются на карте (competitors list),
|
||||||
|
# но исключены из avg/velocity/top_sellers расчётов.
|
||||||
|
"market_pulse": market_pulse,
|
||||||
|
"market_avg_price_per_m2": market_pulse["market_avg_price_per_m2"],
|
||||||
|
"market_data_coverage_pct": market_pulse["coverage_pct"],
|
||||||
# D4 (#36): 24-month pipeline competition
|
# D4 (#36): 24-month pipeline competition
|
||||||
"pipeline_24mo": pipeline_24mo,
|
"pipeline_24mo": pipeline_24mo,
|
||||||
# D2 (#34): velocity-score из objective_corpus_room_month (OBJ-3 migrated)
|
# D2 (#34): velocity-score из objective_corpus_room_month (OBJ-3 migrated)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue