Coalition & Government
Functions for coalition formation and government stability analysis.
Coalition Formation
form_government
Find the most stable coalition given election results.
from electoral_sim import form_government
import numpy as np
seats = np.array([150, 120, 80, 50])
positions = np.array([0.6, -0.3, 0.1, -0.6])
names = ["Right", "Left", "Center", "Far-Left"]
gov = form_government(seats, positions, names)
print(f"Coalition: {gov['coalition_names']}")
print(f"Seats: {gov['seats']}")
print(f"Stability: {gov['stability']:.2f}")
print(f"Success: {gov['success']}")
Returns:
| Key | Description |
|-----|-------------|
| success | Whether a majority coalition was formed |
| coalition | List of party indices |
| coalition_names | List of party names |
| seats | Total coalition seats |
| stability | Predicted stability (0-1) |
minimum_winning_coalitions
Find all coalitions with minimal seats for majority.
from electoral_sim import minimum_winning_coalitions
seats = np.array([45, 35, 15, 5])
mwcs = minimum_winning_coalitions(seats, majority_threshold=0.5)
print(f"Found {len(mwcs)} MWCs:")
for coalition in mwcs:
print(f" {coalition}")
Parameters:
- seats — Array of seats per party
- majority_threshold — Fraction needed for majority (default 0.5)
minimum_connected_winning
Find ideologically connected coalitions (no gaps in position space).
from electoral_sim import minimum_connected_winning
seats = np.array([45, 35, 15, 5])
positions = np.array([0.6, -0.2, 0.1, -0.5])
mcws = minimum_connected_winning(seats, positions)
coalition_strain
Calculate ideological tension within a coalition.
from electoral_sim import coalition_strain
positions = np.array([0.6, -0.2, 0.1]) # Coalition members
seats = np.array([45, 35, 15])
strain = coalition_strain(positions, seats)
print(f"Strain: {strain:.2f}") # Higher = more tension
predict_coalition_stability
Predict coalition stability from strain, majority margin, and party count.
from electoral_sim import predict_coalition_stability
stability = predict_coalition_stability(
strain=0.3,
majority_margin=0.15, # 15% above bare majority
n_parties=3,
model="sigmoid" # "sigmoid", "linear", or "exponential"
)
print(f"Predicted stability: {stability:.2f}")
Parameters:
- strain — Policy strain from coalition_strain()
- majority_margin — Seats above majority / total seats
- n_parties — Number of parties in coalition
- model — "sigmoid", "linear", or "exponential" (default "sigmoid")
Returns: Stability score 0-1 (higher = more stable)
junior_partner_penalty
Calculate electoral penalty for junior coalition partners.
from electoral_sim import junior_partner_penalty
seats = np.array([200, 50, 30]) # 3-party coalition
coalition = [0, 1, 2]
penalties = junior_partner_penalty(seats, coalition)
# Returns: [bonus, penalty, larger_penalty]
Research basis: Junior partners often lose votes in subsequent elections due to credit-claiming by the dominant partner.
allocate_portfolios_laver_shepsle
Portfolio allocation using Laver-Shepsle model.
from electoral_sim import allocate_portfolios_laver_shepsle
coalition = [0, 1, 2]
seats = np.array([200, 100, 50])
positions = np.array([
[0.2, 0.3], # Party 0
[0.5, 0.5], # Party 1
[0.8, 0.7], # Party 2
])
allocations = allocate_portfolios_laver_shepsle(
coalition, seats, positions,
dimensions=["Economy", "Social"]
)
# Returns: {"Economy": 1, "Social": 1} # Party index for each portfolio
form_coalition_with_utility
Form coalition via Policy vs Office tradeoff utility maximization.
from electoral_sim import form_coalition_with_utility
seats = np.array([200, 100, 50])
positions = np.array([0.2, 0.5, 0.8])
coalition, utility = form_coalition_with_utility(
seats, positions,
office_weight=0.5, # 0 = pure policy, 1 = pure office
majority_threshold=0.5, # Fraction for majority
)
print(f"Coalition: {coalition}, Utility: {utility:.2f}")
Parameters:
- seats — Array of seat counts
- positions — Party positions (1D array or 2D with primary dimension used)
- office_weight — α weight: 0.0 = pure policy, 1.0 = pure office-seeking (default 0.5)
- majority_threshold — Threshold for majority (default 0.5)
Returns: (coalition_indices, utility_score)
Research basis: Office-seeking parties maximize cabinet portfolios; policy-seeking parties minimize ideological dispersion. The α parameter blends these motivations.
coalition_feedback
Vote-share adjustments for coalition parties in subsequent elections (junior partner penalty effect).
from electoral_sim import coalition_feedback
party_votes = np.array([0.40, 0.30, 0.20, 0.10]) # vote shares
coalition = [0, 2] # Party 0 (senior) + Party 2 (junior)
adjusted = coalition_feedback(
coalition, party_votes,
junior_penalty=0.05, # 5pp penalty for junior partners
senior_bonus=0.02, # 2pp bonus for senior partner
)
Parameters:
- coalition_parties — List of party indices in coalition
- party_votes — Vote shares from previous election
- junior_penalty — Vote share penalty for junior partners (default 0.05)
- senior_bonus — Vote share bonus for senior partner (default 0.02)
Returns: Adjusted vote share array (renormalized to sum to 1)
party_evolution
Simulate party system evolution: entry of new parties, exit of non-viable ones.
from electoral_sim import party_evolution
votes = np.array([0.40, 0.30, 0.20, 0.08, 0.02])
positions = np.array([-0.5, -0.2, 0.1, 0.4, 0.7])
result = party_evolution(
votes, positions,
viability_threshold=0.03, # Parties below 3% may exit
ideology_closeness=0.2, # New parties fill gaps
rng=np.random.default_rng(42),
)
Parameters:
- party_votes — Current vote shares
- party_positions — Current party positions
- viability_threshold — Minimum vote share to survive (default 0.03)
- ideology_closeness — Gap threshold for new party entry (default 0.2)
- rng — Optional random generator
Returns: Dict with updated votes, positions, and entry/exit indicators
Government Stability
collapse_probability
Calculate probability of government collapse at time T.
from electoral_sim import collapse_probability
prob = collapse_probability(
time=30, # months in office
strain=0.3,
stability=0.7,
model="sigmoid" # or "linear", "exponential"
)
Models:
- sigmoid: S-curve (slow start, rapid mid-term risk, plateau)
- linear: Constant hazard rate
- exponential: Rapidly increasing risk
simulate_government_survival
Monte Carlo simulation of government duration.
from electoral_sim import simulate_government_survival
survival = simulate_government_survival(
strain=0.3,
stability=0.7,
n_simulations=1000,
max_months=60,
seed=42
)
print(f"Mean survival: {survival['mean_survival']:.1f} months")
print(f"Median survival: {survival['median_survival']:.1f} months")
print(f"Full term probability: {survival['full_term_prob']:.1%}")
print(f"Std dev: {survival['std_survival']:.1f} months")
print(f"Early collapse prob: {survival['early_collapse_prob']:.1%}")
print(f"Min/Max: {survival['min_survival']} / {survival['max_survival']} months")
Returns:
| Key | Description |
|---|---|
mean_survival |
Mean months survived |
median_survival |
Median months survived |
std_survival |
Standard deviation of survival times |
full_term_prob |
Probability of surviving full term |
early_collapse_prob |
Probability of collapse before mid-term |
min_survival |
Minimum survival (months) |
max_survival |
Maximum survival (months) |
hazard_rate
Calculate instantaneous hazard rate with bathtub curve (higher at start and end of term).
from electoral_sim import hazard_rate
events = [
{"type": "scandal", "severity": 0.7, "month": 18},
{"type": "economic_crisis", "severity": 0.9, "month": 30},
]
rate = hazard_rate(
time_in_office=24, # months
events=events,
base_hazard=0.02,
)
print(f"Hazard rate at month 24: {rate:.4f}")
Parameters:
- time_in_office — Months in office
- events — List of events with {"type": str, "severity": float} (optional)
- base_hazard — Base hazard rate (default 0.02)
Returns: Hazard rate (probability per unit time)
Event types: scandal (weight 0.3), economic_crisis (0.4), defection (0.5), vote_of_no_confidence (0.8), leadership_challenge (0.3)
cox_proportional_hazard
Cox proportional hazards model for government survival.
from electoral_sim import cox_proportional_hazard
hazard = cox_proportional_hazard(
time_in_office=12, # months
covariates={
"majority_margin": 0.1, # 10% above majority
"coalition_strain": 0.3,
"economic_growth": 0.02,
},
base_hazard=0.01,
coefficients={
"majority_margin": -0.5, # Wider margin reduces hazard
"coalition_strain": 0.8, # Higher strain increases hazard
"economic_growth": -0.3, # Growth reduces hazard
},
)
Parameters:
- time_in_office — Months elapsed
- covariates — Dict of covariate names → values
- base_hazard — Baseline hazard rate (default 0.01)
- coefficients — Dict of covariate names → coefficients (defaults based on Warwick 1994)
GovernmentSimulator
Interactive government simulation with events.
from electoral_sim import GovernmentSimulator
sim = GovernmentSimulator(strain=0.3, stability=0.7, seed=42)
# Add events
sim.add_event("scandal", severity=0.5, month=12)
sim.add_event("economic_shock", severity=0.3, month=24)
# Run simulation
months = sim.simulate(max_months=60)
print(f"Government lasted {months} months")
GovernmentSimulator Methods
step
Advance one month. Returns True if government survives.
survived = sim.step()
if not survived:
print(f"Collapsed at month {sim.months_in_office}")
print(f"Reason: {sim.collapse_reason}")
Returns: bool — True if survived, False if collapsed
summary
Get simulation summary as a dict.
info = sim.summary()
print(f"Coalition: {info['coalition']}")
print(f"Duration: {info['months_in_office']} months")
print(f"Collapsed: {info['collapsed']}")
print(f"Reason: {info['collapse_reason']}")
print(f"Events: {info['n_events']}")
Returns:
| Key | Description |
|---|---|
coalition |
Party names |
months_in_office |
Duration survived |
collapsed |
Whether government fell |
collapse_reason |
"policy_strain" or "time_in_office" if collapsed |
strain |
Coalition strain value |
stability |
Stability score |
n_events |
Number of destabilizing events recorded |