🇬🇧 English · 🇪🇸 Español
Household preferences with exact expenditure-function welfare
puremacro.trade.household is a solver-independent household demand layer:
four preference rules over sector composites, a benchmark-normalized
expenditure function, Marshallian and Hicksian demand, analytic Slutsky
matrices and exact money-metric EV/CV. Two bridges connect it to the trade
calibrations and equilibria of puremacro.trade.
import numpy as np
from puremacro.trade.household import calibrate_household, fit_supernumerary_share
x0 = np.array([[25., 10.], [45., 60.], [30., 30.], [0., 0.]]) # benchmark spending, sectors x regions
eta = np.array([[.6, .5], [.9, 1.1], [1.3, 1.2], [np.nan, np.nan]]) # Engel targets (NaN on absent sectors)
prefs = calibrate_household(x0, "stone_geary", expenditure_elasticities=eta,
supernumerary_share=[.4, .5],
sector_codes=["AGR", "MAN", "SRV", "NONE"], country_codes=["HOME", "ROW"])
print(prefs.summary())
print(prefs.calibration.to_dataframe()) # adding-up repair, admissible bounds, subsistence shares
prices = np.array([[1.3, .8], [.9, 1.25], [1.1, 1.05], [2., 3.]])
budget = np.array([105., 110.])
state = prefs.evaluate(prices, budget, validate=True)
welfare = prefs.welfare(prices, budget, validate=True) # EV/CV against the calibration benchmark
print(welfare.to_dataframe())
S = prefs.slutsky(prices, budget) # (4, 4, 2) substitution matrices
What it computes
All arrays have shape (sector, region), except budgets, utility and welfare,
which have shape (region,). A unit of a sector composite is the quantity that
costs one monetary unit at its benchmark purchaser price, so the benchmark
spending matrix x0 is also the benchmark quantity matrix and benchmark prices
and utility are one. Regional budgets are m0 = x0.sum(0), shares w = x0 / m0.
Origin substitution, taxes and the general-equilibrium budget are external.
| rule | demand | expenditure function |
|---|---|---|
fixed_baskets |
q = x0 m / (m0 sum_i w_i p_i) |
e(p, u) = m0 u sum_i w_i p_i |
cobb_douglas |
q_i = w_i m / p_i |
e(p, u) = m0 u prod_i p_i^w_i |
stone_geary |
q_i = gamma_i + beta_i (m - sum_j p_j gamma_j) / p_i |
e(p, u) = sum_i p_i gamma_i + S0 P(p) u, P(p) = prod_i p_i^beta_i, S0 = lambda m0 |
ces |
q_i = w_i m p_i^-sigma P^(sigma-1) |
e(p, u) = m0 P(p) u, P(p) = (sum_i w_i p_i^(1-sigma))^(1/(1-sigma)) |
For Stone-Geary the surplus is S = m - sum_j p_j gamma_j and the
benchmark-normalized utility is u = S / (S0 P(p)). The full cost-of-living
index at benchmark utility is e(p, 1) / m0; P alone prices discretionary
spending and is not that index. ces with sigma = 0 reproduces fixed baskets
and with sigma = 1 Cobb-Douglas (an expm1/log1p form keeps the index
stable near one). Hicksian demand is h(p, u) = grad_p e(p, u); the analytic
substitution matrices are
LES / Cobb-Douglas: S_ij = S * (beta_i beta_j / (p_i p_j) - delta_ij beta_i / p_i^2)
CES: S_ij = sigma * (h_i h_j / m - delta_ij h_i / p_i)
fixed baskets: S = 0
They are symmetric, negative semidefinite and satisfy S p = 0.
Welfare
With 0 the base state and 1 the comparison state,
Both are positive for welfare gains; cv is the amount removable at new prices,
not the compensation required. Against the calibration benchmark this reduces
to EV = S0 (u1 - 1) for every rule (including fixed baskets) and
CV = m1 - e(p1, 1); for LES CV = P(p1) EV. HouseholdWelfareResult reports
ev, cv, ev_pct_consumption, cv_pct_consumption (both divided by the base
household budget e(p0, u0) so preference systems share one denominator),
utility, base_utility, cost_index and price_index per region.
compute_household_welfare(prefs, p1, m1, base_prices=p0, base_budget=m0) takes
an arbitrary base state; prefs.welfare(p, m) uses the benchmark.
Calibration from Engel targets
At the benchmark the LES expenditure elasticity is eta_i = beta_i / w_i, so
beta = w eta. Engel adding-up sum_i w_i eta_i = 1 is imposed by dividing each
region's targets by their expenditure-weighted mean; the original means, the
maximum adjustment and the repaired targets are recorded in
prefs.calibration (a HouseholdCalibrationResult; repair_adding_up=False
rejects inconsistent targets). Subsistence is gamma = x0 (1 - lambda eta) with
lambda = S0 / m0 the supernumerary share, which must satisfy
0 < lambda <= 1 / max_i eta_i in every region for gamma >= 0. lambda = 1
gives Cobb-Douglas. Unobserved sectors stay exact structural zeros; omitted
targets explicitly mean unit elasticities.
lambda is not identified by input-output data. supernumerary_share=None
(the default) uses the assumed value 0.5 and records
metadata["supernumerary_share_default_is_assumption"] = True (a supplied
share records False); the calibration audit states that Stone-Geary surplus
shares are supplied assumptions or external estimates, not identified by the
input-output accounts and Engel targets. fit_supernumerary_share projects
external compensated own-price elasticity targets onto the LES benchmark
identity xi_ii = -lambda eta_i (1 - beta_i) by regional weighted least
squares, clipped to the admissible interval, and returns a
SupernumeraryFitResult with the unconstrained fit, bounds, clipping flags,
per-sector residuals and weighted RMSE. Zero-weight categories have no
influence on the fit (their target values can be placeholders). A region with
no weighted price response raises.
own = -prefs.supernumerary_share * prefs.eta * (1 - prefs.beta) # implied targets
fit = fit_supernumerary_share(x0, eta, own)
print(fit.summary(), fit.to_dataframe())
Domain and complex step
The evaluation kernels retain complex inputs, so prefs.evaluate(p + 1e-25j * dp, m)
gives exact directional derivatives, and never clip. Pass validate=True (or
call prefs.validate_domain(p, m)) at accepted real states: nonpositive
prices, nonfinite inputs and exhausted surplus raise HouseholdDomainError,
a ValueError subclass. Strictly positive surplus is required for an interior
demand derivative and positive normalized utility.
Relation to the flexible module
puremacro.trade.flexible also carries a Stone-Geary demand
(FlexiblePreferenceConfig), but it is a different system. Its supernumerary
spending buys the benchmark basket in fixed real proportions, and its
subsistence quantities scale with income (tanh smoothing), so it has no
expenditure function. With zero subsistence it equals the fixed_baskets rule
here at every price and budget; with positive subsistence both reproduce the
benchmark at benchmark prices and budget and differ elsewhere (the test suite
checks both statements on a synthetic 2-country, 2-sector calibration).
FlexibleTradeEquilibriumResult.welfare_decomposition therefore remains a
proxy, as its docstring states; this module gives the exact
expenditure-function alternative.
Bridges to trade calibrations and equilibria
import numpy as np
from puremacro.trade import calibrate_trade_model, solve_trade_equilibrium, compute_hicksian_welfare
from puremacro.trade.household import (calibrate_household, household_expenditure_from_calibration,
household_prices_from_result, compute_household_welfare_from_results)
# Analytic 2-country x 2-sector table with three final uses (C, I, X); rows: sellers, taxes, labour, capital.
Z = np.array([[10., 15., 5., 5.], [15., 20., 10., 10.], [5., 5., 12., 18.], [10., 10., 18., 22.]])
y = np.array([100., 150., 120., 180.])
va = y - Z.sum(0) - .05 * y
data = np.zeros((7, 10))
data[:4, :4] = Z
data[4, :4], data[5, :4], data[6, :4] = .05 * y, 2 / 3 * va, va / 3
fd = y - Z.sum(1)
data[:2, 4:] = fd[:2, None] * np.array([.50, .25, .05, .10, .08, .02])
data[2:4, 4:] = fd[2:, None] * np.array([.10, .08, .02, .50, .25, .05])
data[4, 4:] = .02 * data[:4, 4:].sum(0)
calib = calibrate_trade_model(data, ns=2, nc=2, nfd=3, country_codes=["HOME", "ROW"], sector_codes=["GOODS", "SERV"])
x0 = household_expenditure_from_calibration(calib, category=0) # (2, 2) benchmark purchaser spending
rates = np.array([.15, 0.]) # HOME taxes all its imports at 15 percent
base = solve_trade_equilibrium(calib, tol=1e-10)
cf = solve_trade_equilibrium(calib, tau=rates, tau_fd=rates, tol=1e-10)
prices, budget = household_prices_from_result(cf, calib) # (2, 2) composite prices, (2,) budgets
for rule, kw in (("fixed_baskets", {}), ("cobb_douglas", {}), ("stone_geary", {"supernumerary_share": .6}),
("ces", {"ces_elasticity": .5})):
prefs = calibrate_household(x0, rule, sector_codes=calib.sector_codes, country_codes=calib.country_codes, **kw)
welfare = compute_household_welfare_from_results(prefs, calib, cf, base_result=base)
print(rule, np.round(welfare.ev_pct_consumption, 4))
hicks = compute_hicksian_welfare(calib, cf, base_result=base, target_country="HOME")
print("Leontief basket", round(hicks.ev_pct_consumption, 4)) # equals the fixed_baskets household
household_expenditure_from_calibration(calib, category) returns
x0[s, c] = A[s, c] theta[k, c] (l + k + T)[c], where A is the share of
sector s in the category's Leontief origin basket (sum_i afd[i, s, k, c]).
Purchaser spending includes the final-use tax, a uniform share of the
category, so basic-price and purchaser sector shares coincide. Category 1
(investment) is rejected because its spending subtracts foreign saving;
negative cells raise and are named.
household_prices_from_result(result, calib, category=0, tau_fd=None) returns
the tariff-inclusive Leontief composite prices relative to the benchmark,
P[s, c] = sum_i afd[i, s, k, c] p_i tf[i, s, k, c] / sum_i afd[i, s, k, c], and
the category's purchaser budget theta[k] (w l + r k + T).
- Results are re-audited exactly as
compute_hicksian_welfaredoes (recorded tariff schedules, residuals, fields) and valued from the consistent accounting evaluator:tfis the recordedfinal_tariff_multipliers, the budget is the evaluator'sexpenditure.tau_fdis optional; when given (the solve's national rates or a full multiplier array) it is resolved with the solver's conventions and cross-checked against the recorded schedule, and a disagreement raises. - Flexible-model results are not supported by the bridge: compute Armington
composite prices with
puremacro.trade.flexibleand callcompute_household_welfaredirectly.
tol (default 1e-8) compares recorded fields with their recomputed values and
a supplied tau_fd with the recorded schedule. Equilibrium residuals are
checked at the solver's recorded tolerance instead, and domestic final-demand
multipliers must equal one to 1e-14. Errors name the function called and, in
compute_household_welfare_from_results, the failing state (eq_result or
base_result); the welfare metadata key tariff_schedule_verification
records how each state's schedule was verified. Preference labels must equal
the calibration's country_codes and sector_codes unless they are the
generated defaults (C00, S00, ...), and the welfare result carries the
calibration's country labels.
With rule="fixed_baskets" and a single consumption category the bridge
reproduces compute_hicksian_welfare (Leontief basket) exactly; the test suite
checks EV, CV and percentages to 1e-10 relative for every country on the
synthetic calibration with a 15 percent tariff (categories 0 and 2) and on a
non-square 3-country, 2-sector table. Non-homothetic and CES households give
different numbers by construction. Bridged EV/CV inherit the equilibrium's
numeraire and foreign-saving conventions: with nonzero foreign saving and the
default foreign_saving_units="numeraire" the results depend on the country
order, as for compute_hicksian_welfare.
A provider table's consumption category may aggregate household, NPISH and government final consumption (the OECD ICIO final uses HFCE + NPISH + GGFC form category 0 of the bundled table), so a "household" calibrated from it values that aggregate. A household-only split needs a source with separate final uses (see Hicksian consumption welfare).
What is and is not validated
Checked in tests/test_trade_household.py:
- behavioral and duality tests on an analytic 4x2 fixture: benchmark
reproduction for every rule and CES elasticity in {0, 0.4, 1, 2}, budget
adding-up, homogeneity, Engel derivatives,
lambda = 1equals Cobb-Douglas, expenditure duality, Shephard's lemma, Slutsky symmetry and negative semidefiniteness by central differences, complex-step derivatives, domain errors, the admissibility boundary, CES limits, exact recovery of a generatinglambda, binding bounds and zero-weight influence; - analytic Slutsky matrices against central differences in every region,
exact symmetry,
S p = 0and negative semidefiniteness; - a primal SLSQP expenditure-minimization oracle for Stone-Geary, Cobb-Douglas
and CES (0.7, 1.8): minimized expenditure within 1e-6 relative of
e(p, u)and minimizer within 2e-4 of the Hicksian demand; - general-base welfare: identity gives exactly zero, reversed comparisons swap EV and CV, benchmark base reproduces the closed forms; currency scaling changes levels and preserves percentages, utility and indices;
- the calibration bridge on synthetic tables and the relation to the flexible module's demand (equal without subsistence, equal only at the benchmark with it);
- the result bridges on a square synthetic table and a non-square 3-country,
2-sector table: fixed-basket EV/CV against
compute_hicksian_welfarefor every country (categories 0 and 2), an independent certificate (sector purchaser spending read from the model's final-demand flows equalsP_s q_sto 1e-12), rejection of tampered fields and wrong schedules, label checks and error messages.
Not validated here: mapping an input-output final-use category to a household
is a disclosed transfer and reconciliation, not an estimation from household
microdata. Supplied Engel targets generally cannot be matched exactly by LES,
and a calibrated parameter is not thereby empirically identified. LES
substitution restrictions are substantive: matching income elasticities and
projecting compensated own-price targets does not reproduce a general
substitution matrix. The results are static policy comparisons, not lifetime
welfare. No external demand-system targets are bundled; embed the household in
a general-equilibrium experiment through the bridges and the solvers of
puremacro.trade.
Related documentation: consistent trade accounting, Hicksian consumption welfare, Hicksian tariff policy, distributional incidence, quantitative spatial and trade GE.