# -*- coding: utf-8 -*- """Validation empirique de la méthode des comparables — fonctions communes. Données : instantané Vrai Prix (transactions ⋈ units) du 2026-10-10. Toutes les sorties sont versionnées sous out//. """ from __future__ import annotations import json, math, os, sqlite3, time from pathlib import Path import numpy as np import pandas as pd ROOT = Path(os.environ.get("CV_ROOT", Path(__file__).resolve().parents[1])) DB = Path(os.environ.get("CV_DB", ROOT / "data" / "vraiprix-20261010.db")) REF = ROOT / "ref" VERSION = os.environ.get("CV_VERSION", time.strftime("%Y-%m-%d")) OUT = ROOT / "out" / VERSION OUT.mkdir(parents=True, exist_ok=True) (OUT / "fig").mkdir(exist_ok=True) # Périodes (séparation chronologique) TRAIN_END = "2023-12-31" # développement VAL_START, VAL_END = "2024-01-01", "2024-12-31" # choix des configurations TEST_START, TEST_END = "2025-01-01", "2026-09-29" # évaluation finale hors échantillon MONTH0 = "2021-01" # premier mois des données def log(msg: str) -> None: print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True) def connect(path: Path = DB) -> sqlite3.Connection: return sqlite3.connect(f"file:{path}?mode=ro", uri=True, timeout=60) def _json_default(o): if isinstance(o, np.integer): return int(o) if isinstance(o, np.floating): return None if np.isnan(o) else float(o) if isinstance(o, np.bool_): return bool(o) if isinstance(o, np.ndarray): return o.tolist() if isinstance(o, pd.Timestamp): return o.strftime("%Y-%m-%d") if isinstance(o, float) and math.isnan(o): return None return str(o) def save_json(obj, name: str) -> Path: p = OUT / name; p.parent.mkdir(parents=True, exist_ok=True) with open(p, "w", encoding="utf-8") as f: json.dump(obj, f, ensure_ascii=False, indent=2, default=_json_default) return p def load_json(name: str): with open(OUT / name, encoding="utf-8") as f: return json.load(f) def save_parquet(df: pd.DataFrame, name: str) -> Path: p = OUT / name; df.to_parquet(p, index=False); return p def load_parquet(name: str) -> pd.DataFrame: return pd.read_parquet(OUT / name) def month_index(dates) -> np.ndarray: """Mois entier depuis MONTH0 (2021-01 = 0).""" d = pd.to_datetime(dates) y0, m0 = map(int, MONTH0.split("-")) return ((d.dt.year - y0) * 12 + (d.dt.month - m0)).to_numpy() def to_xy_km(lat, lng, lat0=46.8, lng0=-71.2): """Projection équirectangulaire locale (km) : erreur < 1 % pour des distances < 50 km.""" x = (np.asarray(lng, float) - lng0) * 111.32 * np.cos(np.radians(np.asarray(lat, float))) y = (np.asarray(lat, float) - lat0) * 110.57 return x, y def load_repertoire() -> pd.DataFrame: mun = pd.read_csv(REF / "MUN.csv", dtype=str, encoding="utf-8-sig") mun = mun.rename(columns=str.lower) out = pd.DataFrame({ "code_mun": mun["mcode"].str.zfill(5), "mun_nom": mun["munnom"], "region": mun["regadm"].str.extract(r"\((\d+)\)")[0], "region_nom": mun["regadm"].str.replace(r"\s*\(\d+\)", "", regex=True), "mrc": mun["mrc"].str.extract(r"\((\d+)\)")[0], "mrc_nom": mun["mrc"].str.replace(r"\s*\(\d+\)", "", regex=True), }) return out.drop_duplicates("code_mun") # Codages catégoriels (ordre stable) LIEN = {"Détaché": 0, "Jumelé": 1, "En rangée 1 côté": 2, "En rangée plus de 1 côté": 2, "Intégré": 3} GENRE = {"De plain-pied": 0, "Unimodulaire": 0, "À étages entiers": 1, "À niveaux décalés": 2, "À étage mansardé": 3} BTYPE = {"single-story": 0, "full-story": 1, "mansard": 2, "split-level": 3, "single-wide": 4}