🇬🇧 English · 🇪🇸 Español
Distributional household incidence
puremacro.trade.distributional combines observed group spending baskets with
supplied purchaser prices, nominal income changes and fiscal transfers. It
reports exact static equivalent variation (EV) and compensating variation (CV)
using the existing household preference engine.
This is partial-equilibrium household incidence conditional on the supplied prices and incomes. Those prices can come from an audited GE solution or an explicitly assumed scenario. Household demand is not fed back into production, trade or factor prices. The output is not a new GE equilibrium or a causal estimate of a tariff's effect. A survey basket alone does not identify price pass-through, factor-income responses or preference elasticities.
Supply group means and population counts
prepare_household_groups accepts:
| Input | Required meaning |
|---|---|
expenditure |
DataFrame indexed by group, with labeled sectors as columns; mean monetary spending per represented unit, not group totals |
population_weights |
Positive expansion counts by group, in the same represented unit |
baseline_budget |
Optional labeled Series, checked against expenditure row sums |
income_exposure |
Optional group-by-income-source DataFrame of baseline monetary income per represented unit |
monetary_unit, period, population_unit |
Explicit currency scale, expenditure period, and represented unit |
provenance |
Source dictionary; source is required, and synthetic/regression-fixture flags are retained |
Labels must be unique nonempty strings. Inputs are aligned by labels; missing, extra or duplicate labels raise. Purchaser prices must be finite and positive. Spending and income exposures must be finite and nonnegative; every group must have positive total spending. No groups or missing observations are silently dropped or imputed.
For survey observations with expansion weights w_i, construct each group mean
as sum(w_i * spending_i_sector) / sum(w_i) and its population count as
sum(w_i). Use the same complete household sample and denominator for every
sector, retaining households with zero purchases. A published mean among
positive purchasers is not the unconditional group mean. Do not sum survey
weights repeatedly across sector records. Choose household or person weights
consistently with the spending unit.
Group means represent representative groups. Nonlinear welfare evaluated at group means is not the survey-weighted mean welfare of individual households. Use one row per household when that distinction matters and the data support it.
Income exposure may exceed observed spending: saving, noncash resources and
other income can differ. The implemented scenario assumes all incremental
exposed nominal income is spent, holding the remaining baseline spending
fixed. This is a disclosed behavioral assumption, not an estimated propensity
to consume. A source can distinguish both factor and sector, such as
manufacturing_labor or agriculture_capital.
A small synthetic scenario
The following numbers are invented to demonstrate the interface. They are not a calibrated economy, a survey estimate, or a tariff forecast.
import pandas as pd
from puremacro.trade.distributional import (
prepare_household_groups, compute_distributional_welfare,
)
spending = pd.DataFrame(
[[60.0, 40.0], [40.0, 160.0]],
index=["lower_income", "higher_income"], columns=["food", "other"],
)
groups = prepare_household_groups(
spending,
pd.Series([300.0, 100.0], index=spending.index),
income_exposure=pd.DataFrame(
{"labor": [80.0, 100.0], "capital": [20.0, 100.0]}, index=spending.index,
),
monetary_unit="illustrative currency units", period="quarter",
population_unit="households",
provenance={"source": "two-group synthetic illustration", "is_synthetic": True},
)
result = compute_distributional_welfare(
groups,
base_prices=pd.Series({"food": 1.0, "other": 1.0}),
prices=pd.Series({"food": 1.20, "other": 0.90}),
factor_income_changes=pd.Series({"labor": 0.10, "capital": -0.05}),
transfer_total=1400.0,
transfer_weights=pd.Series({"lower_income": 2.0, "higher_income": 1.0}),
rule="cobb_douglas",
)
print(result.groups[["baseline_budget", "income_change", "transfer", "ev", "ev_pct"]])
print(result.aggregate[["total_transfer", "total_ev", "ev_pct", "mean_ev_pct"]])
print(result.summary())
result.plot()
factor_income_changes uses fractions: 0.10 means +10%, and -1 means a
complete loss of the exposed source. Its labels must exactly match the income
exposure columns. Omitting changes means no income shock.
The prices / base_prices ratio rebases sector prices to each supplied
baseline spending basket. Both states and nominal income shocks must use a
common numeraire. Uniformly scaling prices and all nominal resources leaves
real welfare unchanged.
Transfers and aggregation
transfer_total is the aggregate change in revenue allocated to these
represented households, in the spending currency and period. A negative value
is an aggregate withdrawal. It is supplied explicitly; the bridge does not
infer it from GE government income or household factor earnings.
transfer_weights gives nonnegative per-unit allocation scores a_g:
transfer_per_unit[g] = transfer_total * a_g / sum_h(population_weight[h] * a_h)
sum_g(population_weight[g] * transfer_per_unit[g]) = transfer_total
The default is equal transfers per represented unit, not equal totals per
group. In the example, each lower-income household receives four currency
units and each higher-income household two; 300*4 + 100*2 = 1400.
Avoid counting fiscal transfers again inside the supplied income shocks.
Normalized population weights represent a population of size one, so the
transfer total must be expressed on that scale. Convert units explicitly when
a GE table is in millions but the survey uses currency per household.
result.groups reports monetary values per represented unit and percentages
relative to that group's baseline spending. result.aggregate.total_ev and
total_cv sum those values using expansion weights. Aggregate ev_pct divides
total EV by total baseline spending; mean_ev_pct instead averages group EV
percentages using population weights. They answer different questions. Neither
aggregation is a social welfare function or an interpersonal utility comparison.
Preferences and exact attribution
Choose rule="fixed_baskets", "cobb_douglas", "ces" or "stone_geary".
CES takes ces_elasticity. LES takes a labeled group-by-sector
expenditure_elasticities DataFrame and a scalar or group-Series
supernumerary_share. Omitted LES targets mean unit elasticities; its omitted
surplus share is the existing engine's explicit 0.5 assumption. The calibration
audit, adding-up repair, assumed-share flag and elasticity adjustments are
recorded in metadata["preference_calibration"]. Observed expenditure alone
does not estimate these preference parameters.
The household engine defines
EV = e(p0,u1) - e(p0,u0) and CV = e(p1,u1) - e(p1,u0).
Both are positive for gains; CV is the amount removable at new prices.
Percentages use the original observed spending budget as denominator.
The columns price_ev, income_ev, and transfer_ev telescope exactly to
ev; the corresponding _cv columns telescope to cv. These differences
compare welfare anchored to the same original baseline, along the ordered
path prices → factor income → transfers. They are order-dependent,
descriptive attribution, not separate causal effects or a Shapley average.
Every stage is checked against the preference domain. A price-only LES stage
that exhausts subsistence resources raises even if a subsequent transfer
would make the final state feasible.
Bridge to a solved trade model
distributional_welfare_from_results(groups, calib, baseline, counterfactual,
country=..., factor_income_changes=..., **scenario_options) uses
household_prices_from_result to recover one country's sector purchaser
prices and reuses its equilibrium audit. Both states must have converged, use
the same accounting mode, and carry exactly the calibration's ordered country
and sector labels. Household sector labels must match the calibration.
The explicit income mapping is a Series from household income-source labels
to fractional nominal changes. If a researcher maps national wage changes into
survey labor income, that reconciliation remains an explicit modeling choice.
The GE consumption-category budget is not substituted for observed group
budgets. baseline_tau_fd and tau_fd, when given, are checked against the
final-demand schedules recorded on the two results. The supported
sector baskets and flexible-model restrictions are those of the
household price bridge.
Within each sector, every survey group inherits that country's GE origin
basket. Different sector spending shares do not identify group-specific
domestic/import sourcing within a sector. If the selected GE category has
zero origin-basket weight for a sector but a survey group spends on it, the
bridge raises an error. The shared helper's unit-price placeholder for an
unconsumed sector cannot value positive survey spending. To use a separately
estimated sector price or an explicit alternative origin mapping, construct
those purchaser prices and call compute_distributional_welfare directly.
The inherited-origin assumption and unused structural-zero sectors are
recorded in result metadata.
The result retains household provenance plus calibration and both equilibrium
metadata. A top-level is_regression_fixture tag is propagated when any of
these inputs carries it. Results derived from a regression fixture remain
regression-fixture exercises; attaching survey expenditure does not turn the
GE price scenario into empirical evidence.
Results support summary(), to_frame() / to_dataframe(), to_markdown(),
to_latex(), to_typst(), and plot().