SPB Git

spb/wp26_uqo

Public
5.6 KB · 128 lines python
1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""As-of market context (evidence id M1) from sales with ``date <= cutoff`` only.34* municipality (same property class) over the 12 months ending at the cutoff: median price, median price/m2, n;5* monthly municipality index: median price/m2 by month (same class), expressed relative to the first month with6  at least ``MIN_MONTH_N`` sales (base = 100), months up to and including the cutoff month (the last month is7  partial — flagged); falls back to the region series when the municipality is too thin;8* region (same class): median price over 12 months and year-over-year change of that median;9* local liquidity: number of sales (all classes) within 3 km over the 12 months;10* local median assessment ratio (price / roll value in force) within 3 km, same class, 12 months (all classes11  when fewer than ``MIN_LOCAL_N``).12"""13from __future__ import annotations1415from datetime import date16from typing import Any1718import numpy as np19import polars as pl2021from ..config import load_config22from ..schemas import MarketContext23from .index import CLASS_CODE, AnalyticIndex, get_index, subject_to_dict, to_days2425WINDOW_DAYS = 36526LOCAL_RADIUS_KM = 3.027MIN_MONTH_N = 528MIN_LOCAL_N = 529MIN_MUN_N_12M = 20303132def _median(a: np.ndarray) -> float | None:33    a = a[~np.isnan(a)]34    return None if a.size == 0 else float(np.median(a))353637def _monthly_index(idx: AnalyticIndex, rows: np.ndarray, cutoff_days: int) -> list[dict[str, Any]]:38    if rows.size == 0:39        return []40    d = idx.date_days[rows]41    ppm2 = idx.ppm2[rows]42    ok = ~np.isnan(ppm2)43    if not ok.any():44        return []45    months = pl.Series(d[ok].astype(np.int32)).cast(pl.Date).dt.strftime("%Y-%m").to_numpy().astype(object)46    df = pl.DataFrame({"month": months, "ppm2": ppm2[ok]}).group_by("month").agg(pl.col("ppm2").median().alias("med"), pl.len().alias("n")).sort("month")47    df = df.filter(pl.col("n") >= MIN_MONTH_N)48    if df.height == 0:49        return []50    base = float(df["med"][0])51    cutoff_month = pl.Series([cutoff_days], dtype=pl.Int32).cast(pl.Date).dt.strftime("%Y-%m")[0]52    out = []53    for m, med, n in df.iter_rows():54        rec: dict[str, Any] = {"month": m, "index": round(100.0 * med / base, 1), "n": int(n)}55        if m == cutoff_month:56            rec["partial"] = True57        out.append(rec)58    return out596061def market_context(analytic: pl.DataFrame, subject: dict[str, Any] | pl.DataFrame, cutoff: date, cfg: dict[str, Any] | None = None) -> MarketContext:62    """Market context for ``subject`` as of ``cutoff`` (inclusive)."""63    cfg = cfg or load_config("data")64    s = subject_to_dict(subject)65    idx = get_index(analytic)66    cutoff_days = to_days(cutoff)67    start_12m = cutoff_days - WINDOW_DAYS + 168    cls_code = CLASS_CODE.get(s.get("property_class"), -1)69    mun = str(s["mun_code"])70    region = str(s["region_name"])7172    # municipality, same class, last 12 months73    mun_rows = idx.municipality_window(mun, start_12m, cutoff_days)74    mun_rows = mun_rows[idx.cls[mun_rows] == cls_code]75    mun_median = _median(idx.price[mun_rows]) if mun_rows.size else None76    mun_ppm2 = _median(idx.ppm2[mun_rows]) if mun_rows.size else None7778    # monthly index: municipality same-class series since the start of the table, else region series79    mun_hist = idx.municipality_window(mun, -10**9, cutoff_days)80    mun_hist = mun_hist[idx.cls[mun_hist] == cls_code]81    series = _monthly_index(idx, mun_hist, cutoff_days) if mun_rows.size >= MIN_MUN_N_12M else []82    index_level = "municipality"83    if len(series) < 6:84        reg_hist = idx.region_window(region, -10**9, cutoff_days)85        reg_hist = reg_hist[idx.cls[reg_hist] == cls_code]86        series = _monthly_index(idx, reg_hist, cutoff_days)87        index_level = "region"88    if series:89        series[0]["level"] = index_level9091    # region same class: 12 months and previous 12 months92    reg_rows = idx.region_window(region, start_12m, cutoff_days)93    reg_rows = reg_rows[idx.cls[reg_rows] == cls_code]94    reg_prev = idx.region_window(region, start_12m - WINDOW_DAYS, start_12m - 1)95    reg_prev = reg_prev[idx.cls[reg_prev] == cls_code]96    reg_median = _median(idx.price[reg_rows]) if reg_rows.size else None97    reg_prev_median = _median(idx.price[reg_prev]) if reg_prev.size else None98    yoy = None if reg_median is None or not reg_prev_median else round(reg_median / reg_prev_median - 1.0, 4)99100    # local 3 km101    if s.get("x_m") is not None:102        x, y = float(s["x_m"]), float(s["y_m"])103    else:104        from ..data.analytic_schema import project_xy105106        xx, yy = project_xy(np.array([s["lat"]]), np.array([s["lng"]]))107        x, y = float(xx[0]), float(yy[0])108    near = idx.within_radius(x, y, LOCAL_RADIUS_KM * 1000.0)109    d = idx.date_days[near]110    near = near[(d >= start_12m) & (d <= cutoff_days)]111    liquidity = int(near.size)112    near_cls = near[idx.cls[near] == cls_code]113    ratio_rows = near_cls if near_cls.size >= MIN_LOCAL_N else near114    local_ratio = _median(idx.ratio[ratio_rows]) if ratio_rows.size else None115116    return MarketContext(117        as_of=cutoff,118        municipality_median_price_12m=None if mun_median is None else round(mun_median, 0),119        municipality_median_ppm2_12m=None if mun_ppm2 is None else round(mun_ppm2, 1),120        municipality_n_sales_12m=int(mun_rows.size),121        municipality_price_index=series,122        region_median_price_12m=None if reg_median is None else round(reg_median, 0),123        region_yoy_change=yoy,124        local_liquidity_n_sales_3km_12m=liquidity,125        local_median_assessment_ratio_12m=None if local_ratio is None else round(local_ratio, 4),126        mortgage_rate_5y_fixed=None,127    )128
Files · main
Menu

Theme