| name | electoral-analysis |
| description | Election forecasting models, campaign analysis, coalition prediction, voter behavior analysis for Swedish elections |
| license | Apache-2.0 |
Electoral Analysis Skill
🔴 AI FIRST Quality Principle
This skill MUST be applied with the AI FIRST principle: never accept first-pass quality. ALL analysis and content MUST go through minimum 2 complete iterations. After first pass, read ALL output back completely and systematically improve every section — strengthen evidence, deepen analysis, add specific citations, broaden perspectives. Spend ALL allocated time on real work. Single-pass output is NEVER acceptable. NO SHORTCUTS.
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[] = np.exp(-df[] / )
df[] = np.sqrt(df[]) /
df[] = df[] * df[]
aggregated = df.groupby().apply(
x: np.average(x[], weights=x[])
).to_dict()
standard_errors = df.groupby().apply(
x: np.sqrt(np.average((x[] - aggregated[x.name])**, weights=x[]))
).to_dict()
confidence_intervals = {
party: {
: aggregated[party],
: aggregated[party] - * standard_errors[party],
: aggregated[party] + * standard_errors[party]
}
party .parties
}
confidence_intervals
():
polls = .aggregate_polls_weighted()
query =
economic_df = pd.read_sql(query, .db)
economic_score = .calculate_economic_vote(economic_df)
query_incumbent =
incumbency_df = pd.read_sql(query_incumbent, .db)
incumbency_effects = .calculate_incumbency_penalty(incumbency_df)
days_until_election = (election_date - datetime.now()).days
campaign_factor = days_until_election <
forecasts = {}
party .parties:
poll_component = polls[party][] * * campaign_factor
economic_component = economic_score.get(party, ) *
incumbency_component = incumbency_effects.get(party, ) *
forecast = poll_component + economic_component + incumbency_component
forecasts[party] = (, forecast)
total = (forecasts.values())
forecasts = {party: (vote / total) * party, vote forecasts.items()}
forecasts
():
gdp_growth = economic_df[economic_df[] == ][].iloc[]
unemployment = economic_df[economic_df[] == ][].iloc[]
inflation = economic_df[economic_df[] == ][].iloc[]
economic_advantage = ( * gdp_growth) - ( * unemployment) - ( * inflation)
query =
incumbent_parties = pd.read_sql(query, .db)[].tolist()
economic_scores = {}
party .parties:
party incumbent_parties:
economic_scores[party] = economic_advantage / (incumbent_parties)
:
economic_scores[party] =
economic_scores
():
penalties = {}
_, row incumbency_df.iterrows():
row[]:
penalty = (- * row[], -)
penalties[row[]] = penalty
:
penalties[row[]] =
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=):
seat_simulations = {party: [] party .parties}
_ (n_simulations):
sampled_votes = {}
party .parties:
mean = vote_forecasts[party][]
std = (vote_forecasts[party][] - vote_forecasts[party][]) / ( * )
sampled_votes[party] = (, np.random.normal(mean, std))
total = (sampled_votes.values())
sampled_votes = {party: (vote / total) * party, vote sampled_votes.items()}
:
seats = .project_riksdag_seats(sampled_votes)
party .parties:
seat_simulations[party].append(seats.get(party, ))
ValueError:
seat_projections = {}
party .parties:
sims = seat_simulations[party]
seat_projections[party] = {
: (np.median(sims)),
: np.mean(sims),
: (np.percentile(sims, )),
: (np.percentile(sims, )),
: (s > s sims) / (sims)
}
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) + ):
combo combinations(parties, r):
total_seats = (seat_projections[p][] p combo)
total_seats >= :
compatible =
p1, p2 combinations(combo, ):
(p1, p2) incompatible_pairs (p2, p1) incompatible_pairs:
compatible =
compatible:
viable_coalitions.append({
: combo,
: total_seats,
: (combo)
})
viable_coalitions
():
query =
alignment_df = pd.read_sql(query, .db)
stability_score = alignment_df[].mean() *
stability_score
():
coalition_scores = []
coalition viable_coalitions:
parties = coalition[]
seats = coalition[]
size = coalition[]
seat_surplus = seats -
seat_score = (seat_surplus / , ) *
size_score = (, - (size - ) * )
stability = .calculate_coalition_stability(parties)
stability_score = stability *
(parties).issubset({, , , }):
ideology_score =
(parties).issubset({, , }):
ideology_score =
:
ideology_score =
total_score = seat_score + size_score + stability_score + ideology_score
coalition_scores.append({
: .join(parties),
: parties,
: seats,
: total_score,
: {
: seat_score,
: size_score,
: stability_score,
: ideology_score
}
})
total_score = (c[] c coalition_scores)
coalition coalition_scores:
coalition[] = (coalition[] / total_score) *
coalition_scores.sort(key= x: x[], reverse=)
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 >
district_classification
constituency_volatility_score
avg_swing
LIMIT ;
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_description,
: event_impact,
: trend_change,
: ._calculate_p_value(model, X, y)
}
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
avg_support
avg_support
avg_support
threshold_risk,
ROUND(
( stats.norm.cdf(, avg_support, support_volatility)),
) probability_exceeds_threshold
threshold_analysis
avg_support
avg_support ;
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:
🔗 Integration with agentic workflows & analysis artifacts
This skill is consumed by the 11 agentic news workflows in .github/workflows/news-*.md. The authoritative contract lives in .github/prompts/README.md; this skill supplies domain expertise on top of that contract.
🌐 IMF as Primary Source for Economic Conditions Driving Electoral Outcomes
Effective: 2026-04-24
IMF indicators in electoral models
| Electoral driver | IMF indicator | Why IMF over WB |
|---|
| Real-income growth (incumbent advantage) | IMF WEO NGDP_RPCH, NGDPRPC | Freshness + projections; WB lags 12–24 months |
| Unemployment rate (incumbent risk) | IMF WEO LUR | Annual + projections; SCB AKU monthly for tactical reads |
| Inflation (incumbent risk) | IMF WEO PCPIPCH + IFS monthly | Annual + projections; SCB KPI monthly |
| Government debt burden | IMF WEO + FM GGXWDG_NGDP | EDP/GFSM 2014 methodology |
| Fiscal capacity for promises | IMF FM GGSB_NPGDP (cyclically-adjusted balance) | Standard fiscal-room measure |
| Cost-of-living perception | IMF PCPS (commodity benchmarks) | Drivers of headline inflation |
| Currency strength | IMF ER (SEK exchange rates) | Daily series; standard cross-country |
Canonical electoral rule: Every electoral-conditions analysis in Riksdagsmonitor uses IMF projections (T+5) to forecast economic conditions in the election year. World Bank WGI supplements for governance perception. SCB provides Swedish-specific monthly ground truth. See analysis/imf/ and .github/aw/ECONOMIC_DATA_CONTRACT.md v2.1.
🔭 Horizon stratification
Authoritative source: .github/prompts/ext/long-horizon-forecasting.md. Runtime helper: scripts/horizon-context.ts.
Electoral analysis uses horizon bands to calibrate forecast confidence:
| Band | Days | WEP language ceiling | Electoral application |
|---|
72h | 3 | very likely / very unlikely | Imminent vote outcome (plenary scheduled) |
week | 7 | likely / unlikely | Near-term committee votes, polling shifts |
month | 30 | likely / unlikely | Budget cycle impact, campaign events |
quarter | 90 | roughly even / about even | Session-level coalition outlook |
year | 365 | roughly even; stronger requires ≥ 3 cycle-aged sources | Cross-session seat projection, economic conditions (IMF T+1) |
cycle | 1460 | roughly even / unlikely; never likely without ≥ 3 cycle-aged sources | Full election-cycle forecast, long-range economic trends (IMF T+5) |
election | 1460 | scenario-driven; coalition outcomes never above "likely" | Coalition-formation scenarios, government-formation modelling |
Cycle-rollover rules (±30 days of election anchor) are defined in .github/prompts/ext/cycle-rollover.md.