SPB Git forge

spb/wp16_uqo

Public
11commits 1branches 1releases
3.3 MBsize
maindefault branch
2 h agolast push
TeX 52.9% Python 45.7% Shell 1.4%
6.1 KB · 98 lines python
Raw Blame History
1# -*- coding: utf-8 -*-2"""01 — Jeu de données : transactions unifamiliales Vrai Prix ⋈ rôle, nettoyage journalisé,3reventes, géographie, périodes. Sorties : sales.parquet, exclusions.csv, dataset_stats.json."""4from common import *56con = connect()7log(f"lecture {DB}")8sql = """9select t.id, t.date, t.amount, t.lat, t.lng, t.city, t.property_type, t.year_built, t.floor_area,10       t.building_type, t.id_provinc, t.valeur_role as role_vente, t.land_area,11       u.code_mun, u.municipalite, u.type_prop as u_type, u.annee_construction, u.aire_etages_m2,12       u.superficie_terrain_m2, u.front_terrain_m, u.nb_etages, u.nb_logements, u.lien_physique,13       u.genre_construction, u.unite_voisinage, u.dat_cond_marche, u.valeur_terrain, u.valeur_batiment,14       u.valeur_role as role_actuel, u.valeur_anterieure as role_anterieur, u.cubf15from transactions t left join units u on u.id_provinc = t.id_provinc16where t.property_type = 'unifamilial'17"""18df = pd.read_sql(sql, con)19log(f"{len(df):,} transactions unifamiliales brutes")20n0 = len(df)21excl = []  # (règle, n)22def drop(mask, rule):23    global df24    n = int(mask.sum()); excl.append((rule, n)); df = df[~mask].copy(); log(f"  {rule}: -{n:,} → {len(df):,}")2526df["date"] = pd.to_datetime(df["date"])27df = df.sort_values(["id_provinc", "date"]).reset_index(drop=True)28drop(df["id_provinc"].isna() | df["code_mun"].isna(), "R0 sans jumelage au rôle (id_provinc / code_mun)")29drop(df.duplicated(["id_provinc", "date"]), "R1 doublons (même unité, même date)")30drop(~df["u_type"].isin(["unifamilial"]) | (df["nb_logements"].fillna(1) > 1), "R2 unité du rôle non unifamiliale (chalet, plex, terrain…) ou > 1 logement")31drop(df["lat"].isna() | df["lng"].isna() | (df["lat"] < 44.5) | (df["lat"] > 63) | (df["lng"] > -56) | (df["lng"] < -80), "R3 coordonnées absentes ou hors Québec")32fa = df["floor_area"].fillna(df["aire_etages_m2"])33df["floor_area"] = fa34drop(fa.isna() | (fa < 40) | (fa > 600), "R4 aire habitable absente ou hors [40, 600] m²")35la = df["land_area"].fillna(df["superficie_terrain_m2"])36df["land_area"] = la37drop(la.isna() | (la < 80) | (la > 20000), "R5 superficie de terrain absente ou hors [80, 20 000] m²")38yb = df["year_built"].fillna(df["annee_construction"])39df["year_built"] = yb40drop(yb.isna() | (yb < 1850) | (yb > df["date"].dt.year), "R6 année de construction absente, < 1850 ou postérieure à la vente (vente de terrain)")41drop((df["year_built"] == df["date"].dt.year), "R6b vente l'année même de la construction (prix du terrain ou d'une construction inachevée : médiane 346 k$ et ratio au rôle 1,15 contre 450-480 k$ et 1,3 aux âges 1-4)")42drop((df["amount"] < 50000) | (df["amount"] > 3_000_000), "R7 prix hors [50 k$, 3 M$]")43ratio = df["amount"] / df["role_vente"]44drop(df["role_vente"].isna() | (df["role_vente"] <= 0) | (ratio < 0.33) | (ratio > 3.0), "R8 rôle absent ou ratio prix/rôle hors [0,33 ; 3] (transactions présumées non représentatives)")45# reventes rapides avec forte variation (flips / rénovations : les caractéristiques enregistrées ne décrivent plus le bien)46df = df.sort_values(["id_provinc", "date"]).reset_index(drop=True)47g = df.groupby("id_provinc")48prev_date = g["date"].shift(1); prev_amt = g["amount"].shift(1)49dd = (df["date"] - prev_date).dt.days50chg = df["amount"] / prev_amt - 151flip = (dd <= 180) & (chg.abs() > 0.40)52drop(flip.fillna(False), "R9 revente ≤ 180 jours avec variation > 40 % (flip)")53drop(df["lien_physique"].isna() | df["genre_construction"].isna(), "R10 lien physique ou genre de construction absent au rôle")5455# ----------------------------------------------------------------- variables dérivées56rep = load_repertoire()57df = df.merge(rep, on="code_mun", how="left")58drop(df["region"].isna(), "R11 municipalité absente du répertoire MAMH")59df["sale_year"] = df["date"].dt.year60df["month"] = month_index(df["date"])61df["day"] = (df["date"] - pd.Timestamp("2021-01-01")).dt.days.astype(int)62df["age"] = (df["sale_year"] - df["year_built"]).clip(lower=0)63df["ln_price"] = np.log(df["amount"])64df["ln_floor"] = np.log(df["floor_area"])65df["ln_land"] = np.log(df["land_area"])66df["lien"] = df["lien_physique"].map(LIEN).fillna(0).astype(int)67df["genre"] = df["genre_construction"].map(GENRE).fillna(0).astype(int)68df["btype"] = df["building_type"].map(BTYPE).fillna(-1).astype(int)69df["etages"] = df["nb_etages"].fillna(1).clip(1, 3)70df["x_km"], df["y_km"] = to_xy_km(df["lat"], df["lng"])71# millésime du rôle au moment de la vente (rôle courant / antérieur / autre)72df["role_vintage"] = np.where(np.isclose(df["role_vente"], df["role_actuel"].fillna(-1)), 2,73                      np.where(np.isclose(df["role_vente"], df["role_anterieur"].fillna(-1)), 1, 0))74df["ratio_role"] = df["amount"] / df["role_vente"]75df["part_terrain"] = (df["valeur_terrain"] / df["role_actuel"]).clip(0.02, 0.95).fillna(0.2)76df["neuf"] = (df["age"] <= 3).astype(int)77df["period"] = np.select([df["date"] <= TRAIN_END, df["date"] <= VAL_END], ["train", "val"], "test")78df = df.sort_values("date").reset_index(drop=True)79df["idx"] = np.arange(len(df), dtype=np.int32)8081keep = ["idx", "id", "id_provinc", "date", "day", "month", "sale_year", "period", "amount", "ln_price",82        "lat", "lng", "x_km", "y_km", "code_mun", "mun_nom", "mrc", "mrc_nom", "region", "region_nom",83        "unite_voisinage", "floor_area", "ln_floor", "land_area", "ln_land", "year_built", "age", "neuf",84        "lien", "genre", "btype", "etages", "role_vente", "role_vintage", "ratio_role", "part_terrain"]85df = df[keep]86save_parquet(df, "sales.parquet")87pd.DataFrame(excl, columns=["regle", "n"]).to_csv(OUT / "exclusions.csv", index=False)88stats = {89    "brut": n0, "propre": len(df), "exclusions": excl,90    "par_periode": df["period"].value_counts().to_dict(),91    "par_annee": df["sale_year"].value_counts().sort_index().to_dict(),92    "par_region": df.groupby("region_nom").size().sort_values(ascending=False).to_dict(),93    "prix": {k: float(v) for k, v in df["amount"].describe().items()},94    "dates": [str(df["date"].min().date()), str(df["date"].max().date())],95}96save_json(stats, "dataset_stats.json")97log(f"jeu propre : {len(df):,} ventes ({stats['par_periode']})")98