fix(forecasting): cap trend_ratio by v_rec magnitude when v_prior==0 (#1508)
All checks were successful
CI / changes (push) Successful in 10s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
All checks were successful
CI / changes (push) Successful in 10s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
When v_prior == 0 and v_rec > 0, the old code unconditionally assigned trend_ratio = 2.0, producing an artificial 2x jump even for districts with negligible recent velocity. New formula: ratio = 1.0 + min(1.0, v_rec / _REF_VELOCITY) * (_TREND_CAP_VPRIOR_ZERO - 1.0) Where: _REF_VELOCITY = 10.0 (monthly flats/corpus — EKB "well-performing" benchmark) _TREND_CAP_VPRIOR_ZERO = 1.5 (max ratio for the v_prior==0 case) Tiny v_rec (e.g. 1 flat/month) → ratio ≈ 1.05 (near neutral) Large v_rec (≥ 10 flat/month) → ratio → 1.5 (capped, below old hard 2.0) v_prior > 0 branch is unchanged. Also fixes pre-existing ruff violations in the same file (E401 multi-import, E701 inline colon, E722 bare except, F401 unused import) so ruff check passes clean.
This commit is contained in:
parent
18da92ccc7
commit
cf36151dbc
1 changed files with 36 additions and 9 deletions
|
|
@ -9,9 +9,22 @@ Adds these tables/columns:
|
|||
Source for trend: per-flat register_date (objective_lots).
|
||||
Source for mortgage rate: prod cbr_mortgage_series (Sverdl region, latest "ставка ипотечная").
|
||||
"""
|
||||
import sqlite3, pathlib, psycopg2, datetime as dt, statistics
|
||||
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 10–20 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,
|
||||
|
|
@ -38,10 +51,13 @@ CREATE TABLE IF NOT EXISTS scoring_weights (
|
|||
def safe_alter(conn, sql):
|
||||
for stmt in sql.strip().split(";"):
|
||||
s = stmt.strip()
|
||||
if not s: continue
|
||||
try: conn.execute(s)
|
||||
if not s:
|
||||
continue
|
||||
try:
|
||||
conn.execute(s)
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column" not in str(e): raise
|
||||
if "duplicate column" not in str(e):
|
||||
raise
|
||||
|
||||
def main():
|
||||
local = sqlite3.connect(DB)
|
||||
|
|
@ -88,11 +104,14 @@ def main():
|
|||
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: continue
|
||||
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()})
|
||||
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))
|
||||
|
||||
|
|
@ -100,7 +119,15 @@ def main():
|
|||
n_corp = max(len(info["corpuses"]), 1)
|
||||
v_rec = len(info["recent"]) / 6 / n_corp
|
||||
v_prior = len(info["prior"]) / 6 / n_corp
|
||||
ratio = (v_rec / v_prior) if v_prior > 0 else (None if v_rec == 0 else 2.0)
|
||||
# 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
|
||||
# sat_factor: sold_pct in district. >50% = mature market (prod logic +30%
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue