gendesign/site-finder/09_macro_and_trend.py
bot-backend 9e5086890f
Some checks failed
CI / changes (pull_request) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
fix(site-finder): remove dead sat_factor (computed+written, never applied) (#1509)
sat_factor = 1 + ((sold_pct-50)/100)*0.30 was computed in 09_macro_and_trend.py,
written to district_economics.sat_factor, and fetched in server.py and 10_score_v2.py
— but never multiplied into any score. The live market sub-score uses a separate
sat_score = min(100, sold_pct*100/70) directly, so sat_factor was dead code that
would double-count absorption if ever wired in.

- 09_macro_and_trend.py: remove sat_factor computation, ALTER TABLE column, UPDATE
  binding, and debug print column
- 10_score_v2.py: remove sat_factor from SELECT and unpacking
- server.py: remove sat_factor variable assignment and from macro_factors response
- static/index.html: remove sat_factor documentation row
- data/sql/162_drop_district_economics_sat_factor.sql: DROP COLUMN IF EXISTS
2026-06-17 21:08:54 +03:00

190 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Macro context + per-district velocity trend.
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,
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).
Source for mortgage rate: prod cbr_mortgage_series (Sverdl region, latest "ставка ипотечная").
"""
import sqlite3
import pathlib
import psycopg2
import datetime as dt
DB = pathlib.Path(__file__).parent / "analysis.db"
# When v_prior == 0 we cannot compute a real ratio, so we synthesise one that
# reflects the *magnitude* of v_rec rather than assigning a flat 2.0.
# REF_VELOCITY: monthly flats/corpus that is considered "high activity" — above
# this level the district earns a ratio near TREND_CAP_VPRIOR_ZERO; below it
# the ratio slides toward neutral (1.0). Calibrated from EKB corpus data where
# an active corpus typically records 1020 units/month; 10 is a conservative
# "well-performing" benchmark.
_REF_VELOCITY: float = 10.0 # monthly flats per corpus → full-cap reference
_TREND_CAP_VPRIOR_ZERO: float = 1.5 # maximum ratio when v_prior == 0 (< hard 2.0 cap)
EXTRA = """
CREATE TABLE IF NOT EXISTS macro_context (
key TEXT PRIMARY KEY,
value REAL,
label TEXT,
period TEXT,
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
);
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 trend_factor REAL;
ALTER TABLE district_economics ADD COLUMN price_factor REAL;
CREATE TABLE IF NOT EXISTS scoring_weights (
component TEXT PRIMARY KEY,
weight REAL NOT NULL,
note TEXT
);
"""
def safe_alter(conn, sql):
for stmt in sql.strip().split(";"):
s = stmt.strip()
if not s:
continue
try:
conn.execute(s)
except sqlite3.OperationalError as e:
if "duplicate column" not in str(e):
raise
def main():
local = sqlite3.connect(DB)
safe_alter(local, EXTRA)
local.commit()
# ---- 1. Pull macro from prod ----
pg = psycopg2.connect(host="127.0.0.1", port=15432, user="gendesign",
password="2J2SBPMKuS998fiwhtQqDhMI", dbname="gendesign")
cur = pg.cursor()
# Mortgage rate (Sverdl, latest weighted average)
cur.execute("""SELECT period, value FROM cbr_mortgage_series
WHERE region='sverdl' AND title ILIKE '%ставка%ипотечн%'
ORDER BY period DESC LIMIT 1""")
r = cur.fetchone()
if r:
local.execute("""INSERT OR REPLACE INTO macro_context(key,value,label,period)
VALUES ('mortgage_rate_sverdl', ?,
'Средневзв. ставка по ипотеке (Свердл, %)', ?)""",
(float(r[1]), r[0]))
print(f"Mortgage rate Sverdl ({r[0]}): {r[1]}%")
# City average POI density per домрф_kn (для prod-стиля POI factor)
cur.execute("""SELECT count(*)::float / (SELECT count(DISTINCT obj_id) FROM domrf_kn_infrastructure)
FROM domrf_kn_infrastructure
WHERE distance_m <= 1000""")
avg_poi = cur.fetchone()[0]
local.execute("""INSERT OR REPLACE INTO macro_context(key,value,label,period)
VALUES ('city_avg_poi_1km', ?, 'Средний POI/ЖК в 1км (Ekb)', '2026-05')""",
(avg_poi,))
print(f"City avg POI 1km: {avg_poi:.1f}")
pg.close()
# ---- 2. Velocity trend per district (6mo vs prior 6mo) ----
today = dt.date.today()
cut_recent = (today - dt.timedelta(days=180)).isoformat()
cut_prior = (today - dt.timedelta(days=360)).isoformat()
rows = local.execute("""
SELECT district, register_date, project, corpus
FROM objective_lots
WHERE district IS NOT NULL AND register_date IS NOT NULL""").fetchall()
by_d = {}
for d, r, p, c in rows:
try:
rd = dt.date.fromisoformat(r[:10])
except ValueError:
continue
bucket = "recent" if r >= cut_recent else ("prior" if r >= cut_prior else None)
if not bucket:
continue
by_d.setdefault(d, {"recent": [], "prior": [], "corpuses": set()})
by_d[d][bucket].append(rd)
by_d[d]["corpuses"].add((p, c))
for d, info in by_d.items():
n_corp = max(len(info["corpuses"]), 1)
v_rec = len(info["recent"]) / 6 / n_corp
v_prior = len(info["prior"]) / 6 / n_corp
# v_prior > 0: real ratio; v_prior == 0 && v_rec == 0: neutral (None→1.0);
# v_prior == 0 && v_rec > 0: synthesise ratio scaled by v_rec magnitude so
# a tiny v_rec stays near 1.0 while a large v_rec approaches _TREND_CAP_VPRIOR_ZERO.
if v_prior > 0:
ratio: float | None = v_rec / v_prior
elif v_rec == 0:
ratio = None
else:
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
local.execute("""UPDATE district_economics
SET real_velocity_6mo=?, real_velocity_prior_6mo=?,
real_trend_ratio=?, trend_factor=?
WHERE district=?""",
(v_rec, v_prior, ratio, trend_factor, d))
# price_factor = district median / city median
city_med = local.execute(
"SELECT real_median_price_m2 FROM district_economics ORDER BY real_n_lots DESC LIMIT 1"
).fetchone()
# actually weighted by lots
weighted_rows = local.execute(
"SELECT real_median_price_m2, real_n_lots FROM district_economics WHERE real_median_price_m2 IS NOT NULL"
).fetchall()
if weighted_rows:
s = sum((p or 0) * (n or 0) for p, n in weighted_rows)
wn = sum(n or 0 for _, n in weighted_rows)
city_med = s / wn if wn else 100
else:
city_med = 100
local.execute("""INSERT OR REPLACE INTO macro_context(key,value,label,period)
VALUES ('city_med_price_m2', ?, 'Средневзв. цена м² Ekb (тыс ₽)', '2026-05')""",
(city_med,))
print(f"City weighted median price: {city_med:.1f} тыс ₽/м²")
local.execute("""UPDATE district_economics
SET price_factor = real_median_price_m2 / ?
WHERE real_median_price_m2 IS NOT NULL""", (city_med,))
local.commit()
# ---- 3. Default weights ----
weights = [
("education", 0.18, "Школы, садики, ВУЗы"),
("health", 0.10, "Аптеки, поликлиники, больницы"),
("retail", 0.13, "Магазины (большие/средние/малые)"),
("transit", 0.15, "Метро, трамвай, автобус"),
("leisure", 0.09, "Парки, площадки, спорт"),
("economic", 0.30, "Цена, скорость продаж, тренд (Объектив)"),
("market", 0.05, "Конкурентная плотность, sat/trend факторы"),
]
local.execute("DELETE FROM scoring_weights")
for c, w, n in weights:
local.execute("INSERT INTO scoring_weights VALUES (?,?,?)", (c, w, n))
local.commit()
print("\nDistrict trend (top 10 by recent velocity):")
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, 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()
if __name__ == "__main__":
main()