Merge pull request 'fix(site-finder): remove dead sat_factor (computed+written, never applied) (#1509)' (#1698) from fix/score-sat-factor-1509 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions

This commit is contained in:
lekss361 2026-06-17 18:28:45 +00:00
commit c514b992ee
5 changed files with 25 additions and 20 deletions

View file

@ -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;

View file

@ -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()

View file

@ -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(

View file

@ -363,14 +363,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(
@ -463,7 +462,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,

View file

@ -892,7 +892,7 @@
<tr><td><b>jk_count_1km</b></td><td>Число других строящихся ЖК в радиусе 1 км от точки.<br><span class="hint">Парцель: 3 (мало конкурентов рядом — плюс)</span></td></tr>
<tr><td><b>density_score</b></td><td><code>max(0, 100 jk_count_1km × 100/15)</code>. Чем меньше конкурентов — тем выше.<br><span class="hint">Парцель: max(0, 100 3×100/15) = 80</span></td></tr>
<tr><td><b>saturation_score</b></td><td><code>min(100, real_sold_pct × 100/70)</code>. Высокий sold% подтверждает спрос, не штраф.<br>Плато на 70%+ — район proven, дальше плюсы не растут.<br><span class="hint">Старая Сортировка sold 48.8% → 69.7</span></td></tr>
<tr><td><b>sat_factor</b></td><td>prod-style: <code>1 + (sold% 50)/100 × 0.30</code>. Используется отдельно как мультипликатор. Не входит напрямую в market.<br><span class="hint">Зрелые рынки получают +30% к velocity</span></td></tr>
</tbody>
</table>
</details>