thaman / docs /technical_report.tex
Turki Almurahhem
Correct API latency claims: 200-400ms is local, live Space is ~0.7-1.2s
9f1f560
Raw
History Blame Contribute Delete
48 kB
\documentclass[11pt, a4paper, twoside]{report}
% -------------------------------------------------------------------
% PACKAGES β€” UQU Format
% -------------------------------------------------------------------
\usepackage[utf8]{inputenc}
\usepackage{graphicx}
\usepackage{setspace}
\usepackage{geometry}
\usepackage{mathptmx}
\usepackage{titlesec}
\usepackage{fancyhdr}
\usepackage{tocloft}
\usepackage{float}
\usepackage{url}
\usepackage[colorlinks=true, urlcolor=blue, linkcolor=black, citecolor=black]{hyperref}
% -------------------------------------------------------------------
% MARGINS & SPACING
% -------------------------------------------------------------------
\geometry{top=1.5in, bottom=1.0in, left=1.25in, right=1.0in}
\onehalfspacing
\setlength{\parskip}{6pt}
% -------------------------------------------------------------------
% ADDITIONAL PACKAGES
% -------------------------------------------------------------------
\usepackage{amsmath,amssymb}
\usepackage{booktabs}
\usepackage{longtable}
\usepackage{array}
\usepackage{xcolor}
\usepackage{enumitem}
\usepackage{caption}
\usepackage{multirow}
\usepackage{listings}
\usepackage{tcolorbox}
% Arabic text fallback (pdflatex compatible)
\newcommand{\textarabic}[1]{#1}
% -------------------------------------------------------------------
% HEADINGS (Report class)
% -------------------------------------------------------------------
\titleformat{\chapter}[display]
{\normalfont\fontsize{16}{19}\selectfont\bfseries\centering}
{\chaptertitlename\ \thechapter}{6pt}{\MakeUppercase}
\titleformat{\section}
{\normalfont\fontsize{14}{17}\selectfont\bfseries}
{\thesection}{1em}{}
\titleformat{\subsection}
{\normalfont\fontsize{13}{16}\selectfont\bfseries\itshape}
{\thesubsection}{1em}{}
% -------------------------------------------------------------------
% HEADERS & FOOTERS
% -------------------------------------------------------------------
\pagestyle{fancy}
\fancyhf{}
\fancyhead[LO]{THAMAN}
\fancyhead[RE]{\leftmark}
\fancyfoot[R]{\thepage}
\renewcommand{\headrulewidth}{0.4pt}
\renewcommand{\footrulewidth}{0pt}
\fancypagestyle{plain}{\fancyhf{}\renewcommand{\headrulewidth}{0pt}}
\setlength{\headheight}{14pt}
% -------------------------------------------------------------------
% CUSTOM STYLES & COMMANDS
% -------------------------------------------------------------------
\begin{document}
% ── Title page (UQU Format) ───────────────────────────────────────────────────
\begin{titlepage}
\fontfamily{ptm}\selectfont
\noindent
\begin{minipage}[t]{0.6\textwidth}
\raggedright \itshape
BSc Project\\
Dept.\ of Computer Science \& Artificial Intelligence\\
Project ID: CSAI-472-P2-M20\\
2026
\end{minipage}%
\begin{minipage}[t]{0.4\textwidth}
\raggedleft
\includegraphics[width=3cm]{Umm_Al-Qura_University_logo.png}
\end{minipage}
\vspace{2cm}
\begin{center}
{\fontsize{26}{31}\selectfont \textbf{THAMAN}}
\vspace{0.5cm}
{\fontsize{16}{19}\selectfont \textbf{Complete Technical Reference}}
\vspace{0.5cm}
{\fontsize{13}{15}\selectfont Dual-City Automated Valuation Model: New York City \& Riyadh}
\vspace{1cm}
{\fontsize{14}{16}\selectfont \textbf{By:\\[0.3cm]
Turki AL-murahhem\\}}
\end{center}
\vfill
\noindent
\begin{flushleft}
Dept.\ of Computer Science and Artificial Intelligence\\
Faculty of Computer and Information Systems\\
Umm Al-Qura University, KSA\\[0.3cm]
{\small Generated: 2026-05-29 --- NYC Stack v22 Β· Riyadh Stack v12}
\end{flushleft}
\end{titlepage}
\newpage\thispagestyle{empty}\mbox{}\newpage
\tableofcontents
\newpage
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Project Overview}
\label{ch:overview}
THAMAN (\textarabic{Ψ«Ω…Ω†} --- Arabic for ``price'' or ``value'') is an
Automated Valuation Model (AVM) that estimates residential property prices using
machine learning. It operates across two independent real estate markets
simultaneously:
\begin{description}
\item[New York City (NYC)] Predicts total property price in USD
\item[Riyadh, Saudi Arabia] Predicts price per square metre in SAR
\end{description}
Both cities share the same stacking ensemble architecture but are trained on
entirely separate datasets, use different feature sets tailored to each market,
and produce city-specific outputs. The system is deployed as a FastAPI backend
on Hugging Face Spaces with a Leaflet.js web interface.
% ─────────────────────────────────────────────────────────────────────────────
\chapter{System Architecture}
\label{ch:architecture}
\begin{table}[H]
\centering
\caption{Technology stack}
\label{tab:tech_stack}
\begin{tabular}{ll}
\toprule
Layer & Technology \\
\midrule
Data pipeline & Python, Polars \\
Training & scikit-learn, XGBoost, LightGBM, CatBoost, Ridge \\
Model storage & pickle (.pkl), JSON \\
API backend & FastAPI, Uvicorn (port 8000) \\
Spatial index & scipy cKDTree, sklearn BallTree \\
Frontend & Leaflet.js, Chart.js \\
Deployment & Hugging Face Spaces (Docker) \\
Version control & GitHub (\texttt{turkialm/thaman-v2}) \\
\bottomrule
\end{tabular}
\end{table}
\section{API Endpoints}
\begin{description}
\item[\texttt{POST /predict}] NYC property valuation
\item[\texttt{POST /predict/riyadh}] Riyadh property valuation
\item[\texttt{GET /layers/district}] Riyadh district choropleth GeoJSON
\item[\texttt{GET /layers/listings}] Haraj.com.sa active listings GeoJSON
\item[\texttt{GET /nearby}] Nearest comparable NYC sales
\item[\texttt{GET /health}] Liveness check
\item[\texttt{GET /ui}] Frontend HTML
\end{description}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{NYC Model --- Dataset}
\label{ch:nyc_data}
\section{Primary Transaction Data}
\begin{description}
\item[Source] NYC Open Data --- Citywide Rolling Calendar Sales
\item[URL] \url{https://data.cityofnewyork.us/City-Government/NYC-Citywide-Rolling-Calendar-Sales/usep-8jbt}
\item[Coverage] 2022--2026 (deed-recorded by NYC Department of Finance)
\item[Rows loaded] $\approx 212,855$ (after cleaning)
\item[Rows training] 157,329 (oldest 85\% by \texttt{sale\_date})
\item[Rows holdout] 27,763 (newest 15\% by \texttt{sale\_date} --- time-based split)
\item[Target variable] \texttt{sale\_price} (USD total transaction price)
\item[Log-transform] \texttt{log1p(sale\_price)} during training; \texttt{expm1()} at inference
\end{description}
Rows dropped (cleaning):
\begin{itemize}
\item sale\_price $< \$10{,}000$ or $> \$100{,}000{,}000$
\item Missing latitude/longitude or sale\_date
\item Duplicate BBL + sale\_date (kept highest price)
\item Building class codes indicating non-arm's-length transfers
\item Rows where \texttt{gross\_square\_feet} $= 0$ or null
\end{itemize}
\section{Supplementary Data Sources}
\subsection*{PLUTO 25v4}
\begin{description}
\item[File] \texttt{nyc\_pluto\_25v4\_csv/pluto\_25v4.csv}
\item[Joined on] BBL (borough-block-lot identifier)
\item[Columns used] \texttt{assesstot}, \texttt{assessland}
\item[Coverage] $\approx 95\%$ join rate
\item[Purpose] Prior sale price imputation via borough-level assesstot ratios
\end{description}
\subsection*{MTA Subway Stations}
File: \texttt{MTA\_Subway\_Stations\_20260308.csv} (84\,KB, $\approx 496$ stations).
Columns used: GTFS Latitude/Longitude, CBD flag, Daytime Routes count, ADA
accessibility flag.
Generated features: \texttt{dist\_subway\_m}, \texttt{nearest\_station\_is\_cbd},
\texttt{nearest\_station\_route\_count}, \texttt{nearest\_station\_is\_ada}.
\subsection*{HPD Housing Maintenance Code Violations}
File: \texttt{hpd\_violations.csv} (137\,MB --- largest raw file).
Filtered: open violations, 2022+, severity classes A/B/C.
\begin{description}
\item[Class A] Non-hazardous (minor)
\item[Class B] Hazardous (water damage, structural)
\item[Class C] Immediately hazardous (mould, lead paint, heat loss)
\end{description}
Features: \texttt{hpd\_class\_b\_viol\_zip}, \texttt{hpd\_class\_c\_viol\_zip},
\texttt{hpd\_severity\_score\_zip} (= $C \times 3 + B \times 2 + A \times 1$,
log-scaled).
\subsection*{DOB Building Permits}
Filtered: 2022+, permit types A1/A2 (alteration), NB (new building).
Features: \texttt{dob\_reno\_permit\_count}, \texttt{dob\_newbld\_permit\_count}
(both log-scaled).
\subsection*{NYC 311 Service Requests}
Coverage: 2019--2025, by NTA.
Features: \texttt{noise\_density\_nta}, \texttt{livability\_complaint\_rate},
\texttt{rat\_density\_nta} (v11), \texttt{heat\_density\_nta} (v11).
Winsorisation: \texttt{noise\_density\_nta} capped at p99 = 176.67;
\texttt{livability\_complaint\_rate} capped at 60.99.
\subsection*{ACRIS Prior Sale Data}
Coverage: $\approx 70\%$ have a prior sale on record. Imputation:
\[
\hat{P}_\text{prior} = \texttt{assesstot} \times r_\text{borough},
\quad r_\text{borough} = \text{median}\!\left(\frac{P_\text{prior}}{\texttt{assesstot}}\right)
\]
Result: 99\% coverage after imputation.
\subsection*{Mortgage Rates}
Feature: \texttt{mortgage\_rate\_30yr} (US 30-year fixed rate for the sale month,
sourced from Freddie Mac PMMS weekly survey).
% ─────────────────────────────────────────────────────────────────────────────
\chapter{NYC Model --- Feature Engineering}
\label{ch:nyc_features}
All feature engineering runs in Polars (not pandas) for speed on 200K+ rows.
Final feature CSV: \texttt{data/processed/features\_v5.csv} (121\,MB).
\section{Distance Features (log-transformed)}
Every raw distance feature \texttt{dist\_X\_m} gets a \texttt{log1p} companion:
\[
\texttt{log\_dist\_X\_m} = \log(1 + \texttt{dist\_X\_m})
\]
Rationale: distance effects on price decay non-linearly. Log-transform
linearises this for gradient-boosted trees.
\section{Urban Gravity Centres}
Four major employment hubs, Euclidean distances (metres):
\begin{description}
\item[\texttt{dist\_midtown\_manhattan\_m}] $(40.7549,\,-73.9840)$
\item[\texttt{dist\_downtown\_manhattan\_m}] $(40.7074,\,-74.0113)$
\item[\texttt{dist\_downtown\_brooklyn\_m}] $(40.6928,\,-73.9903)$
\item[\texttt{dist\_long\_island\_city\_m}] $(40.7447,\,-73.9485)$
\end{description}
\section{Walk Score Proxy}
\begin{equation}
W = \text{clip}\!\left(
0.35\,\tilde{c}_\text{transit} +
0.15\,\tilde{c}_\text{bus} +
0.30\,\tilde{c}_\text{poi} +
0.10\,\tilde{c}_\text{bike} +
0.10\,\tilde{c}_\text{park},\;
0,\,100
\right)
\end{equation}
Each $\tilde{c}_i$ is MinMax-scaled on training data; scaler parameters saved in
\texttt{meta.json}.
\section{Interaction Features}
\begin{lstlisting}
sqft_per_floor = gross_square_feet / numfloors
income_over_crime = median_income_nta / (crime_rate_nta + 1)
density_index = residential_units / gross_square_feet * 1000
log_sqft_x_floors = log1p(gross_square_feet) * log1p(numfloors)
log_land_sqft = log1p(land_square_feet)
lot_coverage = gross_square_feet / land_square_feet # clipped [0,10]
bldg_vol_proxy = log1p(gross_square_feet * numfloors)
prior_price_psf = prior_sale_price / gross_square_feet
\end{lstlisting}
\section{Target Encoding}
Four encodings computed on training set (raw mean, $k=0$):
\begin{itemize}
\item \texttt{bldgclass\_encoded} --- $\approx 200+$ unique NYC building class codes
\item \texttt{borough\_bldg\_encoded} --- borough (1--5) $\times$ bldgclass first letter
\item \texttt{nta\_encoded} --- 212 NTA codes
\item \texttt{nta\_bldg\_encoded} --- NTA $\times$ bldgclass first letter (fine-grained)
\end{itemize}
Fallback: unknown codes at inference $\to$ global training-set mean log-price.
\section{QoL Winsorisation}
\begin{table}[H]
\centering
\caption{Winsorisation caps (stored in \texttt{meta.json})}
\begin{tabular}{lr}
\toprule
Feature & p99 cap \\
\midrule
\texttt{crime\_rate\_nta} & 267.56 \\
\texttt{noise\_density\_nta} & 176.67 \\
\texttt{livability\_complaint\_rate}& 60.99 \\
\bottomrule
\end{tabular}
\end{table}
\section{Complete Feature List (134 features)}
\begin{longtable}{lp{10cm}}
\caption{All 134 model features by category} \label{tab:all_features}\\
\toprule
Category & Features \\
\midrule
\endfirsthead
\midrule
Category & Features \\
\midrule
\endhead
\bottomrule
\endfoot
Location (2) & \texttt{latitude, longitude} \\
Structural (5) & \texttt{building\_age, numfloors, gross\_square\_feet, land\_square\_feet, residential\_units} \\
Zoning (5) & \texttt{builtfar, residfar, commfar, facilfar, far\_utilization} \\
Property type (5) & \texttt{has\_elevator, is\_condo, is\_multifamily, is\_single\_fam, is\_mixed\_use} \\
Macro (5) & \texttt{borough, sale\_year, sale\_month\_sin, sale\_month\_cos, mortgage\_rate\_30yr} \\
Transit (7) & \texttt{dist\_subway\_m, dist\_express\_subway\_m, dist\_bus\_m, nearest\_station\_is\_express, nearest\_station\_is\_cbd, nearest\_station\_route\_count, nearest\_station\_is\_ada} \\
Log distances (9) & \texttt{log\_dist\_subway\_m, log\_dist\_express\_subway\_m, log\_dist\_bus\_m, log\_dist\_school\_m, log\_dist\_elem\_school\_m, log\_dist\_park\_m, log\_dist\_hospital\_m, log\_dist\_waterfront\_m, log\_dist\_bike\_lane\_m} \\
Proximity (6) & \texttt{dist\_school\_m, dist\_park\_m, dist\_hospital\_m, dist\_waterfront\_m, dist\_bike\_lane\_m, dist\_elem\_school\_m} \\
Urban gravity (4) & \texttt{dist\_midtown\_manhattan\_m, dist\_downtown\_manhattan\_m, dist\_downtown\_brooklyn\_m, dist\_long\_island\_city\_m} \\
Borough flag (3) & \texttt{is\_manhattan, crime\_x\_manhattan, crime\_x\_non\_manhattan} \\
Walk score (1) & \texttt{walk\_score\_proxy} \\
Amenities (9) & \texttt{poi\_count\_500m, poi\_cafe\_500m, poi\_restaurant\_500m, poi\_gym\_500m, poi\_grocery\_500m, poi\_bar\_500m, poi\_pharmacy\_500m, airbnb\_count\_500m, tree\_count\_200m} \\
QoL signals (6) & \texttt{crime\_rate\_nta, noise\_density\_nta, livability\_complaint\_rate, rat\_density\_nta, heat\_density\_nta, population\_2020} \\
Income (2) & \texttt{median\_income\_nta, borough\_income\_deviation} \\
Air quality (2) & \texttt{pm25\_mean, no2\_mean} \\
Schools (3) & \texttt{school\_district, district\_avg\_score, district\_school\_count} \\
Prior sale (6) & \texttt{prior\_sale\_price, price\_appreciation, years\_since\_prior\_sale, is\_flip, has\_prior\_sale, prior\_price\_psf} \\
Assessment (2) & \texttt{assesstot, assessland} \\
Renovation (2) & \texttt{renovated\_since\_2018, years\_since\_renovation} \\
Interactions (8) & \texttt{sqft\_per\_floor, income\_over\_crime, density\_index, log\_sqft\_x\_floors, log\_land\_sqft, lot\_coverage, bldg\_vol\_proxy, prior\_price\_psf} \\
Target-encoded (7) & \texttt{bldgclass\_encoded, borough\_bldg\_encoded, nta\_encoded, nta\_bldg\_encoded, nta\_sale\_count, nta\_median\_psf, nta\_price\_trend\_slope} \\
HPD violations (3) & \texttt{hpd\_class\_b\_viol\_zip, hpd\_class\_c\_viol\_zip, hpd\_severity\_score\_zip} \\
DOB permits (2) & \texttt{dob\_reno\_permit\_count, dob\_newbld\_permit\_count} \\
MTA quality (3) & \texttt{nearest\_station\_is\_cbd, nearest\_station\_route\_count, nearest\_station\_is\_ada} \\
\end{longtable}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{NYC Model --- Training Methodology}
\label{ch:nyc_training}
\section{Train / Holdout Split}
Time-based (not random). Split: oldest 85\% $\to$ training (157,329 rows),
newest 15\% $\to$ holdout (27,763 rows). Sorted by \texttt{sale\_date}
(ascending). Prevents temporal leakage.
\section{Cross-Validation Strategy}
Type: Spatial GroupKFold. Folds: 10. Groups: \texttt{ntacode} (212 NTAs).
Standard K-Fold would put adjacent sales from the same neighbourhood in both
train and validation, inflating CV scores via spatial autocorrelation.
GroupKFold ensures all sales from a given NTA are either fully in train or
fully in validation for each fold.
\section{Base Learner Hyperparameters}
\begin{table}[H]
\centering
\caption{Detailed hyperparameters}
\begin{tabular}{lrrrr}
\toprule
Parameter & XGB-A & XGB-B & LGB & CatBoost \\
\midrule
n\_estimators & 5,000 & 5,000 & 5,000 & 3,000 \\
learning\_rate & 0.02 & 0.05 & 0.04 & 0.025 \\
max\_depth & 7 & 4 & --- & 8 \\
num\_leaves & --- & --- & 127 & --- \\
min\_child\_weight & 8 & 15 & 15 & --- \\
subsample & 0.80 & 0.65 & 0.80 & --- \\
colsample\_bytree & 0.60 & 0.75 & 0.70 & --- \\
gamma & 0.1 & 0.3 & --- & --- \\
reg\_alpha (L1) & 0.3 & 1.0 & 0.3 & --- \\
reg\_lambda (L2) & 1.5 & 2.5 & 1.5 & 1.5 \\
bagging\_temperature & --- & --- & --- & 0.8 \\
border\_count & --- & --- & --- & 64 \\
random\_seed & 42 & 7 & 42 & 42 \\
Best round & 3,843 & 5,000 & 952 & 3,000 \\
\bottomrule
\end{tabular}
\end{table}
\section{Meta-Learner}
Two candidates evaluated:
\begin{description}
\item[LightGBM meta] n\_estimators=500, lr=0.05, num\_leaves=15;
holdout $R^2 = 0.6349$
\item[Ridge meta] alpha=1.0, positive=True; holdout $R^2 = 0.6495$
\textbf{(selected)}
\end{description}
Ridge wins because by the meta-stage the four base learners have exhausted
non-linear relationships. Remaining variance is mostly noise; Ridge's L2
penalty prevents meta-level overfitting.
\section{Luxury Sub-Model}
Separate XGBoost trained on Manhattan sales $\geq \$2{,}500{,}000$ ($n
\approx 2{,}000+$). Soft blend at inference:
\begin{align}
\alpha &= \text{clip}\!\left(\frac{\hat{P} - 2.5\text{M}}{5.0\text{M} - 2.5\text{M}},\;0,\;1\right) \\
P_\text{final} &= (1-\alpha)\,\hat{P}_\text{stack} + \alpha\,\hat{P}_\text{luxury}
\end{align}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{NYC Model --- Results}
\label{ch:nyc_results}
\section{Overall Holdout Performance (Stack v22)}
\begin{table}[H]
\centering
\caption{NYC holdout results --- Stack v22 (27,763 sales)}
\begin{tabular}{lrrr}
\toprule
Model & $R^2$ & MAE (USD) & MedAPE \\
\midrule
XGB-A (deep) & 0.6445 & --- & 20.56\% \\
XGB-B (shallow) & 0.6387 & --- & 20.67\% \\
LightGBM (wide) & 0.6432 & --- & 20.84\% \\
CatBoost (high-cap) & 0.6482 & --- & 20.60\% \\
\textbf{Stack v22} & \textbf{0.6495} & \textbf{\$1,047,004} & \textbf{20.32\%} \\
\bottomrule
\end{tabular}
\end{table}
\section{Performance by Borough}
\begin{table}[H]
\centering
\caption{Borough-level holdout performance}
\begin{tabular}{lrrr}
\toprule
Borough & $n$ & MedAPE & MAE (USD) \\
\midrule
Manhattan & 5,411 & 35.16\% & \$2,714,327 \\
Bronx & 2,735 & 21.08\% & \$1,531,532 \\
Brooklyn & 7,032 & 20.64\% & \$857,322 \\
Queens & 9,533 & 17.43\% & \$366,226 \\
Staten Island & 3,052 & 14.41\% & \$220,216 \\
\bottomrule
\end{tabular}
\end{table}
\section{Performance by Price Tier}
\begin{table}[H]
\centering
\caption{Price tier holdout performance}
\begin{tabular}{lrrr}
\toprule
Tier & $n$ & MedAPE & MAE (USD) \\
\midrule
$<$ \$500K & 6,497 & 27.10\% & \$225,470 \\
\$500K--\$1M & 10,980 & 15.12\% & \$182,149 \\
\$1M--\$3M & 7,978 & 21.30\% & \$454,712 \\
\$3M--\$10M & 1,707 & 39.77\% & \$2,180,172 \\
\bottomrule
\end{tabular}
\end{table}
\section{SHAP Feature Importance (Top 10)}
\begin{table}[H]
\centering
\caption{Top 10 features by mean $|\text{SHAP}|$}
\begin{tabular}{rlrr}
\toprule
Rank & Feature & $|\text{SHAP}|$ & Category \\
\midrule
1 & \texttt{bldgclass\_encoded} & 0.2121 & Target encoding \\
2 & \texttt{borough\_bldg\_encoded} & 0.1188 & Target encoding \\
3 & \texttt{gross\_square\_feet} & 0.0980 & Structural \\
4 & \texttt{prior\_sale\_price} & 0.0750 & Prior sale \\
5 & \texttt{dist\_downtown\_manhattan\_m} & 0.0725 & Urban gravity \\
6 & \texttt{latitude} & 0.0585 & Location \\
7 & \texttt{land\_square\_feet} & 0.0538 & Structural \\
8 & \texttt{price\_appreciation} & 0.0367 & Prior sale \\
9 & \texttt{has\_prior\_sale} & 0.0335 & Prior sale \\
10 & \texttt{builtfar} & 0.0324 & Zoning \\
\bottomrule
\end{tabular}
\end{table}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Riyadh Model --- Dataset}
\label{ch:riyadh_data}
\section{Dataset Structure: Fundamentally Different from NYC}
NYC uses individual property transaction records ($\approx 200$K rows, one row
$=$ one sale). Riyadh uses district-level quarterly aggregates (6,910 rows, one
row $=$ one district $\times$ one quarter combination).
This fundamentally shapes the model:
\begin{itemize}
\item Target $=$ median(\texttt{sale\_price\_sar\_sqm}) for that district-quarter
\item Spatial features $=$ computed at the district centroid lat/lon
\item ``Structural'' features $=$ district-level medians from SA\_Aqar
\item No individual building characteristics available
\end{itemize}
\section{Transaction Data Sources}
\subsection*{Source 1: Ministry of Justice XLSX Report (2018--2023)}
File: \texttt{data/raw/quarter\_report SI.xlsx}.
Key column: \texttt{Meter\_Price\_W\_Avg\_IQR} --- IQR-winsorised average
price per m$^2$. Minimum deed count filter: \texttt{deed\_counts} $\geq 3$.
\begin{tcolorbox}[colback=red!5,colframe=red!60]
\textbf{Warning:} The column \texttt{RealEstatePrice\_SUM / deed\_counts}
gives average deal value per transaction, NOT price per m$^2$. Using it
produces 4M+ SAR/m$^2$ garbage values. Only
\texttt{Meter\_Price\_W\_Avg\_IQR} is used.
\end{tcolorbox}
\subsection*{Source 2: Saudi Open Data API --- 2024 Quarterly CSVs}
Files: \texttt{sales\_riyadh\_2024\_Q1.csv}, \texttt{Q3.csv}, \texttt{Q4.csv}
(Q2 not available). Engineering challenge: two columns both named
\texttt{\makebox{Ψ§Ω„Ψ±Ψ¨ΨΉ}} after stripping trailing spaces. Fixed by
pattern-matching on \texttt{Ψ±Ω‚Ω…} keyword.
\subsection*{Source 3: Saudi Open Data API --- 2025 Quarterly CSVs}
Files: \texttt{sales\_riyadh\_2025\_Q1.csv}, \texttt{Q2.csv}, \texttt{Q3.csv}.
These become the holdout set (all rows with \texttt{quarter\_id} $\geq$ 20251).
\section{Critical Bug: 2024 Quarter ID Encoding}
\begin{tcolorbox}[colback=red!5,colframe=red!60]
\textbf{Bug found and fixed.} Quarter ID formula: \texttt{quarter\_id = year
$\times$ 10 + sale\_quarter}. In 2024 API CSVs, the raw
\texttt{sale\_quarter} already contained the full code (e.g., 20241 for 2024
Q1). When multiplied by 10: $2024 \times 10 + 20241 = 40481$ instead of the
correct 20241. So 2024 quarter IDs were 40481, 40483, 40484 --- sorting after
all 2025 IDs, pushing all 867 2024 rows into holdout.
\medskip
\textbf{Fix:} \texttt{scripts/fix\_riyadh\_2024\_quarters.py} remaps
$40481 \to 20241$, $40483 \to 20243$, $40484 \to 20244$ and corrects REI
values. Training cutoff changed to hardcoded \texttt{cutoff\_qid = 20251}.
\medskip
\textbf{Impact:} Holdout MedAPE improved from 23.43\% $\to$ 15.59\% (via v12).
Holdout $R^2$ improved from 0.675 $\to$ 0.800.
\end{tcolorbox}
\section{Dataset Statistics}
\begin{table}[H]
\centering
\caption{Riyadh dataset summary}
\begin{tabular}{ll}
\toprule
Attribute & Value \\
\midrule
Total rows & 6,910 \\
Training rows & 5,531 (quarter\_id $<$ 20251) \\
Holdout rows & 1,730 (quarter\_id $\geq$ 20251) \\
Districts & 163 unique \\
Features used & 149 \\
Target (median) & 3,303 SAR/m$^2$ \\
Target (range) & 221.82 -- 12,565.21 SAR/m$^2$ \\
\midrule
\multicolumn{2}{l}{\textit{Rows by property type}} \\
Residential Plot (Ω‚Ψ·ΨΉΨ© Ψ£Ψ±ΨΆ-Ψ³ΩƒΩ†Ω‰) & 3,062 (44.3\%) \\
Villa (ΩΩŠΩ„Ψ§) & 2,174 (31.5\%) \\
Apartment (Ψ΄Ω‚Ψ©) & 1,661 (24.0\%) \\
Building (ΨΉΩ…Ψ§Ψ±Ψ©) & 13 (0.2\%) \\
\bottomrule
\end{tabular}
\end{table}
\section{Spatial Data Sources}
\begin{description}
\item[Metro Transit] \texttt{metro-stations-in-riyadh-by-metro-line-and-station-type-2024.geojson}
(72\,KB). Features: \texttt{dist\_metro\_m}, \texttt{log\_dist\_metro\_m},
\texttt{metro\_stations\_1km}, \texttt{nearest\_metro\_line\_num},
\texttt{nearest\_metro\_type\_cd}, \texttt{dist\_metro\_line1\_m}.
\item[Bus Stops] \texttt{bus-stops-in-riyadh-by-bus-route-direction-and-shelter-type-2024.geojson}
(1.6\,MB). Features: \texttt{dist\_bus\_m}, \texttt{log\_dist\_bus\_m},
\texttt{bus\_stops\_500m}, \texttt{brt\_stops\_500m}.
\item[Commercial Services] \texttt{commercial-services-by-category-sub-municipality-and-district-2024.geojson}
(2.1\,MB). 10 commercial features including hypermarket/bank/restaurant counts.
\item[Air Quality] 12 RCRC monitoring stations; pollutants: NO$_2$, SO$_2$,
PM$_{10}$, O$_3$. Composite \texttt{air\_quality\_score} (0--100, higher=cleaner).
\item[QoL POIs] 6 types (mosques, malls, schools, hospitals, parks, entertainment):
distance, log-distance, count within 500\,m each = 18 QoL features.
\end{description}
\section{Macroeconomic Data}
\begin{description}
\item[REI] Real Estate Price Index from Saudi General Authority for Statistics.
Base: 2023 Q4 $= 100$. Features: \texttt{rei\_residential\_qtr\_idx},
\texttt{rei\_apt\_idx}, \texttt{rei\_yoy\_change}, \texttt{rei\_qoq\_change}.
\item[Salary] SAMA (Saudi Arabian Monetary Authority) annual statistical report.
Features: \texttt{avg\_saudi\_salary\_yr}, \texttt{salary\_yoy\_change}.
\end{description}
\section{SA\_Aqar Rental Listings}
File: \texttt{data/raw/SA\_Aqar.csv} ($\approx 50$K listings). Aggregated to
district-level medians:
\begin{itemize}
\item \texttt{aqar\_median\_size\_sqm} --- median listing size
\item \texttt{aqar\_median\_bedrooms} --- median bedroom count
\item \texttt{aqar\_median\_property\_age} --- median age (years)
\item \texttt{aqar\_rent\_per\_sqm} --- median asking rent per m$^2$
\end{itemize}
Note: these are \emph{rental} listing attributes, not transaction attributes---
they serve as proxies for typical property characteristics in each district.
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Riyadh Model --- Feature Engineering}
\label{ch:riyadh_features}
\section{Connectivity Score}
\begin{equation}
S_\text{conn} = \text{clip}\!\left(
0.30\,\tilde{c}_\text{metro} +
0.25\,\tilde{c}_\text{commercial} +
0.20\,\tilde{c}_\text{bus} +
0.15\,(1/\tilde{d}_\text{metro}) +
0.10\,\tilde{c}_\text{intersect},\;
0,\,100
\right)
\end{equation}
Scaler parameters stored in \texttt{riyadh\_meta.json}.
\section{Target Encoding (District Level)}
\begin{description}
\item[\texttt{district\_encoded}] Arabic district name $\to$ mean
$\log(P_{\text{sqm}}+1)$ on training rows
\item[\texttt{district\_type\_encoded}] district $\times$ property type
$\to$ mean $\log(P_{\text{sqm}}+1)$
\end{description}
Fallback: city median log-price (\texttt{global\_mean\_log} = 7.8645).
\section{Complete Feature List (149 features)}
\begin{longtable}{lp{10cm}}
\caption{Riyadh model: all 149 features by category} \\
\toprule Category & Features \\ \midrule \endfirsthead
\midrule Category & Features \\ \midrule \endhead \bottomrule \endfoot
Location (2) & \texttt{district\_lat, district\_lon} \\
Property type (4) & \texttt{is\_apartment, is\_villa, is\_residential\_plot, is\_building} \\
Metro transit (6) & \texttt{dist\_metro\_m, log\_dist\_metro\_m, metro\_stations\_1km, nearest\_metro\_line\_num, nearest\_metro\_type\_cd, dist\_metro\_line1\_m} \\
Bus (4) & \texttt{dist\_bus\_m, log\_dist\_bus\_m, bus\_stops\_500m, brt\_stops\_500m} \\
Traffic (4) & \texttt{dist\_major\_intersection\_m, log\_dist\_intersection\_m, intersections\_1km, intersections\_500m} \\
Commercial (10) & \texttt{commercial\_count\_1km, commercial\_density\_score, district\_commercial\_count, district\_commercial\_mix, hypermarket\_count\_1km, supermarket\_count\_1km, bank\_count\_1km, restaurant\_count\_1km, hotel\_count\_1km, gas\_station\_count\_1km} \\
Air quality (6) & \texttt{no2\_nearest\_mean, so2\_nearest\_mean, pm10\_nearest\_mean, o3\_nearest\_mean, dist\_air\_station\_m, air\_quality\_score} \\
Macro/REI (6) & \texttt{rei\_residential\_qtr\_idx, rei\_apt\_idx, rei\_yoy\_change, rei\_qoq\_change, avg\_saudi\_salary\_yr, salary\_yoy\_change} \\
District aggregates (5) & \texttt{district\_median\_price\_sqm, district\_transaction\_volume, district\_price\_vs\_city\_avg, district\_price\_trend\_slope, district\_median\_price\_apt\_sqm} \\
Target-encoded (2) & \texttt{district\_encoded, district\_type\_encoded} \\
Time (4) & \texttt{sale\_year, sale\_quarter\_sin, sale\_quarter\_cos, log\_deed\_count} \\
Mosque (3) & \texttt{dist\_mosque\_m, log\_dist\_mosque\_m, mosque\_count\_500m} \\
Mall (3) & \texttt{dist\_mall\_m, log\_dist\_mall\_m, mall\_count\_500m} \\
School (3) & \texttt{dist\_school\_m, log\_dist\_school\_m, school\_count\_500m} \\
Hospital (3) & \texttt{dist\_hospital\_m, log\_dist\_hospital\_m, hospital\_count\_500m} \\
Park (3) & \texttt{dist\_park\_m, log\_dist\_park\_m, park\_count\_500m} \\
Entertainment (3) & \texttt{dist\_entertain\_m, log\_dist\_entertain\_m, entertain\_count\_500m} \\
Connectivity (1) & \texttt{riyadh\_connectivity\_score} \\
Structural/rental (4) & \texttt{aqar\_median\_size\_sqm, aqar\_median\_bedrooms, aqar\_median\_property\_age, aqar\_rent\_per\_sqm} \\
\end{longtable}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Riyadh Model --- Training Methodology}
\label{ch:riyadh_training}
\section{Train / Holdout Split}
Time-based on \texttt{quarter\_id}:
\begin{description}
\item[Training] \texttt{quarter\_id} $< 20251$ $\to$ 2018 Q1--2024 Q4 (5,531 rows)
\item[Holdout] \texttt{quarter\_id} $\geq 20251$ $\to$ 2025 Q1--Q3 (1,730 rows)
\end{description}
\section{Cross-Validation}
Spatial GroupKFold, 5 folds, groups = \texttt{district\_ar} (163 unique). Fewer
folds than NYC because dataset is smaller.
\section{Hyperparameters}
\begin{table}[H]
\centering
\caption{Riyadh base learner hyperparameters}
\begin{tabular}{lrrr}
\toprule
Parameter & XGBoost & LightGBM & CatBoost \\
\midrule
n\_estimators & 1,500 & 1,500 & 1,500 \\
learning\_rate & 0.03 & 0.03 & 0.03 \\
max\_depth & 5 & 5 & 5 \\
subsample & 0.70 & 0.70 & --- \\
colsample\_bytree& 0.70 & 0.70 & --- \\
reg\_alpha & 0.5 & 0.5 & --- \\
reg\_lambda & 2.0 & 2.0 & 2.0 \\
early\_stopping & 50 & 50 & 50 \\
random\_seed & 42 & 42 & 42 \\
\bottomrule
\end{tabular}
\end{table}
\section{Meta-Learner}
Ridge (alpha=1.0, positive=\textbf{False} --- no constraint on Riyadh due to
small dataset).
Ridge meta coefficients:
\begin{itemize}
\item XGBoost: 0.278
\item LightGBM: 0.002
\item CatBoost: 0.721
\end{itemize}
CatBoost dominates (72\%) due to ordered boosting and symmetric tree structure
benefiting small datasets.
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Riyadh Model --- Results}
\label{ch:riyadh_results}
\begin{table}[H]
\centering
\caption{Riyadh model performance}
\begin{tabular}{lrr}
\toprule
Evaluation & $R^2$ & MedAPE \\
\midrule
OOF (5-fold spatial GroupKFold) & 0.9348 & 8.25\% \\
Holdout (2025 Q1--Q3, $n=1{,}730$) & 0.8014 & 15.59\% \\
\bottomrule
\end{tabular}
\par\small Holdout MAE: 986 SAR/m$^2$.
\end{table}
\section{Performance by Property Type}
\begin{table}[H]
\centering
\begin{tabular}{lrl}
\toprule
Type & MedAPE & Notes \\
\midrule
Apartment (Ψ΄Ω‚Ψ©) & 12.83\% & Best-performing \\
Villa (ΩΩŠΩ„Ψ§) & 12.39\% & Best absolute \\
Plot (Ω‚Ψ·ΨΉΨ© Ψ£Ψ±ΨΆ) & 20.82\% & Hardest; land speculation \\
Building (ΨΉΩ…Ψ§Ψ±Ψ©) & 18.61\% & \\
\bottomrule
\end{tabular}
\end{table}
\section{OOF-to-Holdout Gap}
OOF MedAPE: 8.25\% vs. Holdout MedAPE: 15.59\% (gap = 7.3\,pp). Three factors:
\begin{enumerate}
\item \textbf{Temporal shift}: holdout is Q1--Q3 2025 (post-Metro, new market
conditions).
\item \textbf{OOF temporal overlap}: GroupKFold by district $\Rightarrow$ same
quarter appears across folds, inflating OOF.
\item \textbf{Small dataset}: 5,531 rows across 163 districts $\Rightarrow$
sparse coverage in some districts.
\end{enumerate}
\section{Comparison to v1}
\begin{table}[H]
\centering
\begin{tabular}{lrr}
\toprule
Version & Holdout MedAPE & Holdout $R^2$ \\
\midrule
v1 (buggy 2024 encoding) & 23.43\% & 0.675 \\
v11 (current) & 15.59\% & 0.800 \\
\bottomrule
\end{tabular}
\end{table}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Algorithm Details and Library Versions}
\label{ch:algorithms}
\section{Libraries}
\begin{table}[H]
\centering
\caption{Library versions}
\begin{tabular}{lll}
\toprule
Library & Version & Purpose \\
\midrule
Python & 3.11 & Runtime \\
Polars & 0.20+ & Data loading, feature engineering \\
NumPy & 1.26+ & Numerical arrays \\
SciPy & 1.12+ & cKDTree, linregress \\
scikit-learn & 1.4+ & GroupKFold, Ridge, MinMaxScaler, BallTree \\
XGBoost & 2.0+ & Gradient boosted trees \\
LightGBM & 4.3+ & Leaf-wise gradient boosting \\
CatBoost & 1.2+ & Symmetric tree boosting \\
joblib & 1.3+ & Model serialisation \\
FastAPI & 0.111+ & REST API framework \\
Uvicorn & 0.29+ & ASGI server \\
Pydantic & 2.x & Schema validation \\
\bottomrule
\end{tabular}
\end{table}
\section{XGBoost}
Uses second-order Taylor expansion of the loss function to fit decision trees
sequentially. Key settings: \texttt{objective=reg:squarederror},
\texttt{tree\_method=hist} (faster on large datasets), early stopping.
Two NYC variants: XGB-A (deep, lr=0.02) and XGB-B (shallow, lr=0.05) for
ensemble diversity.
\section{LightGBM}
Leaf-wise tree growth (grows the leaf with the highest gain next) rather than
level-wise. \texttt{num\_leaves=127} in NYC. In Riyadh, LightGBM earns only
$\approx 0.2\%$ of the Ridge meta weight.
\section{CatBoost}
Symmetric (oblivious) trees: every split in a given level uses the same feature
and threshold. Ordered boosting prevents target leakage during training. CatBoost
dominates the Riyadh meta blend (72\%) due to ordered boosting and symmetric tree
structure benefiting small datasets.
\section{Ridge Meta-Learner}
Input matrix $S \in \mathbb{R}^{n \times k}$ (4 columns for NYC, 3 for Riyadh).
Loss:
\[
\min_w \|Sw - y\|^2 + \alpha\|w\|^2
\]
with $\alpha = 1.0$. For NYC: \texttt{positive=True} (blending, not arbitrage).
\section{GroupKFold}
\begin{table}[H]
\centering
\begin{tabular}{lll}
\toprule
City & Folds & Groups \\
\midrule
NYC & 10 & NTA code (212 groups, $\approx 31$ per fold) \\
Riyadh & 5 & district\_ar (163 groups, $\approx 29$ per fold) \\
\bottomrule
\end{tabular}
\end{table}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Key Differences: NYC vs.\ Riyadh}
\label{ch:differences}
\begin{longtable}{lll}
\caption{NYC vs.\ Riyadh model comparison} \\
\toprule Dimension & NYC & Riyadh \\ \midrule \endfirsthead
\midrule Dimension & NYC & Riyadh \\ \midrule \endhead \bottomrule \endfoot
Data granularity & Individual transactions & District-quarter aggregates \\
Dataset size & 185,092 train rows & 5,531 train rows \\
Holdout size & 27,763 rows & 1,730 rows \\
Target unit & USD total price & SAR per m$^2$ \\
Target range & \$50K -- \$100M & 222 -- 12,565 SAR/m$^2$ \\
Years covered & 2022--2026 & 2018--2025 \\
Base learners & 4 (XGB-A, XGB-B, LGB, CAT) & 3 (XGB, LGB, CAT) \\
CV folds & 10-fold GroupKFold & 5-fold GroupKFold \\
CV grouping & NTA code & district\_ar \\
Meta-learner & Ridge (positive=True) & Ridge (positive=False) \\
Features & 134 & 149 \\
Target encoding & 4 types & 2 types \\
Prior sale signal & Yes (99\% via assesstot)& No \\
Structural feats & sqft, floors, age, etc. & SA\_Aqar medians only \\
Infrastructure & MTA subway, bus, bike & Metro (6 lines), bus, traffic \\
QoL signals & Crime, noise, HPD, 311 & Air quality, mosques, malls \\
Macro signals & Mortgage rates & REI index, salary data \\
OOF MedAPE & --- & 8.25\% \\
Holdout MedAPE & 20.32\% & 15.59\% \\
Holdout $R^2$ & 0.6495 & 0.8014 \\
Holdout MAE & \$1,047,004 USD & 986 SAR/m$^2$ \\
\end{longtable}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Haraj Listings Layer --- External Validation}
\label{ch:haraj}
\begin{description}
\item[Source] Haraj.com.sa (Saudi Arabia's largest classifieds marketplace)
\item[Script] \texttt{scripts/scrape\_saudi\_listings.py}
\item[CSV] \texttt{data/raw/saudi\_listings\_haraj\_20260518.csv}
\item[Listings] 1,615 active listings: 444 apartments, 630 villas, 526 plots, 15 buildings
\end{description}
\begin{table}[H]
\centering
\caption{Haraj validation results}
\begin{tabular}{lr}
\toprule
Metric & Value \\
\midrule
Overall MedAPE & 54.33\% \\
Mean APE & 71.08\% \\
Within 20\% of asking& 186 / 1,615 (11.5\%) \\
Median asking price & 5,232 SAR/m$^2$ \\
Training median & 2,902 SAR/m$^2$ \\
Implied asking markup& $\approx 80\%$ \\
\bottomrule
\end{tabular}
\end{table}
THAMAN was trained on deed-recorded transaction prices. Haraj shows
pre-negotiation asking prices. The 80\% overall asking premium is consistent
with documented Saudi residential market negotiation margins of 20--50\%
(Al-Otaibi \& Al-Subaihi, 2021).
% ─────────────────────────────────────────────────────────────────────────────
\chapter{API Implementation Details}
\label{ch:api}
\section{NYC Prediction Endpoint (\texttt{POST /predict})}
Processing steps:
\begin{enumerate}
\item \texttt{\_build\_feature\_row()}: compute all 134 features, apply
winsorisation caps, walk\_score\_proxy, v11 ZIP/NTA features
\item \texttt{ThamanScorer.predict\_single()}: predict log-price from 4 base
learners $\to$ Ridge meta-learner $\to$ luxury sub-model (if applicable)
\item SHAP explanation via CatBoost \texttt{TreeExplainer} (top 10 drivers)
\item Comparable nearby sales lookup (BallTree on 185K-sale index)
\item Return price, confidence interval, grade tier, top\_drivers
\end{enumerate}
\section{Riyadh Prediction Endpoint (\texttt{POST /predict/riyadh})}
Request fields: \texttt{latitude}, \texttt{longitude}, \texttt{property\_type}
(Arabic), \texttt{area\_sqm}.
Processing steps:
\begin{enumerate}
\item \texttt{RiyadhSpatialLookup.predict\_features(lat, lon, type, year, quarter)}:
find nearest centroid via cKDTree, look up 149 features
\item \texttt{scorer.predict\_riyadh()}: 3 base models $\to$ Ridge meta $\to$
expm1() $\to$ SAR/m$^2$
\item Multiply by \texttt{area\_sqm} for \texttt{predicted\_total\_sar}
\item Confidence: $\pm$ 15.59\% (global holdout MedAPE)
\item SHAP via CatBoost TreeExplainer
\end{enumerate}
Response: \texttt{predicted\_price\_sqm}, \texttt{predicted\_total\_sar},
\texttt{confidence\_low/high\_sqm}, \texttt{district\_ar}, \texttt{r2\_test},
\texttt{medape\_pct}, \texttt{spatial\_features} dict, \texttt{top\_drivers} list.
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Known Limitations and Bugs Fixed}
\label{ch:bugs}
\begin{description}
\item[Bug 1 --- Riyadh 2024 Quarter ID Encoding (FIXED)] Root cause:
\texttt{year $\times$ 10 + sale\_quarter} where \texttt{sale\_quarter}
in 2024 CSV was already a full quarter code (20241). Produced 40481
instead of 20241. Effect: 867 rows (all of 2024) classified as holdout.
Fix: \texttt{scripts/fix\_riyadh\_2024\_quarters.py}.
Impact: MedAPE $23.43\% \to 15.59\%$ (via v12).
\item[Bug 2 --- Stale district\_medape in riyadh\_meta.json (FIXED)]
\texttt{meta\_dict.update(\{\ldots\})} preserved the old district\_medape
table from buggy v1 training. 12/146 districts were capped at 50\%
confidence. Fix: removed \texttt{district\_medape} key; all predictions
use global 15.59\% MedAPE.
\item[Bug 3 --- REI Values Corrupted for 2024 Rows (FIXED)] Wrong quarter\_ids
caused \_closest\_qid lookup to assign 2025 Q3 REI (100.44) to all 2024
rows. Fix: hardcoded correct quarterly REI values for Q1/Q3/Q4 2024.
\item[Limitation 1 --- No Individual Riyadh Transactions] Ministry of Justice
only publishes district-level aggregates. Until parcel-level data is
released, the Riyadh model cannot capture within-district variance.
\item[Limitation 2 --- OOF Temporal Overlap (Riyadh)] The OOF MedAPE of 8.25\%
is optimistic relative to the true out-of-time holdout (15.59\%) because
the same calendar quarter appears across all GroupKFold folds.
\item[Limitation 3 --- NYC Luxury Cap Sensitivity] Properties above \$10M
are handled by the luxury sub-model. Truly exceptional sales
($> \$50$M) are not well-covered by the training distribution.
\end{description}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Model Version History}
\label{ch:versions}
\section{NYC Model}
\begin{table}[H]
\centering
\caption{NYC model version history}
\begin{tabular}{llll}
\toprule
Version & Year & $R^2$ / MedAPE & Key change \\
\midrule
v1 & 2022 & 0.40 / 32\% & Single XGBoost, 3 features \\
v2 & 2023 & 0.48 / --- & XGBoost + feature engineering \\
v6 & 2024 & 0.645 / --- & NTA-level target encoding \\
v10 & 2025 & 0.646 / 20.30\% & NTA price trend slope; 94 features \\
v21 & 2026 & 0.6516 / 20.10\% & BBL building history LoO \$/sqft; 131 features \\
v22 & 2026 & \textbf{0.6495 / 20.32\%} & NTA$\times$bldgtype temporal lag; 134 features \\
\bottomrule
\end{tabular}
\end{table}
\section{Riyadh Model}
\begin{table}[H]
\centering
\caption{Riyadh model version history}
\begin{tabular}{llll}
\toprule
Version & Date & Holdout MedAPE / $R^2$ & Key change \\
\midrule
v1 & May 2026 & 23.43\% / 0.675 & First Riyadh model; 2024 quarter\_id bug \\
v11 & May 2026 & \textbf{15.59\% / 0.800} & Type-stratified lags + metro + hub distances; 149 features \\
\bottomrule
\end{tabular}
\end{table}
% ─────────────────────────────────────────────────────────────────────────────
\chapter{Deployment and Reproducibility}
\label{ch:deployment}
\begin{description}
\item[Hugging Face Space] \url{https://huggingface.co/spaces/Turki-Almurahhem/thaman}
\item[GitHub] \url{https://github.com/turkialm/thaman-v2}
\end{description}
\section{Reproduce NYC Model}
\begin{lstlisting}[language=bash]
python scripts/prepare_v12_features.py
python training/train_stack_v12.py
# Output: models/thaman_stack.pkl, models/xgboost_model.json, models/meta.json
\end{lstlisting}
\section{Reproduce Riyadh Model}
\begin{lstlisting}[language=bash]
python scripts/riyadh_feature_engineering.py
python scripts/fix_riyadh_2024_quarters.py
python training/train_stack_riyadh_v2.py
# Output: models/riyadh_stack.pkl, models/riyadh_meta.json
\end{lstlisting}
\section{Run Locally}
\begin{lstlisting}[language=bash]
cd /path/to/new_try
uvicorn api.main:app --port 8000
# Open: http://localhost:8000/ui
\end{lstlisting}
Cold-start time: $\approx 30\,\text{s}$ (spatial KD-tree indexes loading).
API latency (warm): 200--400\,ms per prediction locally (including SHAP computation); $\sim$0.7--1.2\,s on the live Space (free-tier CPU + network overhead).
\vfill
\begin{center}
{\small Generated: 2026-05-22 --- THAMAN Technical Reference}
\end{center}
\end{document}