Note
Go to the end to download the full example code.
Simulation Results#
This example demonstrates how to collect and analyze simulation results
using the SimulationResults class. Learn how to:
Collect data during simulation runs
Access data via
results["Producer.price"]orresults.Producer.priceUse
results.get()for programmatic access with optional aggregationUse
results.available()to discover collected variablesExport data to pandas DataFrames
Generate summary statistics
The results module makes it easy to extract insights from simulations without manual data collection.
Basic Data Collection#
Pass collect=True to sim.run() to collect data automatically.
This returns a SimulationResults object containing time series data.
import numpy as np
import bamengine as bam
# Run simulation with data collection
# We collect Worker employed data without aggregation to calculate unemployment
sim = bam.Simulation.init(n_firms=100, n_households=500, seed=42)
results = sim.run(
n_periods=50,
collect={
"Producer": True,
"Worker": ["employed"], # Boolean employed status for each worker
"Employer": True,
"Borrower": True,
"Lender": True,
"Consumer": True,
# Capture timing: when to snapshot each variable during the period
# Worker.employed should be captured after production runs (steady state)
"capture_timing": {
"Worker.employed": "firms_run_production",
},
},
)
print(f"Collected results: {results}")
print("\nMetadata:")
print(f" Periods simulated: {results.metadata.get('n_periods', 'N/A')}")
print(f" Firms: {results.metadata.get('n_firms', 'N/A')}")
print(f" Households: {results.metadata.get('n_households', 'N/A')}")
Collected results: SimulationResults(periods=50, firms=100, households=500, roles=[Worker, Producer, Employer, Borrower, Lender, Consumer], relationships=[None])
Metadata:
Periods simulated: 50
Firms: 100
Households: 500
Discovering Available Data#
Use results.available() to list all collected variables as
"Name.variable" strings.
All available data:
Borrower.credit_demand
Borrower.gross_profit
Borrower.loan_apps_head
Borrower.loan_apps_targets
Borrower.net_profit
Borrower.net_worth
Borrower.projected_fragility
Borrower.retained_profit
Borrower.total_funds
Borrower.wage_bill
Consumer.income
Consumer.income_to_spend
Consumer.largest_prod_prev
Consumer.propensity
Consumer.savings
Consumer.shop_visits_head
Consumer.shop_visits_targets
Economy.avg_price
Economy.inflation
Economy.n_bank_bankruptcies
Economy.n_firm_bankruptcies
Employer.current_labor
Employer.desired_labor
Employer.n_vacancies
Employer.recv_job_apps
Employer.recv_job_apps_head
Employer.total_funds
Employer.wage_bill
Employer.wage_offer
Employer.wage_shock
Lender.credit_supply
Lender.equity_base
Lender.interest_rate
Lender.opex_shock
Lender.recv_loan_apps
Lender.recv_loan_apps_head
Producer.breakeven_price
Producer.desired_production
Producer.expected_demand
Producer.inventory
Producer.labor_productivity
Producer.price
Producer.price_shock
Producer.prod_mask_dn
Producer.prod_mask_up
Producer.prod_shock
Producer.production
Producer.production_prev
Worker.employed
Primary Data Access#
The primary API uses bracket notation results["Name.variable"]
or attribute access results.Name.variable. Economy metrics are
always collected automatically (no need to request them).
# Bracket access (recommended for most use cases)
avg_price = results["Economy.avg_price"]
inflation = results["Economy.inflation"]
# Attribute access (convenient for interactive exploration)
avg_price_attr = results.Economy.avg_price
print(f"\nAverage price (bracket): {bam.ops.mean(avg_price):.3f}")
print(f"Average price (attr): {bam.ops.mean(avg_price_attr):.3f}")
# Role data works the same way
worker_employed = results["Worker.employed"]
print(f"Worker employed shape: {worker_employed.shape}")
# Helper function to calculate unemployment rate from Worker employed data
def calc_unemployment_rate(worker_employed: np.ndarray) -> np.ndarray:
"""Calculate unemployment rate per period from Worker employed boolean array.
Args:
worker_employed: 2D boolean array of shape (n_periods, n_workers) where
True indicates employed, False indicates unemployed.
Returns:
1D array of unemployment rates per period.
"""
# Employment rate = mean of employed (True=1, False=0) across workers
# Unemployment rate = 1 - employment rate
return 1.0 - np.mean(worker_employed.astype(float), axis=1)
# Calculate unemployment rate from Worker employed data
unemployment = calc_unemployment_rate(worker_employed)
print("\nUnemployment rate:")
print(f" Mean: {bam.ops.mean(unemployment):.2%}")
print(f" Final: {unemployment[-1]:.2%}")
print("\nAverage price:")
print(f" Mean: {bam.ops.mean(avg_price):.3f}")
print(f" Final: {avg_price[-1]:.3f}")
Average price (bracket): 0.590
Average price (attr): 0.590
Worker employed shape: (50, 500)
Unemployment rate:
Mean: 0.14%
Final: 0.00%
Average price:
Mean: 0.590
Final: 0.760
Accessing Role Data#
Role data contains per-period snapshots of agent states. With dict-form collect, data is full per-agent arrays (periods x agents) by default.
# Access Producer price data - shape is (periods, firms) with full per-agent data
price_data = results["Producer.price"]
print(f"Price data shape: {price_data.shape}")
# Calculate mean price per period
avg_prices = np.mean(price_data, axis=1)
print(f"Price trend (first 5): {avg_prices[:5].round(3)}")
Price data shape: (50, 100)
Price trend (first 5): [0.504 0.504 0.504 0.503 0.502]
Programmatic Access with get()#
Use results.get() for programmatic access, especially when the role
or variable name comes from a variable. It also supports on-the-fly
aggregation.
# get() for role data
prices_via_get = results.get("Producer", "price")
print("\nUsing get():")
print(f" results.get('Producer', 'price').shape: {prices_via_get.shape}")
# get() for economy data - use "Economy" as the name
price_via_get = results.get("Economy", "avg_price")
print(f" results.get('Economy', 'avg_price').shape: {price_via_get.shape}")
# Aggregation on-the-fly (useful when you have full per-agent data)
full_data_sim = bam.Simulation.init(n_firms=50, n_households=250, seed=42)
full_data_results = full_data_sim.run(
n_periods=20,
collect={
"Producer": ["price"], # Specific variables for Producer
},
)
# Get full 2D data (periods x firms)
prices_2d = full_data_results.get("Producer", "price")
print(f"\n Full data shape: {prices_2d.shape}")
# Get mean aggregated on-the-fly
prices_mean = full_data_results.get("Producer", "price", aggregate="mean")
print(f" With aggregate='mean': {prices_mean.shape}")
Using get():
results.get('Producer', 'price').shape: (50, 100)
results.get('Economy', 'avg_price').shape: (50,)
Full data shape: (20, 50)
With aggregate='mean': (20,)
Legacy Access#
The underlying data is also available via role_data, economy_data,
and relationship_data dictionaries. The data property merges
them into a single dict with an “Economy” key.
Legacy dict access:
results.role_data keys: ['Worker', 'Producer', 'Employer', 'Borrower', 'Lender', 'Consumer']
results.economy_data keys: ['avg_price', 'inflation', 'n_firm_bankruptcies', 'n_bank_bankruptcies']
results.data keys: ['Worker', 'Producer', 'Employer', 'Borrower', 'Lender', 'Consumer', 'Economy']
Visualizing Results#
Plot key economic indicators over time.
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
# Unemployment rate
ax1 = axes[0, 0]
if len(unemployment) > 0:
ax1.plot(unemployment * 100, linewidth=2, color="tab:blue")
ax1.set_ylabel("Unemployment Rate (%)")
ax1.set_title("Unemployment")
ax1.grid(True, alpha=0.3)
# Average price
ax2 = axes[0, 1]
if len(avg_price) > 0:
ax2.plot(avg_price, linewidth=2, color="tab:green")
ax2.set_ylabel("Price Level")
ax2.set_title("Average Market Price")
ax2.grid(True, alpha=0.3)
# Inflation
ax3 = axes[1, 0]
if len(inflation) > 0:
ax3.plot(inflation * 100, linewidth=2, color="tab:orange")
ax3.set_ylabel("Inflation Rate (%)")
ax3.set_title("Annual Inflation")
ax3.axhline(y=0, color="black", linestyle="--", alpha=0.5)
ax3.grid(True, alpha=0.3)
# Production - compute mean across firms since data is 2D
ax4 = axes[1, 1]
production_data = results["Producer.production"]
# Average across firms (axis=1) since data is (periods, firms)
avg_production = np.mean(production_data, axis=1)
ax4.plot(avg_production, linewidth=2, color="tab:red")
ax4.set_ylabel("Avg Production")
ax4.set_title("Average Firm Production")
ax4.grid(True, alpha=0.3)
for ax in axes.flat:
ax.set_xlabel("Period")
plt.tight_layout()
plt.show()

