.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/extensions/example_buffer_stock.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_extensions_example_buffer_stock.py: ========================================= Buffer-Stock Consumption Extension ========================================= This example implements the buffer-stock consumption extension from Section 3.9.4 of Macroeconomics from the Bottom-up, replacing the baseline mean-field MPC with an individual adaptive rule based on buffer-stock saving theory. Key Equations ------------- **Buffer-Stock MPC (Equation 3.20):** .. math:: c_t = 1 + \frac{d_t - h \cdot g_t}{1 + g_t} Where: - :math:`h` is the desired savings-income ratio - :math:`g_t = W_t / W_{t-1} - 1` is the income growth rate - :math:`d_t = S_t / W_{t-1} - h` is the divergence from desired ratio **Fresh Start Formula (just re-employed):** .. math:: c_t = 1 - h + S_t / W_t This example demonstrates: - Defining replacement events with ``@event(replace=...)`` to swap pipeline behavior - How the buffer-stock MPC differs from the baseline mean-field rule - Fitting heavy-tailed distributions (Singh-Maddala, Dagum, GB2) to wealth data - CCDF plotting on log-log axes (Figure 3.8 from the book) For detailed validation with bounds and statistical annotations, run: python -m validation.scenarios.buffer_stock .. GENERATED FROM PYTHON SOURCE LINES 42-46 Import Dependencies ------------------- We import BAM Engine and the decorators needed to define custom components. .. GENERATED FROM PYTHON SOURCE LINES 46-51 .. code-block:: Python import bamengine as bam from bamengine import Float, event, ops, role from extensions.rnd import RND_EVENTS, RnD .. GENERATED FROM PYTHON SOURCE LINES 52-57 Define Custom Role: BufferStock -------------------------------- The BufferStock role tracks previous-period income and the computed MPC (propensity) for each household. .. GENERATED FROM PYTHON SOURCE LINES 57-67 .. code-block:: Python @role class BufferStock: """Buffer-stock consumption state for households.""" prev_income: Float # W_{t-1}: previous period income propensity: Float # c_t: buffer-stock MPC (may exceed 1.0) .. GENERATED FROM PYTHON SOURCE LINES 68-74 Define Replacement Events ------------------------- Two events replace the baseline consumption logic. ``@event(replace=...)`` removes the original event from the pipeline and inserts the new one in its place. .. GENERATED FROM PYTHON SOURCE LINES 74-143 .. code-block:: Python @event(replace="consumers_calc_propensity") class ConsumersCalcBufferStockPropensity: """Compute individual MPC using buffer-stock adaptive rule. Three cases: 1. Normal (both incomes positive): Full Eq. 3.20 2. Fresh start (just re-employed): c = 1 - h + S/W 3. Unemployed (no income): c = 1/h (gradual savings drawdown) """ def execute(self, sim: bam.Simulation) -> None: con = sim.get_role("Consumer") buf = sim.get_role("BufferStock") income = con.income prev_income = buf.prev_income savings = con.savings h = sim.buffer_stock_h normal = (prev_income > 0) & (income > 0) fresh = (prev_income <= 0) & (income > 0) c = ops.full(len(income), 1 / h) # default: c=1/h (unemployed drawdown) if ops.any(normal): g = income[normal] / prev_income[normal] - 1.0 g = ops.maximum(g, -0.99) # g=-1.0 leads to singularity, so cap at -0.99 d = savings[normal] / prev_income[normal] - h c[normal] = 1.0 + (d - h * g) / (1.0 + g) if ops.any(fresh): c[fresh] = 1.0 - h + savings[fresh] / income[fresh] c = ops.maximum(c, 0.0) ops.assign(buf.propensity, c) @event(replace="consumers_decide_income_to_spend") class ConsumersDecideBufferStockSpending: """Allocate spending budget using buffer-stock MPC. Employed: budget = c * income Unemployed: budget = c * savings """ def execute(self, sim: bam.Simulation) -> None: con = sim.get_role("Consumer") buf = sim.get_role("BufferStock") c = buf.propensity income = con.income savings = con.savings employed = income > 0 budget = ops.where(employed, c * income, c * savings) budget = ops.minimum(budget, savings + income) budget = ops.maximum(budget, 0.0) ops.assign(buf.prev_income, income) ops.assign(con.income_to_spend, budget) new_savings = savings + income - budget new_savings = ops.maximum(new_savings, 0.0) ops.assign(con.savings, new_savings) con.income.fill(0.0) .. GENERATED FROM PYTHON SOURCE LINES 144-149 Attach Extensions ----------------- ``use_role()`` accepts ``n_agents`` for non-firm roles (e.g., household-level). ``use_events()`` applies pipeline hooks (after/before/replace) from event classes. .. GENERATED FROM PYTHON SOURCE LINES 149-162 .. code-block:: Python def attach_extensions(sim): """Attach BufferStock + RnD roles and apply extension events to pipeline.""" sim.use_role(BufferStock, n_agents=sim.n_households) sim.use_role(RnD) sim.use_events( ConsumersCalcBufferStockPropensity, ConsumersDecideBufferStockSpending, *RND_EVENTS, ) .. GENERATED FROM PYTHON SOURCE LINES 163-169 Initialize and Run ------------------ Buffer-stock parameter ``h`` controls the desired savings-income ratio. We scale initial conditions by K=100,000 so the wealth CCDF x-axis spans [10,000 – 1,000,000], matching Figure 3.8 from the book. .. GENERATED FROM PYTHON SOURCE LINES 169-194 .. code-block:: Python sim = bam.Simulation.init( # Buffer-stock specific parameters buffer_stock_h=2.0, # Scale initial conditions by K=100,000 so the wealth CCDF x-axis # spans [10,000 – 1,000,000], matching Figure 3.8 from the book. price_init=50_000, # K × 0.5 (default) savings_init=100_000, # K × 1.0 (default) equity_base_init=500_000, # K × 5.0 (default) # net_worth scales automatically: 2.5 * 50,000 * 6.0 = 750,000 # Growth+ specific parameters sigma_min=0.0, sigma_max=0.1, sigma_decay=-1.0, # Seed seed=0, ) attach_extensions(sim) print(f"Buffer-stock simulation: {sim.n_firms} firms, {sim.n_households} households") print(f" buffer_stock_h = {sim.buffer_stock_h}") results = sim.run() print(f"Completed: {results.metadata['runtime_seconds']:.2f}s") .. rst-class:: sphx-glr-script-out .. code-block:: none Buffer-stock simulation: 100 firms, 500 households buffer_stock_h = 2.0 Completed: 13.75s .. GENERATED FROM PYTHON SOURCE LINES 195-204 Fit Wealth Distribution ----------------------- We fit the distribution of household wealth (savings) in the last period to three heavy-tailed distributions: Singh-Maddala (Burr Type XII), Dagum, and GB2 (Beta Prime). Data is normalized by its median before fitting to avoid numerical overflow in scipy's power-law PDF evaluations (x**k overflows when x ~ 50,000+). The CCDF is scale-invariant, so we evaluate on normalized x but plot against original x. .. GENERATED FROM PYTHON SOURCE LINES 204-218 .. code-block:: Python import numpy as np from scipy import stats as sp_stats savings = results["Consumer.savings"] last_period_savings = savings[-1] fit_scale = float(np.median(last_period_savings)) norm_savings = last_period_savings / fit_scale sm_params = sp_stats.burr12.fit(norm_savings, floc=0) dagum_params = sp_stats.mielke.fit(norm_savings, floc=0) gb2_params = sp_stats.betaprime.fit(norm_savings, floc=0) .. GENERATED FROM PYTHON SOURCE LINES 219-225 Visualize Wealth CCDF (Figure 3.8) ----------------------------------- We plot the complementary cumulative distribution function (CCDF) of wealth on log-log axes, comparing the simulated data to the fitted distributions. The CCDF is calculated as 1 - CDF, where CDF is the cumulative distribution function. .. GENERATED FROM PYTHON SOURCE LINES 225-276 .. code-block:: Python import matplotlib.pyplot as plt import numpy as np fig, ax = plt.subplots(figsize=(8, 6)) sorted_wealth = ops.sort(last_period_savings) n = len(sorted_wealth) ccdf = 1.0 - ops.arange(1, n + 1) / n ax.scatter( sorted_wealth, ccdf, s=12, facecolors="none", edgecolors="black", linewidths=0.5, label="Simulated", zorder=5, ) x_range = np.linspace(sorted_wealth[0], sorted_wealth[-1], 500) x_norm = x_range / fit_scale # normalized x for distribution evaluation # Calculate CCDF (survival function) for each fitted distribution. # We use .sf() instead of 1-cdf() for better numerical stability in the tail. # Note: scipy's 'burr12' distribution is a reparameterization of the Singh-Maddala distribution. sm_ccdf = sp_stats.burr12.sf(x_norm, *sm_params) ax.plot(x_range, sm_ccdf, color="#0000FE", linewidth=1.5, label="Singh-Maddala") # Note: scipy's 'mielke' distribution is a reparameterization of the Dagum distribution. dagum_ccdf = sp_stats.mielke.sf(x_norm, *dagum_params) ax.plot(x_range, dagum_ccdf, color="#FE00FE", linewidth=1.5, label="Dagum") # Note: scipy's 'betaprime' distribution is a reparameterization of the GB2 distribution. gb2_ccdf = sp_stats.betaprime.sf(x_norm, *gb2_params) ax.plot(x_range, gb2_ccdf, color="#00FEFE", linewidth=1.5, label="GB2") ax.set_xscale("log") ax.set_yscale("log") ax.set_xlim(1e4, 1e7) ax.legend(fontsize=8) ax.set_title("Fitting of the CCDF of personal wealth (Figure 3.8)", fontweight="bold") ax.set_xlabel("Household wealth (savings)") ax.set_ylabel("Complementary cumulative distribution") ax.grid(True, linestyle="--", alpha=0.3) plt.tight_layout() plt.show() .. image-sg:: /auto_examples/extensions/images/sphx_glr_example_buffer_stock_001.png :alt: Fitting of the CCDF of personal wealth (Figure 3.8) :srcset: /auto_examples/extensions/images/sphx_glr_example_buffer_stock_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 277-293 Alternative: Extension Bundle ------------------------------ The above example defines all buffer-stock components inline for educational purposes. For convenience, the pre-built ``BUFFER_STOCK`` bundle activates everything in one call: .. code-block:: python from extensions.rnd import RND from extensions.buffer_stock import BUFFER_STOCK, BUFFER_STOCK_COLLECT sim = bam.Simulation.init(seed=0) sim.use(RND) sim.use(BUFFER_STOCK) results = sim.run(collect=BUFFER_STOCK_COLLECT) .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 14.313 seconds) .. _sphx_glr_download_auto_examples_extensions_example_buffer_stock.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: example_buffer_stock.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: example_buffer_stock.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: example_buffer_stock.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_