"""Scoring v2 — uses prod-style macro factors + per-flat metrics from Objective. Components (all 0..100): education 20% школы (×1) + садики (×1) + ВУЗы (×0.3) — distance score health 10% аптеки (×1) + поликлиники (×1) + больницы (×0.5) retail 13% max(big, 0.7×med) — distance score transit 15% max(metro, 0.85×tram, 0.7×bus) — distance score leisure 9% парки + площадки + спорт economic 30% real_*: price_position 50% + velocity_real 25% + liquidity 25% velocity adjusted by trend_factor (clamp 0.7..2.0, prod-style) market 3% competitive density (jk_count_1km) + saturation factor (informational; small weight) Macro context shown alongside but not in score: mortgage_rate_sverdl, city_avg_poi_1km, city_med_price_m2. """ import sqlite3, pathlib, math DB = pathlib.Path(__file__).parent / "analysis.db" EDU = [("kindergarten", 300, 1000, 1.0), ("school", 400, 1500, 1.0), ("university", 1000, 5000, 0.3)] 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_M = [("metro", 1000, 3000, 1.0)] TRANSIT_T = [("tram_stop", 400, 1500, 1.0)] TRANSIT_B = [("bus_stop", 200, 800, 1.0)] LEISURE = [("park", 500, 2000, 1.0), ("playground", 200, 700, 1.0), ("sports", 500, 2000, 0.7)] 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 comp(conn, sid, cats): tw = sum(c[3] for c in cats); s = 0 for cat, ideal, mx, w in cats: d = conn.execute("SELECT MIN(distance_m) FROM pois WHERE site_id=? AND category=?", (sid, cat)).fetchone()[0] s += w * dist_score(d, ideal, mx) return s / tw if tw else 0 def hav(la1, lo1, la2, lo2): R = 6371000; p1, p2 = math.radians(la1), math.radians(la2) dp = math.radians(la2-la1); dl = math.radians(lo2-lo1) a = math.sin(dp/2)**2 + math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2 return 2*R*math.asin(math.sqrt(a)) def main(): conn = sqlite3.connect(DB) weights = {r[0]: r[1] for r in conn.execute("SELECT component, weight FROM scoring_weights").fetchall()} if not weights: weights = {"education":0.18,"health":0.10,"retail":0.13,"transit":0.15, "leisure":0.09,"economic":0.30,"market":0.05} print("Weights:", weights) sites = conn.execute("SELECT site_id, lat, lon FROM sites").fetchall() site_coords = {sid: (la, lo) for sid, la, lo in sites} # Bounds for normalization (computed once) prices = sorted([r[0] for r in conn.execute( "SELECT real_median_price_m2 FROM district_economics WHERE real_median_price_m2 IS NOT NULL").fetchall()]) pmin, pmax = (prices[0], prices[-1]) if prices else (100, 200) if pmax <= pmin: pmax = pmin + 1 vels = sorted([r[0] for r in conn.execute( "SELECT real_velocity_6mo FROM district_economics WHERE real_velocity_6mo IS NOT NULL").fetchall()]) vmax = vels[int(len(vels) * 0.9)] if vels else 8 # Citywide median jk_count_1km — for market component counts = sorted([r[0] for r in conn.execute( "SELECT count(*) FROM sites s2 JOIN sites s ON s2.site_id != s.site_id " "WHERE 1=0 GROUP BY s.site_id").fetchall()]) # noop, computed below conn.execute("DELETE FROM features") conn.execute("DELETE FROM scores") conn.execute("DELETE FROM scores_total") for sid, lat, lon in sites: # POI features for cat in ["kindergarten","school","university","pharmacy","clinic","hospital", "shop_big","shop_med","shop_small","bus_stop","tram_stop","metro", "park","playground","sports"]: n = conn.execute("SELECT MIN(distance_m) FROM pois WHERE site_id=? AND category=?", (sid, cat)).fetchone()[0] c500 = conn.execute("SELECT count(*) FROM pois WHERE site_id=? AND category=? AND distance_m<=500", (sid, cat)).fetchone()[0] c1k = conn.execute("SELECT count(*) FROM pois WHERE site_id=? AND category=? AND distance_m<=1000", (sid, cat)).fetchone()[0] for k, v in (("nearest_m", n), ("count_500m", c500), ("count_1km", c1k)): conn.execute("INSERT INTO features(site_id,feature,value) VALUES (?,?,?)", (sid, f"{cat}_{k}", float(v) if v is not None else None)) # Locational components edu = comp(conn, sid, EDU) health = comp(conn, sid, HEALTH) retail = max(comp(conn, sid, RETAIL_BIG), 0.7 * comp(conn, sid, RETAIL_MED)) transit = max(comp(conn, sid, TRANSIT_M), 0.85 * comp(conn, sid, TRANSIT_T), 0.7 * comp(conn, sid, TRANSIT_B)) leisure = comp(conn, sid, LEISURE) # Economic + market econ_row = conn.execute("""SELECT de.real_median_price_m2, de.real_velocity_6mo, de.real_trend_ratio, de.months_to_sellout, de.sat_factor, de.real_sold_pct, de.real_n_lots FROM site_district sd JOIN district_economics de USING (district) WHERE sd.site_id=?""", (sid,)).fetchone() if econ_row: price, v_rec, trend, mts, sat, sold_pct, n_lots = econ_row # price_score: linear by district median price percentile p_score = max(0, min(100, ((price or 0) - pmin) * 100 / (pmax - pmin))) # velocity_score: real recent (6mo), capped at p90 v_score = max(0, min(100, (v_rec or 0) * 100 / vmax)) if vmax else 0 # trend modifier (prod-style): clamp 0.7..2.0 → 0.5..1.0 multiplier on velocity tf = max(0.7, min(2.0, trend or 1.0)) v_score = v_score * (0.5 + 0.5 * (tf / 2.0)) # 0.7→0.675, 1.0→0.75, 2.0→1.0 # liquidity_score from months_to_sellout liq_score = max(0, 100 - min(mts or 24, 24) * 100 / 24) if mts else 50 economic = 0.50 * p_score + 0.25 * v_score + 0.25 * liq_score else: economic = 0; trend = None; sat = None; sold_pct = None; n_lots = None # Competitive density: number of OTHER ЖК within 1km n_jk_1km = sum( 1 for sid2, (la2, lo2) in site_coords.items() if sid2 != sid and sid2.startswith("jk:") and hav(lat, lon, la2, lo2) <= 1000 ) # market component: 50/50 saturation × density penalty # density: 0..15 jks → 100..0 (linearly capped) density_score = max(0, 100 - n_jk_1km * 100 / 15) # saturation: optimal 50% sold (proven absorption, room to grow). 0% raw, 100% over-saturated sat_score = 100 - abs((sold_pct or 50) - 50) * 2 if sold_pct is not None else 50 market = 0.5 * density_score + 0.5 * sat_score conn.execute("INSERT INTO features(site_id,feature,value) VALUES (?,?,?)", (sid, "jk_count_1km", n_jk_1km)) comps = {"education": edu, "health": health, "retail": retail, "transit": transit, "leisure": leisure, "economic": economic, "market": market} for c, val in comps.items(): conn.execute("INSERT INTO scores(site_id,component,score_0_100) VALUES (?,?,?)", (sid, c, val)) weighted = sum(weights.get(k, 0) * v for k, v in comps.items()) conn.execute("INSERT INTO scores_total(site_id,weighted) VALUES (?,?)", (sid, weighted)) conn.commit() # Ranks rows = conn.execute("""SELECT s.site_id, st.weighted FROM sites s JOIN scores_total st USING (site_id) ORDER BY st.weighted DESC""").fetchall() for rank, (sid, _) in enumerate(rows, 1): conn.execute("UPDATE scores_total SET rank_overall=? WHERE site_id=?", (rank, sid)) # district rank by_dist = {} for sid, _ in rows: d = conn.execute("SELECT district FROM site_district WHERE site_id=?", (sid,)).fetchone() by_dist.setdefault(d[0] if d else "—", []).append(sid) for d, 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() # Print parcel summary pid = "parcel:66:41:0204016:10" p = conn.execute("SELECT weighted, rank_overall, rank_district FROM scores_total WHERE site_id=?", (pid,)).fetchone() print(f"\n=== Parcel ===") print(f" total: {p[0]:.2f}/100 overall #{p[1]} district #{p[2]}") print(f" components:") for r in conn.execute("SELECT component, score_0_100 FROM scores WHERE site_id=? ORDER BY component", (pid,)).fetchall(): wt = weights.get(r[0], 0) print(f" {r[0]:<10} {r[1]:>6.1f} × {wt*100:>4.0f}% = {r[1]*wt:>5.2f}") n_jk = conn.execute("SELECT count(*) FROM sites WHERE kind='jk'").fetchone()[0] print(f" vs {n_jk} строящихся ЖК Ekb") conn.close() if __name__ == "__main__": main()