Custom Data Collection#
Use a dictionary to specify exactly what data to collect.
Keys are role names (or “Economy”), values are True for all variables
or a list of specific variable names.
# Collect only specific roles (economy metrics are always included automatically)
custom_results = bam.Simulation.init(n_firms=100, n_households=500, seed=42).run(
n_periods=30,
collect={
"Producer": True, # All Producer variables
"Worker": True, # All Worker variables
"aggregate": "mean", # Average across agents
},
)
print("Custom collection:")
print(f" Roles collected: {list(custom_results.role_data.keys())}")
Custom collection:
Roles collected: ['Producer', 'Worker']
Full Agent-Level Data#
Dict-form collect returns full per-agent data by default (larger arrays).
# Warning: This collects full arrays - can be memory intensive!
full_results = bam.Simulation.init(n_firms=50, n_households=250, seed=42).run(
n_periods=20,
collect={
"Producer": ["price", "production"], # Specific variables only
},
)
prices_full = full_results["Producer.price"]
print(f"Full price data shape: {prices_full.shape}")
print(f" (periods x firms): ({prices_full.shape[0]} x {prices_full.shape[1]})")
# Access individual firm's price history
firm_0_prices = prices_full[:, 0]
print(f"Firm 0 price history: {firm_0_prices[:5].round(3)}...")
Full price data shape: (20, 50)
(periods x firms): (20 x 50)
Firm 0 price history: [0.5 0.5 0.5 0.5 0.5]...
Export to pandas DataFrame#
Convert results to pandas DataFrames for further analysis. Note: pandas is an optional dependency.
try:
import pandas
# Run a new simulation for DataFrame export
df_results = bam.Simulation.init(n_firms=100, n_households=500, seed=42).run(
n_periods=50,
collect=True,
)
# Get all data as a single DataFrame (aggregated)
df = df_results.to_dataframe(aggregate="mean")
print("Full DataFrame:")
print(f" Shape: {df.shape}")
print(f" Columns: {list(df.columns)[:5]}...")
# Get economy metrics only
df_economy = df_results.economy_metrics
print("\nEconomy metrics DataFrame:")
print(df_economy.head())
# Get specific role data
df_producer = df_results.get_role_data("Producer", aggregate="mean")
print(f"\nProducer DataFrame columns: {list(df_producer.columns)}")
except ImportError:
print("pandas not installed. Install with: pip install pandas")
Full DataFrame:
Shape: (50, 52)
Columns: ['Producer.production.mean', 'Producer.production_prev.mean', 'Producer.inventory.mean', 'Producer.expected_demand.mean', 'Producer.desired_production.mean']...
Economy metrics DataFrame:
avg_price inflation n_firm_bankruptcies n_bank_bankruptcies
period
0 0.500000 0.000000 4 0
1 0.500000 0.000000 4 0
2 0.500000 0.000000 4 0
3 0.499194 0.000000 4 0
4 0.498755 -0.001612 4 0
Producer DataFrame columns: ['Producer.production.mean', 'Producer.production_prev.mean', 'Producer.inventory.mean', 'Producer.expected_demand.mean', 'Producer.desired_production.mean', 'Producer.labor_productivity.mean', 'Producer.breakeven_price.mean', 'Producer.price.mean', 'Producer.prod_shock.mean', 'Producer.prod_mask_up.mean', 'Producer.prod_mask_dn.mean', 'Producer.price_shock.mean']
Summary Statistics#
Get descriptive statistics for all collected metrics.
try:
import pandas # noqa: F401 - check if pandas is installed
summary_results = bam.Simulation.init(n_firms=100, n_households=500, seed=42).run(
n_periods=100,
collect=True,
)
# Get summary statistics
summary = summary_results.summary
print("Summary Statistics:")
print(summary[["mean", "std", "min", "max"]].round(4))
except ImportError:
print("pandas not installed for summary statistics")
Summary Statistics:
mean std min max
Producer.production.mean 2.4818 0.1147 2.1976 2.5947
Producer.production_prev.mean 2.4818 0.1147 2.1976 2.5947
Producer.inventory.mean 0.5006 0.2330 0.0000 0.9006
Producer.expected_demand.mean 2.4236 0.1266 2.1378 2.6229
Producer.desired_production.mean 2.4236 0.1266 2.1378 2.6229
Producer.labor_productivity.mean 0.5000 0.0000 0.5000 0.5000
Producer.breakeven_price.mean 0.4886 0.0952 0.0000 0.6285
Producer.price.mean 0.7289 0.1530 0.5024 0.9703
Producer.prod_shock.mean 0.0501 0.0028 0.0433 0.0552
Producer.prod_mask_up.mean 0.3027 0.2167 0.0100 1.0000
Producer.prod_mask_dn.mean 0.2516 0.1156 0.0000 0.5200
Producer.price_shock.mean 0.0501 0.0028 0.0443 0.0575
Worker.employer.mean 42.3932 13.8191 1.6520 49.8120
Worker.employer_prev.mean 44.4916 12.6545 -1.0000 49.5920
Worker.wage.mean 0.2266 0.0816 0.0099 0.3372
Worker.periods_left.mean 3.5008 1.9385 0.0560 6.6080
Worker.contract_expired.mean 0.1148 0.2694 0.0000 0.9440
Worker.fired.mean 0.0025 0.0069 0.0000 0.0520
Worker.job_apps_head.mean -1.0000 0.0000 -1.0000 -1.0000
Employer.desired_labor.mean 5.1148 0.3582 4.4200 5.9600
Employer.current_labor.mean 4.2899 1.3551 0.2800 5.0000
Employer.wage_offer.mean 0.2590 0.0413 0.1674 0.3300
Employer.wage_bill.mean 1.2800 0.2275 0.8088 1.7387
Employer.n_vacancies.mean 0.2467 0.2135 0.0000 1.0400
Employer.total_funds.mean 12.1788 1.8487 7.6912 15.0621
Employer.recv_job_apps_head.mean -1.0000 0.0000 -1.0000 -1.0000
Employer.wage_shock.mean 0.0106 0.0064 0.0019 0.0271
Borrower.net_worth.mean 12.1788 1.8487 7.6912 15.0621
Borrower.total_funds.mean 12.1788 1.8487 7.6912 15.0621
Borrower.wage_bill.mean 1.2800 0.2275 0.8088 1.7387
Borrower.credit_demand.mean 0.0029 0.0093 0.0000 0.0515
Borrower.projected_fragility.mean 0.0263 0.0708 0.0000 0.4539
Borrower.gross_profit.mean 0.0725 0.1127 -0.0885 0.3916
Borrower.net_profit.mean 0.0723 0.1126 -0.0885 0.3916
Borrower.retained_profit.mean 0.0471 0.1065 -0.0972 0.3524
Borrower.loan_apps_head.mean -0.3932 1.1959 -1.0000 3.8200
Lender.equity_base.mean 4.1240 1.0118 2.3605 5.0000
Lender.credit_supply.mean 41.2986 10.1039 23.3304 50.0000
Lender.interest_rate.mean 0.0209 0.0004 0.0186 0.0214
Lender.recv_loan_apps_head.mean -1.0000 0.0000 -1.0000 -1.0000
Lender.opex_shock.mean 0.0488 0.0089 0.0293 0.0708
Consumer.income.mean 0.0000 0.0000 0.0000 0.0000
Consumer.savings.mean 0.2532 0.1590 0.0991 0.9332
Consumer.income_to_spend.mean 0.0000 0.0000 0.0000 0.0000
Consumer.propensity.mean 0.7781 0.0292 0.6639 0.8269
Consumer.largest_prod_prev.mean 47.7601 2.6988 41.5620 53.3240
Consumer.shop_visits_head.mean 499.0000 0.0000 499.0000 499.0000
Shareholder.dividends.mean 0.0050 0.0018 0.0017 0.0088
avg_price 0.7224 0.1517 0.4988 0.9646
inflation 0.0166 0.0420 -0.0519 0.0905
n_firm_bankruptcies 3.8900 2.2870 0.0000 13.0000
n_bank_bankruptcies 0.0200 0.1407 0.0000 1.0000
Comparing Multiple Simulation Runs#
Run multiple simulations and compare their results.
# Run with different random seeds
n_runs = 5
all_unemployment = []
print("Running ensemble of simulations...")
for i in range(n_runs):
run_results = bam.Simulation.init(n_firms=100, n_households=500, seed=42 + i).run(
n_periods=50,
collect={
"Worker": ["employed"],
"capture_timing": {"Worker.employed": "firms_run_production"},
},
)
# Calculate unemployment from Worker employed data
unemp_rate = calc_unemployment_rate(run_results["Worker.employed"])
all_unemployment.append(unemp_rate)
# Plot ensemble results
if all_unemployment:
fig, ax = plt.subplots(figsize=(10, 6))
for i, unemp in enumerate(all_unemployment):
ax.plot(bam.ops.multiply(unemp, 100), alpha=0.5, label=f"Run {i + 1}")
# Calculate and plot mean unemployment across runs
# Stack arrays and compute mean along axis 0
stacked = bam.ops.asarray([list(u) for u in all_unemployment])
mean_unemployment = bam.ops.multiply(bam.ops.mean(stacked, axis=0), 100)
ax.plot(mean_unemployment, "k-", linewidth=2, label="Mean")
ax.set_xlabel("Period")
ax.set_ylabel("Unemployment Rate (%)")
ax.set_title(f"Ensemble of {n_runs} Simulation Runs")
ax.legend(loc="upper right")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Summary across runs
final_rates = bam.ops.asarray([unemp[-1] * 100 for unemp in all_unemployment])
print("\nFinal unemployment rates:")
print(f" Mean: {bam.ops.mean(final_rates):.2f}%")
print(f" Std: {bam.ops.std(final_rates):.2f}%")
print(f" Range: {bam.ops.min(final_rates):.2f}% - {bam.ops.max(final_rates):.2f}%")

