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:
|
||||
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 {
|
||||
"cad_num": cad_num,
|
||||
"source": source,
|
||||
|
|
@ -2170,6 +2221,12 @@ def analyze_parcel(
|
|||
"note": "Бонус к score: <5км +3.0, 5-10км +1.5, 10-15км +0.5, >15км 0",
|
||||
},
|
||||
"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
|
||||
"pipeline_24mo": pipeline_24mo,
|
||||
# D2 (#34): velocity-score из objective_corpus_room_month (OBJ-3 migrated)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue