| name | electoral-analysis |
| description | Election forecasting models, campaign analysis, coalition prediction, voter behavior analysis for Swedish elections |
| license | Apache-2.0 |
Electoral Analysis Skill
Purpose
This skill provides comprehensive methodologies for analyzing Swedish electoral dynamics, forecasting election outcomes, predicting coalition formations, and assessing campaign effectiveness. It integrates statistical modeling, polling analysis, and historical trend analysis to produce high-confidence intelligence products for democratic accountability assessment.
When to Use This Skill
Apply this skill when:
- ✅ Forecasting election outcomes (seat projections, vote shares)
- ✅ Analyzing polling trends and calculating poll aggregates
- ✅ Predicting coalition formation post-election
- ✅ Assessing swing voter behavior and electoral volatility
- ✅ Evaluating campaign effectiveness and messaging impact
- ✅ Calculating electoral system effects (proportional representation, thresholds)
- ✅ Identifying marginal constituencies and competitive races
Do NOT use for:
- ❌ Individual voter predictions (violates privacy, no granular data)
- ❌ Local/municipal elections (different dynamics, separate models)
- ❌ EU Parliament elections (different party configurations)
Swedish Electoral System Context
Key Electoral Characteristics
graph TB
subgraph "Electoral Framework"
A[349 Riksdag Seats]
A --> B[310 Constituency Seats<br/>29 constituencies]
A --> C[39 Leveling Seats<br/>National proportionality]
end
subgraph "Allocation Rules"
D[Modified Sainte-Laguë]
E[4% National Threshold]
F[12% Constituency Threshold]
D --> G[Seat Distribution]
E --> G
F --> G
end
subgraph "Electoral Cycle"
H[4-Year Fixed Term]
I[September Elections]
J[Sunday Voting]
H & I & J --> K[Election Day 2026<br/>September 13]
end
subgraph "Forecasting Inputs"
L[Historical Results<br/>1970-2022]
M[Opinion Polls<br/>Monthly tracking]
N[Demographic Shifts]
O[Campaign Events]
L & M & N & O --> P[Election Model]
end
P --> Q[Seat Projections]
P --> R[Coalition Scenarios]
style A fill:#e1f5ff
style D fill:#ffeb99
style P fill:#ffe6cc
style Q fill:#ccffcc
style R fill:#ccffcc
1. Election Forecasting Models
Polling Aggregation & Trend Estimation
Purpose: Combine multiple polls to estimate current vote intention with confidence intervals.
import pandas as pd
import numpy as np
from scipy import stats
from datetime import datetime, timedelta
class SwedishElectionForecaster:
"""
Electoral forecasting for Swedish Riksdag elections
Supports: Predictive Intelligence Framework
"""
def __init__(self, db_connection):
self.db = db_connection
self.parties = ['S', 'M', 'SD', 'C', 'V', 'KD', 'L', 'MP']
self.threshold = 4.0
def aggregate_polls_weighted(self, lookback_days=90):
"""
Weighted poll aggregation using recency and sample size
Data Source: External polling data (Novus, Sifo, YouGov, Demoskop)
Intelligence Product: Current vote intention estimates
"""
query = f"""
SELECT
poll_date,
polling_company,
sample_size,
party,
percentage
FROM opinion_polls
WHERE poll_date >= CURRENT_DATE - INTERVAL '{lookback_days} days'
ORDER BY poll_date DESC
"""
df = pd.read_sql(query, self.db)
df['days_ago'] = (pd.Timestamp.now() - pd.to_datetime(df['poll_date'])).dt.days
df['recency_weight'] = np.exp(-df['days_ago'] / 30)
df['sample_weight'] = np.sqrt(df['sample_size']) / 1000
df['total_weight'] = df['recency_weight'] * df['sample_weight']
aggregated = df.groupby('party').apply(
lambda x: np.average(x['percentage'], weights=x['total_weight'])
).to_dict()
standard_errors = df.groupby('party').apply(
lambda x: np.sqrt(np.average((x['percentage'] - aggregated[x.name])**2, weights=x['total_weight']))
).to_dict()
confidence_intervals = {
party: {
'estimate': aggregated[party],
'lower_95': aggregated[party] - 1.96 * standard_errors[party],
'upper_95': aggregated[party] + 1.96 * standard_errors[party]
}
for party in self.parties
}
return confidence_intervals
def structural_forecast_model(self, election_date):
"""
Structural model combining polls, fundamentals, and historical patterns
Model Components:
1. Current polling average (weighted 50%)
2. Economic indicators (weighted 25%)
3. Incumbency advantage/disadvantage (weighted 15%)
4. Campaign effects (weighted 10%)
"""
polls = self.aggregate_polls_weighted()
query = """
SELECT
indicator_name,
value,
year
FROM world_bank_data
WHERE country_code = 'SWE'
AND indicator_name IN ('GDP growth', 'Unemployment rate', 'Inflation')
AND year = EXTRACT(YEAR FROM CURRENT_DATE) - 1
"""
economic_df = pd.read_sql(query, self.db)
economic_score = self.calculate_economic_vote(economic_df)
query_incumbent = """
SELECT
party,
in_government,
government_duration_years
FROM current_government_status
"""
incumbency_df = pd.read_sql(query_incumbent, self.db)
incumbency_effects = self.calculate_incumbency_penalty(incumbency_df)
days_until_election = (election_date - datetime.now()).days
campaign_factor = 1.0 if days_until_election < 30 else 0.5
forecasts = {}
for party in self.parties:
poll_component = polls[party]['estimate'] * 0.5 * campaign_factor
economic_component = economic_score.get(party, 0) * 0.25
incumbency_component = incumbency_effects.get(party, 0) * 0.15
forecast = poll_component + economic_component + incumbency_component
forecasts[party] = max(0, forecast)
total = sum(forecasts.values())
forecasts = {party: (vote / total) * 100 for party, vote in forecasts.items()}
return forecasts
def calculate_economic_vote(self, economic_df):
"""
Model economic voting: Good economy benefits incumbents
Formula: ΔVote = β₁*GDP_growth + β₂*Unemployment_change + β₃*Inflation
Coefficients based on Swedish electoral research
"""
gdp_growth = economic_df[economic_df['indicator_name'] == 'GDP growth']['value'].iloc[0]
unemployment = economic_df[economic_df['indicator_name'] == 'Unemployment rate']['value'].iloc[0]
inflation = economic_df[economic_df['indicator_name'] == 'Inflation']['value'].iloc[0]
economic_advantage = (0.5 * gdp_growth) - (0.3 * unemployment) - (0.2 * inflation)
query = "SELECT party FROM current_government_status WHERE in_government = TRUE"
incumbent_parties = pd.read_sql(query, self.db)['party'].tolist()
economic_scores = {}
for party in self.parties:
if party in incumbent_parties:
economic_scores[party] = economic_advantage / len(incumbent_parties)
else:
economic_scores[party] = 0
return economic_scores
def calculate_incumbency_penalty(self, incumbency_df):
"""
Model incumbency fatigue: Long-serving governments lose support
Penalty = -0.5% per year in government (capped at -5%)
"""
penalties = {}
for _, row in incumbency_df.iterrows():
if row['in_government']:
penalty = min(-0.5 * row['government_duration_years'], -5.0)
penalties[row['party']] = penalty
else:
penalties[row['party']] = 0
return penalties
Seat Projection Algorithm
Purpose: Convert vote share forecasts to seat allocations using Modified Sainte-Laguë method.
def project_riksdag_seats(self, vote_shares):
"""
Project Riksdag seat distribution from vote share forecasts
Method: Modified Sainte-Laguë with 4% threshold
Output: 349 seats allocated across parties
"""
qualified_parties = {
party: vote for party, vote in vote_shares.items()
if vote >= self.threshold
}
if len(qualified_parties) == 0:
raise ValueError("No parties exceed 4% threshold")
seats_allocated = {party: 0 for party in qualified_parties}
for seat_num in range(349):
quotients = {}
for party, vote_pct in qualified_parties.items():
if seats_allocated[party] == 0:
divisor = 1.4
else:
divisor = 2 * seats_allocated[party] + 1
quotients[party] = vote_pct / divisor
winning_party = max(quotients, key=quotients.get)
seats_allocated[winning_party] += 1
return seats_allocated
def monte_carlo_seat_simulation(self, vote_forecasts, n_simulations=10000):
"""
Monte Carlo simulation for seat projection confidence intervals
Method: Sample from vote share distributions, calculate seats
Output: Probability distribution of seat outcomes
"""
seat_simulations = {party: [] for party in self.parties}
for _ in range(n_simulations):
sampled_votes = {}
for party in self.parties:
mean = vote_forecasts[party]['estimate']
std = (vote_forecasts[party]['upper_95'] - vote_forecasts[party]['lower_95']) / (2 * 1.96)
sampled_votes[party] = max(0, np.random.normal(mean, std))
total = sum(sampled_votes.values())
sampled_votes = {party: (vote / total) * 100 for party, vote in sampled_votes.items()}
try:
seats = self.project_riksdag_seats(sampled_votes)
for party in self.parties:
seat_simulations[party].append(seats.get(party, 0))
except ValueError:
continue
seat_projections = {}
for party in self.parties:
sims = seat_simulations[party]
seat_projections[party] = {
'median': int(np.median(sims)),
'mean': np.mean(sims),
'lower_95': int(np.percentile(sims, 2.5)),
'upper_95': int(np.percentile(sims, 97.5)),
'probability_in_riksdag': sum(s > 0 for s in sims) / len(sims)
}
return seat_projections
2. Coalition Formation Prediction
Purpose: Forecast which coalition is most likely to form government post-election.
class CoalitionPredictor:
"""
Coalition formation analysis using game theory and historical patterns
Supports: Decision Intelligence Framework
"""
def __init__(self, db_connection):
self.db = db_connection
def enumerate_viable_coalitions(self, seat_projections):
"""
Generate all mathematically viable coalition combinations
Criteria:
1. Total seats ≥ 175 (majority)
2. Ideologically compatible parties
3. No historical vetoes (e.g., no party wants coalition with SD except M/KD)
"""
from itertools import combinations
parties = list(seat_projections.keys())
viable_coalitions = []
incompatible_pairs = [
('S', 'M'),
('S', 'SD'),
('V', 'M'),
('V', 'KD'),
('MP', 'SD'),
('L', 'V'),
]
for r in range(1, len(parties) + 1):
for combo in combinations(parties, r):
total_seats = sum(seat_projections[p]['median'] for p in combo)
if total_seats >= 175:
compatible = True
for p1, p2 in combinations(combo, 2):
if (p1, p2) in incompatible_pairs or (p2, p1) in incompatible_pairs:
compatible = False
break
if compatible:
viable_coalitions.append({
'parties': combo,
'total_seats': total_seats,
'size': len(combo)
})
return viable_coalitions
def calculate_coalition_stability(self, coalition_parties):
"""
Assess coalition stability using voting alignment history
Data Source: view_riksdagen_party_coalition_agreeableness
Output: Stability score 0-100
"""
query = f"""
SELECT
p1.party as party_a,
p2.party as party_b,
AVG(CASE WHEN p1.party_position = p2.party_position THEN 1.0 ELSE 0.0 END) as alignment_rate
FROM view_riksdagen_party_ballot_support_annual_summary p1
JOIN view_riksdagen_party_ballot_support_annual_summary p2
ON p1.ballot_id = p2.ballot_id
AND p1.party < p2.party
WHERE p1.party IN {tuple(coalition_parties)}
AND p2.party IN {tuple(coalition_parties)}
AND p1.vote_date >= CURRENT_DATE - INTERVAL '4 years'
GROUP BY p1.party, p2.party
"""
alignment_df = pd.read_sql(query, self.db)
stability_score = alignment_df['alignment_rate'].mean() * 100
return stability_score
def predict_coalition_probability(self, viable_coalitions):
"""
Assign formation probability to each viable coalition
Factors:
1. Seat surplus (more seats = more stable)
2. Coalition size (fewer parties = easier negotiation)
3. Historical stability (voting alignment)
4. Ideological cohesion
"""
coalition_scores = []
for coalition in viable_coalitions:
parties = coalition['parties']
seats = coalition['total_seats']
size = coalition['size']
seat_surplus = seats - 175
seat_score = min(seat_surplus / 50, 1.0) * 30
size_score = max(0, 30 - (size - 1) * 10)
stability = self.calculate_coalition_stability(parties)
stability_score = stability * 0.3
if set(parties).issubset({'M', 'KD', 'L', 'C'}):
ideology_score = 10
elif set(parties).issubset({'S', 'V', 'MP'}):
ideology_score = 10
else:
ideology_score = 5
total_score = seat_score + size_score + stability_score + ideology_score
coalition_scores.append({
'coalition': ' + '.join(parties),
'parties': parties,
'seats': seats,
'probability_score': total_score,
'factors': {
'seat_surplus': seat_score,
'size_penalty': size_score,
'stability': stability_score,
'ideology': ideology_score
}
})
total_score = sum(c['probability_score'] for c in coalition_scores)
for coalition in coalition_scores:
coalition['formation_probability'] = (coalition['probability_score'] / total_score) * 100
coalition_scores.sort(key=lambda x: x['formation_probability'], reverse=True)
return coalition_scores
3. Swing Voter Analysis
Purpose: Identify and model voters likely to switch parties between elections.
WITH election_volatility AS (
SELECT
constituency_name,
election_year,
party_name,
percentage,
ABS(percentage - LAG(percentage) OVER (
PARTITION BY constituency_name, party_name
ORDER BY election_year
)) as vote_swing
FROM constituency_election_results
WHERE election_year >= 2010
),
constituency_volatility_score AS (
SELECT
constituency_name,
AVG(vote_swing) as avg_swing,
MAX(vote_swing) as max_swing,
STDDEV(vote_swing) as swing_volatility
FROM election_volatility
WHERE vote_swing IS NOT NULL
GROUP BY constituency_name
)
SELECT
constituency_name,
ROUND(avg_swing, 2) as avg_swing_pct,
ROUND(max_swing, 2) as max_swing_pct,
ROUND(swing_volatility, 2) as volatility,
CASE
WHEN avg_swing > 5.0 THEN 'HIGH VOLATILITY - Swing District'
WHEN avg_swing > 3.0 THEN 'MODERATE VOLATILITY'
ELSE 'LOW VOLATILITY - Safe District'
END as district_classification
FROM constituency_volatility_score
ORDER BY avg_swing DESC
LIMIT 20;
4. Campaign Effectiveness Analysis
Purpose: Measure impact of campaign events on polling and vote intention.
def analyze_campaign_event_impact(self, event_date, event_description):
"""
Interrupted time series analysis for campaign event impact
Method: Compare polling trend before/after event
Example Events: Leader debates, scandals, policy announcements
"""
query = f"""
SELECT
poll_date,
party,
percentage
FROM opinion_polls
WHERE poll_date BETWEEN '{event_date - timedelta(days=60)}'
AND '{event_date + timedelta(days=60)}'
ORDER BY poll_date
"""
df = pd.read_sql(query, self.db)
df['post_event'] = (df['poll_date'] > event_date).astype(int)
df['days_since_start'] = (df['poll_date'] - df['poll_date'].min()).dt.days
impact_results = {}
for party in df['party'].unique():
party_df = df[df['party'] == party].copy()
from sklearn.linear_model import LinearRegression
X = party_df[['days_since_start', 'post_event']]
X['interaction'] = X['days_since_start'] * X['post_event']
y = party_df['percentage']
model = LinearRegression()
model.fit(X, y)
time_trend = model.coef_[0]
event_impact = model.coef_[1]
trend_change = model.coef_[2]
impact_results[party] = {
'event': event_description,
'immediate_impact': event_impact,
'trend_change': trend_change,
'statistical_significance': self._calculate_p_value(model, X, y)
}
return impact_results
5. Threshold Watch (4% Electoral Threshold)
Purpose: Monitor parties at risk of falling below 4% threshold.
WITH recent_polls AS (
SELECT
party,
poll_date,
percentage,
ROW_NUMBER() OVER (PARTITION BY party ORDER BY poll_date DESC) as recency_rank
FROM opinion_polls
WHERE poll_date >= CURRENT_DATE - INTERVAL '90 days'
),
threshold_analysis AS (
SELECT
party,
AVG(percentage) as avg_support,
STDDEV(percentage) as support_volatility,
MIN(percentage) as min_support,
MAX(percentage) as max_support,
COUNT(*) as poll_count
FROM recent_polls
WHERE recency_rank <= 10
GROUP BY party
)
SELECT
party,
ROUND(avg_support, 2) as current_support,
ROUND(support_volatility, 2) as volatility,
ROUND(min_support, 2) as lowest_poll,
ROUND(max_support, 2) as highest_poll,
CASE
WHEN avg_support < 4.0 THEN '🔴 BELOW THRESHOLD - No seats'
WHEN avg_support < 4.5 THEN '🟠 CRITICAL RISK - Within margin of error'
WHEN avg_support < 5.0 THEN '🟡 MODERATE RISK - Close to threshold'
ELSE '🟢 SAFE - Above threshold'
END as threshold_risk,
ROUND(
100 * (1 - stats.norm.cdf(4.0, avg_support, support_volatility)),
1
) as probability_exceeds_threshold
FROM threshold_analysis
WHERE avg_support <= 6.0
ORDER BY avg_support ASC;
ISMS Compliance Mapping
ISO 27001:2022 Controls
A.5.9 - Inventory of Information and Other Associated Assets
- Electoral data sources cataloged and classified
- Polling methodology documented for transparency
A.5.33 - Protection of Records
- Historical election results maintained with integrity
- Version control for forecast models
NIST CSF 2.0 Functions
IDENTIFY (ID)
- ID.RA-1: Electoral volatility risks identified through swing analysis
- ID.RA-2: Threat intelligence on foreign election interference integrated
DETECT (DE)
- DE.AE-3: Event data aggregated and correlated (polling anomalies, manipulation)
CIS Controls v8.1
CIS Control 3: Data Protection
- 3.1: Establish data inventory (electoral data, polling sources)
- 3.12: Segment data processing and storage based on classification
CIS Control 12: Network Infrastructure Management
- 12.4: Deny unauthorized communication over network (protect polling data feeds)
Hack23 ISMS Policy References
Data Classification Policy
Privacy Policy
AI Policy
Threat Modeling
References
Official Documentation:
CIA Platform Documentation:
Academic Sources:
- "Forecasting Elections" - Nate Silver (FiveThirtyEight methodology)
- "The Signal and the Noise" - Nate Silver (Bayesian forecasting)
- "Election Forecasting in Sweden" - Swedish National Election Studies
- "Modified Sainte-Laguë Method" - Electoral Studies Journal
Polling Organizations: