SPB Git forge

spb/wp18_uqo

Public
6commits 1branches 0releases
5.4 MBsize
maindefault branch
yesterdaylast push
TeX 53.2% Python 46.6%
6.0 KB · 97 lines python
Raw Blame History
1"""WP18 — calibration targets and model moments.23The model is illustrative: parameters come from configs/baseline.yaml with their provenance4("illustrative" unless a verified source is given). This module (i) lists the verified Canadian5moments that can be compared with the model (research/literature_review.md §1 and §7; all in6research/literature_matrix.csv), (ii) computes the corresponding model moments from a pooled7cross-section, and (iii) writes results/data/calibration_targets.csv. No moment is fitted by an8estimator; the comparison is reported as is. Units: model values are in units of the average95-year labour income (so "x annual incomes" = 5 x model units).10"""11from __future__ import annotations1213import numpy as np14import pandas as pd1516from .distributions import PeriodRecord, gini, rank_rank_slope17from .model import Params1819# Verified targets (source key = BibTeX key in paper/references.bib). Values quoted verbatim in20# research/literature_review.md; "comparison only" = not used to set any parameter.21VERIFIED_TARGETS = [22    dict(moment="ownership_rate_all", description="Share of households owning (model: all ages; Canada: plateau near 3/4 by 65, Hou 2010)",23         target=0.75, target_note="plateau near age 65, about three quarters (age profile, not the aggregate)",24         source="hou2010homeownership", status="verified, comparison only"),25    dict(moment="median_nw_ratio_owner_renter_under35", description="Median net worth, owners / non-owners, families under 35",26         target=457100 / 44000, target_note="$457,100 vs $44,000 (SFS 2023)", source="statcan2024survey",27         status="verified, comparison only"),28    dict(moment="share_with_mortgage", description="Share of families holding a mortgage", target=0.39,29         target_note="39 % of families (SFS 2023)", source="statcan2024survey", status="verified, comparison only"),30    dict(moment="share_first_buyers_with_gift", description="Share of first-time buyers receiving a family gift",31         target=0.28, target_note="28 % in 2021 (CIBC analysis quoted by Mirdamadi and Khalid 2023)",32         source="mirdamadi2023parents", status="verified (secondary quotation), comparison only"),33    dict(moment="mean_gift_in_annual_incomes", description="Average family gift to first-time buyers, in years of average income",34         target=np.nan, target_note="about $80,000 in 2021 (CIBC via Mirdamadi and Khalid 2023); no verified income denominator",35         source="mirdamadi2023parents", status="verified amount, denominator illustrative"),36    dict(moment="supply_elasticity", description="Long-run price elasticity of housing supply", target=np.nan,37         target_note="no verified Canadian estimate (literature_review §7); eps = 1 illustrative, range 0.05-2 in experiment 7",38         source="—", status="ILLUSTRATIVE"),39]404142def model_moments(P: Params, rec: PeriodRecord, p: float, rho: float) -> dict:43    age = rec.age44    own = rec.owner1()45    nw = rec.net_worth1(P)46    under35 = age <= 247    mid = (age >= 3) & (age <= 8)48    old = age >= 949    m = {}50    m["ownership_rate_all"] = float(own.mean())51    m["ownership_rate_under35"] = float(own[under35].mean())52    m["ownership_rate_35_64"] = float(own[mid].mean())53    m["ownership_rate_65plus"] = float(own[old].mean())54    m["ownership_rate_by_age"] = [float(own[age == j].mean()) for j in range(1, P.J + 1)]55    med_o = float(np.median(nw[under35 & own])) if (under35 & own).any() else np.nan56    med_r = float(np.median(nw[under35 & ~own])) if (under35 & ~own).any() else np.nan57    m["median_nw_owner_under35"] = med_o58    m["median_nw_renter_under35"] = med_r59    m["median_nw_ratio_owner_renter_under35"] = med_o / med_r if med_r and med_r > 1e-6 else np.inf60    m["mean_nw_ratio_owner_renter_under35"] = float(nw[under35 & own].mean() / max(nw[under35 & ~own].mean(), 1e-9))61    m["share_with_mortgage"] = float((rec.b1 > 1e-8).mean())62    m["share_owners_with_mortgage"] = float((rec.b1[own] > 1e-8).mean()) if own.any() else np.nan63    first_buy = (rec.choice == 2) & (rec.h0 < 0) & (~rec.is_old)64    gifts = rec.gift_received[first_buy]65    m["share_first_buyers_with_gift"] = float((gifts > 1e-6).mean()) if first_buy.any() else np.nan66    g1 = rec.gift_received[(age == 1) & ~rec.is_old]67    m["mean_gift_received"] = float(g1.mean())68    m["mean_gift_in_annual_incomes"] = float(g1.mean() * P.period_years)69    m["median_gift_received"] = float(np.median(g1))70    m["share_gift_positive"] = float((g1 > 1e-6).mean())71    e7 = rec.estate_received[(age == P.j_estate) & rec.is_old]72    e7 = e7[np.isfinite(e7)]73    m["mean_estate_received"] = float(e7.mean()) if len(e7) else np.nan74    m["share_estate_positive"] = float((e7 > 1e-6).mean()) if len(e7) else np.nan75    m["gini_net_worth"] = gini(nw)76    m["gini_net_worth_age7"] = gini(nw[age == P.j_gift])77    m["gini_consumption"] = gini(rec.c)78    hv = np.where(own, P.h_grid[np.maximum(rec.h1, 0)], 0.0) * p79    m["median_house_value_to_annual_income"] = float(np.median(hv[own]) * P.period_years / 1.0) if own.any() else np.nan80    m["mean_ltv_new_loans"] = float((rec.Bnew[rec.Bnew > 0] / hv[rec.Bnew > 0]).mean()) if (rec.Bnew > 0).any() else np.nan81    m["rent_price_ratio_annual"] = float(rho / p / P.period_years)82    m["share_forced_sale"] = float((rec.flag == 1).mean())83    m["share_policy_clipped"] = float((rec.flag == 2).mean())84    # mobility: parent tenure at gift vs child tenure at the same age (j_gift) — young members know parent tenure85    child7 = (age == P.j_gift) & rec.is_old86    # the old member at age 7 was the young member of the same slot: parent tenure stored at birth is not87    # carried after promotion in the record; mobility statistics are computed in simulation.py from the panel.88    return m899091def targets_table(P: Params, moments: dict) -> pd.DataFrame:92    rows = []93    for t in VERIFIED_TARGETS:94        rows.append(dict(moment=t["moment"], description=t["description"], target=t["target"], target_note=t["target_note"],95                         model=moments.get(t["moment"], np.nan), source=t["source"], status=t["status"]))96    return pd.DataFrame(rows)97