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 trend: per-flat register_date (objective_lots).
|
||||||
Source for mortgage rate: prod cbr_mortgage_series (Sverdl region, latest "ставка ипотечная").
|
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"
|
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 = """
|
EXTRA = """
|
||||||
CREATE TABLE IF NOT EXISTS macro_context (
|
CREATE TABLE IF NOT EXISTS macro_context (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
|
|
@ -38,10 +51,13 @@ CREATE TABLE IF NOT EXISTS scoring_weights (
|
||||||
def safe_alter(conn, sql):
|
def safe_alter(conn, sql):
|
||||||
for stmt in sql.strip().split(";"):
|
for stmt in sql.strip().split(";"):
|
||||||
s = stmt.strip()
|
s = stmt.strip()
|
||||||
if not s: continue
|
if not s:
|
||||||
try: conn.execute(s)
|
continue
|
||||||
|
try:
|
||||||
|
conn.execute(s)
|
||||||
except sqlite3.OperationalError as e:
|
except sqlite3.OperationalError as e:
|
||||||
if "duplicate column" not in str(e): raise
|
if "duplicate column" not in str(e):
|
||||||
|
raise
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
local = sqlite3.connect(DB)
|
local = sqlite3.connect(DB)
|
||||||
|
|
@ -88,11 +104,14 @@ def main():
|
||||||
WHERE district IS NOT NULL AND register_date IS NOT NULL""").fetchall()
|
WHERE district IS NOT NULL AND register_date IS NOT NULL""").fetchall()
|
||||||
by_d = {}
|
by_d = {}
|
||||||
for d, r, p, c in rows:
|
for d, r, p, c in rows:
|
||||||
try: rd = dt.date.fromisoformat(r[:10])
|
try:
|
||||||
except: continue
|
rd = dt.date.fromisoformat(r[:10])
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
bucket = "recent" if r >= cut_recent else ("prior" if r >= cut_prior else None)
|
bucket = "recent" if r >= cut_recent else ("prior" if r >= cut_prior else None)
|
||||||
if not bucket: continue
|
if not bucket:
|
||||||
by_d.setdefault(d, {"recent":[], "prior":[], "corpuses":set()})
|
continue
|
||||||
|
by_d.setdefault(d, {"recent": [], "prior": [], "corpuses": set()})
|
||||||
by_d[d][bucket].append(rd)
|
by_d[d][bucket].append(rd)
|
||||||
by_d[d]["corpuses"].add((p, c))
|
by_d[d]["corpuses"].add((p, c))
|
||||||
|
|
||||||
|
|
@ -100,7 +119,15 @@ def main():
|
||||||
n_corp = max(len(info["corpuses"]), 1)
|
n_corp = max(len(info["corpuses"]), 1)
|
||||||
v_rec = len(info["recent"]) / 6 / n_corp
|
v_rec = len(info["recent"]) / 6 / n_corp
|
||||||
v_prior = len(info["prior"]) / 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
|
# Prod-style clamping
|
||||||
trend_factor = max(0.7, min(2.0, ratio)) if ratio else 1.0
|
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%
|
# sat_factor: sold_pct in district. >50% = mature market (prod logic +30%
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue