SPB Git

spb/wp3_uqo

Public

UQO Working Paper No. 3 — Hedonic housing price models for the US: parametric, quantile, and machine-learning approaches.

R03 Stage E (exhibits): every table as a self-contained siunitx/threeparttable float (43 fragments, 36 inserted), 13 vector figures in one Okabe-Ito design system with float wrappers, generator rewritten (08 + 08b + src/wp3/exhibits.py), README tables generated from inline numbers, wrapper blocks replaced by single \input lines (EDIT-13, DATA-04, ADVE-09/11/13, ECON-11/12, THEO-07, EDIT-10 corrigenda table)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 4, 2026) parent 770ace6 Browse files

95 changed files +5,692 −2,469

95 changed files
modified README.md +22 −24
@@ -38,13 +38,12 @@
38 38
39 39 The paper compares three frameworks for hedonic housing valuation on a single large dataset:
40 40
41 −| Framework | Question it answers | Headline result |
41 +<!-- generated by scripts/08_tables_figures_v2.py::t_readme from results/inline_numbers.json; do not edit by hand -->
42 +| Framework | Question it answers | Headline result (v2 design, trimmed sample) |
42 43 |---|---|---|
43 −| **Semi-log OLS** (62 regressors; HC3, ZIP3- and state-clustered SE; state-by-state) | Average conditional association between an attribute and log listing price, and how much it hides | R² = 0.634; area elasticity 0.63 (0.06–1.15 across states); lot elasticity 0.095 (recoded) |
44 −| **Quantile regression** (τ ∈ {0.10, 0.25, 0.50, 0.75, 0.90}; paired cluster bootstrap) | How attribute gradients vary across the price distribution | Equality rejected for 10 of 13 variables (11 under the independence test) |
45 −| **Gradient boosting + SHAP** (XGBoost, LightGBM; random / 10-state / GroupKFold / LOSO) | How well prices can be predicted, in seen and unseen markets, and which features drive predictions | R² = 0.833 (random) vs. 0.425 (10 states) vs. 0.653 (LOSO median); within-state 0.62 |
46 −
47 −```
44 +| **Semi-log OLS** (62 regressors + lot-missing indicator; HC3, ZIP3- and state-clustered SE, wild cluster bootstrap; state-by-state) | Average conditional association between an attribute and log asking price, and how much it hides | R² = 0.649; living-area elasticity 0.587 (p10–p90 across states 0.32–0.72); lot elasticity 0.097; 23 of 63 slopes significant at 5 % by wild cluster bootstrap |
45 +| **Quantile regression** (τ ∈ {0.10, 0.25, 0.50, 0.75, 0.90}; paired ZIP3-cluster bootstrap, Holm / Romano–Wolf) | How attribute gradients vary across the price distribution | β(0.10) ≠ β(0.90) for 10 of 14 variables by percentile CI, 8 after Holm and Romano–Wolf |
46 +| **Gradient boosting + SHAP** (XGBoost, LightGBM, RF; random / ZIP5 / ZIP3 / state blocks / LOSO / 10-state; nested round selection, 5 seeds) | How well prices can be predicted in seen and unseen markets, and which features drive predictions | OLS − XGBoost gap: -18.7 pp [-19.9, -17.5] (random), -6.3 pp [-7.6, -4.7] (LOSO), +2.6 pp [-0.1, +7.4] (10 states) |
48 47 ln P_i = α + x_i'β + μ_g(i) + ε_i g = Census region → state → ZIP3
49 48 ```
50 49
@@ -57,24 +56,23 @@ at 1,000). All estimates are *listing-price* gradients (asking prices), not caus
57 56
58 57 ## 🏆 Headline results
59 58
60 −| Model | Random R² | 10-state holdout R² | Within-state R² (10 states) | RMSE (random) |
61 −|---|---:|---:|---:|---:|
62 −| OLS (62 regressors) | 0.630 | **0.539** (implausible records removed; −2×10⁶ with them) | 0.560 | 0.491 |
63 −| OLS + State FE | 0.678 | — | — | 0.459 |
64 −| OLS + ZIP3 FE | 0.725 | — | — | 0.423 |
65 −| OLS, state-by-state (fully interacted) | 0.751 | — | — | — |
66 −| Random Forest | 0.784 | 0.542 | — | 0.375 |
67 −| LightGBM | 0.809 | — | — | 0.353 |
68 −| **XGBoost, 1,000 rounds** | **0.833** | **0.425** | **0.618** | **0.330** |
69 −| XGBoost, 75–100 rounds | 0.75–0.77 | 0.544–0.550 | — | — |
70 −| XGBoost, no region dummies | 0.830 | 0.519 | 0.621 | — |
71 −| XGBoost, no school variables | 0.817 | 0.377 | 0.596 | — |
72 −| XGBoost + 6 missingness indicators | 0.835 | 0.446 | 0.623 | — |
73 −| XGBoost + lat/lon | 0.870 | 0.464 | — | — |
74 −| XGBoost, grouped 5-fold by state (pooled) | — | 0.620 | 0.657 | — |
75 −| XGBoost, leave-one-state-out (51 models) | — | median 0.653 | — | — |
76 −
77 −- **Geography does the heavy lifting in OLS**: region dummies → state FE → 885 ZIP3 FE raises out-of-sample R² from
59 +<!-- generated by scripts/08_tables_figures_v2.py::t_readme from results/inline_numbers.json; do not edit by hand -->
60 +| Model | Random R² | ZIP3 blocks R² | State blocks R² | LOSO pooled R² | 10-state holdout R² | Within-state R² (10 states, oracle) |
61 +|---|---:|---:|---:|---:|---:|---:|
62 +| OLS (62 regressors + lot indicator) | 0.646 | 0.635 | 0.580 | 0.573 | 0.541 | 0.573 |
63 +| OLS + state FE | 0.690 | 0.673 | — | — | — | — |
64 +| OLS + ZIP3 FE | 0.762 | — | — | — | — | — |
65 +| OLS, state-by-state (fully interacted) | 0.752 | — | — | — | — | — |
66 +| Random forest (re-specified in v2) | 0.800 | — | — | — | 0.583 | 0.620 |
67 +| LightGBM, nested rounds | 0.809 | 0.759 | 0.631 | 0.643 | 0.565 | 0.643 |
68 +| **XGBoost, nested rounds (5 seeds)** | **0.833** | **0.766** | **0.628** | **0.636** | **0.515** | **0.637** |
69 +| XGBoost, 1,000 rounds (v1.1 comparator) | 0.833 | 0.768 | 0.615 | 0.623 | 0.425 | 0.628 |
70 +| XGBoost, 100 rounds | 0.770 | 0.736 | 0.634 | 0.639 | 0.538 | 0.631 |
71 +| XGBoost, no region dummies (nested) | — | — | 0.623 | 0.625 | 0.527 | — |
72 +| XGBoost, no school variables (nested) | — | — | 0.622 | 0.623 | 0.490 | — |
73 +| XGBoost + 6 missingness indicators (nested) | 0.835 | 0.768 | 0.634 | 0.631 | 0.522 | — |
74 +
75 +OLS − XGBoost (nested) gap with 95 % ZIP3-cluster bootstrap CI: random -18.7 pp [-19.9, -17.5]; ZIP5 blocks -16.8 pp [-17.8, -15.8]; ZIP3 blocks -13.2 pp [-14.5, -11.9]; state blocks -4.8 pp [-6.2, -3.5]; LOSO -6.3 pp [-7.6, -4.7] (XGBoost better in 44 of 51 states); ten-state +2.6 pp [-0.1, +7.4]. LOSO median R²: OLS 0.551, XGBoost nested 0.634; feasible level correction with k = 20 listings raises LOSO XGBoost from 0.635 to 0.699 (oracle 0.712).
78 76 0.630 → 0.678 → 0.725; residual Moran's *I* = 0.2745 (SD 0.0076); tree-ensemble test residuals: 0.079 (XGBoost).
79 77 - **Inference**: ZIP3- and state-clustered SEs are 7.6× and 11.6× HC3 (medians); significant coefficients 57 → 35 → 33 of 63.
80 78 - **One market or 51?** State-by-state living-area elasticity 0.06–1.15; bedroom, pool, luxury and HOA coefficients change
added figures/v2_fig_ablation.pdf +0 −0

Binary file not shown.

added figures/v2_fig_ablation.png +0 −0

Binary file not shown.

added figures/v2_fig_feasible_level.pdf +0 −0

Binary file not shown.

added figures/v2_fig_feasible_level.png +0 −0

Binary file not shown.

added figures/v2_fig_gap_by_design.pdf +0 −0

Binary file not shown.

modified figures/v2_fig_gap_by_design.png +0 −0

Binary file not shown.

added figures/v2_fig_geographic_prices.pdf +0 −0

Binary file not shown.

added figures/v2_fig_geographic_prices.png +0 −0

Binary file not shown.

added figures/v2_fig_loso_ols_vs_xgb.pdf +0 −0

Binary file not shown.

modified figures/v2_fig_loso_ols_vs_xgb.png +0 −0

Binary file not shown.

added figures/v2_fig_model_comparison.pdf +0 −0

Binary file not shown.

added figures/v2_fig_model_comparison.png +0 −0

Binary file not shown.

added figures/v2_fig_ols_diagnostics.pdf +0 −0

Binary file not shown.

added figures/v2_fig_ols_diagnostics.png +0 −0

Binary file not shown.

added figures/v2_fig_price_distribution.pdf +0 −0

Binary file not shown.

added figures/v2_fig_price_distribution.png +0 −0

Binary file not shown.

added figures/v2_fig_qr_coefficients.pdf +0 −0

Binary file not shown.

added figures/v2_fig_qr_coefficients.png +0 −0

Binary file not shown.

added figures/v2_fig_shap_dependence.pdf +0 −0

Binary file not shown.

added figures/v2_fig_shap_dependence.png +0 −0

Binary file not shown.

added figures/v2_fig_shap_summary.pdf +0 −0

Binary file not shown.

added figures/v2_fig_shap_summary.png +0 −0

Binary file not shown.

added figures/v2_fig_state_heterogeneity_ci.pdf +0 −0

Binary file not shown.

modified figures/v2_fig_state_heterogeneity_ci.png +0 −0

Binary file not shown.

added figures/v2_fig_trees_curve_seeds.pdf +0 −0

Binary file not shown.

modified figures/v2_fig_trees_curve_seeds.png +0 −0

Binary file not shown.

