TeX 53.2%
Python 46.6%
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