Merge pull request 'fix(site-finder): volume-weighted median_price_m2 (#1511) + consistent-period MTS (#1512)' (#1690) from fix/match-economics-1511-1512 into main
This commit is contained in:
commit
40c58c15b1
1 changed files with 51 additions and 25 deletions
|
|
@ -5,7 +5,10 @@ Output tables:
|
|||
district_economics — per Objective-район aggregates over last 90 days
|
||||
site_district — which Objective-район each site belongs to
|
||||
"""
|
||||
import sqlite3, pathlib, re, math
|
||||
import math
|
||||
import pathlib
|
||||
import re
|
||||
import sqlite3
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
DB = pathlib.Path(__file__).parent / "analysis.db"
|
||||
|
|
@ -43,7 +46,8 @@ CREATE TABLE IF NOT EXISTS site_district (
|
|||
"""
|
||||
|
||||
def normalize(s):
|
||||
if not s: return ""
|
||||
if not s:
|
||||
return ""
|
||||
s = s.lower().strip()
|
||||
s = re.sub(r'^(жк|жилой\s+комплекс|жилой\s+квартал|жилые\s+кварталы)\s+', '', s)
|
||||
s = re.sub(r'["«»\'`]+', '', s)
|
||||
|
|
@ -53,11 +57,13 @@ def normalize(s):
|
|||
def fuzzy(a, b):
|
||||
return SequenceMatcher(None, normalize(a), normalize(b)).ratio()
|
||||
|
||||
def hav(la1,lo1,la2,lo2):
|
||||
R=6371000; p1,p2=math.radians(la1),math.radians(la2)
|
||||
dp=math.radians(la2-la1); dl=math.radians(lo2-lo1)
|
||||
a=math.sin(dp/2)**2+math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2
|
||||
return 2*R*math.asin(math.sqrt(a))
|
||||
def hav(la1, lo1, la2, lo2):
|
||||
R = 6371000
|
||||
p1, p2 = math.radians(la1), math.radians(la2)
|
||||
dp = math.radians(la2 - la1)
|
||||
dl = math.radians(lo2 - lo1)
|
||||
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
|
||||
return 2 * R * math.asin(math.sqrt(a))
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB)
|
||||
|
|
@ -74,7 +80,8 @@ def main():
|
|||
best = (0.0, None)
|
||||
for proj in objective_projects:
|
||||
s = fuzzy(name, proj)
|
||||
if s > best[0]: best = (s, proj)
|
||||
if s > best[0]:
|
||||
best = (s, proj)
|
||||
if best[0] >= 0.80:
|
||||
conn.execute("INSERT INTO jk_objective_match VALUES (?,?,?,?)",
|
||||
(site_id, best[1], best[0], "fuzzy"))
|
||||
|
|
@ -90,17 +97,17 @@ def main():
|
|||
conn.execute("DELETE FROM district_economics")
|
||||
placeholders = ",".join(["?"]*len(last3))
|
||||
|
||||
# Latest stock per (project, corpus) — taken from the latest month each corpus appears
|
||||
# Latest stock per (project, corpus) — taken from the single most-recent month in the window
|
||||
# (#1512): using a per-corpus latest caused mixed periods (stale stock for inactive corpuses).
|
||||
# Fix: pin stock to last3[0] (the most recent month) so numerator and denominator of MTS
|
||||
# are aligned to the same period for all corpuses in the district.
|
||||
latest_month = last3[0]
|
||||
latest_stock = {}
|
||||
for r in conn.execute(f"""
|
||||
WITH ranked AS (
|
||||
SELECT project, corpus, district, month, stock_m2, stock_lots,
|
||||
ROW_NUMBER() OVER (PARTITION BY project, corpus ORDER BY month DESC) rn
|
||||
FROM objective_corp_month
|
||||
WHERE month IN ({placeholders})
|
||||
)
|
||||
SELECT project, corpus, district, stock_m2, stock_lots FROM ranked WHERE rn=1
|
||||
""", last3).fetchall():
|
||||
for r in conn.execute("""
|
||||
SELECT project, corpus, district, stock_m2, stock_lots
|
||||
FROM objective_corp_month
|
||||
WHERE month = ?
|
||||
""", (latest_month,)).fetchall():
|
||||
latest_stock[(r[0], r[1])] = (r[2], r[3] or 0, r[4] or 0)
|
||||
|
||||
# Per-district aggregates from window
|
||||
|
|
@ -114,7 +121,8 @@ def main():
|
|||
by_dist = {}
|
||||
for r in rows:
|
||||
d = r[0]
|
||||
if not d: continue
|
||||
if not d:
|
||||
continue
|
||||
by_dist.setdefault(d, []).append(r)
|
||||
|
||||
for d, rs in by_dist.items():
|
||||
|
|
@ -124,9 +132,24 @@ def main():
|
|||
wn = sum((r[6] or 0) * (r[5] or 0) for r in rs)
|
||||
wd = sum((r[5] or 0) for r in rs)
|
||||
wp = wn / wd if wd else None
|
||||
# median price (priced deals only)
|
||||
prices = sorted([r[6] for r in rs if r[6] and (r[4] or 0) > 0])
|
||||
med = prices[len(prices)//2] if prices else None
|
||||
# median price — volume-weighted by sold_volume_m2 (#1511)
|
||||
# Each corpus×month row contributes avg_price_m2 weighted by sold_volume_m2 so that
|
||||
# high-volume months/rooms count proportionally (not equal-weight per row).
|
||||
# Falls back to weight=1 for rows with priced deals but missing volume figure.
|
||||
pw = [(r[6], max(r[5] or 0, 1.0)) for r in rs if r[6] and (r[4] or 0) > 0]
|
||||
if pw:
|
||||
pw.sort(key=lambda x: x[0])
|
||||
total_w = sum(w for _, w in pw)
|
||||
half = total_w / 2.0
|
||||
cum = 0.0
|
||||
med = None
|
||||
for price, w in pw:
|
||||
cum += w
|
||||
if cum >= half:
|
||||
med = price
|
||||
break
|
||||
else:
|
||||
med = None
|
||||
# offer prices
|
||||
offers = [r[8] for r in rs if r[8] and r[8] > 0]
|
||||
avg_off = sum(offers)/len(offers) if offers else None
|
||||
|
|
@ -175,7 +198,8 @@ def main():
|
|||
# vote among k=5 nearest matched ЖК within 2 km
|
||||
cands = []
|
||||
for sid2, (lat2, lon2, dist2) in matched_idx.items():
|
||||
if sid2 == sid: continue
|
||||
if sid2 == sid:
|
||||
continue
|
||||
d = hav(lat, lon, lat2, lon2)
|
||||
cands.append((d, sid2, dist2))
|
||||
cands.sort()
|
||||
|
|
@ -187,8 +211,10 @@ def main():
|
|||
rep = next(c for c in top if c[2] == top_dist)
|
||||
obj_id = None
|
||||
if rep[1] and rep[1].startswith("jk:"):
|
||||
try: obj_id = int(rep[1][3:])
|
||||
except: pass
|
||||
try:
|
||||
obj_id = int(rep[1][3:])
|
||||
except ValueError:
|
||||
pass
|
||||
conn.execute("INSERT INTO site_district VALUES (?,?,?,?,?)",
|
||||
(sid, top_dist, "knn_vote_5", obj_id, rep[0]))
|
||||
conn.commit()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue