2. Basic Simulation
This tutorial covers the core simulation mechanics in detail, including how the business model evolves over time and how to interpret simulation results.
Tip: For a quick insured-vs-uninsured comparison without managing individual objects, use
run_analysis()— see Tutorial 1: Getting Started.
2.1. The Widget Manufacturer Model
The framework models a manufacturing business with the following financial dynamics:
Revenue Generation: Revenue = Assets × Asset Turnover Ratio
Operating Income: Operating Income = Revenue × Operating Margin
Net Income: Net Income = Operating Income × (1 - Tax Rate)
Growth: Assets grow through retained earnings
2.1.1. Creating a Manufacturer
from ergodic_insurance import Config, ManufacturerConfig, WidgetManufacturer
# Quick start — use defaults ($10M assets, 8% margin, 50-year horizon)
config = Config()
manufacturer = WidgetManufacturer(config.manufacturer)
# Or customize specific parameters
config = Config(
manufacturer=ManufacturerConfig(
initial_assets=10_000_000,
asset_turnover_ratio=1.0,
base_operating_margin=0.08,
retention_ratio=1.0,
ppe_ratio=0.5,
)
)
manufacturer = WidgetManufacturer(config.manufacturer)
# Check initial state
print(f"Assets: ${manufacturer.assets:,.0f}")
print(f"Equity: ${manufacturer.equity:,.0f}")
print(f"Expected Revenue: ${manufacturer.assets * config.manufacturer.asset_turnover_ratio:,.0f}")
2.2. Running a Year Step-by-Step
The step() method advances the simulation by one year:
# Run one year of operations
metrics = manufacturer.step(
growth_rate=0.05 # 5% asset growth target
)
# Examine year-end metrics
print(f"Revenue: ${metrics['revenue']:,.0f}")
print(f"Operating Income: ${metrics['operating_income']:,.0f}")
print(f"Net Income: ${metrics['net_income']:,.0f}")
print(f"New Assets: ${metrics['assets']:,.0f}")
print(f"ROE: {metrics['roe']:.2%}")
2.3. Processing Insurance Claims
When a loss occurs, the manufacturer processes it through insurance:
# Simulate a large loss event
claim_amount = 2_000_000 # $2M claim
# Process through insurance with deductible and limit
manufacturer.process_insurance_claim(
claim_amount=claim_amount,
deductible=100_000, # $100K retained by company
insurance_limit=10_000_000 # $10M policy limit
)
# Run the year with the claim impact
metrics = manufacturer.step(
letter_of_credit_rate=0.015 # 1.5% collateral cost
)
print(f"Net Income after claim: ${metrics['net_income']:,.0f}")
print(f"Outstanding Claims: ${metrics['claim_liabilities']:,.0f}")
2.4. Understanding Claim Development
Claims don’t pay out all at once. The framework models realistic claim development:
# Claim payment schedule (cumulative percentages)
# Year 1: 10%
# Year 2: 30% (+20%)
# Year 3: 50% (+20%)
# Year 4: 65% (+15%)
# Year 5: 75% (+10%)
# Year 6: 83% (+8%)
# Year 7: 90% (+7%)
# Year 8: 95% (+5%)
# Year 9: 98% (+3%)
# Year 10: 100% (+2%)
# The manufacturer tracks collateral requirements during development
print(f"Current Collateral: ${manufacturer.collateral:,.0f}")
2.5. The Simulation Class
For multi-year simulations, use the Simulation class:
from ergodic_insurance import Simulation, ManufacturingLossGenerator
# Reset manufacturer
manufacturer = WidgetManufacturer(config)
# Create claim generator
claims = ManufacturingLossGenerator.create_simple(
frequency=0.15, # 15% annual claim probability
severity_mean=800_000, # $800K average claim
severity_std=960_000, # High variability (1.2x mean)
seed=42 # Reproducible results
)
# Create simulation
sim = Simulation(
manufacturer=manufacturer,
loss_generator=claims,
time_horizon=30 # 30-year horizon
)
# Run simulation
results = sim.run()
2.6. Analyzing Simulation Results
2.6.1. Basic Statistics
import numpy as np
print("=== Simulation Summary ===")
print(f"Survived: {'Yes' if results.insolvency_year is None else 'No'}")
print(f"Final Assets: ${results.assets[-1]:,.0f}")
print(f"Final Equity: ${results.equity[-1]:,.0f}")
print(f"Total Claims: {results.claim_counts.sum():.0f}")
print(f"Total Claim Amounts: ${results.claim_amounts.sum():,.0f}")
# ROE analysis
valid_roe = results.roe[~np.isnan(results.roe)]
print(f"\nROE Statistics:")
print(f" Mean: {np.mean(valid_roe):.2%}")
print(f" Median: {np.median(valid_roe):.2%}")
print(f" Std Dev: {np.std(valid_roe):.2%}")
print(f" Min: {np.min(valid_roe):.2%}")
print(f" Max: {np.max(valid_roe):.2%}")
2.6.2. Time-Weighted vs Simple Average ROE
The time-weighted ROE captures the true compound growth experience:
# Simple arithmetic mean
simple_avg = np.mean(valid_roe)
# Time-weighted (geometric) mean
time_weighted = results.calculate_time_weighted_roe()
# Rolling ROE for trend analysis
rolling_5yr = results.calculate_rolling_roe(window=5)
print(f"Simple Average ROE: {simple_avg:.2%}")
print(f"Time-Weighted ROE: {time_weighted:.2%}")
print(f"Difference: {(simple_avg - time_weighted):.2%}")
# The difference indicates non-ergodic behavior
if simple_avg > time_weighted:
print("-> Volatility drag is reducing actual growth")
2.6.3. Export to DataFrame
import pandas as pd
# Full time series
df = results.to_dataframe()
# Add derived metrics
df['cumulative_return'] = (df['equity'] / df['equity'].iloc[0]) - 1
df['year_over_year_growth'] = df['assets'].pct_change()
# Display
print(df.head(10))
# Save to file
df.to_csv('simulation_results.csv', index=False)
2.7. Visualizing Results
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
# Asset growth
axes[0, 0].plot(results.years, results.assets / 1e6)
axes[0, 0].set_xlabel('Year')
axes[0, 0].set_ylabel('Assets ($M)')
axes[0, 0].set_title('Asset Growth Over Time')
axes[0, 0].grid(True, alpha=0.3)
# Equity evolution
axes[0, 1].plot(results.years, results.equity / 1e6)
axes[0, 1].set_xlabel('Year')
axes[0, 1].set_ylabel('Equity ($M)')
axes[0, 1].set_title('Equity Evolution')
axes[0, 1].grid(True, alpha=0.3)
# ROE over time
axes[1, 0].bar(results.years, results.roe * 100, alpha=0.7)
axes[1, 0].axhline(y=simple_avg * 100, color='r', linestyle='--', label='Mean')
axes[1, 0].set_xlabel('Year')
axes[1, 0].set_ylabel('ROE (%)')
axes[1, 0].set_title('Annual Return on Equity')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)
# Claim events
axes[1, 1].bar(results.years, results.claim_amounts / 1e6, color='orange', alpha=0.7)
axes[1, 1].set_xlabel('Year')
axes[1, 1].set_ylabel('Claim Amount ($M)')
axes[1, 1].set_title('Claim Events')
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('simulation_visualization.png', dpi=150)
plt.show()
2.8. Handling Insolvency
When equity falls below the insolvency tolerance, the simulation terminates:
# Check for insolvency
if results.insolvency_year is not None:
print(f"Company went insolvent in year {results.insolvency_year}")
# Analyze pre-insolvency trajectory
pre_insolvency = results.equity[:results.insolvency_year + 1]
print(f"Peak equity: ${max(pre_insolvency):,.0f}")
print(f"Final equity: ${pre_insolvency[-1]:,.0f}")
else:
print("Company survived the full simulation period")
# Calculate compound annual growth rate (CAGR)
initial = results.equity[0]
final = results.equity[-1]
years = len(results.years)
cagr = (final / initial) ** (1 / years) - 1
print(f"Equity CAGR: {cagr:.2%}")
2.9. Multiple Simulations with Different Seeds
To understand the range of outcomes:
from ergodic_insurance import ManufacturerConfig, WidgetManufacturer, ManufacturingLossGenerator, Simulation
# Run multiple simulations
n_simulations = 10
outcomes = []
for seed in range(n_simulations):
# Fresh manufacturer for each run
mfg = WidgetManufacturer(config)
# Claim generator with different seed
claims = ManufacturingLossGenerator.create_simple(
frequency=0.15,
severity_mean=800_000,
severity_std=960_000,
seed=seed
)
# Run simulation
sim = Simulation(manufacturer=mfg, loss_generator=claims, time_horizon=30)
results = sim.run()
outcomes.append({
'seed': seed,
'survived': results.insolvency_year is None,
'final_equity': results.equity[-1] if results.insolvency_year is None else 0,
'time_weighted_roe': results.calculate_time_weighted_roe()
})
# Summarize outcomes
survived = sum(1 for o in outcomes if o['survived'])
print(f"\nSurvival Rate: {survived}/{n_simulations} ({survived/n_simulations:.0%})")
print(f"Mean Final Equity (survivors): ${np.mean([o['final_equity'] for o in outcomes if o['survived']]):,.0f}")
print(f"Mean Time-Weighted ROE: {np.mean([o['time_weighted_roe'] for o in outcomes]):.2%}")
2.10. Next Steps
Tutorial 3: Configuring Insurance - Add insurance to your simulation
Tutorial 4: Optimization Workflow – Use the optimizer to automatically find the best deductible and limit for your business
Tutorial 5: Analyzing Results – Deep dive into ergodic analysis, volatility drag, and DuPont decomposition
Tutorial 6: Advanced Scenarios – Monte Carlo simulations, market cycles, and multi-line programs