modified paper/appendix/appendix.tex +20 −290
@@ -18,254 +18,36 @@ controls. Cluster-robust and wild-bootstrap inference for the main coefficients
18 18 are on median \inum{se_ratio_zip3_hc3_median_v2} times the HC3 values and state-cluster
19 19 standard errors \inum{se_ratio_state_hc3_median_v2} times.
20 20
21 −% BEGIN WRAPPER v2_ols_full (delete this block and keep \input{tables/v2_ols_full} once the fragment is a complete float)
22 −\begin{center}
23 −\scriptsize
24 −\renewcommand{\arraystretch}{0.9}
25 −\begin{longtable}{lrrlrr}
26 −\caption{Full OLS Regression Results, Standardised and Natural Units (Trimmed Sample, HC3 Standard Errors)}
27 −\label{tab:ols_full} \\
28 −\toprule
29 − & \multicolumn{3}{c}{Standardised} & \multicolumn{2}{c}{Natural units} \\
30 −\cmidrule(lr){2-4} \cmidrule(lr){5-6}
31 −Variable & Coef. & (SE) & Sig. & Coef. & (SE) \\
32 −\midrule
33 −\endfirsthead
34 −\caption[]{Full OLS Regression Results (continued)} \\
35 −\toprule
36 −Variable & Coef. & (SE) & Sig. & Coef. & (SE) \\
37 −\midrule
38 −\endhead
39 −\midrule
40 −\multicolumn{6}{r}{\textit{continued on the next page}} \\
41 −\endfoot
42 −\bottomrule
43 −\endlastfoot
44 −% Generated by scripts/08_tables_figures_v2.py from results/primary/ols_v2_standardized.csv and ols_v2_unstandardized.csv
45 −\tabinput{v2_ols_full} \\
46 −\end{longtable}
47 −\end{center}
48 −\noindent{\footnotesize\textit{Notes:} Trimmed sample ($N = \inum{ols_v2_n}$), corrected
49 −design (lot size recoded with indicator, parking winsorised at 20, stories at 10). HC3
50 −standard errors in parentheses; stars refer to HC3 and overstate precision
51 −(\cref{sec:cluster_results}). Continuous variables standardised in the first block.
52 −Reference categories: roof flat, construction brick, foundation basement, region Midwest.
53 −*** $p<0.001$, ** $p<0.01$, * $p<0.05$.}
54 −% END WRAPPER v2_ols_full
21 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
22 +\input{tables/v2_ols_full}
55 23
56 24 \section{Additional Inference Checks}
57 25 \label{app:inference}
58 26
59 −% BEGIN WRAPPER v2_conley (delete this block and keep \input{tables/v2_conley} once the fragment is a complete float)
60 −\begin{table}[htbp]
61 −\centering
62 −\caption{Conley Spatial-HAC Standard Errors Compared with HC0 and State-Cluster Standard Errors}
63 −\label{tab:conley}
64 −\begin{threeparttable}
65 −\scriptsize
66 −\setlength{\tabcolsep}{4pt}
67 −\begin{tabular}{lrrrrrr}
68 −\toprule
69 − & & & State & \multicolumn{3}{c}{Conley, Bartlett kernel} \\
70 −\cmidrule(lr){5-7}
71 −Variable & Coef. & HC0 & cluster & 25 km & 50 km & 100 km \\
72 −\midrule
73 −% Generated by scripts/08_tables_figures_v2.py from results/inference_v2/conley_hac_v2.csv
74 −\tabinput{v2_conley} \\
75 −\bottomrule
76 −\end{tabular}
77 −\begin{tablenotes}
78 −\footnotesize
79 −\item \textit{Notes:} Standardised specification estimated on a random subsample of
80 −\inum{d_n_conley_sample} listings of the trimmed sample (the Conley estimator is
81 −quadratic in the sample size); coefficients therefore differ slightly from
82 −\cref{tab:cluster_se}. Bartlett kernel on great-circle distances with the stated
83 −bandwidth. The last row is the median ratio of each standard error to HC0 over the
84 −slopes.
85 −\end{tablenotes}
86 −\end{threeparttable}
87 −\end{table}
88 −% END WRAPPER v2_conley
27 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
28 +\input{tables/v2_conley}
89 29
90 −% BEGIN WRAPPER v2_kennedy (delete this block and keep \input{tables/v2_kennedy} once the fragment is a complete float)
91 −\begin{table}[htbp]
92 −\centering
93 −\caption{Percentage Effects of Dummy Variables: Exact Transformation and Kennedy Correction Under HC3, ZIP3-Cluster and State-Cluster Variances}
94 −\label{tab:kennedy}
95 −\begin{threeparttable}
96 −\small
97 −\setlength{\tabcolsep}{4pt}
98 −\begin{tabular}{lrrrrrr}
99 −\toprule
100 − & & Exact & \multicolumn{3}{c}{Kennedy (\%), variance from} & $t$ \\
101 −\cmidrule(lr){4-6}
102 −Variable & Coef. & (\%) & HC3 & ZIP3 & State & (state) \\
103 −\midrule
104 −% Generated by scripts/08_tables_figures_v2.py from results/inference_v2/kennedy_clustered_v2.csv
105 −\tabinput{v2_kennedy} \\
106 −\bottomrule
107 −\end{tabular}
108 −\begin{tablenotes}
109 −\small
110 −\item \textit{Notes:} Natural-unit specification, trimmed sample. ``Exact'' is
111 −$100(\exp\hat\beta - 1)$; the Kennedy columns apply \cref{eq:halvorsen} with the stated
112 −variance estimate. The correction matters only for the imprecise waterfront dummy.
113 −\end{tablenotes}
114 −\end{threeparttable}
115 −\end{table}
116 −% END WRAPPER v2_kennedy
30 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
31 +\input{tables/v2_kennedy}
117 32
118 −% BEGIN WRAPPER v2_qr_cluster_se (delete this block and keep \input{tables/v2_qr_cluster_se} once the fragment is a complete float)
119 −\begin{table}[htbp]
120 −\centering
121 −\caption{Quantile-Regression Coefficients at $\tau = 0.10$ and $0.90$ with Kernel and ZIP3-Cluster-Bootstrap Standard Errors}
122 −\label{tab:qr_cluster_se}
123 −\begin{threeparttable}
124 −\small
125 −\setlength{\tabcolsep}{4pt}
126 −\begin{tabular}{lrrrrrr}
127 −\toprule
128 − & \multicolumn{3}{c}{$\tau = 0.10$} & \multicolumn{3}{c}{$\tau = 0.90$} \\
129 −\cmidrule(lr){2-4} \cmidrule(lr){5-7}
130 −Variable & Coef. & (kernel SE) & (ZIP3 boot.\ SE) & Coef. & (kernel SE) & (ZIP3 boot.\ SE) \\
131 −\midrule
132 −% Generated by scripts/08_tables_figures_v2.py from results/qr/qr_cluster_se_v2.csv
133 −\tabinput{v2_qr_cluster_se} \\
134 −\bottomrule
135 −\end{tabular}
136 −\begin{tablenotes}
137 −\small
138 −\item \textit{Notes:} Standardised coefficients on the 150,000-listing subsample. Kernel
139 −standard errors assume independent observations; the ZIP3-bootstrap standard errors are
140 −the standard deviations of the coefficient across the \inum{iqr_v2_n_draws} paired
141 −cluster resamples of \cref{tab:iqr_tests}.
142 −\end{tablenotes}
143 −\end{threeparttable}
144 −\end{table}
145 −% END WRAPPER v2_qr_cluster_se
33 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
34 +\input{tables/v2_qr_cluster_se}
146 35
147 36 \section{Geographic Designs: Intervals, Decomposition, Fold Membership and State Detail}
148 37 \label{app:geo_tables}
149 38 \label{app:folds}
150 39
151 −% BEGIN WRAPPER v2_gap_bootstrap (delete this block and keep \input{tables/v2_gap_bootstrap} once the fragment is a complete float)
152 −\begin{table}[htbp]
153 −\centering
154 −\caption{The OLS--Ensemble Difference in Out-of-Sample $R^2$ Under Every Design and Comparator, with ZIP3-Cluster Bootstrap Standard Errors and Intervals}
155 −\label{tab:gap_bootstrap}
156 −\scriptsize
157 −\setlength{\tabcolsep}{3pt}
158 −\begin{adjustbox}{max width=\textwidth}
159 −\begin{tabular}{llrrrrl}
160 −\toprule
161 −Design & Comparator & $R^2$ OLS & $R^2$ comparator (SD) & Difference & Boot.\ SE & 95\% CI \\
162 −\midrule
163 −% Generated by scripts/08_tables_figures_v2.py from results/geo_inference/gap_bootstrap.csv
164 −\tabinput{v2_gap_bootstrap} \\
165 −\bottomrule
166 −\end{tabular}
167 −\end{adjustbox}
168 −\vspace{4pt}
169 −\begin{minipage}{\textwidth}\footnotesize
170 −\textit{Notes:} Difference $= R^2(\text{OLS}) - R^2(\text{comparator})$ on the same
171 −evaluated listings; 1,000 resamples of the ZIP3 clusters of the evaluated listings;
172 −percentile intervals. Comparator $R^2$ is the mean over five seeds with the seed standard
173 −deviation in parentheses where available.
174 −\end{minipage}
175 −\end{table}
176 −% END WRAPPER v2_gap_bootstrap
40 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
41 +\input{tables/v2_gap_bootstrap}
177 42
178 −% BEGIN WRAPPER v2_sse_decomposition (delete this block and keep \input{tables/v2_sse_decomposition} once the fragment is a complete float)
179 −\begin{table}[p]
180 −\centering
181 −\caption{Decomposition of the Out-of-Sample Squared Error into Within-State and Between-State (Level) Components, Every Design and Model}
182 −\label{tab:sse_decomposition}
183 −\scriptsize
184 −\setlength{\tabcolsep}{2.5pt}
185 −\renewcommand{\arraystretch}{0.82}
186 −\begin{adjustbox}{max width=\textwidth}
187 −\begin{tabular}{llrrrrrrr}
188 −\toprule
189 − & & & $R^2_{\text{within}}$ & \multicolumn{2}{c}{SSE share of test variance} & Between-state & $R^2_{\text{within}}$ & SSE between \\
190 −\cmidrule(lr){5-6}
191 −Design & Model & $R^2$ & (state, oracle) & within state & between state & variance share & (ZIP, oracle) & ZIP share \\
192 −\midrule
193 −% Generated by scripts/08_tables_figures_v2.py from results/geo_inference/sse_decomposition.csv
194 −\tabinput{v2_sse_decomposition} \\
195 −\bottomrule
196 −\end{tabular}
197 −\end{adjustbox}
198 −\vspace{4pt}
199 −\begin{minipage}{\textwidth}\footnotesize
200 −\textit{Notes:} $\mathrm{SSE} = \sum_i (e_i - \bar e_{s(i)})^2 + \sum_s n_s \bar e_s^{\,2}$,
201 −each part divided by the total sum of squares of log prices in the evaluated set;
202 −$1 - R^2$ equals the sum of the two shares. ``Between-state variance share'' is the share
203 −of the test variance of log prices that lies between state means. For the ZIP-blocked
204 −designs the last two columns repeat the decomposition at the block level. Model labels:
205 −``ols full ols'' is OLS with region dummies, ``ols state fe no region ols'' OLS with
206 −state fixed effects, ``xgb full nested'' XGBoost with nested rounds, ``fixed1000'' and
207 −``fixed100'' the fixed rules, ``plus missing'' the variant with missingness indicators,
208 −``lgb'' LightGBM.
209 −\end{minipage}
210 −\end{table}
211 −% END WRAPPER v2_sse_decomposition
43 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
44 +\input{tables/v2_sse_decomposition}
212 45
213 −% BEGIN WRAPPER v2_fold_membership (delete this block and keep \input{tables/v2_fold_membership} once the fragment is a complete float)
214 −\CatchFileDef{\foldmembershiprows}{tables/v2_fold_membership.tex}{\catcode`\_=12}
215 −\begin{table}[htbp]
216 −\centering
217 −\caption{Fold Membership of the State-Blocked Designs}
218 −\label{tab:fold_membership}
219 −\begin{threeparttable}
220 −\small
221 −\begin{tabular}{llrrp{6.5cm}}
222 −\toprule
223 −Design & Fold & States & $N_{\text{test}}$ & Jurisdictions held out \\
224 −\midrule
225 −% Generated by scripts/08_tables_figures_v2.py from results/geo/fold_membership.csv
226 −% (read with a literal underscore because the fold identifier `ten_state' is unescaped in the fragment)
227 −\foldmembershiprows \\
228 −\bottomrule
229 −\end{tabular}
230 −\begin{tablenotes}
231 −\small
232 −\item \textit{Notes:} \texttt{GroupKFold} assigns states to folds so that listing counts
233 −are balanced; the ZIP5 and ZIP3 folds (\inum{n_zip5} and \inum{n_zip3_clusters} groups)
234 −are not listed.
235 −\end{tablenotes}
236 −\end{threeparttable}
237 −\end{table}
238 −% END WRAPPER v2_fold_membership
46 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
47 +\input{tables/v2_fold_membership}
239 48
240 −% BEGIN WRAPPER v2_loso_states (delete this block and keep \input{tables/v2_loso_states} once the fragment is a complete float)
241 −\begin{table}[p]
242 −\centering
243 −\caption{Leave-One-State-Out Accuracy of OLS and the Tree Ensembles, by Held-Out Jurisdiction}
244 −\label{tab:loso_states}
245 −\begin{threeparttable}
246 −\scriptsize
247 −\setlength{\tabcolsep}{3pt}
248 −\renewcommand{\arraystretch}{0.86}
249 −\begin{tabular}{lrrrrrrrr}
250 −\toprule
251 − & & \multicolumn{5}{c}{$R^2$} & \multicolumn{2}{c}{Mean error (log points)} \\
252 −\cmidrule(lr){3-7} \cmidrule(lr){8-9}
253 −State & $N$ & OLS & XGB nested & XGB 1,000 & XGB 100 & LGB nested & OLS & XGB nested \\
254 −\midrule
255 −% Generated by scripts/08_tables_figures_v2.py from results/geo_inference/loso_per_state.csv
256 −\tabinput{v2_loso_states} \\
257 −\bottomrule
258 −\end{tabular}
259 −\begin{tablenotes}
260 −\footnotesize
261 −\item \textit{Notes:} Each row is a separate set of models trained on all trimmed-sample
262 −listings outside the state; booster values are means over five seeds. Sorted by the
263 −XGBoost nested-rounds $R^2$. Mean error is predicted minus actual log price; a value of
264 −$+0.10$ means the model over-prices the state by about 10\%.
265 −\end{tablenotes}
266 −\end{threeparttable}
267 −\end{table}
268 −% END WRAPPER v2_loso_states
49 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
50 +\input{tables/v2_loso_states}
269 51
270 52 \section{The Zestimate: Ratio to the Asking Price and Selection}
271 53 \label{app:zestimate}
@@ -300,29 +82,8 @@ not independent of the list price.
300 82 \end{threeparttable}
301 83 \end{table}
302 84
303 −% BEGIN WRAPPER v2_zestimate_selection (delete this block and keep \input{tables/v2_zestimate_selection} once the fragment is a complete float)
304 −\begin{table}[htbp]
305 −\centering
306 −\caption{Observables of Listings With and Without a Zestimate}
307 −\label{tab:zestimate_selection}
308 −\begin{threeparttable}
309 −\small
310 −\begin{tabular}{lrrr}
311 −\toprule
312 −Variable & Mean, with Zestimate & Mean, without & Standardised difference \\
313 −\midrule
314 −% Generated by scripts/08_tables_figures_v2.py from results/stored_checks/zestimate_selection.csv
315 −\tabinput{v2_zestimate_selection} \\
316 −\bottomrule
317 −\end{tabular}
318 −\begin{tablenotes}
319 −\small
320 −\item \textit{Notes:} Analytical sample; the standardised difference is the difference in
321 −means divided by the pooled standard deviation.
322 −\end{tablenotes}
323 −\end{threeparttable}
324 −\end{table}
325 −% END WRAPPER v2_zestimate_selection
85 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
86 +\input{tables/v2_zestimate_selection}
326 87
327 88 \section{Data Availability and Reproducibility}
328 89 \label{app:reproducibility}
@@ -457,36 +218,5 @@ listed in \cref{app:reproducibility}.
457 218 full-test-set statistics of \cref{tab:moran} replace them.
458 219 \end{enumerate}
459 220
460 −% BEGIN WRAPPER v2_qr_legacy_check (delete this block and keep \input{tables/v2_qr_legacy_check} once the fragment is a complete float)
461 −\begin{table}[htbp]
462 −\centering
463 −\caption{Reproduction of the Version 1.2 Quantile Regressions on the Original Design}
464 −\label{tab:qr_legacy_check}
465 −\begin{threeparttable}
466 −\scriptsize
467 −\setlength{\tabcolsep}{3pt}
468 −\begin{tabular}{rrrrrrrrr}
469 −\toprule
470 − & \multicolumn{2}{c}{Pseudo-$R^2$} & \multicolumn{2}{c}{$|\Delta|$ coefficients} & \multicolumn{2}{c}{$\ln$(lot)} & \multicolumn{2}{c}{Parking} \\
471 −\cmidrule(lr){2-3} \cmidrule(lr){4-5} \cmidrule(lr){6-7} \cmidrule(lr){8-9}
472 −$\tau$ & stored & corrected & median & max (excl.\ parking) & stored & rebuilt & stored & rebuilt \\
473 −\midrule
474 −% Generated by scripts/08_tables_figures_v2.py from results/qr/qr_legacy_check.csv
475 −\tabinput{v2_qr_legacy_check} \\
476 −\bottomrule
477 −\end{tabular}
478 −\begin{tablenotes}
479 −\footnotesize
480 −\item \textit{Notes:} The five quantile regressions of version 1.2 re-estimated on the
481 −original design with the rebuilt code. ``Stored'' pseudo-$R^2$ is the value printed in
482 −version 1.2, which equals $1 - R^1(\tau)$; ``corrected'' is $R^1(\tau)$. The median absolute
483 −difference across the 63 coefficients is \inum{d_qr_legacy_median_diff_max} at every
484 −quantile and the largest difference excluding parking is \inum{d_qr_legacy_max_excl_parking_le05}
485 −at $\tau \le 0.50$; at $\tau = 0.75$ and $0.90$ the parking coefficient, which depends on
486 −a column with values up to $10^{11}$ and is solver-dependent, does not reproduce and the
487 −largest remaining difference rises to \inum{d_qr_legacy_max_excl_parking_075} and
488 −\inum{d_qr_legacy_max_excl_parking_090}.
489 −\end{tablenotes}
490 −\end{threeparttable}
491 −\end{table}
492 −% END WRAPPER v2_qr_legacy_check
221 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
222 +\input{tables/v2_qr_legacy_check}
added paper/figures/v2_fig_ablation.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_ablation from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_ablation.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_ablation.pdf}
5 +\caption{Cumulative feature-ablation cascade for XGBoost (1,000 rounds with early stopping, trimmed v2 sample): mean out-of-sample $R^2$ over seeds 42--46 with the min--max range shaded (Table~\ref{tab:ablation}). The neighbourhood block adds 16.0 pp under random validation but 3.8 pp out of state, and the full model loses out-of-state accuracy when the region dummies enter; the seed ranges are too narrow to be visible under the random split.}
6 +\label{fig:ablation}
7 +\end{figure}
added paper/figures/v2_fig_feasible_level.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_feasible from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_feasible_level.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_feasible_level.pdf}
5 +\caption{Feasible level correction of out-of-state predictions. Each curve is the pooled held-out $R^2$ after adding to every prediction the mean residual on $k$ listings drawn at random from the held-out state (median over 100 draws; shaded interquartile range; $k = 0$ is the raw model); dotted horizontal lines are the oracle corrections that use the full held-out state (Table~\ref{tab:feasible_level}). The $x$-axis is symmetric-log. Twenty observed listings recover most of the oracle gain for every model under both designs.}
6 +\label{fig:feasible_level}
7 +\end{figure}
added paper/figures/v2_fig_gap_by_design.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_gap from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_gap_by_design.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_gap_by_design.pdf}
5 +\caption{The OLS-versus-ensemble gap by validation design. Points are $R^2(\text{OLS}) - R^2(\text{ensemble})$ in percentage points on the pooled out-of-fold predictions of the trimmed v2 sample (Table~\ref{tab:designs}); bars are 95\% percentile intervals from 1,000 ZIP3-cluster bootstrap draws of the test listings. XGBoost and LightGBM with rounds selected on the training groups (nested) and XGBoost with 1,000 fixed rounds (the v1.1 comparator). Negative values favour the ensemble. The ensembles are more accurate under every design except the single ten-state partition, where the interval for the nested XGBoost comparator covers zero.}
6 +\label{fig:gap_by_design}
7 +\end{figure}
added paper/figures/v2_fig_geographic_prices.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_map from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_geographic_prices.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_geographic_prices.pdf}
5 +\caption{Log asking prices of a 50,000-listing random subsample of the analytical sample by location (viridis scale; plate carr\'ee projection, aspect ratio 1.25). The coastal gradient and the uneven listing density are both visible: sparse areas reflect few Zillow listings in the extract, not few homes.}
6 +\label{fig:geographic}
7 +\end{figure}
added paper/figures/v2_fig_loso_ols_vs_xgb.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_loso from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_loso_ols_vs_xgb.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=0.92\textwidth]{figures/v2_fig_loso_ols_vs_xgb.pdf}
5 +\caption{Leave-one-state-out $R^2$ by held-out state, OLS versus XGBoost with nested round selection (trimmed v2 sample; each state is predicted by models trained on the other fifty jurisdictions). States are sorted by the XGBoost value; a horizontal segment joins the two models; XGBoost bars are $\pm$2 seed standard deviations (seeds 42--46; OLS is deterministic). Starred states form the ten-state holdout of v1.1. XGBoost is more accurate in 44 of 51 states (sign test $p = 1.2e-07$; Table~\ref{tab:loso_summary}); state-level values in Table~\ref{tab:loso_states}.}
6 +\label{fig:loso}
7 +\end{figure}
added paper/figures/v2_fig_model_comparison.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_model_comparison from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_model_comparison.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_model_comparison.pdf}
5 +\caption{Predicted against actual log asking prices on the random-split test set (trimmed v2 sample, seed 42; 60,000-listing random subset per panel, hexagonal bins with a viridis count scale): (a) OLS, (b) XGBoost, (c) LightGBM; dashed line = 45-degree line. The $R^2$ printed in each panel is computed on all 156,770 test listings (Table~\ref{tab:ml_comparison}). OLS compresses the tails; the ensembles track them.}
6 +\label{fig:model_comparison}
7 +\end{figure}
added paper/figures/v2_fig_ols_diagnostics.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_diagnostics from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_ols_diagnostics.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_ols_diagnostics.pdf}
5 +\caption{Diagnostics of the v2 OLS model on the 156,770 test listings of the random split (seed 42; out-of-sample residuals): (a) residuals against fitted values (hexagonal bins of a 100,000-listing random subset, viridis scale = number of listings); (b) normal quantile--quantile plot of the same subset; (c) residual density with a normal density of the same mean and variance. Test-set residual skewness 0.15 and kurtosis 5.43 (in-sample 0.12 and 5.38, Table~\ref{tab:ols_results}); the fan-shaped spread motivates heteroskedasticity-robust errors and the clustered inference of Table~\ref{tab:cluster_se} addresses the spatial dependence that this plot cannot show.}
6 +\label{fig:diagnostics}
7 +\end{figure}
added paper/figures/v2_fig_price_distribution.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_price from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_price_distribution.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_price_distribution.pdf}
5 +\caption{Distribution of asking prices in the analytical sample ($N = 788,842$ active listings; Table~\ref{tab:descriptive}): (a) levels, histogram of 100 bins truncated at \$3,000k for legibility (16,651 listings lie above the cut); (b) natural logarithm, the dependent variable of every model; (c) box plots of log prices by Census region (NE = Northeast, MW = Midwest; median, quartiles, whiskers at 1.5 interquartile ranges, outliers omitted; the share of listings is given under each region). Listing prices are asking prices, not transaction prices.}
6 +\label{fig:price_dist}
7 +\end{figure}
added paper/figures/v2_fig_qr_coefficients.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_qr from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_qr_coefficients.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_qr_coefficients.pdf}
5 +\caption{Quantile-regression coefficient paths on the v2 design for eight attributes (random subsample of 150,000 listings; continuous regressors standardised, Table~\ref{tab:qr_results}). Shaded bands are 95\% intervals from the kernel standard errors, which assume independent observations; the black bars at $\tau = 0.10$ and $0.90$ are 95\% intervals from the paired ZIP3-cluster bootstrap (999 draws, Table~\ref{tab:qr_cluster_se}) and are several times wider. The dashed line is the OLS coefficient with its state-clustered 95\% interval.}
6 +\label{fig:qr_coefficients}
7 +\end{figure}
added paper/figures/v2_fig_shap_dependence.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_shap_dependence from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_shap_dependence.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_shap_dependence.pdf}
5 +\caption{SHAP dependence of the XGBoost model on six features (v2 design, 9,937 explanation listings): each point is one listing's contribution against the feature value in natural units; the line joins the mean contribution within 25 quantile bins. The concave living-area and saturating bathroom contributions are non-linearities the semi-log form cannot capture; the step pattern of the school rating reflects its state-median support (Table~\ref{tab:missing}).}
6 +\label{fig:shap_dependence}
7 +\end{figure}
added paper/figures/v2_fig_shap_summary.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_shap_summary from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_shap_summary.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_shap_summary.pdf}
5 +\caption{SHAP summary plot of the XGBoost model on the v2 design (TreeSHAP on the 9,937 stored explanation listings of the random test set; the fifteen features with the largest mean $|\phi|$, Table~\ref{tab:shap_importance}). Each point is one listing; its horizontal position is the feature's contribution to the predicted log price and its colour the percentile rank of the feature value (viridis: dark = low, yellow = high). Contributions describe the fitted prediction function, not implicit prices; the school rating is a state median for most listings and acts as a state identifier.}
6 +\label{fig:shap_summary}
7 +\end{figure}
added paper/figures/v2_fig_state_heterogeneity_ci.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_state_het from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_state_heterogeneity_ci.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_state_heterogeneity_ci.pdf}
5 +\caption{State-by-state OLS coefficients in natural units with inference (trimmed v2 sample, Table~\ref{tab:state_heterogeneity}): (a) the elasticity of the asking price with respect to living area; (b) the garage coefficient. Circles are the state estimates with 95\% confidence intervals from ZIP3-clustered standard errors; orange ticks are empirical-Bayes posterior means that shrink imprecise small-state estimates towards the precision-weighted mean; the dashed line is the pooled coefficient with state fixed effects. The extreme elasticities (DE at the bottom, HI at the top) carry the widest intervals.}
6 +\label{fig:state_heterogeneity}
7 +\end{figure}
added paper/figures/v2_fig_trees_curve_seeds.tex +7 −0
@@ -0,0 +1,7 @@
1 +% Generated by scripts/08b_figures_v2.py::fig_trees from results/ (see caption) -- do not edit by hand (figure file figures/v2_fig_trees_curve_seeds.pdf).
2 +\begin{figure}[htbp]
3 +\centering
4 +\includegraphics[width=\textwidth]{figures/v2_fig_trees_curve_seeds.pdf}
5 +\caption{Out-of-sample $R^2$ of the full XGBoost model as a function of the number of boosting rounds (trimmed v2 sample). Lines are means over seeds 42--46 and shaded bands the min--max range across seeds; the dotted vertical line marks the number of rounds (175) chosen by the inner GroupKFold(3) rule on the training states for the ten-state design. The held-out-state curve peaks between 65 and 70 rounds depending on the seed and declines to 0.425 at 1,000 rounds, while the early-stopping set drawn from the training states keeps rising; the peak is not observable without the test states (Table~\ref{tab:trees_curve}).}
6 +\label{fig:trees_curve}
7 +\end{figure}
modified paper/sections/data.tex +12 −122
@@ -89,32 +89,8 @@ price---builder plan listings of the same model home and relisted properties---n
89 89 (\inum{d_share_duplicate_rows_pct}\% of the sample); they are kept, and
90 90 \cref{sec:duplicates} shows that they do not affect the random-split comparison.
91 91
92 −% BEGIN WRAPPER v2_attrition (delete this block and keep \input{tables/v2_attrition} once the fragment is a complete float)
93 −\begin{table}[htbp]
94 −\centering
95 −\caption{Sample Construction from the Raw Extract}
96 −\label{tab:attrition}
97 −\begin{threeparttable}
98 −\footnotesize
99 −\setlength{\tabcolsep}{4pt}
100 −\begin{tabular}{p{5.6cm}rrp{4.4cm}}
101 −\toprule
102 −Step & $N$ remaining & Removed & Criterion \\
103 −\midrule
104 −% Generated by scripts/08_tables_figures_v2.py from results/build/attrition.csv
105 −\tabinput{v2_attrition} \\
106 −\bottomrule
107 −\end{tabular}
108 −\begin{tablenotes}
109 −\small
110 −\item \textit{Notes:} Filters applied sequentially by \texttt{scripts/00\_build\_sample.py}
111 −to the raw Zillow extract of 8 January 2026 (\inum{n_raw} listings). The filters act on
112 −price, living area, room counts and location only; secondary attributes are screened in
113 −\cref{sec:data_quality}.
114 −\end{tablenotes}
115 −\end{threeparttable}
116 −\end{table}
117 −% END WRAPPER v2_attrition
92 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
93 +\input{tables/v2_attrition}
118 94
119 95 \subsection{Variables}
120 96 \label{sec:variables}
@@ -147,43 +123,11 @@ frozen lookup of the raw strings that is committed with the code.
147 123 \inum{d_median_walk} and \inum{d_median_transit}), consistent with a stock dominated by
148 124 suburban and exurban single-family homes.
149 125
150 −% BEGIN WRAPPER v2_descriptives (delete this block and keep \input{tables/v2_descriptives} once the fragment is a complete float)
151 −\begin{table}[htbp]
152 −\centering
153 −\caption{Descriptive Statistics of the Analytical Sample ($N = \inum{n_sample}$)}
154 −\label{tab:descriptive}
155 −\begin{threeparttable}
156 −\small
157 −\begin{tabular}{lrrrrr}
158 −\toprule
159 −Variable & Mean & Std.\ dev. & Min & Median & Max \\
160 −\midrule
161 −% Generated by scripts/08_tables_figures_v2.py from results/build/descriptives.csv
162 −\tabinput{v2_descriptives} \\
163 −\bottomrule
164 −\end{tabular}
165 −\begin{tablenotes}
166 −\small
167 −\item \textit{Notes:} Listing prices, not transaction prices. Property age is
168 −$2026 - \text{year built}$ after state-median imputation of year built; imputed values
169 −included. The lot-size row is the imputed field before screening and shows the corrupt
170 −extremes removed in \cref{sec:data_quality}. The school rating is a state median for
171 −\inum{share_school_rating_imputed_pct}\% of listings (\cref{tab:missing}). Regional
172 −shares: South \inum{d_region_share_South_pct}\%, West \inum{d_region_share_West_pct}\%,
173 −Midwest \inum{d_region_share_Midwest_pct}\%, Northeast \inum{d_region_share_Northeast_pct}\%.
174 −\end{tablenotes}
175 −\end{threeparttable}
176 −\end{table}
177 −% END WRAPPER v2_descriptives
126 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
127 +\input{tables/v2_descriptives}
178 128
179 −\begin{figure}[htbp]
180 − \centering
181 − \includegraphics[width=\textwidth]{figures/fig1_price_distribution.png}
182 − \caption{Distribution of listing prices in the analytical sample
183 − ($N = \inum{n_sample}$): (a) dollar levels; (b) natural logarithm. The logarithmic
184 − transformation removes most of the right skew.}
185 − \label{fig:price_dist}
186 −\end{figure}
129 +% Generated by scripts/08b_figures_v2.py (complete float; vector PDF in figures/)
130 +\input{figures/v2_fig_price_distribution}
187 131
188 132 \subsection{Missing Data, Imputation and Coding}
189 133 \label{sec:missing}
@@ -194,33 +138,8 @@ regressor input, the share of the analytical sample for which the raw field is m
194 138 and the rule applied by the rebuilt sample-construction code. Three facts condition the
195 139 interpretation of the results.
196 140
197 −% BEGIN WRAPPER v2_missingness (delete this block and keep \input{tables/v2_missingness} once the fragment is a complete float)
198 −\begin{table}[htbp]
199 −\centering
200 −\caption{Missing Raw Inputs and the Imputation Rule Applied ($N = \inum{n_sample}$)}
201 −\label{tab:missing}
202 −\begin{threeparttable}
203 −\footnotesize
204 −\begin{tabular}{p{5.0cm}rrp{6.3cm}}
205 −\toprule
206 −Raw field (regressor) & Missing & Share (\%) & Rule applied \\
207 −\midrule
208 −% Generated by scripts/08_tables_figures_v2.py from results/build/missingness_rules.csv
209 −\tabinput{v2_missingness} \\
210 −\bottomrule
211 −\end{tabular}
212 −\begin{tablenotes}
213 −\small
214 −\item \textit{Notes:} Missingness measured in the raw extract for the \inum{n_sample}
215 −retained listings and rules read from \texttt{src/wp3/build.py}, which reproduces the
216 −analytical sample bit for bit. ``State median'' denotes imputation by the median of
217 −observed values within the listing's state. Climate-risk factors (flood, fire, heat,
218 −wind, air) are missing for every record and excluded. Stories and parking spaces are
219 −state-median imputed; the imputed values are tabulated in the repository.
220 −\end{tablenotes}
221 −\end{threeparttable}
222 −\end{table}
223 −% END WRAPPER v2_missingness
141 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
142 +\input{tables/v2_missingness}
224 143
225 144 First, the three school variables are missing for \inum{missing_share_avg_school_rating_pct}\%
226 145 of listings and imputed with state medians. For those listings ``average school rating''
@@ -243,31 +162,8 @@ replaces that value by the log of the state-median lot and adds an indicator for
243 162 affected listings; \cref{sec:lot_recoding} quantifies the consequences, which are a
244 163 fivefold change in the lot elasticity.
245 164
246 −% BEGIN WRAPPER v2_lot_reconciliation (delete this block and keep \input{tables/v2_lot_reconciliation} once the fragment is a complete float)
247 −\begin{table}[htbp]
248 −\centering
249 −\caption{Reconciliation of Listings with $\ln(\text{lot}) = 0$ in the Original Coding}
250 −\label{tab:lot_reconciliation}
251 −\begin{threeparttable}
252 −\small
253 −\begin{tabular}{lrr}
254 −\toprule
255 −Class of raw lot-size value & Listings & Share of sample (\%) \\
256 −\midrule
257 −% Generated by scripts/08_tables_figures_v2.py from results/build/lot_reconciliation.csv
258 −\tabinput{v2_lot_reconciliation} \\
259 −\bottomrule
260 −\end{tabular}
261 −\begin{tablenotes}
262 −\small
263 −\item \textit{Notes:} Classes are mutually exclusive and sum to the number of listings
264 −whose $\ln(\text{lot})$ was zero in the original coding. The design matrix of this paper
265 −recodes all five classes to the log of the state-median lot size and flags them with the
266 −lot-missing indicator.
267 −\end{tablenotes}
268 −\end{threeparttable}
269 −\end{table}
270 −% END WRAPPER v2_lot_reconciliation
165 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
166 +\input{tables/v2_lot_reconciliation}
271 167
272 168 The remaining rules are conventional but not innocuous. Transit Score is set to zero when
273 169 missing (\inum{missing_share_transit_score_pct}\%), on the assumption that properties
@@ -308,11 +204,5 @@ and stories at 10 for every model, and the \emph{trimmed sample} of
308 204 the estimation sample for every model in the paper; the full sample of \inum{n_sample}
309 205 listings is retained only where the text says so.
310 206
311 −\begin{figure}[htbp]
312 − \centering
313 − \includegraphics[width=\textwidth]{figures/fig7_geographic_prices.png}
314 − \caption{Geographic distribution of log listing prices, 50,000-listing random
315 − subsample of the analytical sample. Colour indicates log price; sparse areas reflect
316 − few Zillow listings rather than few homes.}
317 − \label{fig:geographic}
318 −\end{figure}
207 +% Generated by scripts/08b_figures_v2.py (complete float; vector PDF in figures/)
208 +\input{figures/v2_fig_geographic_prices}
modified paper/sections/results.tex +49 −575
@@ -41,43 +41,8 @@ under HC3 variances; under state-cluster variances it matters only for waterfron
41 41 (\inum{d_kennedy_wf_naive_pct}\% exact against \inum{d_kennedy_wf_state_pct}\% corrected;
42 42 \cref{tab:kennedy}).
43 43
44 −% BEGIN WRAPPER v2_cluster_se (delete this block and keep \input{tables/v2_cluster_se} once the fragment is a complete float)
45 −\begin{table}[htbp]
46 −\centering
47 −\caption{OLS Coefficients with HC3, ZIP3-Cluster, State-Cluster and Wild-Bootstrap Inference (Standardised, Trimmed Sample)}
48 −\label{tab:cluster_se}
49 −\begin{threeparttable}
50 −\scriptsize
51 −\setlength{\tabcolsep}{3.5pt}
52 −\renewcommand{\arraystretch}{0.95}
53 −\begin{tabular}{lrrrrrrrr}
54 −\toprule
55 − & & \multicolumn{3}{c}{Standard error} & State/HC3 & $t$ & Wild $p$ & $G_{\mathrm{eff}}$ \\
56 −\cmidrule(lr){3-5}
57 −Variable & Coef. & HC3 & ZIP3 & State & ratio & (state) & (state) & (state) \\
58 −\midrule
59 −% Generated by scripts/08_tables_figures_v2.py from results/inference_v2/cluster_se_v2.csv, significance_counts_v2.csv, wild_bootstrap_*.csv
60 −\tabinput{v2_cluster_se} \\
61 −\bottomrule
62 −\end{tabular}
63 −\begin{tablenotes}
64 −\footnotesize
65 −\item \textit{Notes:} Standardised specification of \cref{eq:ols} with region dummies,
66 −$N = \inum{ols_v2_n}$ (trimmed sample), \inum{n_slopes_v2} slopes (\inum{n_slopes}
67 −regressors plus the lot-missing indicator); selected coefficients shown, full table in
68 −\cref{tab:ols_full}. Cluster-robust standard errors follow \cref{eq:cluster} with
69 −\inum{n_zip3_clusters} ZIP3 groups or 51 states. ``Wild $p$'' is the $p$-value of a wild
70 −cluster bootstrap by state with Webb six-point weights and 9,999 replications under the
71 −null; $G_{\mathrm{eff}}$ is the effective number of state clusters of
72 −\citet{carter2017asymptotic}. The summary rows count slopes significant at 5\% under
73 −each estimator, after Holm and Benjamini--Hochberg adjustment, and under the wild
74 −bootstrap (Webb / Rademacher weights). Reassigning the \inum{n_listings_missing_zip}
75 −listings without a ZIP code to the nearest ZIP3 centroid changes no standard error by
76 −more than \inum{max_rel_se_change_zip3_reassignment_pct}\%.
77 −\end{tablenotes}
78 −\end{threeparttable}
79 −\end{table}
80 −% END WRAPPER v2_cluster_se
44 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
45 +\input{tables/v2_cluster_se}
81 46
82 47 \subsubsection{Coefficient Signs That Are Not Implicit Prices}
83 48 \label{sec:signs}
@@ -122,35 +87,8 @@ because XGBoost does not observe ZIP codes and \cref{sec:ablation} shows that it
122 87 recovers location from state-level proxies, so the two models exploit different
123 88 information.
124 89
125 −% BEGIN WRAPPER v2_fe_progression (delete this block and keep \input{tables/v2_fe_progression} once the fragment is a complete float)
126 −\begin{table}[htbp]
127 −\centering
128 −\caption{OLS $R^2$ Under Progressively Finer Geographic Controls}
129 −\label{tab:fe_comparison}
130 −\begin{threeparttable}
131 −\footnotesize
132 −\begin{tabular}{lllrr}
133 −\toprule
134 −Specification & Geographic FE & $N_{\text{FE}}$ & In-sample $R^2$ & Out-of-sample $R^2$ \\
135 −\midrule
136 −% Generated by scripts/08_tables_figures_v2.py from results/tables/legacy_fe_progression.csv and results/primary/model_comparison_v2.csv
137 −\tabinput{v2_fe_progression} \\
138 −\bottomrule
139 −\end{tabular}
140 −\begin{tablenotes}
141 −\small
142 −\item \textit{Notes:} The first three rows use the original design (full sample of
143 −\inum{n_sample} listings, $\ln(\text{lot}) = 0$ for \inum{d_share_ln_lot_zero_pct}\% of
144 −listings) and are reproduced by the rebuilt code; the ``v2'' rows use the corrected
145 −design on the trimmed sample (random split of \inum{d_n_train_random_v2} training and
146 −\inum{d_n_test_random_v2} test listings). Out-of-sample $R^2$ on the random 20\% holdout
147 −(seed 42). Geographic fixed effects replace the three region dummies. The
148 −\inum{n_zip3_prefixes} ZIP3 prefixes plus one code for listings with a missing ZIP give
149 −\inum{n_zip3_clusters} groups.
150 −\end{tablenotes}
151 −\end{threeparttable}
152 −\end{table}
153 −% END WRAPPER v2_fe_progression
90 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
91 +\input{tables/v2_fe_progression}
154 92
155 93 \subsubsection{Inference Under Spatial Dependence}
156 94 \label{sec:cluster_results}
@@ -212,36 +150,8 @@ the spatial information that hedonic residuals retain. The earlier subsample-bas
212 150 statistics, computed on 5,000-listing national draws, measured regional rather than
213 151 neighbourhood dependence and are superseded.
214 152
215 −% BEGIN WRAPPER v2_moran_full (delete this block and keep \input{tables/v2_moran_full} once the fragment is a complete float)
216 −\begin{table}[htbp]
217 −\centering
218 −\caption{Moran's $I$ on Test-Set Residuals of OLS, LightGBM and XGBoost, Random Split, Full Test Set}
219 −\label{tab:moran}
220 −\begin{threeparttable}
221 −\small
222 −\setlength{\tabcolsep}{4pt}
223 −\begin{tabular}{lrrrrrrr}
224 −\toprule
225 − & & \multicolumn{3}{c}{$k$-nearest neighbours ($k = 8$)} & \multicolumn{3}{c}{Distance bands, $I$} \\
226 −\cmidrule(lr){3-5} \cmidrule(lr){6-8}
227 −Model & $N$ & $I$ & $z$ & Median $d_8$ (km) & 0--1 km & 1--5 km & 5--25 km \\
228 −\midrule
229 −% Generated by scripts/08_tables_figures_v2.py from results/inference_v2/moran_full_test_v2.csv
230 −\tabinput{v2_moran_full} \\
231 −\bottomrule
232 −\end{tabular}
233 −\begin{tablenotes}
234 −\small
235 −\item \textit{Notes:} Row-standardised eight-nearest-neighbour weights built from listing
236 −coordinates on the complete random test set; $z$ from 999 permutations, all
237 −$p < 0.001$. Distance-band rows use binary weights within the stated band. The first
238 −panel scores the stored predictions of the original design (full sample); the second
239 −panel the predictions of the corrected design on the trimmed sample, which are the
240 −estimates used in the text.
241 −\end{tablenotes}
242 −\end{threeparttable}
243 −\end{table}
244 −% END WRAPPER v2_moran_full
153 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
154 +\input{tables/v2_moran_full}
245 155
246 156 \subsection{One Market or Fifty-One? State-by-State Estimates}
247 157 \label{sec:state_heterogeneity}
@@ -254,53 +164,11 @@ interacted specifications is compared on the random holdout.
254 164 \cref{tab:state_heterogeneity} summarises the distribution of the state coefficients and
255 165 \cref{fig:state_heterogeneity} displays two of them with their confidence bands.
256 166
257 −% BEGIN WRAPPER v2_state_heterogeneity (delete this block and keep \input{tables/v2_state_heterogeneity} once the fragment is a complete float)
258 −\begin{table}[htbp]
259 −\centering
260 −\caption{State-by-State OLS Coefficients: Distribution Across 51 Jurisdictions, Sign Reversals with Inference, and Homogeneity Tests (Natural Units)}
261 −\label{tab:state_heterogeneity}
262 −\begin{threeparttable}
263 −\scriptsize
264 −\setlength{\tabcolsep}{3pt}
265 −\begin{tabular}{lrrrrrrrrrr}
266 −\toprule
267 − & \multicolumn{2}{c}{Pooled, state FE} & \multicolumn{3}{c}{State coefficients} & \multicolumn{3}{c}{States whose coefficient} & & Homog. \\
268 −\cmidrule(lr){2-3} \cmidrule(lr){4-6} \cmidrule(lr){7-9}
269 −Variable & Coef. & (SE) & $p_{10}$ & Median & $p_{90}$ & differs in sign & \ldots{} with $|t|>2$ & CI excludes pooled & $\hat\tau$ & $p$ \\
270 −\midrule
271 −% Generated by scripts/08_tables_figures_v2.py from results/inference_v2/state_heterogeneity_summary_v2.csv
272 −\tabinput{v2_state_heterogeneity} \\
273 −\bottomrule
274 −\end{tabular}
275 −\begin{tablenotes}
276 −\footnotesize
277 −\item \textit{Notes:} Natural-unit specification on the trimmed sample
278 −($N = \inum{n_trimmed_sample}$). ``Pooled, state FE'' adds 50 state fixed effects to the
279 −pooled regression, with ZIP3-cluster standard errors. State regressions by OLS within each
280 −jurisdiction with ZIP3-cluster standard errors; ``differs in sign'' counts states whose
281 −point estimate has the opposite sign from the pooled estimate, ``with $|t|>2$'' those
282 −where the opposite-signed estimate is also significant, and ``CI excludes pooled'' states
283 −whose 95\% interval excludes the pooled value. $\hat\tau$ is the DerSimonian--Laird
284 −estimate of the between-state standard deviation of the coefficient; the homogeneity $p$
285 −is from a cluster-robust $Q$ test of equal coefficients across states. The designs of the
286 −smallest states remain near-singular (maximum condition number
287 −\inum{state_regression_max_cond_number}), so their estimates should be read with their
288 −intervals.
289 −\end{tablenotes}
290 −\end{threeparttable}
291 −\end{table}
292 −% END WRAPPER v2_state_heterogeneity
293 −
294 −\begin{figure}[htbp]
295 − \centering
296 − \includegraphics[width=\textwidth]{figures/v2_fig_state_heterogeneity_ci.png}
297 − \caption{State-by-state OLS coefficients with 95\% ZIP3-cluster confidence intervals,
298 − natural units: living-area elasticity and garage coefficient by state, with the pooled
299 − state-fixed-effects estimate as a reference line and empirical-Bayes shrinkage bands.
300 − States are ordered by the point estimate; the smallest states have the widest
301 − intervals.}
302 − \label{fig:state_heterogeneity}
303 −\end{figure}
167 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
168 +\input{tables/v2_state_heterogeneity}
169 +
170 +% Generated by scripts/08b_figures_v2.py (complete float; vector PDF in figures/)
171 +\input{figures/v2_fig_state_heterogeneity_ci}
304 172
305 173 The pooled living-area elasticity of \inumdd{d_het_ln_sqft_pooled} averages state
306 174 elasticities with an inter-decile range of \inum{ln_sqft_state_p10_v2}--\inum{ln_sqft_state_p90_v2};
@@ -334,31 +202,8 @@ implication is the one drawn by the submarket literature \citep{bourassa2003subm
334 202 goodman2003segmentation}: the national hedonic equation is a listing-weighted average of
335 203 distinct local price functions, and its coefficients describe no particular market.
336 204
337 −% BEGIN WRAPPER v2_pooled_vs_interacted (delete this block and keep \input{tables/v2_pooled_vs_interacted} once the fragment is a complete float)
338 −\begin{table}[htbp]
339 −\centering
340 −\caption{Random-Holdout $R^2$ of Pooled, State-Fixed-Effects and State-by-State OLS}
341 −\label{tab:pooled_vs_interacted}
342 −\begin{threeparttable}
343 −\small
344 −\begin{tabular}{lr}
345 −\toprule
346 −Specification & Out-of-sample $R^2$ \\
347 −\midrule
348 −% Generated by scripts/08_tables_figures_v2.py from results/inference_v2/pooled_vs_interacted_v2.csv
349 −\tabinput{v2_pooled_vs_interacted} \\
350 −\bottomrule
351 −\end{tabular}
352 −\begin{tablenotes}
353 −\small
354 −\item \textit{Notes:} Random 80/20 split (seed 42) of the trimmed sample
355 −(\inum{d_n_train_random_v2} training, \inum{d_n_test_random_v2} test listings); natural
356 −units, corrected design. The fully interacted model estimates a separate regression in
357 −each state on the training listings of that state.
358 −\end{tablenotes}
359 −\end{threeparttable}
360 −\end{table}
361 −% END WRAPPER v2_pooled_vs_interacted
205 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
206 +\input{tables/v2_pooled_vs_interacted}
362 207
363 208 \subsection{Quantile Regression and Inter-Quantile Tests}
364 209 \label{sec:qr_results}
@@ -389,35 +234,8 @@ interactions, this coefficient is the marginal association at the sample mean of
389 234 interacted variables and is the least precisely estimated of the set
390 235 (\cref{sec:qr_stability}).
391 236
392 −% BEGIN WRAPPER v2_qr_coefficients (delete this block and keep \input{tables/v2_qr_coefficients} once the fragment is a complete float)
393 −\begin{table}[htbp]
394 −\centering
395 −\caption{Quantile-Regression Coefficients at Five Quantiles (Standardised, Corrected Design)}
396 −\label{tab:qr_results}
397 −\begin{threeparttable}
398 −\footnotesize
399 −\begin{tabular}{lrrrrrr}
400 −\toprule
401 −Variable & $\tau=0.10$ & $\tau=0.25$ & $\tau=0.50$ & $\tau=0.75$ & $\tau=0.90$ & OLS \\
402 −\midrule
403 −% Generated by scripts/08_tables_figures_v2.py from results/qr/qr_coefficients_v2.csv, qr_pseudo_r2_v2.csv, qr_convergence_v2.csv
404 −\tabinput{v2_qr_coefficients} \\
405 −\bottomrule
406 −\end{tabular}
407 −\begin{tablenotes}
408 −\small
409 −\item \textit{Notes:} Random subsample of 150,000 listings of the trimmed sample
410 −(seed 42); continuous variables standardised; all \inum{n_slopes} regressors and the
411 −lot-missing indicator included, selected coefficients shown. Stars from the kernel-based
412 −standard errors of each fit, which assume independent observations and are reported for
413 −orientation only; ZIP3-cluster-bootstrap standard errors for $\tau = 0.10$ and $0.90$ are
414 −in \cref{tab:qr_cluster_se}. The OLS column is the standardised estimate of
415 −\cref{tab:cluster_se}. Pseudo-$R^2$ is $R^1(\tau)$ of \citet{koenker1999goodness}.
416 −*** $p<0.001$, ** $p<0.01$, * $p<0.05$.
417 −\end{tablenotes}
418 −\end{threeparttable}
419 −\end{table}
420 −% END WRAPPER v2_qr_coefficients
237 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
238 +\input{tables/v2_qr_coefficients}
421 239
422 240 \subsubsection{Inter-Quantile Tests}
423 241
@@ -440,39 +258,8 @@ distribution of asking prices is thus differentially stretched and compressed by
440 258 structural attributes, while the tail differences of the rarer amenities and of the West
441 259 dummy are estimated with far less precision than independence-based statistics suggest.
442 260
443 −% BEGIN WRAPPER v2_iqr_bootstrap (delete this block and keep \input{tables/v2_iqr_bootstrap} once the fragment is a complete float)
444 −\begin{table}[htbp]
445 −\centering
446 −\caption{Inter-Quantile Tests, $\hat\beta(0.10) - \hat\beta(0.90)$, with Paired Bootstrap Covariance and Multiplicity Control}
447 −\label{tab:iqr_tests}
448 −\begin{threeparttable}
449 −\scriptsize
450 −\setlength{\tabcolsep}{3pt}
451 −\begin{tabular}{lrrrrlrrrr}
452 −\toprule
453 − & & \multicolumn{3}{c}{$z$-statistic} & 95\% CI & \multicolumn{4}{c}{$p$-value (ZIP3 bootstrap)} \\
454 −\cmidrule(lr){3-5} \cmidrule(lr){7-10}
455 −Variable & Difference & Indep. & Boot.\ (obs.) & Boot.\ (ZIP3) & (ZIP3, percentile) & Normal & Holm & BH & Romano--Wolf \\
456 −\midrule
457 −% Generated by scripts/08_tables_figures_v2.py from results/qr/iqr_bootstrap_v2.csv
458 −\tabinput{v2_iqr_bootstrap} \\
459 −\bottomrule
460 −\end{tabular}
461 −\begin{tablenotes}
462 −\footnotesize
463 −\item \textit{Notes:} Both quantile regressions estimated on the same 150,000-listing
464 −subsample. ``Indep.'' uses the kernel standard errors of each fit and sets the covariance
465 −to zero. Bootstrap columns use \inum{iqr_v2_n_draws} paired replications in which both
466 −regressions are re-estimated on a resample drawn by observation or by ZIP3 cluster
467 −(\inum{iqr_v2_n_draws_converged_zip3} of \inum{iqr_v2_n_draws} cluster draws converged).
468 −The ``Normal'' $p$ uses the ZIP3-bootstrap standard error with a normal approximation;
469 −Holm and BH adjust it over the \inum{iqr_v2_n_vars} tests; Romano--Wolf uses the joint
470 −bootstrap distribution. A star on the interval marks a percentile interval that excludes
471 −zero. Variables sorted by $|z|$ (ZIP3).
472 −\end{tablenotes}
473 −\end{threeparttable}
474 −\end{table}
475 −% END WRAPPER v2_iqr_bootstrap
261 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
262 +\input{tables/v2_iqr_bootstrap}
476 263
477 264 \subsection{Machine Learning Under Random Validation}
478 265 \label{sec:ml_random}
@@ -497,36 +284,8 @@ The ranking XGBoost $>$ LightGBM $>$ random forest $>$ OLS under random validati
497 284 a sampling accident, even after spatial dependence in the test set is allowed for.
498 285 Exact duplicates across the split do not drive it (\cref{sec:duplicates}).
499 286
500 −% BEGIN WRAPPER v2_ml_comparison (delete this block and keep \input{tables/v2_ml_comparison} once the fragment is a complete float)
501 −\begin{table}[htbp]
502 −\centering
503 −\caption{Out-of-Sample Predictive Performance Under Random Validation (Trimmed Sample, Corrected Design)}
504 −\label{tab:ml_comparison}
505 −\footnotesize
506 −\setlength{\tabcolsep}{3.5pt}
507 −\begin{adjustbox}{max width=\textwidth}
508 −\begin{tabular}{lrrrrrrr}
509 −\toprule
510 − & \multicolumn{3}{c}{Log-price scale} & \multicolumn{3}{c}{Dollar scale} & Best \\
511 −\cmidrule(lr){2-4} \cmidrule(lr){5-7}
512 −Model & $R^2$ (SD) & RMSE & MAE & MAPE (\%) & MdAPE (\%) & MAE (\$) & iteration \\
513 −\midrule
514 −% Generated by scripts/08_tables_figures_v2.py from results/primary/model_comparison_v2.csv
515 −\tabinput{v2_ml_comparison} \\
516 −\bottomrule
517 −\end{tabular}
518 −\end{adjustbox}
519 −\vspace{4pt}
520 −\begin{minipage}{\textwidth}\small
521 −\textit{Notes:} Random 80/20 split of the trimmed sample, seed 42; boosters
522 −averaged over seeds 42--46 with the seed standard deviation of $R^2$ in parentheses.
523 −MAPE and MdAPE follow \cref{eq:mdape} after exponentiating predicted log prices. The
524 −random forest, Ridge, Lasso and Elastic Net rows are re-specified for this version
525 −(the code of the earlier rows was lost; \cref{app:reproducibility}); the penalties of the
526 −regularised models are chosen by five-fold cross-validation.
527 −\end{minipage}
528 −\end{table}
529 −% END WRAPPER v2_ml_comparison
287 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
288 +\input{tables/v2_ml_comparison}
530 289
531 290 \subsection{Validation Design and the OLS--Ensemble Gap}
532 291 \label{sec:ml_geo}
@@ -570,49 +329,11 @@ deviation \inumt{r2_ten_state_xgb_full_fixed1000_sd}), the nested model
570 329 \inumt{r2_ten_state_xgb_full_nested} and the fixed 100-round model
571 330 \inumt{r2_ten_state_xgb_full_fixed100}.
572 331
573 −\begin{figure}[htbp]
574 − \centering
575 − \includegraphics[width=0.95\textwidth]{figures/v2_fig_gap_by_design.png}
576 − \caption{Difference in out-of-sample $R^2$, OLS minus comparator, under each
577 − validation design, with 95\% ZIP3-cluster bootstrap intervals (1,000 draws). Negative
578 − values favour the ensemble. Comparators: XGBoost with rounds selected inside the
579 − training groups, with 100 and with 1,000 rounds, and LightGBM with nested rounds.
580 − Trimmed sample; boosters averaged over five seeds.}
581 − \label{fig:gap_by_design}
582 −\end{figure}
583 −
584 −% BEGIN WRAPPER v2_designs_all (delete this block and keep \input{tables/v2_designs_all} once the fragment is a complete float)
585 −\begin{table}[p]
586 −\centering
587 −\caption{OLS and Tree Ensembles Under Seven Validation Designs: Out-of-Sample $R^2$, Oracle Within-State $R^2$, Dollar Error, Selected Rounds and the Interval of the OLS--Ensemble Difference}
588 −\label{tab:designs}
589 −\scriptsize
590 −\setlength{\tabcolsep}{3pt}
591 −\renewcommand{\arraystretch}{0.92}
592 −\begin{adjustbox}{max width=\textwidth}
593 −\begin{tabular}{llrrrrrl}
594 −\toprule
595 −Design & Model & $N_{\text{test}}$ & $R^2$ (SD) & $R^2_{\text{within}}$ (oracle) & MdAPE (\%) & Rounds & OLS $-$ model, 95\% CI \\
596 −\midrule
597 −% Generated by scripts/08_tables_figures_v2.py from results/geo/metrics.csv, metrics_pooled.csv and results/geo_inference/gap_bootstrap.csv
598 −\tabinput{v2_designs_all} \\
599 −\bottomrule
600 −\end{tabular}
601 −\end{adjustbox}
602 −\vspace{4pt}
603 −\begin{minipage}{\textwidth}\footnotesize
604 −\textit{Notes:} Trimmed sample. Blocked designs score the pooled out-of-fold
605 −predictions; the ten-state holdout withholds CA, NY, TX, FL, OH, CO, NC, WA, IL and GA.
606 −``Nested rounds'' are chosen by inner three-fold grouped cross-validation on the training
607 −groups; the number shown is the selection (mean over folds for the grouped designs).
608 −$R^2$ of the boosters is the mean over seeds 42--46 with the seed standard deviation in
609 −parentheses. $R^2_{\text{within}}$ is the oracle within-state $R^2$ of \cref{eq:within},
610 −an upper bound that uses the test-set state means. The last column is the 95\% percentile
611 −interval of $R^2(\text{OLS}) - R^2(\text{model})$ from 1,000 resamples of the ZIP3
612 −clusters of the evaluated listings.
613 −\end{minipage}
614 −\end{table}
615 −% END WRAPPER v2_designs_all
332 +% Generated by scripts/08b_figures_v2.py (complete float; vector PDF in figures/)
333 +\input{figures/v2_fig_gap_by_design}
334 +
335 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
336 +\input{tables/v2_designs_all}
616 337
617 338 \subsubsection{Blocked Cross-Validation at Three Scales}
618 339 \label{sec:blocked}
@@ -637,34 +358,8 @@ than ZIP5 is not attempted; county identifiers are missing for
637 358 \inum{d_county_fips_null_pct}\% of listings, but ZIP-based blocking covers the range from
638 359 neighbourhood to state.
639 360
640 −% BEGIN WRAPPER v2_grouped_cv (delete this block and keep \input{tables/v2_grouped_cv} once the fragment is a complete float)
641 −\begin{table}[htbp]
642 −\centering
643 −\caption{Grouped Five-Fold Cross-Validation Blocked by ZIP5, ZIP3 and State: Fold-Level and Pooled $R^2$}
644 −\label{tab:grouped_cv}
645 −\begin{threeparttable}
646 −\footnotesize
647 −\setlength{\tabcolsep}{3.5pt}
648 −\begin{tabular}{llrrrrrrr}
649 −\toprule
650 − & & \multicolumn{5}{c}{$R^2$ by held-out fold} & Pooled & $R^2_{\text{within}}$ \\
651 −\cmidrule(lr){3-7}
652 −Blocks & Model & 1 & 2 & 3 & 4 & 5 & $R^2$ & (oracle) \\
653 −\midrule
654 −% Generated by scripts/08_tables_figures_v2.py from results/geo/metrics.csv
655 −\tabinput{v2_grouped_cv} \\
656 −\bottomrule
657 −\end{tabular}
658 −\begin{tablenotes}
659 −\small
660 −\item \textit{Notes:} Folds formed by \texttt{GroupKFold} on the block identifier; each
661 −model trained on the other four folds. Booster values are means over five seeds.
662 −``Pooled'' scores the concatenated out-of-fold predictions. Fold membership for the
663 −state blocks is listed in \cref{tab:fold_membership}.
664 −\end{tablenotes}
665 −\end{threeparttable}
666 −\end{table}
667 −% END WRAPPER v2_grouped_cv
361 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
362 +\input{tables/v2_grouped_cv}
668 363
669 364 \subsubsection{Leave-One-State-Out}
670 365 \label{sec:loso}
@@ -699,73 +394,14 @@ the additional capacity helping small states and hurting the large ones. The re-
699 394 forest reaches \inumt{r2_ten_state_rf_v2} under the ten-state holdout, between OLS and
700 395 the boosters.
701 396
702 −% BEGIN WRAPPER v2_loso_summary (delete this block and keep \input{tables/v2_loso_summary} once the fragment is a complete float)
703 −\begin{table}[htbp]
704 −\centering
705 −\caption{Leave-One-State-Out Validation: Distribution of Accuracy Across 51 Held-Out Jurisdictions}
706 −\label{tab:loso_summary}
707 −\begin{threeparttable}
708 −\scriptsize
709 −\setlength{\tabcolsep}{3.5pt}
710 −\begin{tabular}{lrrrrrrrrr}
711 −\toprule
712 − & \multicolumn{6}{c}{$R^2$ across states} & States & States & Median \\
713 −\cmidrule(lr){2-7}
714 −Model & Median & Weighted & $p_{10}$ & $p_{90}$ & Min & Max & $> 0.5$ & $< 0$ & $|$bias$|$ \\
715 −\midrule
716 −% Generated by scripts/08_tables_figures_v2.py from results/geo_inference/loso_summaries.csv
717 −\tabinput{v2_loso_summary} \\
718 −\bottomrule
719 −\end{tabular}
720 −\begin{tablenotes}
721 −\footnotesize
722 −\item \textit{Notes:} 51 models per row, each trained on all trimmed-sample listings
723 −outside the held-out jurisdiction; booster values are means over five seeds. ``Weighted''
724 −is the listing-weighted mean of state $R^2$; $|$bias$|$ is the median across states of the
725 −absolute mean error of predicted minus actual log price in the held-out state.
726 −\end{tablenotes}
727 −\end{threeparttable}
728 −\end{table}
729 −% END WRAPPER v2_loso_summary
730 −
731 −% BEGIN WRAPPER v2_loso_tests (delete this block and keep \input{tables/v2_loso_tests} once the fragment is a complete float)
732 −\begin{table}[htbp]
733 −\centering
734 −\caption{Paired Comparison of OLS and Tree Ensembles Across the 51 Leave-One-State-Out Folds}
735 −\label{tab:loso_tests}
736 −\begin{threeparttable}
737 −\scriptsize
738 −\setlength{\tabcolsep}{3pt}
739 −\begin{tabular}{lrrrrrr}
740 −\toprule
741 − & \multicolumn{2}{c}{OLS $-$ ensemble, $R^2$} & States OLS & Sign test & Wilcoxon & Listing-weighted \\
742 −\cmidrule(lr){2-3}
743 −Comparison & Mean & Median & better & $p$ & $p$ & difference \\
744 −\midrule
745 −% Generated by scripts/08_tables_figures_v2.py from results/geo_inference/loso_paired_tests.csv
746 −\tabinput{v2_loso_tests} \\
747 −\bottomrule
748 −\end{tabular}
749 −\begin{tablenotes}
750 −\small
751 −\item \textit{Notes:} Differences in state-level $R^2$ between OLS and the comparator
752 −over the 51 held-out jurisdictions; the sign test and the Wilcoxon signed-rank test treat
753 −the states as the units. The listing-weighted difference weights each state by its number
754 −of listings.
755 −\end{tablenotes}
756 −\end{threeparttable}
757 −\end{table}
758 −% END WRAPPER v2_loso_tests
759 −
760 −\begin{figure}[htbp]
761 − \centering
762 − \includegraphics[width=0.9\textwidth]{figures/v2_fig_loso_ols_vs_xgb.png}
763 − \caption{Leave-one-state-out $R^2$ of OLS and of XGBoost with nested rounds, by
764 − held-out state. States above the diagonal are predicted better by the ensemble;
765 − marker size is proportional to the square root of the state's sample size, and the
766 − states of the ten-state holdout are marked. Booster values are means over five seeds.}
767 − \label{fig:loso}
768 −\end{figure}
397 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
398 +\input{tables/v2_loso_summary}
399 +
400 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
401 +\input{tables/v2_loso_tests}
402 +
403 +% Generated by scripts/08b_figures_v2.py (complete float; vector PDF in figures/)
404 +\input{figures/v2_fig_loso_ols_vs_xgb}
769 405
770 406 \subsection{Where the Out-of-State Error Comes From}
771 407 \label{sec:mechanisms}
@@ -817,35 +453,8 @@ An AVM deployed in a new market with a handful of local listings therefore recov
817 453 of its within-market accuracy, and the ordering of models after the correction is the
818 454 ordering of their within-market accuracy, which favours the ensembles.
819 455
820 −% BEGIN WRAPPER v2_feasible_level (delete this block and keep \input{tables/v2_feasible_level} once the fragment is a complete float)
821 −\begin{table}[htbp]
822 −\centering
823 −\caption{Feasible Level Correction: Pooled Out-of-State $R^2$ After Estimating Each Held-Out State's Level from $k$ Local Listings}
824 −\label{tab:feasible_level}
825 −\scriptsize
826 −\setlength{\tabcolsep}{2.5pt}
827 −\begin{adjustbox}{max width=\textwidth}
828 −\begin{tabular}{llrrrrrrr}
829 −\toprule
830 − & & Raw & \multicolumn{5}{c}{Corrected with $k$ listings (median [IQR] over 100 draws)} & Oracle \\
831 −\cmidrule(lr){4-8}
832 −Design & Model & $R^2$ & $k = 5$ & $k = 20$ & $k = 50$ & $k = 100$ & $k = 200$ & level \\
833 −\midrule
834 −% Generated by scripts/08_tables_figures_v2.py from results/geo_inference/feasible_level_correction.csv
835 −\tabinput{v2_feasible_level} \\
836 −\bottomrule
837 −\end{tabular}
838 −\end{adjustbox}
839 −\vspace{4pt}
840 −\begin{minipage}{\textwidth}\footnotesize
841 −\textit{Notes:} For each held-out state, $k$ listings are drawn at random, the mean
842 −prediction error on them is subtracted from the predictions of the remaining listings of
843 −that state, and the pooled $R^2$ is recomputed; median and interquartile range over 100
844 −draws. ``Oracle level'' uses the exact test-set mean error of each state. ``xgb full
845 −nested'' and ``xgb full fixed1000'' denote XGBoost with nested and with 1,000 rounds.
846 −\end{minipage}
847 −\end{table}
848 −% END WRAPPER v2_feasible_level
456 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
457 +\input{tables/v2_feasible_level}
849 458
850 459 \subsubsection{Boosting Rounds and Region Dummies}
851 460 \label{sec:trees}
@@ -867,44 +476,11 @@ below the peak. The pattern is the national-scale counterpart of the observation
867 476 \citet{deppner2024accounting} that random resampling selects overly complex boosters,
868 477 and of the spatial-tuning problem of \citet{schratz2019hyperparameter}.
869 478
870 −\begin{figure}[htbp]
871 − \centering
872 − \includegraphics[width=0.9\textwidth]{figures/v2_fig_trees_curve_seeds.png}
873 − \caption{Out-of-sample $R^2$ of the full XGBoost model as a function of boosting
874 − rounds under the ten-state holdout: on the ten held-out states, on the early-stopping
875 − subset drawn from the training states, and on the random test set. Lines are means
876 − over five seeds; bands show the range across seeds. The nested selection and the
877 − fixed rules of 100 and 1,000 rounds are marked.}
878 − \label{fig:trees_curve}
879 −\end{figure}
880 −
881 −% BEGIN WRAPPER v2_trees_curve (delete this block and keep \input{tables/v2_trees_curve} once the fragment is a complete float)
882 −\begin{table}[htbp]
883 −\centering
884 −\caption{Out-of-Sample $R^2$ by Number of Boosting Rounds, Full XGBoost Model, Five Seeds}
885 −\label{tab:trees_curve}
886 −\begin{threeparttable}
887 −\small
888 −\begin{tabular}{rrrrrr}
889 −\toprule
890 − & Random & Early-stopping & \multicolumn{3}{c}{Ten held-out states} \\
891 −\cmidrule(lr){4-6}
892 −Rounds & test set & set (training states) & Mean & SD & Range over seeds \\
893 −\midrule
894 −% Generated by scripts/08_tables_figures_v2.py from results/geo/trees_curves.csv
895 −\tabinput{v2_trees_curve} \\
896 −\bottomrule
897 −\end{tabular}
898 −\begin{tablenotes}
899 −\small
900 −\item \textit{Notes:} Full model with the corrected design on the trimmed sample. The
901 −random-test column comes from the models trained on the random 80\% split; the other
902 −columns from the models trained on the forty remaining states. Means and standard
903 −deviations over seeds 42--46.
904 −\end{tablenotes}
905 −\end{threeparttable}
906 −\end{table}
907 −% END WRAPPER v2_trees_curve
479 +% Generated by scripts/08b_figures_v2.py (complete float; vector PDF in figures/)
480 +\input{figures/v2_fig_trees_curve_seeds}
481 +
482 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
483 +\input{tables/v2_trees_curve}
908 484
909 485 The capacity effect is not a property of boosting alone; it operates through the region
910 486 dummies. \cref{tab:two_by_two} crosses the rounds rule with the removal of the region
@@ -929,34 +505,8 @@ state medians for most listings. The earlier reading that the imputed school var
929 505 \emph{cause} the extrapolation failure is therefore not supported by the design's own
930 506 ablation; what they do is inflate random-split accuracy, as the next subsection shows.
931 507
932 −% BEGIN WRAPPER v2_two_by_two (delete this block and keep \input{tables/v2_two_by_two} once the fragment is a complete float)
933 −\begin{table}[htbp]
934 −\centering
935 −\caption{Region Dummies, School Variables and Boosting Rounds: Out-of-State $R^2$ of XGBoost Under Three Geographic Designs}
936 −\label{tab:two_by_two}
937 −\begin{threeparttable}
938 −\footnotesize
939 −\setlength{\tabcolsep}{3pt}
940 −\begin{tabular}{llrrrr}
941 −\toprule
942 − & & \multicolumn{3}{c}{$R^2$ by rounds rule (SD over seeds)} & $R^2_{\text{within}}$ \\
943 −\cmidrule(lr){3-5}
944 −Design & Variant & Nested & 100 & 1,000 & (oracle, nested) \\
945 −\midrule
946 −% Generated by scripts/08_tables_figures_v2.py from results/geo_inference/two_by_two_region_rounds.csv
947 −\tabinput{v2_two_by_two} \\
948 −\bottomrule
949 −\end{tabular}
950 −\begin{tablenotes}
951 −\small
952 −\item \textit{Notes:} ``no region'' removes the three Census-region dummies, ``no
953 −school'' the average school rating, school count and nearest-school distance, ``plus
954 −missing'' adds six missingness indicators. Variants without a standard deviation were run
955 −with one seed. Blocked designs score the pooled out-of-fold predictions.
956 −\end{tablenotes}
957 −\end{threeparttable}
958 −\end{table}
959 −% END WRAPPER v2_two_by_two
508 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
509 +\input{tables/v2_two_by_two}
960 510
961 511 \subsubsection{Ablation: What Drives the Random-Split Gain?}
962 512 \label{sec:ablation}
@@ -995,37 +545,8 @@ The state-level information in the features thus helps in sample in proportion t
995 545 precisely it identifies the state, and transfers out of state only when it is a
996 546 continuous proxy for the level rather than a partition.
997 547
998 −% BEGIN WRAPPER v2_ablation (delete this block and keep \input{tables/v2_ablation} once the fragment is a complete float)
999 −\begin{table}[htbp]
1000 −\centering
1001 −\caption{XGBoost Feature Ablation Under Random and Ten-State Validation, Five Seeds: Nested Stages and the Components of the Neighbourhood Block}
1002 −\label{tab:ablation}
1003 −\begin{threeparttable}
1004 −\scriptsize
1005 −\setlength{\tabcolsep}{3pt}
1006 −\begin{tabular}{lrrrrrrr}
1007 −\toprule
1008 − & & Random $R^2$ & \multicolumn{3}{c}{Ten-state holdout} & $\Delta$ ten-state & Realised \\
1009 −\cmidrule(lr){4-6}
1010 −Specification & Features & 1,000 r.\ (SD) & $R^2$ 1,000 r.\ (SD) & $R^2$ 100 r. & $R^2_{\text{within}}$ & vs previous & rounds \\
1011 −\midrule
1012 −% Generated by scripts/08_tables_figures_v2.py from results/geo/ablation.csv
1013 −\tabinput{v2_ablation} \\
1014 −\bottomrule
1015 −\end{tabular}
1016 −\begin{tablenotes}
1017 −\footnotesize
1018 −\item \textit{Notes:} Stages 1--6 add feature blocks cumulatively; 4a--4e add one
1019 −component of the neighbourhood block to stage 3, or modify stage 4; 6a--6d modify the
1020 −full model. Means over seeds 42--46 with the seed standard deviation in parentheses;
1021 −1,000 rounds with early stopping on a random 10\% subset of the training data, the
1022 −realised number of rounds in the last column. $R^2_{\text{within}}$ is the oracle
1023 −within-state $R^2$ at 1,000 rounds. The full model excludes the auxiliary state and
1024 −coordinate columns.
1025 −\end{tablenotes}
1026 −\end{threeparttable}
1027 −\end{table}
1028 −% END WRAPPER v2_ablation
548 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
549 +\input{tables/v2_ablation}
1029 550
1030 551 \subsection{SHAP Analysis and Cross-Model Stability}
1031 552 \label{sec:shap_results}
@@ -1046,33 +567,8 @@ a concrete instance of the general warning that SHAP decomposes the fitted predi
1046 567 function, not the data-generating process \citep{lundberg2017unified, rudin2019stop}: a
1047 568 feature can be important because of what it proxies.
1048 569
1049 −% BEGIN WRAPPER v2_shap_importance (delete this block and keep \input{tables/v2_shap_importance} once the fragment is a complete float)
1050 −\begin{table}[htbp]
1051 −\centering
1052 −\caption{Top 20 Features by Mean Absolute SHAP Value, XGBoost, with LightGBM and Random-Forest Values}
1053 −\label{tab:shap_importance}
1054 −\begin{threeparttable}
1055 −\small
1056 −\begin{tabular}{rlrrr}
1057 −\toprule
1058 −Rank & Feature & XGBoost & LightGBM & Random forest \\
1059 −\midrule
1060 −% Generated by scripts/08_tables_figures_v2.py from results/primary/shap_importance_v2.csv
1061 −\tabinput{v2_shap_importance} \\
1062 −\bottomrule
1063 −\end{tabular}
1064 −\begin{tablenotes}
1065 −\small
1066 −\item \textit{Notes:} Mean absolute contribution to the deviation of the predicted log
1067 −price from its mean, on \inum{d_n_shap_rows} random test listings of the random split;
1068 −ranks by the XGBoost value. TreeSHAP values for XGBoost and LightGBM; the random-forest
1069 −column uses the Saabas path-attribution approximation. School rating and nearest-school
1070 −distance are state medians for \inum{share_school_rating_imputed_pct}\% of listings.
1071 −These are predictive contributions, not implicit prices.
1072 −\end{tablenotes}
1073 −\end{threeparttable}
1074 −\end{table}
1075 −% END WRAPPER v2_shap_importance
570 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
571 +\input{tables/v2_shap_importance}
1076 572
1077 573 \cref{tab:shap_stability} reports pairwise Spearman rank correlations of the mean absolute
1078 574 SHAP values across the three ensembles. XGBoost and LightGBM produce nearly identical
@@ -1084,27 +580,5 @@ in these features; it does not convert any entry into an implicit price, and in
1084 580 of the school variables it reflects the stability of a state identifier that every
1085 581 algorithm finds useful.
1086 582
1087 −% BEGIN WRAPPER v2_shap_cross_model (delete this block and keep \input{tables/v2_shap_cross_model} once the fragment is a complete float)
1088 −\begin{table}[htbp]
1089 −\centering
1090 −\caption{Cross-Model Stability of SHAP Importance: Spearman Rank Correlations}
1091 −\label{tab:shap_stability}
1092 −\begin{threeparttable}
1093 −\small
1094 −\begin{tabular}{lr}
1095 −\toprule
1096 −Pair of models & Spearman $\rho$ \\
1097 −\midrule
1098 −% Generated by scripts/08_tables_figures_v2.py from results/primary/shap_cross_model_v2.csv
1099 −\tabinput{v2_shap_cross_model} \\
1100 −\bottomrule
1101 −\end{tabular}
1102 −\begin{tablenotes}
1103 −\small
1104 −\item \textit{Notes:} Spearman $\rho$ over the mean $|\phi_k|$ values of all features of
1105 −each model, computed on the same \inum{d_n_shap_rows} test listings; ``xgb'', ``lgb''
1106 −and ``rf'' denote XGBoost, LightGBM and the random forest (Saabas approximation).
1107 −\end{tablenotes}
1108 −\end{threeparttable}
1109 −\end{table}
1110 −% END WRAPPER v2_shap_cross_model
583 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
584 +\input{tables/v2_shap_cross_model}
modified paper/sections/robustness.tex +8 −99
@@ -30,32 +30,8 @@ of \cref{sec:qr_results}, whose lot path under the corrected coding rises from
30 30 uncorrected design, which also carried the corrupt parking column, are reproduced and
31 31 documented in \cref{app:corrections}.
32 32
33 −% BEGIN WRAPPER v2_lot_recoding (delete this block and keep \input{tables/v2_lot_recoding} once the fragment is a complete float)
34 −\begin{table}[htbp]
35 −\centering
36 −\caption{Lot-Size Coding and the Lot-Size Elasticity (Natural-Unit OLS, Full and Trimmed Samples)}
37 −\label{tab:lot_recoding}
38 −\begin{threeparttable}
39 −\scriptsize
40 −\setlength{\tabcolsep}{3pt}
41 −\begin{tabular}{lrrrrrr}
42 −\toprule
43 −Coding & $N$ & $R^2$ & $\hat\beta_{\ln\text{lot}}$ & (SE) & $\hat\beta_{\ln\text{sqft}}$ & Indicator \\
44 −\midrule
45 −% Generated by scripts/08_tables_figures_v2.py from results/inference_v2/lot_recoding_v2.csv
46 −\tabinput{v2_lot_recoding} \\
47 −\bottomrule
48 −\end{tabular}
49 −\begin{tablenotes}
50 −\small
51 −\item \textit{Notes:} \inum{n_slopes}-regressor natural-unit specification; HC3 standard
52 −errors. ``v1.1 coding'' is the coding of the earlier estimates; ``v2'' the coding used
53 −throughout this paper. State-median lots are computed from reported positive lots.
54 −``Indicator'' is the coefficient on the lot-missing indicator.
55 −\end{tablenotes}
56 −\end{threeparttable}
57 −\end{table}
58 −% END WRAPPER v2_lot_recoding
33 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
34 +\input{tables/v2_lot_recoding}
59 35
60 36 \subsection{Quantile-Regression Subsample Stability}
61 37 \label{sec:qr_stability}
@@ -79,32 +55,8 @@ markets, and its state-cluster $t$-statistic in the OLS model is
79 55 \inumdd{d_t_state_on_waterfront}. The stability table thus agrees with the clustered
80 56 inference on which coefficients the data pin down.
81 57
82 −% BEGIN WRAPPER v2_qr_stability (delete this block and keep \input{tables/v2_qr_stability} once the fragment is a complete float)
83 −\begin{table}[htbp]
84 −\centering
85 −\caption{Quantile-Regression Stability at $\tau = 0.50$ Across Ten Random Subsamples}
86 −\label{tab:qr_stability}
87 −\begin{threeparttable}
88 −\small
89 −\begin{tabular}{lrrrr}
90 −\toprule
91 −Variable & Mean coefficient & CV (\%) & Sign stability (\%) & Converged \\
92 −\midrule
93 −% Generated by scripts/08_tables_figures_v2.py from results/qr/qr_stability_v2.csv
94 −\tabinput{v2_qr_stability} \\
95 −\bottomrule
96 −\end{tabular}
97 −\begin{tablenotes}
98 −\small
99 −\item \textit{Notes:} CV is the standard deviation divided by the absolute mean of the
100 −standardised coefficient across ten random subsamples of 150,000 listings (seeds listed
101 −in \texttt{src/wp3/config.py}); sign stability is the share of subsamples in which the
102 −coefficient has the sign of its mean; ``Converged'' counts subsamples in which the
103 −interior-point solver converged. Sorted by CV.
104 −\end{tablenotes}
105 −\end{threeparttable}
106 −\end{table}
107 −% END WRAPPER v2_qr_stability
58 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
59 +\input{tables/v2_qr_stability}
108 60
109 61 \subsection{Duplicates Across the Random Split}
110 62 \label{sec:duplicates}
@@ -120,30 +72,8 @@ from \inum{r2_random_v2_xgb} to \inum{r2_random_without_duplicates_xgb_full_nest
120 72 XGBoost; the duplicates are predicted better than average by both models, but they are too
121 73 few to affect the comparison.
122 74
123 −% BEGIN WRAPPER v2_duplicates (delete this block and keep \input{tables/v2_duplicates} once the fragment is a complete float)
124 −\begin{table}[htbp]
125 −\centering
126 −\caption{Random-Split $R^2$ With and Without Test Listings That Have an Exact Duplicate in the Training Set}
127 −\label{tab:duplicates}
128 −\begin{threeparttable}
129 −\small
130 −\begin{tabular}{lrrrrr}
131 −\toprule
132 −Model & $N_{\text{test}}$ & With duplicate & $R^2$, all & $R^2$, no duplicates & $R^2$, duplicates only \\
133 −\midrule
134 −% Generated by scripts/08_tables_figures_v2.py from results/geo_inference/duplicates_random_split_v2.csv
135 −\tabinput{v2_duplicates} \\
136 −\bottomrule
137 −\end{tabular}
138 −\begin{tablenotes}
139 −\small
140 −\item \textit{Notes:} Duplicates defined on ZIP code, living area, bedrooms, bathrooms,
141 −year built and price. ``xgb full nested'' and ``xgb full fixed1000'' denote XGBoost with
142 −nested and with 1,000 rounds (seed 42).
143 −\end{tablenotes}
144 −\end{threeparttable}
145 −\end{table}
146 −% END WRAPPER v2_duplicates
75 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
76 +\input{tables/v2_duplicates}
147 77
148 78 \subsection{Seed Dispersion of the Small Differences}
149 79 \label{sec:seeds}
@@ -157,29 +87,8 @@ the gain from the missingness indicators under random validation; the gains from
157 87 missingness indicators under the ten-state holdout (nested and 1,000 rounds) do not,
158 88 and are therefore not interpreted.
159 89
160 −% BEGIN WRAPPER v2_seed_flags (delete this block and keep \input{tables/v2_seed_flags} once the fragment is a complete float)
161 −\begin{table}[htbp]
162 −\centering
163 −\caption{Seed Dispersion of Small Differences Between XGBoost Variants}
164 −\label{tab:seed_flags}
165 −\begin{threeparttable}
166 −\small
167 −\begin{tabular}{lrrl}
168 −\toprule
169 −Difference in $R^2$ & Value & SD over seeds & Exceeds 2 SD \\
170 −\midrule
171 −% Generated by scripts/08_tables_figures_v2.py from results/geo/metrics.csv
172 −\tabinput{v2_seed_flags} \\
173 −\bottomrule
174 −\end{tabular}
175 −\begin{tablenotes}
176 −\small
177 −\item \textit{Notes:} The standard deviation of a difference is the root of the sum of
178 −the two seed variances (five seeds each).
179 −\end{tablenotes}
180 −\end{threeparttable}
181 −\end{table}
182 −% END WRAPPER v2_seed_flags
90 +% Generated by scripts/08_tables_figures_v2.py (complete float; see the comment inside the fragment)
91 +\input{tables/v2_seed_flags}
183 92
184 93 \subsection{Summary}
185 94 \label{sec:robustness_matrix}
modified paper/tables/v2_ablation.tex +40 −15
Large diff (56 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_attrition.tex +26 −6
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_cluster_se.tex +59 −34
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_conley.tex +48 −26
Large diff (75 lines) is not loaded by default to keep the page fast.Load diff
added paper/tables/v2_corrigenda.tex +39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_descriptives.tex +37 −17
Large diff (63 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_designs_all.tex +104 −60
Large diff (164 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_duplicates.tex +23 −3
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_fe_progression.tex +27 −3
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_feasible_level.tex +36 −6
Large diff (42 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_fold_membership.tex +28 −6
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_gap_bootstrap.tex +63 −35
Large diff (98 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_grouped_cv.tex +32 −9
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added paper/tables/v2_headline.tex +29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added paper/tables/v2_implausible.tex +32 −0
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
added paper/tables/v2_interpretation.tex +26 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_iqr_bootstrap.tex +37 −15
Large diff (53 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_kennedy.tex +30 −9
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added paper/tables/v2_legacy_ml_comparison.tex +29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_loso_states.tex +53 −30
Large diff (104 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_loso_summary.tex +27 −6
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_loso_tests.tex +26 −5
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_lot_recoding.tex +33 −12
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_lot_reconciliation.tex +28 −7
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_missingness.tex +30 −10
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_ml_comparison.tex +38 −10
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_moran_full.tex +30 −8
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_ols_full.tex +57 −68
Large diff (126 lines) is not loaded by default to keep the page fast.Load diff
added paper/tables/v2_ols_main.tex +59 −0
Large diff (59 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_paired_loss.tex +23 −3
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_pooled_vs_interacted.tex +24 −3
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_qr_cluster_se.tex +35 −13
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_qr_coefficients.tex +54 −17
Large diff (72 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_qr_legacy_check.tex +27 −5
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_qr_stability.tex +35 −15
Large diff (50 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_seed_flags.tex +24 −4
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_shap_cross_model.tex +23 −3
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_shap_importance.tex +42 −20
Large diff (62 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_sse_decomposition.tex +106 −60
Large diff (166 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_state_heterogeneity.tex +35 −13
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_trees_curve.tex +33 −12
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_two_by_two.tex +40 −15
Large diff (55 lines) is not loaded by default to keep the page fast.Load diff
modified paper/tables/v2_zestimate_selection.tex +47 −14
Large diff (61 lines) is not loaded by default to keep the page fast.Load diff
modified results/geo_inference/seed_dispersion_flags.csv +4 −4
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
modified results/inline_numbers.json +2074 −359
Large diff (3,722 lines) is not loaded by default to keep the page fast.Load diff
modified run_all.sh +3 −1
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
modified scripts/08_tables_figures_v2.py +990 −398
Large diff (1,708 lines) is not loaded by default to keep the page fast.Load diff
added scripts/08b_figures_v2.py +422 −0
Large diff (422 lines) is not loaded by default to keep the page fast.Load diff
added scripts/preview_exhibits.sh +14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added src/wp3/exhibits.py +243 −0
Large diff (243 lines) is not loaded by default to keep the page fast.Load diff
added src/wp3/plotting_v2.py +66 −0
Large diff (67 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme