Analysis Methods
Overview
Section titled “Overview”The Observatorio del Congreso pipeline runs six analysis modules that transform raw roll-call voting data into political structure and power insights. The processing chain goes from individual vote records to ideological positions, co-voting networks, community partitions, centrality rankings, and formal vs. empirical power indices.
| Module | Input | Output | Core Algorithm |
|---|---|---|---|
| W-NOMINATE | Vote matrix (legislators x events) | Ideal point coordinates | SVD + Newton-Raphson |
| Co-Voting | Vote records | NxN similarity matrix + graph | Agreement counting |
| Communities | Co-voting graph | Partition dict (node -> community) | Louvain modularity |
| Centrality | Co-voting graph | Per-node scores | Weighted degree, betweenness |
| Power Indices | Seat counts per party | Shapley-Shubik, Banzhaf values | Combinatorial enumeration |
| Empirical Power | Vote records + seat counts | Critical party frequencies | Roll-call analysis |
W-NOMINATE: Ideal Point Estimation
Section titled “W-NOMINATE: Ideal Point Estimation”W-NOMINATE (Weighted Nominal Three-Step Estimation) is the standard algorithm for estimating legislator ideological positions from roll call votes, developed by Poole & Rosenthal (1985, 1997).
References:
- Poole & Rosenthal (1985). “A Spatial Model for Legislative Roll Call Analysis”. American Journal of Political Science, 29(2), 357-384.
- Poole & Rosenthal (1997). Congress: A Political-Economic History of Roll Call Voting. Oxford University Press.
- Poole (2005). Spatial Models of Parliamentary Voting. Cambridge University Press.
How It Works
Section titled “How It Works”The algorithm takes a binary vote matrix and recovers legislator ideal points in a low-dimensional policy space.
Step 1: Binarization. Raw vote strings are mapped to binary values:
| Vote type | Binary value |
|---|---|
a_favor | 1 (Yea) |
en_contra | 0 (Nay) |
abstencion | NaN (excluded) |
ausente | NaN (excluded) |
Step 2: Filtering. Low-information votes and inactive legislators are removed:
min_votes = 10 # minimum binary votes per legislatormin_participants = 10 # minimum binary participants per vote eventlopsided_threshold = 0.975 # filter near-unanimous votesStep 3: Estimation. The algorithm estimates two parameters per legislator (coordinates in 2D space) and two per vote event:
- Ideal points (x_i, y_i): each legislator’s position in the policy space
- Salience weights (beta): how sharply a legislator’s utility drops with distance from the cutting plane
- Normal vectors (w_j): define the cutting plane separating Yea from Nay for each vote
Initialization uses SVD decomposition of the binary matrix, then Newton-Raphson optimization maximizes the classification likelihood.
Quality Metrics
Section titled “Quality Metrics”| Metric | Description | Interpretation |
|---|---|---|
| Classification rate | % of votes correctly predicted by the model | Higher = better fit; typical range 85-95% |
| APRE | Aggregate Proportional Reduction in Error | Improvement over baseline (majority prediction); 0.0 = no improvement, 1.0 = perfect |
Implementation Variants
Section titled “Implementation Variants”nominate_by_legislatura: runs W-NOMINATE separately for each legislature, producing independent ideal point spaces per periodnominate_cross_legislatura: combines all legislatures into a single run, placing all legislators in a shared space for direct comparison
Dependencies
Section titled “Dependencies”scipy (svd, minimize, norm), numpy, pandas
Co-Voting Analysis
Section titled “Co-Voting Analysis”Co-voting measures how often each pair of legislators vote the same way. This is the foundation for all network-based analysis.
Construction Pipeline
Section titled “Construction Pipeline”- Load data: votes, persons, and organizations from SQLite
- Party normalization:
normalize_party()maps mixedvote.groupvalues to canonical organization IDs - Primary party assignment:
get_primary_party()assigns each legislator to their most frequent party - Build matrix:
build_covotacion_matrix()produces an NxN numpy matrix where entry (i,j) = agreement count between legislators i and j, normalized to 0-1 - Build graph:
build_graph()converts the matrix to a NetworkX graph with:- Nodes: legislators, with attributes for party, gender
- Edges: co-voting pairs, with
weight= normalized similarity
# Simplified co-voting weight calculationfor i, j in legislator_pairs: shared_votes = votes_i.intersection(votes_j) total_votes = votes_i.union(votes_j) weight = len(shared_votes) / len(total_votes)Output
Section titled “Output”The module returns a dict containing:
| Key | Type | Description |
|---|---|---|
matrix | NxN numpy array | Pairwise co-voting similarity |
graph | networkx.Graph | Weighted co-voting network |
party_map | dict | person_id -> party mapping |
org_map | dict | org_id -> party name mapping |
persons_df | DataFrame | Legislator metadata |
Community Detection (Louvain)
Section titled “Community Detection (Louvain)”The Louvain algorithm detects communities of legislators who vote similarly, going beyond formal party labels to reveal actual voting blocs.
Algorithm
Section titled “Algorithm”Louvain performs two-phase iterative optimization:
- Local moving: each node moves to the neighbor community that yields the largest modularity gain
- Aggregation: communities are collapsed into super-nodes, and the process repeats
The resolution parameter controls community granularity:
| Resolution | Effect |
|---|---|
| < 1.0 | Fewer, larger communities (coarser) |
| 1.0 (default) | Standard modularity |
| > 1.0 | More, smaller communities (finer) |
Output
Section titled “Output”detect_communities() returns a partition dict mapping each node_id to a community_id.
analyze_communities() produces detailed analysis per community:
- Party composition: count and percentage of each party within the community
- Purity metric: percentage of the dominant party (100% = pure party bloc)
- Cross-party legislators: individuals whose community differs from their formal party
- Sub-blocks: detection of internal factions within large parties (specifically MORENA sub-bloques)
Dependencies
Section titled “Dependencies”python-louvain (community package), networkx
Centrality Metrics
Section titled “Centrality Metrics”Centrality identifies structurally important legislators in the co-voting network. Two complementary metrics are used.
Weighted Degree Centrality
Section titled “Weighted Degree Centrality”centrality[node] = weighted_degree(node) / max_weighted_degreeEach node’s weighted degree is the sum of its edge weights (total co-voting intensity with all other legislators), normalized by the maximum weighted degree in the graph. Values range from 0.0 to 1.0.
Interpretation: high weighted degree = legislator co-votes heavily with many others, indicating alignment with the dominant coalition.
Betweenness Centrality
Section titled “Betweenness Centrality”betweenness = nx.betweenness_centrality(graph, weight=None)Betweenness is computed unweighted (weight=None). This is a deliberate choice:
Interpretation: high betweenness = legislator sits on the shortest paths between different communities, acting as a potential broker or swing voter.
| Metric | Weight handling | Captures |
|---|---|---|
| Weighted Degree | Uses weights | Overall co-voting intensity |
| Betweenness | Ignores weights (weight=None) | Structural bridging position |
Power Indices (Nominal)
Section titled “Power Indices (Nominal)”Nominal power indices calculate how much bargaining power each party has based solely on seat counts, assuming all members vote with their party.
Shapley-Shubik Index
Section titled “Shapley-Shubik Index”For a party p, the Shapley-Shubik index averages marginal power across all N! permutations of parties:
for permutation in all_permutations(parties): coalition = set() for party in permutation: coalition.add(party) if coalition reaches majority AND coalition - {party} does not: party gets one "pivot" point break # repeat for all N! permutationsss_index[party] = pivots[party] / factorial(N)A party is critical (a pivot) at position k in a permutation if the coalition of the first k parties reaches majority, but removing that party drops it below majority.
Banzhaf Index
Section titled “Banzhaf Index”For a party p, count all winning coalitions where p is critical (its defection flips the outcome):
for coalition in all_subsets(parties): if coalition is winning AND coalition - {party} is losing: party is critical in this coalitionbanzhaf_index[party] = critical_count[party] / total_critical_countSeat Assignment
Section titled “Seat Assignment”Multi-membership (legislators belonging to more than one party across their career) is resolved by:
- Collect all party memberships for each legislator
- Assign to the party where the legislator cast the most votes
- Ties broken by most recent
start_datemembership
Per-Chamber Analysis
Section titled “Per-Chamber Analysis”Both indices support separate analysis for Diputados (camara='D') and Senado (camara='S').
Empirical Power Analysis
Section titled “Empirical Power Analysis”Nominal power assumes party discipline. Empirical power measures what actually happens in roll-call votes.
Critical Parties per Vote
Section titled “Critical Parties per Vote”For each vote event, the module identifies which parties were necessary to reach the majority threshold:
winning_coalition = parties that voted with the majorityfor party in winning_coalition: seats_without = majority_seats - party_seats if seats_without < majority_threshold: party is "critical" for this voteEmpirical Power Index
Section titled “Empirical Power Index”empirical_power[party] = times_critical[party] / total_vote_eventsThis produces a 0.0-1.0 score reflecting how often a party’s votes were actually decisive.
Swing Voters and Close Votes
Section titled “Swing Voters and Close Votes”The module identifies:
- Close votes: vote events where the margin was narrow (near the majority threshold)
- Swing voters: individual legislators whose vote could have changed the outcome
- Top dissenters: legislators who voted against their party line most frequently, ranked by dissent count
Power Comparison
Section titled “Power Comparison”The key output is a four-way comparison:
| Index | Basis | What It Measures |
|---|---|---|
| Nominal (seats) | Seat count | Formal representation |
| Shapley-Shubik | Seat distribution | Bargaining power (permutation-based) |
| Banzhaf | Seat distribution | Bargaining power (coalition-based) |
| Empirical | Actual votes | Real-world relevance |
Divergences between nominal and empirical power reveal parties that are formally small but strategically critical (or vice versa).
Temporal Dynamics
Section titled “Temporal Dynamics”All methods support temporal analysis across legislatures.
Per-Legislature Analysis
Section titled “Per-Legislature Analysis”Each legislature gets its own independent analysis:
- Separate W-NOMINATE ideal point spaces
- Independent co-voting graphs and community detection
- Chamber-specific power indices reflecting seat changes
Cross-Legislature Comparison
Section titled “Cross-Legislature Comparison”nominate_cross_legislaturaplaces all legislators in a shared ideal point space, enabling direct comparison across time periods- Community evolution tracking: which legislators shift communities between legislatures, and what that implies about coalition realignment
Modularity Trends
Section titled “Modularity Trends”Tracking the Louvain modularity score across legislatures reveals changes in voting bloc cohesion:
- Rising modularity: parties are voting more cohesively, sharper partisan divisions
- Declining modularity: cross-party voting increasing, blocs dissolving or realigning
- Sudden drops: may indicate a major legislative event (reform vote, leadership change) that disrupted normal voting patterns