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>
95 changed files +5,692 −2,469
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} |