TeX 62.3%
Python 37.6%
1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Task A models (property-level price at a future sale date). All expose fit(X, y, meta) / predict(X, meta) → point (log price)3and, where available, predict_quantiles(X, meta, taus) → (n, len(taus)). `meta` carries the categorical keys for the4hierarchical model (municipality / MRC / region codes, horizon)."""5from __future__ import annotations67import os89import numpy as np1011# NOTE (macOS): LightGBM / XGBoost (libomp) and PyTorch cannot train in the same process without a segmentation fault or a12# deadlock in the OpenMP runtime. Tree models live here (lazy imports); neural models live in models_a_torch.py; the runner13# executes the two families in separate processes (`--family trees|torch`).14from sklearn.ensemble import RandomForestRegressor15from sklearn.linear_model import ElasticNet, Ridge16from sklearn.svm import SVR1718TAUS = np.array([0.05, 0.25, 0.5, 0.75, 0.95])192021class OLS:22 name = "hedonic_ols"23 def fit(self, X, y, meta=None):24 Xa = np.column_stack([np.ones(len(X)), X]); self.b = np.linalg.lstsq(Xa, y, rcond=None)[0]25 r = y - Xa @ self.b; self.sigma = r.std(); return self26 def predict(self, X, meta=None):27 return np.column_stack([np.ones(len(X)), X]) @ self.b28 def predict_quantiles(self, X, meta=None, taus=TAUS):29 from scipy.stats import norm30 m = self.predict(X); return m[:, None] + self.sigma * norm.ppf(taus)[None, :]313233class RidgeM(OLS):34 name = "ridge"35 def fit(self, X, y, meta=None):36 self.mu = X.mean(0); self.sd = X.std(0) + 1e-9; self.m = Ridge(alpha=1.0).fit((X - self.mu) / self.sd, y)37 self.sigma = (y - self.m.predict((X - self.mu) / self.sd)).std(); return self38 def predict(self, X, meta=None):39 return self.m.predict((X - self.mu) / self.sd)404142class ElasticNetM(RidgeM):43 name = "elastic_net"44 def fit(self, X, y, meta=None):45 self.mu = X.mean(0); self.sd = X.std(0) + 1e-9; self.m = ElasticNet(alpha=0.001, l1_ratio=0.5, max_iter=3000).fit((X - self.mu) / self.sd, y)46 self.sigma = (y - self.m.predict((X - self.mu) / self.sd)).std(); return self474849class RF:50 name = "random_forest"51 def fit(self, X, y, meta=None):52 self.m = RandomForestRegressor(n_estimators=150, max_depth=18, min_samples_leaf=5, max_features=0.5, n_jobs=int(os.environ.get("WP23_JOBS", "8")), random_state=0).fit(X, y); return self53 def predict(self, X, meta=None):54 return self.m.predict(X)555657class XGB:58 name = "xgboost"59 def fit(self, X, y, meta=None):60 import xgboost as xgb61 self.m = xgb.XGBRegressor(n_estimators=600, learning_rate=0.05, max_depth=8, subsample=0.8, colsample_bytree=0.8, tree_method="hist", n_jobs=int(os.environ.get("WP23_JOBS", "8")), random_state=0).fit(X, y); return self62 def predict(self, X, meta=None):63 return self.m.predict(X)646566class LGBM:67 name = "lightgbm"68 def __init__(self, quantiles: bool = True):69 self.quantiles = quantiles70 def fit(self, X, y, meta=None):71 import lightgbm as lgb72 kw = dict(n_estimators=800, learning_rate=0.04, num_leaves=63, min_child_samples=30, subsample=0.8, subsample_freq=1, colsample_bytree=0.8, n_jobs=int(os.environ.get("WP23_JOBS", "8")), random_state=0, verbose=-1)73 self.m = lgb.LGBMRegressor(objective="l2", **kw).fit(X, y)74 if self.quantiles:75 self.q = {t: lgb.LGBMRegressor(objective="quantile", alpha=float(t), **{**kw, "n_estimators": 400}).fit(X, y) for t in TAUS}76 return self77 def predict(self, X, meta=None):78 return self.m.predict(X)79 def predict_quantiles(self, X, meta=None, taus=TAUS):80 Q = np.column_stack([self.q[t].predict(X) for t in taus]); return np.sort(Q, axis=1)818283class SVRM:84 name = "svr"85 def fit(self, X, y, meta=None, n_max=15000):86 rng = np.random.default_rng(0); idx = rng.choice(len(X), min(n_max, len(X)), replace=False)87 self.mu = X.mean(0); self.sd = X.std(0) + 1e-9; self.m = SVR(C=3.0, epsilon=0.05, gamma="scale").fit((X[idx] - self.mu) / self.sd, y[idx]); return self88 def predict(self, X, meta=None):89 return self.m.predict((X - self.mu) / self.sd)909192def registry(family: str = "trees", fast: bool = False) -> list:93 if family == "torch":94 from wp23.models_a_torch import HSHN, MLP95 return [MLP(), HSHN("partial"), HSHN("full"), HSHN("none")]96 base = [OLS(), RidgeM(), ElasticNetM(), LGBM(), XGB()]97 if not fast:98 base += [RF(), SVRM()]99 return base100