Running ensemble of simulations...
Final unemployment rates:
Mean: 0.60%
Std: 0.91%
Range: 0.00% - 2.40%
Collecting Relationship Data#
Relationships (like LoanBook) can be collected alongside role data.
Unlike roles, relationships are opt-in only and NOT included with collect=True.
# Manually add some loans to demonstrate relationship data collection
rel_sim = bam.Simulation.init(n_firms=50, n_households=250, seed=42)
loans = rel_sim.get_relationship("LoanBook")
loans.append_loans_for_lender(
lender_idx=np.intp(0),
borrower_indices=np.array([0, 1, 2, 3, 4], dtype=np.int64),
amount=np.array([1000.0, 1500.0, 2000.0, 500.0, 750.0]),
rate=np.array([0.02, 0.03, 0.025, 0.018, 0.022]),
)
rel_results = rel_sim.run(
n_periods=20,
collect={
"Producer": ["price"], # Role data
"LoanBook": ["principal", "rate", "debt"], # Relationship data
"aggregate": "sum", # Sum across all active loans
},
)
print("Relationship data collection:")
print(f" Available: {rel_results.available()}")
# Access via bracket notation
total_principal = rel_results["LoanBook.principal"]
print(f" Total principal over time shape: {total_principal.shape}")
print(f" Initial total principal: {total_principal[0]:.2f}")
# Access via get()
total_debt = rel_results.get("LoanBook", "debt")
print(f" Total debt (last period): {total_debt[-1]:.2f}")
Relationship data collection:
Available: ['Economy.avg_price', 'Economy.inflation', 'Economy.n_bank_bankruptcies', 'Economy.n_firm_bankruptcies', 'LoanBook.debt', 'LoanBook.principal', 'LoanBook.rate', 'Producer.price']
Total principal over time shape: (20,)
Initial total principal: 0.00
Total debt (last period): 0.00
Analyzing Loan Distribution#
Without aggregation (the default for dict-form collect), you get full edge data per period as variable-length arrays. Useful for analyzing distributions but cannot be exported to DataFrame.
loan_dist_sim = bam.Simulation.init(n_firms=50, n_households=250, seed=42)
loans = loan_dist_sim.get_relationship("LoanBook")
# Add loans with varying amounts
loans.append_loans_for_lender(
lender_idx=np.intp(0),
borrower_indices=np.array([0, 1, 2], dtype=np.int64),
amount=np.array([100.0, 200.0, 300.0]),
rate=np.array([0.02, 0.03, 0.025]),
)
dist_results = loan_dist_sim.run(
n_periods=5,
collect={
"LoanBook": ["principal"], # Full edge data (variable-length per period)
},
)
principal_per_period = dist_results["LoanBook.principal"]
print("\nLoan distribution (full per-edge data):")
print(f" Type: {type(principal_per_period).__name__}")
print(f" Number of periods: {len(principal_per_period)}")
if principal_per_period:
print(f" Period 0 loans: {len(principal_per_period[0])} active")
print(f" Period 0 principals: {principal_per_period[0]}")
Loan distribution (full per-edge data):
Type: list
Number of periods: 5
Period 0 loans: 0 active
Period 0 principals: []
Key Takeaways#
Basic collection:
Use
collect=True(the default) for full per-agent data collectionEconomy metrics are always collected automatically
Use
collect={"Producer": ["price"]}for specific variablesUse
Truefor all variables:{"Worker": True}
Data access (primary API):
results["Producer.price"]– bracket notation (recommended)results.Producer.price– attribute access (interactive use)results.get("Producer", "price")– programmatic accessresults.get("Producer", "price", aggregate="mean")– with aggregationresults.available()– discover all collected variables
Per-agent data and capture timing:
Dict-form
collectreturns full per-agent data by default (shape: periods x agents)Use
capture_timingto control when variables are captured during each periodExample:
"capture_timing": {"Worker.employed": "firms_run_production"}
Computing derived metrics:
Unemployment rate can be computed from
Worker.employed:1.0 - np.mean(employed.astype(float), axis=1)When computing from per-agent data, aggregate across agents with
axis=1
Relationship data:
Relationships (like
LoanBook) are opt-in: use"LoanBook": ["principal"]NOT included with
collect=True(must specify explicitly)Aggregations:
sum(total),mean(average),std(variation)Without aggregation (default): list of variable-length arrays (can’t export to DataFrame)
Access via
results["LoanBook.principal"]orresults.get("LoanBook", "principal")
Legacy access (still works):
results.economy_data,results.role_data,results.relationship_dataresults.datafor unified dict access (includes “Economy” key and relationships)Export to pandas with
to_dataframe()for further analysisGet quick statistics with
results.summary
Total running time of the script: (0 minutes 7.784 seconds)