"""Compute features and weighted parcel score. Per-site feature vector: edu_kindergarten_nearest_m, edu_school_nearest_m, edu_university_nearest_m health_pharmacy_nearest_m, health_clinic_nearest_m, health_hospital_nearest_m retail_big_nearest_m, retail_med_nearest_m, retail_small_nearest_m transit_bus_nearest_m, transit_tram_nearest_m, transit_metro_nearest_m leisure_park_nearest_m, leisure_playground_nearest_m, leisure_sports_nearest_m + counts within 500m / 1000m for each category Component scores 0..100 (higher = better): education = avg(score(kindergarten,500m), score(school,800m)) health = avg(score(pharmacy,500m), score(clinic,1000m)) retail = max(score(shop_big,1000m), 0.7*score(shop_med,500m)) transit = max(score(metro,1500m), 0.8*score(tram,500m), 0.6*score(bus,300m)) leisure = avg(score(park,800m), score(playground,400m), score(sports,800m)) Distance-to-score: piecewise linear, 0..ideal_m → 100 ideal_m..max_m → 100..0 >max_m → 0 Total weighted (sums to 1.0): education 0.30 (садики/школы — главное для семейных, ядро ЦА девелопера) health 0.15 retail 0.20 transit 0.20 leisure 0.15 """ import sqlite3, pathlib DB = pathlib.Path(__file__).parent / "analysis.db" # (category, ideal_m, max_m, weight_in_component) EDU = [("kindergarten", 300, 1000, 1.0), ("school", 400, 1500, 1.0), ("university", 1000, 5000, 0.3)] # nice-to-have HEALTH = [("pharmacy", 300, 1000, 1.0), ("clinic", 500, 2000, 1.0), ("hospital", 1500, 5000, 0.5)] RETAIL_BIG = [("shop_big", 500, 2000, 1.0)] RETAIL_MED = [("shop_med", 300, 1000, 1.0)] TRANSIT_MAIN = [("metro", 1000, 3000, 1.0)] TRANSIT_TRAM = [("tram_stop", 400, 1500, 1.0)] TRANSIT_BUS = [("bus_stop", 200, 800, 1.0)] LEISURE = [("park", 500, 2000, 1.0), ("playground", 200, 700, 1.0), ("sports", 500, 2000, 0.7)] WEIGHTS = { "education": 0.20, "health": 0.10, "retail": 0.15, "transit": 0.15, "leisure": 0.10, "economic": 0.30, } # Economic sub-weights (sum to 1, used inside `economic` component) ECON_WEIGHTS = {"price": 0.50, "velocity": 0.25, "liquidity": 0.25} def dist_score(d_m, ideal, mx): if d_m is None: return 0.0 if d_m <= ideal: return 100.0 if d_m >= mx: return 0.0 return 100.0 * (mx - d_m) / (mx - ideal) def component_score(conn, site_id, cats): """Average distance-score across the categories listed (with weights).""" total_w = sum(c[3] for c in cats) s = 0.0 for cat, ideal, mx, w in cats: nearest = conn.execute( "SELECT MIN(distance_m) FROM pois WHERE site_id=? AND category=?", (site_id, cat) ).fetchone()[0] s += w * dist_score(nearest, ideal, mx) return s / total_w if total_w else 0.0 def features(conn, site_id): """Materialize feature vector — nearest distance + counts within 500/1000m.""" feats = {} cats = ["kindergarten","school","university","pharmacy","clinic","hospital", "shop_big","shop_med","shop_small","bus_stop","tram_stop","metro", "park","playground","sports"] for cat in cats: nearest = conn.execute( "SELECT MIN(distance_m) FROM pois WHERE site_id=? AND category=?", (site_id, cat)).fetchone()[0] c500 = conn.execute( "SELECT count(*) FROM pois WHERE site_id=? AND category=? AND distance_m<=500", (site_id, cat)).fetchone()[0] c1000 = conn.execute( "SELECT count(*) FROM pois WHERE site_id=? AND category=? AND distance_m<=1000", (site_id, cat)).fetchone()[0] feats[f"{cat}_nearest_m"] = nearest feats[f"{cat}_count_500m"] = c500 feats[f"{cat}_count_1km"] = c1000 return feats def economic_score(conn, site_id, econ_bounds): """Returns (component_0_100, breakdown dict) using REAL per-flat district aggregates. Prefers real_* columns from objective_lots (Поквартирные/Лоты, 303k lots); falls back to weighted_price_m2 + deals_per_month_avg from corp_sum if missing. """ row = conn.execute("""SELECT COALESCE(de.real_median_price_m2, de.weighted_price_m2) AS price, COALESCE(de.real_velocity_per_month, de.deals_per_month_avg) AS v, de.months_to_sellout, de.real_sold_pct FROM site_district sd JOIN district_economics de USING (district) WHERE sd.site_id=?""", (site_id,)).fetchone() if not row: return 0.0, {} price, v, mts, sold_pct = row pmin, pmax, vmax, mts_cap = econ_bounds p_score = max(0.0, min(100.0, (price - pmin) * 100.0 / (pmax - pmin))) if price else 0.0 v_score = max(0.0, min(100.0, (v or 0) * 100.0 / vmax)) if mts is None: liq_score = 0.0 else: liq_score = max(0.0, 100.0 - min(mts, mts_cap) * 100.0 / mts_cap) total = (ECON_WEIGHTS["price"] * p_score + ECON_WEIGHTS["velocity"] * v_score + ECON_WEIGHTS["liquidity"] * liq_score) return total, {"price_score": p_score, "velocity_score": v_score, "liquidity_score": liq_score, "median_price_m2": price, "velocity": v, "months_to_sellout": mts, "district_sold_pct": sold_pct} def main(): conn = sqlite3.connect(DB) sites = conn.execute("SELECT site_id, kind FROM sites").fetchall() conn.execute("DELETE FROM features") conn.execute("DELETE FROM scores") conn.execute("DELETE FROM scores_total") # Compute econ bounds from district_economics — prefer real_* prices = sorted([r[0] for r in conn.execute( "SELECT COALESCE(real_median_price_m2, weighted_price_m2) FROM district_economics " "WHERE COALESCE(real_median_price_m2, weighted_price_m2) IS NOT NULL").fetchall()]) if not prices: prices = [100, 200] pmin, pmax = prices[0], prices[-1] if pmax <= pmin: pmax = pmin + 1 vels = sorted([r[0] for r in conn.execute( "SELECT COALESCE(real_velocity_per_month, deals_per_month_avg) FROM district_economics " "WHERE COALESCE(real_velocity_per_month, deals_per_month_avg) IS NOT NULL").fetchall()]) vmax = vels[int(len(vels) * 0.9)] if vels else 1.0 if vmax <= 0: vmax = 1.0 econ_bounds = (pmin, pmax, vmax, 24.0) print(f"Econ bounds (real): price [{pmin:.1f}, {pmax:.1f}] тыс, vel_p90={vmax:.2f}, mts_cap=24 мес") for site_id, kind in sites: # features for k, v in features(conn, site_id).items(): conn.execute("INSERT INTO features(site_id,feature,value) VALUES (?,?,?)", (site_id, k, float(v) if v is not None else None)) # component scores edu = component_score(conn, site_id, EDU) health = component_score(conn, site_id, HEALTH) # retail = max(big, 0.7*med). med alone is enough for daily needs. big = component_score(conn, site_id, RETAIL_BIG) med = component_score(conn, site_id, RETAIL_MED) retail = max(big, 0.7 * med) # transit: best of metro / tram / bus metro = component_score(conn, site_id, TRANSIT_MAIN) tram = component_score(conn, site_id, TRANSIT_TRAM) bus = component_score(conn, site_id, TRANSIT_BUS) transit = max(metro, 0.85 * tram, 0.7 * bus) leisure = component_score(conn, site_id, LEISURE) econ, _ = economic_score(conn, site_id, econ_bounds) comps = {"education": edu, "health": health, "retail": retail, "transit": transit, "leisure": leisure, "economic": econ} for c, v in comps.items(): conn.execute("INSERT INTO scores(site_id,component,score_0_100) VALUES (?,?,?)", (site_id, c, v)) weighted = sum(WEIGHTS[c] * v for c, v in comps.items()) conn.execute("INSERT INTO scores_total(site_id,weighted) VALUES (?,?)", (site_id, weighted)) conn.commit() # Compute ranks rows = conn.execute("""SELECT s.site_id, s.kind, s.district, st.weighted FROM sites s JOIN scores_total st USING (site_id) ORDER BY st.weighted DESC""").fetchall() for rank, (sid, kind, _, _) in enumerate(rows, 1): conn.execute("UPDATE scores_total SET rank_overall=? WHERE site_id=?", (rank, sid)) # district rank by_dist = {} for sid, kind, dist, _ in rows: by_dist.setdefault(dist or "—", []).append(sid) for dist, sids in by_dist.items(): for rank, sid in enumerate(sids, 1): conn.execute("UPDATE scores_total SET rank_district=? WHERE site_id=?", (rank, sid)) conn.commit() # Summary parcel_id = "parcel:66:41:0204016:10" p = conn.execute("SELECT weighted, rank_overall FROM scores_total WHERE site_id=?", (parcel_id,)).fetchone() n = conn.execute("SELECT count(*) FROM sites").fetchone()[0] print(f"\n=== Parcel score ===") print(f" weighted: {p[0]:.1f}/100 rank: {p[1]}/{n}") print("\n Components:") for c, v in conn.execute("SELECT component, score_0_100 FROM scores WHERE site_id=?", (parcel_id,)).fetchall(): print(f" {c:<10} {v:.1f}") print("\n Nearest POIs (m):") for k, v in conn.execute( "SELECT feature, value FROM features WHERE site_id=? AND feature LIKE '%_nearest_m'", (parcel_id,)).fetchall(): print(f" {k:<35} {('%.0f m'%v) if v is not None else '—'}") print("\n=== Top-5 ЖК in Ekb ===") for r in conn.execute("""SELECT s.name, s.district, st.weighted, st.rank_overall FROM sites s JOIN scores_total st USING (site_id) WHERE s.kind='jk' ORDER BY st.weighted DESC LIMIT 5""").fetchall(): print(f" #{r[3]:>3} {r[2]:>5.1f} [{(r[1] or '—'):<25}] {r[0]}") conn.close() if __name__ == "__main__": main()