diff --git a/data/sql/162_drop_district_economics_sat_factor.sql b/data/sql/162_drop_district_economics_sat_factor.sql new file mode 100644 index 00000000..e36e0586 --- /dev/null +++ b/data/sql/162_drop_district_economics_sat_factor.sql @@ -0,0 +1,12 @@ +-- Remove dead column district_economics.sat_factor (#1509). +-- +-- sat_factor = 1 + ((sold_pct - 50) / 100) * 0.30 was computed in 09_macro_and_trend.py +-- and written here, but was never read by any scoring logic (10_score_v2.py, server.py). +-- The live market sub-score uses sat_score = min(100, sold_pct * 100 / 70) directly, +-- so sat_factor double-counts absorption without being applied anywhere. +-- All code references removed in the same commit as this migration. +BEGIN; + +ALTER TABLE district_economics DROP COLUMN IF EXISTS sat_factor; + +COMMIT; diff --git a/site-finder/09_macro_and_trend.py b/site-finder/09_macro_and_trend.py index da022764..dada3955 100644 --- a/site-finder/09_macro_and_trend.py +++ b/site-finder/09_macro_and_trend.py @@ -3,7 +3,7 @@ Adds these tables/columns: macro_context — single-row mortgage rate, city avg price, etc. district_economics + (real_velocity_6mo, real_velocity_prior_6mo, real_trend_ratio, - sat_factor, trend_factor, price_factor) + trend_factor, price_factor) scoring_weights — single-row config (so we can change weights from UI later) Source for trend: per-flat register_date (objective_lots). @@ -37,7 +37,6 @@ CREATE TABLE IF NOT EXISTS macro_context ( ALTER TABLE district_economics ADD COLUMN real_velocity_6mo REAL; ALTER TABLE district_economics ADD COLUMN real_velocity_prior_6mo REAL; ALTER TABLE district_economics ADD COLUMN real_trend_ratio REAL; -ALTER TABLE district_economics ADD COLUMN sat_factor REAL; ALTER TABLE district_economics ADD COLUMN trend_factor REAL; ALTER TABLE district_economics ADD COLUMN price_factor REAL; @@ -130,16 +129,11 @@ def main(): ratio = 1.0 + min(1.0, v_rec / _REF_VELOCITY) * (_TREND_CAP_VPRIOR_ZERO - 1.0) # Prod-style clamping trend_factor = max(0.7, min(2.0, ratio)) if ratio else 1.0 - # sat_factor: sold_pct in district. >50% = mature market (prod logic +30% - # multiplier on velocity for mature districts since absorption is proven) - sold_pct = local.execute( - "SELECT real_sold_pct FROM district_economics WHERE district=?", (d,)).fetchone() - sat_factor = 1 + ((sold_pct[0] or 50) - 50) / 100 * 0.30 if sold_pct and sold_pct[0] else 1.0 local.execute("""UPDATE district_economics SET real_velocity_6mo=?, real_velocity_prior_6mo=?, - real_trend_ratio=?, sat_factor=?, trend_factor=? + real_trend_ratio=?, trend_factor=? WHERE district=?""", - (v_rec, v_prior, ratio, sat_factor, trend_factor, d)) + (v_rec, v_prior, ratio, trend_factor, d)) # price_factor = district median / city median city_med = local.execute( @@ -181,14 +175,14 @@ def main(): local.commit() print("\nDistrict trend (top 10 by recent velocity):") - print(f"{'район':<22}{'price_f':>8}{'sat':>6}{'trend':>6}{'v_rec':>6}{'v_pr':>6}{'ratio':>6}") - for r in local.execute("""SELECT district, price_factor, sat_factor, trend_factor, + print(f"{'район':<22}{'price_f':>8}{'trend':>6}{'v_rec':>6}{'v_pr':>6}{'ratio':>6}") + for r in local.execute("""SELECT district, price_factor, trend_factor, real_velocity_6mo, real_velocity_prior_6mo, real_trend_ratio FROM district_economics WHERE real_velocity_6mo IS NOT NULL ORDER BY real_velocity_6mo DESC LIMIT 10""").fetchall(): - d,pf,sf,tf,vr,vp,rr = r - print(f"{d:<22}{pf or 0:>8.2f}{sf or 0:>6.2f}{tf or 0:>6.2f}{vr or 0:>6.2f}{vp or 0:>6.2f}{rr or 0:>6.2f}") + d, pf, tf, vr, vp, rr = r + print(f"{d:<22}{pf or 0:>8.2f}{tf or 0:>6.2f}{vr or 0:>6.2f}{vp or 0:>6.2f}{rr or 0:>6.2f}") local.close() diff --git a/site-finder/10_score_v2.py b/site-finder/10_score_v2.py index 5fb39c8d..78ec1eac 100644 --- a/site-finder/10_score_v2.py +++ b/site-finder/10_score_v2.py @@ -113,12 +113,12 @@ def main(): # 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 + 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, v_rec, trend, mts, 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 @@ -130,7 +130,7 @@ def main(): 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 + economic = 0; trend = None; sold_pct = None; n_lots = None # Competitive density: number of OTHER ЖК within 1km n_jk_1km = sum( diff --git a/site-finder/server.py b/site-finder/server.py index e4274f89..93de7247 100644 --- a/site-finder/server.py +++ b/site-finder/server.py @@ -359,14 +359,13 @@ def score_point(lat, lon, weights, what_if=None, remove_cats=None): with _conn() as c: admin, obj_district = assign_district(lat, lon, c) economics = None - trend_factor = sat_factor = 1.0 + trend_factor = 1.0 if obj_district: row = c.execute("SELECT * FROM district_economics WHERE district=?", (obj_district,)).fetchone() economics = dict(row) if row else None if economics: trend_factor = economics.get("trend_factor") or 1.0 - sat_factor = economics.get("sat_factor") or 1.0 # bounds for normalization prices = sorted([r[0] for r in c.execute( @@ -459,7 +458,7 @@ def score_point(lat, lon, weights, what_if=None, remove_cats=None): "admin_district": admin, "district": obj_district, "economics": economics, - "macro_factors": {"trend_factor": trend_factor, "sat_factor": sat_factor, + "macro_factors": {"trend_factor": trend_factor, "n_jk_1km": n_jk_1km, "city_pmin": pmin, "city_pmax": pmax, "city_vmax": vmax}, "features": feats, diff --git a/site-finder/static/index.html b/site-finder/static/index.html index c72f1b6c..dc68c357 100644 --- a/site-finder/static/index.html +++ b/site-finder/static/index.html @@ -892,7 +892,7 @@ jk_count_1kmЧисло других строящихся ЖК в радиусе 1 км от точки.
Парцель: 3 (мало конкурентов рядом — плюс) density_scoremax(0, 100 − jk_count_1km × 100/15). Чем меньше конкурентов — тем выше.
Парцель: max(0, 100 − 3×100/15) = 80 saturation_scoremin(100, real_sold_pct × 100/70). Высокий sold% подтверждает спрос, не штраф.
Плато на 70%+ — район proven, дальше плюсы не растут.
Старая Сортировка sold 48.8% → 69.7 - sat_factorprod-style: 1 + (sold% − 50)/100 × 0.30. Используется отдельно как мультипликатор. Не входит напрямую в market.
Зрелые рынки получают +30% к velocity +