SPB Git

spb/wp2_uqo Public

UQO Working Paper No. 2 — Decoding Real Estate Descriptions: text-based hedonic analysis of housing listings.

TeX 73.8% Python 26%
3.8 KB · 105 lines python
Raw Blame History
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher — contact@spboucher.ai3#4"""Step 3 — Fit hedonic models A-E and export the paper's result tables.56Inputs : data/processed/hedonic_maison_results.csv7Outputs: results/model_comparison.csv        (Table 4 of the paper)8         results/coefficients_model_D.csv    (full model, Table 5/6 source)9         results/coefficients_model_E.csv    (parsimonious model)10         results/descriptive_stats.csv       (Table 2 source)11         results/similarity_stats.csv        (Table 3 source)12"""1314import sys15from pathlib import Path1617import numpy as np18import pandas as pd1920sys.path.insert(0, str(Path(__file__).resolve().parents[1]))2122from src import config23from src.models import fit_all_models24from src.references import ENGLISH_LABELS, SIM_COLS2526MODEL_NAMES = {27    "A": "Structural only",28    "B": "+ Description length",29    "C": "+ Semantic similarities (20)",30    "D": "Full model (B + C)",31    "E": "Parsimonious (significant only)",32}333435def coefficient_table(model, variables):36    """Coefficients with HC3 errors, p-values and percentage price impacts."""37    rows = []38    for v in variables:39        if v not in model.params:40            continue41        coef = model.params[v]42        rows.append({43            "variable": v,44            "label": ENGLISH_LABELS.get(v.replace("sim_", ""), v),45            "coefficient": coef,46            "std_error": model.bse[v],47            "t_value": model.tvalues[v],48            "p_value": model.pvalues[v],49            "impact_pct": (np.exp(coef) - 1) * 100,50        })51    return pd.DataFrame(rows)525354def main():55    df = pd.read_csv(config.ANALYSIS_CSV)56    print(f"{len(df):,} observations")5758    models, specs, sig_sims = fit_all_models(df)59    config.RESULTS_DIR.mkdir(parents=True, exist_ok=True)6061    comparison = pd.DataFrame({62        "model": list(models),63        "specification": [MODEL_NAMES[k] for k in models],64        "r2": [m.rsquared for m in models.values()],65        "adj_r2": [m.rsquared_adj for m in models.values()],66        "aic": [m.aic for m in models.values()],67        "bic": [m.bic for m in models.values()],68        "k": [int(m.df_model) for m in models.values()],69        "n": [int(m.nobs) for m in models.values()],70    })71    comparison["delta_r2_vs_A"] = comparison["r2"] - models["A"].rsquared72    comparison.to_csv(config.RESULTS_DIR / "model_comparison.csv", index=False)73    print(comparison.round(4).to_string(index=False))7475    # F-test of the semantic block (D vs A), as reported in the paper76    mA, mD = models["A"], models["D"]77    df_diff = mD.df_model - mA.df_model78    f_stat = ((mA.ssr - mD.ssr) / df_diff) / (mD.ssr / mD.df_resid)79    from scipy import stats80    f_pval = 1 - stats.f.cdf(f_stat, df_diff, mD.df_resid)81    print(f"\nJoint F-test (D vs A): F = {f_stat:.2f}, p = {f_pval:.2e}")82    print(f"Significant similarities in D (p<0.05): {len(sig_sims)}/{len(SIM_COLS)}")8384    coefficient_table(mD, specs["D"]).to_csv(85        config.RESULTS_DIR / "coefficients_model_D.csv", index=False)86    coefficient_table(models["E"], specs["E"]).to_csv(87        config.RESULTS_DIR / "coefficients_model_E.csv", index=False)8889    desc_vars = ["price", "log_price"] + config.STRUCTURAL_VARS + ["remarks_length"]90    df[desc_vars].describe().T.to_csv(config.RESULTS_DIR / "descriptive_stats.csv")9192    sim_stats = df[SIM_COLS].describe().T93    sim_stats.index = [ENGLISH_LABELS[c.replace("sim_", "")] for c in sim_stats.index]94    sim_stats["corr_log_price"] = [95        df[c].corr(df["log_price"]) for c in SIM_COLS96    ]97    sim_stats.to_csv(config.RESULTS_DIR / "similarity_stats.csv")9899    print(f"\n-> {config.RESULTS_DIR}/model_comparison.csv, coefficients_model_D.csv, "100          f"coefficients_model_E.csv, descriptive_stats.csv, similarity_stats.csv")101102103if __name__ == "__main__":104    main()105