Group Details Private

administrators

  • RE: Is Duopoly More Resistant to Fascism?

    @cfrank If others have examples, simulations, or citations for my claim that "over time the two opposing factions become more and more polarized” I'd love to have those on hand too. I'm not sure I'm referencing any one thing I've read or heard in particular, but more putting multiple things together to get the big picture.

    On the math alone I think you're right that the center is an important block for the two parties to court as well, but in practice I think that that incentive is outweighed by the other perverse social incentives to demonize the other side, to punish "traitors" considering switching, and so on.

    Cancelling people who question the party line costs center voters, but it also discourages others from following. I think this was the subliminal Democratic Party tactic over the last decade that paid dividends for a while but then ultimately lost them the "big-tent" advantage and the presidency. I'm not saying it was an intentional strategy. There are big cultural forces at play here. That's obviously my own personal opinion.

    Back on topic: As the narrative gets dominated by two polarized factions and the middle is silenced, the real middle (the center of public opinion) almost ceases to be a part of the political spectrum because it's doesn't actually map on to the left, right, and swing voter boxes.

    Identifying and presenting consensus win-win policy and then getting it passed is the goal. We need to incentivise and empower that one way or another.

    posted in Political Theory
  • RE: Is Duopoly More Resistant to Fascism?

    @SaraWolk I agree with you. When I say majoritarian, I only mean in the sense of Lijphart’s ontology, because you’re right that ultimately it isn’t even actually majoritarian but largely an illusion of it.

    I’m curious about examples of this: “the center-squeeze effect ensures that over time the two opposing factions become more and more polarized.” I feel you know more than I do in this area, but it seems possible to me that there could be a stable polarization that doesn’t necessarily explode. In principle, as long as there is a large enough population of centrists, if one party leans too far in one direction, naively I would imagine the other could gain more power by appealing to those centrists than by appealing to the fringes.

    Naivety aside, I think you’re probably right. The problem is that even if there is a population of centrists, if the representatives aren’t held accountable to them, then the parties themselves seem to have no good reason not to polarize once they’ve duopolized the political market. I’m really just curious about what causes that—is polarization actually steadily preferred? Or is it just a matter of time and drift before one party tips over the edge? It seems like the opposite of Hotelling’s law.

    I also don’t think multiparty systems or PR for instance alone would solve the problem, there are examples of both systems falling to authoritarianism, and resistance depends on many contingent factors that ultimately bring about your main point, which is preventing extremists from gaining leverage or control.

    Hopefully we can get some technical voting reform and see whether things change. Approval would be great.

    posted in Political Theory
  • RE: Is Duopoly More Resistant to Fascism?

    @cfrank I have been thinking about the same questions. I think that the safeguard to fascism is ensuring checks against polarizing factions taking control.

    In FPTP two party domination, the center-squeeze effect ensures that over time the two opposing factions become more and more polarized and this gives the illusion of majoritarianism, but as we know, the electability bias from voters having to vote for the frontrunner on their side can wildly inflate the perceived popularity of those frontrunners. In practice the moderate and third party voices are silenced and we see super polarizing candidates like Donald Trump (who initially only had some 25% of the vote in the 2016 primary) winning decisive control of their party. His own party has little they can do if they don't like his leadership, and the opposing party also has no leverage whatsoever if they can't beat him head to head. This last presidential election we saw both parties put forward candidates with record low approval ratings, but nobody else had a chance of challenging them at the same time. This is textbook polarized entrenched two party domination.

    Meanwhile, I'm not convinced that a multi-party system on it's own will address any of that and it could make it worse. Just as choose one ballots can create a center-squeeze in FPTP, they can do the same in PR, resulting in a donut of polarized factions represented and little to no representation for the middle. In an election where the quota to win is 10% for example, a candidate could theoretically be strongly opposed by 90% of the electorate. Meanwhile, other factions could win with their standard-bearer also preferred by 10% of the electorate, but also strongly supported by many more and only strongly opposed by a slim minority.

    When some winners are hyper-polarizing and others are not, it not only allows for the rise of dangerous factions who are more likely to bring about civil war, it also creates a lopsided and unstable winner-set. That's not the idealized definition of proportionality we're aiming for even though it would technically pass PR criteria. Theoretically, we should be able to do better.

    The magic of a more expressive ballot or especially a 5 star ballot is that voters can show not only who their favorites are, but how much they like and dislike candidates from other factions. In an ideal system, this data could then be used to:
    a) ensure that factions who deserve a seat at the table get one, and
    b) ensure that candidates or factions that are seen as dangerous and harmful by others are not platformed when better alternatives exist.

    In single-winner STAR, voters who are in the minority who are not going to get their favorites elected still have a strong vote against their worst case-scenario in the runoff. This is a massive check on authoritarianism and fascism. This is amazing and we don't need to switch to PR to get this windfall.

    And, in a top shelf 5 STAR-PR system, theoretically we could do the same, using scores to identify which candidates and factions meet quota rules, while using scores and runoffs or preference data to identify the most polarizing and most opposed candidates.

    At best, PR systems boast that legislatures where all perspectives are represented at the table. These legislatures are more likely to put forward more broadly acceptable legislation, but at their worst, they can give extremist factions massive leverage to cause stagnation or tear the system apart from the inside. For example, a super polarizing candidate like Israel's Ben Gvir who was elected with only 3.5% of the vote has the power to make or break the majority coalition and call a new election with a vote of no confidence if Netanyahu defies him. At worst, PR systems can give small polarizing factions extremely disproportionate leverage. Some of this can only be reformed with governmental system reforms such as higher quotas, but some of it can be fixed with the voting method itself.

    Again, I think with a more expressive ballot and a hybrid ordinal and cardinal (STAR) approach we can do better.

    posted in Political Theory
  • Is Duopoly More Resistant to Fascism?

    This might be kind of anathema to our movement… but I think it’s a really important question to ask. While a duopoly exploits voters, it also establishes a pre-assembled, entrenched, large opposition faction to fascism, which might be diluted in a multi-party government.

    I think this is something to investigate in terms of historical evidence, and also something to keep in mind during reform efforts. I wonder whether, theoretically, it would be better to keep a duopolistic structure of government in the House and Senate, but also enable a third house with multi-party deliberations. This is obviously just day-dreaming.

    I used to consider that multiparty democracy would dilute authoritarian movements from the bottom, but right now the answer isn’t clear to me. I guess we’re seeing one “case study” play out in real time…

    Any thoughts welcome. Thanks!

    FOLLOW UP:

    Based on my research, important factors for resisting fascism external to the style of democracy are horizontal and vertical separations of power (ex: checks and balances and federalism), the strength of democratic norms, and possibly also the priming of democratic resistance by salient failures of other states.

    Commonly, authoritarians will do away with multiparty consensus structures and replace them with majoritarian systems (which are more efficient, especially when they’re only for show). In terms of fascism, the main route in majoritarian democracies with duopolistic structure is capture of one major party by a radical faction (as we see in the U.S. now), and while the opposition faction is likely to be large and organized, there’s also the concerning fact that there is essentially no other horizontal obstacle to the fascist movement. When this resistance fails, what remains is vertical opposition, e.g. federalism, or external opposition (other states).

    The issue with multiparty consensus democracies is that they can be too fluid and fail to offer resistance to organized radicals unless coalitions are primed and strong, and can be less efficient than majoritarian governments (which don’t have to deliberate as much).

    I think a “dual-phase” system would be most resilient if set up properly, because strong, fast, pre-existing opposition could be paired with a more slowly deliberating but broader coalition—basically, the large opposition party can act as a stopper to give time while a more overwhelming consensus forms.

    posted in Political Theory
  • RE: Alternative approval ballots

    This looks quite nice. Presumably for PR elections? I came up with something similarish when doing a mixed member system that used score/approval ballots, and you could vote for candidates and their parties together / separately.

    posted in Election Policy and Reform
  • RE: Idea for truly proportional representation

    @toby-pereira I agree with this. Something in that spirit I am considering is that the power allocations can still be traced back to ballots. For example, if the seated representatives and powers were {A:45, B:35, C:20}, in principle, those single seats could be subdivided into multiple seats of roughly equal power, depending on the candidate pool (i.e. how many candidates are available).

    Possibly, a sub-election could be run to determine the representatives within the A:45 group, etc. Maybe they could be given 4 seats, the B:35 group 3 seats, and C:20 2 seats. That could refine representation, some candidates might pick up multiple seats. It’s probably getting messy and complicated, it essentially becomes a hierarchical partitioning of ballots. I’m not sure what to make of that prospect, it starts looking like a network/phylogenetic tree or forest architecture, and that can become arbitrary fast.

    I do see what you mean. An individual voter may actually prefer a particular coalition of candidates, rather than just want to get their top guy in. I wonder if non-strict rankings and distributed power would mitigate this issue, or for instance, if the A:45 group's sub-election guaranteed a seat for A, and ran the election on the remaining candidates, that might align with the spirit of preference for whole coalitions.

    You definitely are more familiar with this space than I am, so I wager some of my objections may be non-issues when one considers alternative PR methods. But it seems to me that in this case, pushing for coalitions that respect single individual preferences for whole coalitions can lean toward reduced diversity and reduced minority representation. Is that inaccurate? Or is that a common tradeoff issue in PR systems?

    “Would the weighting purely count towards their voting power in the elected body, or does it have other effects such as more time to speak?”

    Yeah, it does beg some questions.

    EDIT: After multiple adjustments made to guard against clone dependence and tactical voting, there is a non-monotonicity issue in my latest version (/branch, it is not my original concept so I don’t claim ownership in any way), where a minority faction can gain strictly preferred representation in the form of a seated candidate by merely withholding approval for that candidate. I have a concrete example of this, and may try to see how to address it. It may be due to something unnecessary.

    posted in Voting Theoretic Criteria
  • RE: Idea for truly proportional representation

    I've seen weighted seats proposed before. It is a fairly intuitive idea, so nothing new. But my instinct is that I don't think it's such a good idea. I think there is something to be said for a parliament made up of people with equal power.

    Would the weighting purely count towards their voting power in the elected body, or does it have other effects such as more time to speak?

    I think one problem is that it there might be a "celebrity" effect. If multiple candidates are standing for one party, the best well known one is likely to take most of the power available to that party without necessarily being "better".

    Also while it's based on votes, voters don't get a say in this weighting. I might prefer candidate A to B (from the same party, or having similar ideals) by a small amount but might still prefer them to have equal power in parliament rather than having all the power directed to A. So I'd have to weigh up what I think other people will vote for and then vote in the opposite direction to balance it out.

    If democracy was working properly in the first place, there should be enough candidates out there to represent your views without having pin everything on potentially just one candidate - a single point of failure.

    posted in Voting Theoretic Criteria
  • RE: Idea for truly proportional representation

    @toby-pereira Awesome! I also reached out to the original author and linked them here.
    I hashed out more adjustments and a Python code chunk that runs elections with detailed audits. It's a bit sophisticated... and currently it actually removes 0-power seats rather than keeping them as a ceremonial role, probably they should still be seated in case subsequent power adjustments are computed. But the latest version and some examples are below.

    The description is not very straightforward either, but the results look pretty good to me.

    """
    Median-T Satiation with Dynamic Prefix Tightening + Candidate-wise RUS
    Exact implementation of the specified method with detailed auditing
    
    METHOD SPECIFICATION:
    ====================
    1. Ballots: RAC (Rank with Approval Cutoff) - each voter ranks all candidates and approves top a_i
    2. T = median(a_i) computed once at start (upper median if even)
    3. Fill seats K=1 to N iteratively
    4. DYNAMIC PREFIX TIGHTENING: Once satiated, a voter's active approvals are always 
       the prefix up to their CURRENT top-ranked seated winner. As better candidates 
       are seated, the prefix tightens upward. It never loosens.
    5. CANDIDATE-WISE RUS: Identify specific "consensus triggers" (candidates that would
       satiate ALL remaining voters). Mark only those candidates as non-satiating, but
       allow satiation based on other winners in the set.
    6. TIEBREAK PRIORITY: Prefer non-flagged candidates over RUS-flagged ones when breaking ties.
    """
    
    from dataclasses import dataclass, field
    from typing import List, Dict, Tuple, Set, Optional
    import math
    import pandas as pd
    from collections import defaultdict
    import copy
    
    class AuditLog:
        """Detailed logging of each step in the process"""
        def __init__(self, verbose: bool = True):
            self.entries = []
            self.verbose = verbose
        
        def log(self, phase: str, rule: str, details: str, data: dict = None):
            entry = {
                "phase": phase,
                "rule": rule,
                "details": details,
                "data": data or {}
            }
            self.entries.append(entry)
            if self.verbose:
                print(f"[{phase}] {rule}")
                print(f"  → {details}")
                if data:
                    for k, v in data.items():
                        print(f"    {k}: {v}")
                print()
    
    @dataclass
    class VoterGroup:
        """Represents a group of voters with identical preferences"""
        n: int                          # Number of voters in group
        rank: List[str]                 # Strict ranking of all candidates
        a: int                          # Approval cutoff (approve top a candidates)
        saturated: bool = False         # Whether group is saturated
        satiation_prefix_end: Optional[str] = None  # H: highest-ranked winner when satiated
        
        def get_prefix_candidates(self) -> Set[str]:
            """
            Get candidates in the satiation prefix (at or above H in ranking)
            """
            if not self.saturated or not self.satiation_prefix_end:
                return set(self.rank)  # All candidates if not saturated
            
            prefix = set()
            for c in self.rank:
                prefix.add(c)
                if c == self.satiation_prefix_end:
                    break
            return prefix
        
        def active_approvals(self, current_winners: List[str] = None) -> Set[str]:
            """
            RULE: Active Approvals with Dynamic Prefix Tightening
            - Unsaturated: approve top a_i candidates
            - Saturated: approve only candidates in prefix up to CURRENT top-ranked winner
            """
            if not self.saturated:
                return set(self.rank[:self.a])
            
            # Saturated: dynamically compute H based on current winners
            if current_winners:
                current_H = self.top_in_set(current_winners)
                if current_H:
                    # Build prefix up to current H
                    prefix = set()
                    for c in self.rank:
                        prefix.add(c)
                        if c == current_H:
                            break
                    original_approvals = set(self.rank[:self.a])
                    return original_approvals & prefix
            
            # Fallback to stored prefix
            prefix = self.get_prefix_candidates()
            original_approvals = set(self.rank[:self.a])
            return original_approvals & prefix
        
        def can_influence_h2h(self, cand_a: str, cand_b: str, current_winners: List[str] = None) -> bool:
            """
            RULE: Active Ranking Influence with Dynamic Prefix
            - Unsaturated: can influence all head-to-heads
            - Saturated: can only influence if BOTH candidates are in dynamically computed prefix
            """
            if not self.saturated:
                return True
            
            # Compute current prefix based on current winners
            if current_winners:
                current_H = self.top_in_set(current_winners)
                if current_H:
                    # Build prefix up to current H
                    prefix = set()
                    for c in self.rank:
                        prefix.add(c)
                        if c == current_H:
                            break
                    return cand_a in prefix and cand_b in prefix
            
            # Fallback to stored prefix
            prefix = self.get_prefix_candidates()
            return cand_a in prefix and cand_b in prefix
        
        def prefers(self, a: str, b: str, current_winners: List[str] = None) -> int:
            """
            Return 1 if a>b, -1 if b>a, 0 if tie
            Only valid if can_influence_h2h returns True
            """
            if not self.can_influence_h2h(a, b, current_winners):
                return 0  # No influence
            
            pos = {c: i for i, c in enumerate(self.rank)}
            ia, ib = pos.get(a, math.inf), pos.get(b, math.inf)
            if ia < ib: return 1
            if ib < ia: return -1
            return 0
        
        def top_in_set(self, winners: List[str]) -> Optional[str]:
            """
            Return highest-ranked candidate from winners (H for this voter)
            """
            for c in self.rank:
                if c in winners:
                    return c
            return None
        
        def get_rank_position(self, candidate: str) -> int:
            """Get 0-based position of candidate in ranking"""
            try:
                return self.rank.index(candidate)
            except ValueError:
                return math.inf
        
        def would_satiate(self, winners: List[str], T: int) -> bool:
            """
            RULE: Satiation Test
            Voter satiates if ANY winner appears within top min(T, a_i) ranks
            """
            if self.saturated:
                return False
            
            threshold = min(T, self.a)
            for w in winners:
                pos = self.get_rank_position(w)
                if pos < threshold:
                    return True
            return False
    
    @dataclass
    class Election:
        """Manages the election process with detailed auditing"""
        candidates: List[str]
        groups: List[VoterGroup]
        tie_order: List[str] = field(default_factory=list)
        audit: AuditLog = field(default_factory=AuditLog)
        
        def __post_init__(self):
            if not self.tie_order:
                self.tie_order = list(self.candidates)
            self.T = None  # Will be computed once
        
        def compute_T(self) -> int:
            """
            RULE: T Calculation
            T = median of all approval cutoffs (upper median if even)
            Computed ONCE at the start, stays fixed
            """
            if self.T is not None:
                return self.T
                
            a_list = []
            for g in self.groups:
                a_list.extend([g.a] * g.n)  # Expand by voter count
            a_list.sort()
            
            m = len(a_list)
            if m == 0:
                self.T = 0
            elif m % 2 == 1:
                self.T = a_list[m//2]
            else:
                self.T = a_list[m//2]  # Upper median for even
            
            self.audit.log(
                "INITIALIZATION", 
                "T Calculation (Median of Approval Cutoffs)",
                f"Median of {m} voters' approval cutoffs = {self.T}",
                {"all_cutoffs": a_list, "T": self.T}
            )
            return self.T
        
        def tally_active_approvals(self, current_winners: List[str] = None) -> Dict[str, int]:
            """
            RULE: Approval Tallying with Dynamic Prefix Tightening
            Count active approvals from all groups (with dynamic H adjustment for satiated voters)
            """
            tallies = {c: 0 for c in self.candidates}
            
            for i, g in enumerate(self.groups):
                approved = g.active_approvals(current_winners)
                for c in approved:
                    if c in self.candidates:  # Only count if still in race
                        tallies[c] += g.n
            
            # Only return tallies for current candidates
            return {c: tallies[c] for c in self.candidates}
        
        def head_to_head(self, a: str, b: str, current_winners: List[str] = None) -> int:
            """
            RULE: Head-to-Head Tiebreaking with Dynamic Prefix
            Use rankings from voters who can influence this comparison
            """
            a_score = b_score = 0
            
            for g in self.groups:
                pref = g.prefers(a, b, current_winners)
                if pref > 0:
                    a_score += g.n
                elif pref < 0:
                    b_score += g.n
            
            if a_score > b_score: return 1
            if b_score > a_score: return -1
            return 0
        
        def select_provisional_winners(self, K: int, iteration: int, previous_winners: List[str] = None, 
                                       non_satiating_candidates: Set[str] = None) -> List[str]:
            """
            RULE: Pick Provisional Winners
            1. Tally active approvals (based on previous winners for dynamic prefix)
            2. Take top K by approval count
            3. Tiebreak: prioritize non-flagged over RUS-flagged, then head-to-head, then fixed order
            """
            if non_satiating_candidates is None:
                non_satiating_candidates = set()
                
            tallies = self.tally_active_approvals(previous_winners)
            
            self.audit.log(
                f"K={K} ITER-{iteration}",
                "Active Approval Tally",
                f"Current approval counts (with previous winners: {previous_winners})",
                {"tallies": tallies, "non_satiating": list(non_satiating_candidates)}
            )
            
            # Group by approval count
            by_tally = defaultdict(list)
            for c, t in tallies.items():
                by_tally[t].append(c)
            
            # Sort tallies descending
            sorted_candidates = []
            for tally in sorted(by_tally.keys(), reverse=True):
                tied = by_tally[tally]
                
                if len(tied) == 1:
                    sorted_candidates.extend(tied)
                else:
                    # Tiebreak needed
                    self.audit.log(
                        f"K={K} ITER-{iteration}",
                        "Tiebreaking",
                        f"{len(tied)} candidates tied with {tally} approvals",
                        {"tied_candidates": tied}
                    )
                    
                    # Sort by: (1) non-flagged before flagged, (2) head-to-head wins, (3) fixed order
                    def tiebreak_key(cand):
                        is_flagged = 1 if cand in non_satiating_candidates else 0
                        wins = sum(1 for other in tied if other != cand and self.head_to_head(cand, other, previous_winners) > 0)
                        return (is_flagged, -wins, self.tie_order.index(cand))
                    
                    tied_sorted = sorted(tied, key=tiebreak_key)
                    sorted_candidates.extend(tied_sorted)
            
            winners = sorted_candidates[:K]
            self.audit.log(
                f"K={K} ITER-{iteration}",
                "Provisional Winners Selected",
                f"Top {K} candidates by approval with tiebreaking",
                {"winners": winners}
            )
            
            return winners
        
        def apply_satiation(self, winners: List[str], T: int, non_satiating_candidates: Set[str]) -> Tuple[List[int], Set[str]]:
            """
            RULE: Median-T Satiation with Candidate-wise RUS
            Returns (list of newly satiated group indices, set of consensus triggers identified)
            """
            # First, identify consensus triggers (candidates that would satiate ALL unsaturated voters)
            # Skip candidates already flagged as non-satiating
            unsaturated_groups = [g for g in self.groups if not g.saturated]
            consensus_triggers = set()
            
            if unsaturated_groups:
                # Check each winner to see if it's a consensus trigger
                for w in winners:
                    # Skip already-flagged candidates
                    if w in non_satiating_candidates:
                        continue
                        
                    is_consensus = True
                    for g in unsaturated_groups:
                        pos = g.get_rank_position(w)
                        threshold = min(T, g.a)
                        if pos >= threshold:  # This candidate doesn't trigger this voter
                            is_consensus = False
                            break
                    if is_consensus:
                        consensus_triggers.add(w)
                
                if consensus_triggers:
                    self.audit.log(
                        f"K={len(winners)}",
                        "New Consensus Triggers Identified",
                        f"Candidates {consensus_triggers} would satiate ALL remaining voters",
                        {"consensus_triggers": list(consensus_triggers)}
                    )
            
            # Now apply satiation, but ignore consensus triggers as satiation causes
            newly_satiated = []
            
            for gi, g in enumerate(self.groups):
                if g.saturated:
                    continue
                    
                # Find highest-ranked winner that's NOT a consensus trigger or already non-satiating
                ignore_for_satiation = consensus_triggers | non_satiating_candidates
                
                # Check if this voter would satiate based on non-ignored winners
                threshold = min(T, g.a)
                for w in winners:
                    if w in ignore_for_satiation:
                        continue
                    pos = g.get_rank_position(w)
                    if pos < threshold:
                        # This voter satiates based on winner w
                        H = g.top_in_set(winners)  # Still use actual top winner for prefix
                        g.saturated = True
                        g.satiation_prefix_end = H
                        newly_satiated.append(gi)
                        
                        prefix = g.get_prefix_candidates()
                        self.audit.log(
                            f"K={len(winners)}",
                            f"Group {gi+1} Satiated",
                            f"H={H}, satiated via {w}, retaining prefix of {len(prefix)} candidates",
                            {"group_size": g.n, "H": H, "trigger": w, "prefix": list(prefix)}
                        )
                        break
            
            return newly_satiated, consensus_triggers
        
        def assign_power(self, winners: List[str]) -> Dict[str, int]:
            """
            RULE: Power Assignment
            Each voter assigns power to their highest-ranked winner
            """
            power = {w: 0 for w in winners}
            
            for g in self.groups:
                rep = g.top_in_set(winners)
                if rep:
                    power[rep] += g.n
            
            return power
        
        def eliminate_zero_power(self, winners: List[str], power: Dict[str, int]) -> List[str]:
            """
            RULE: Zero-Power Elimination
            Remove winners with no voter support
            """
            eliminated = [w for w in winners if power[w] == 0]
            
            if eliminated:
                self.audit.log(
                    f"K={len(winners)}",
                    "Zero-Power Elimination",
                    f"Removing {len(eliminated)} candidates with no support",
                    {"eliminated": eliminated}
                )
                
                # Remove from candidate list
                self.candidates = [c for c in self.candidates if c not in eliminated]
                self.tie_order = [c for c in self.tie_order if c in self.candidates]
                
                # Update satiation prefixes if needed
                for g in self.groups:
                    if g.satiation_prefix_end in eliminated:
                        # Find next candidate in prefix that's still valid
                        prefix_cands = []
                        for c in g.rank:
                            if c in self.candidates:
                                prefix_cands.append(c)
                            if c == g.satiation_prefix_end:
                                break
                        
                        # Update H to last valid candidate in prefix, or None
                        g.satiation_prefix_end = prefix_cands[-1] if prefix_cands else None
                        if g.satiation_prefix_end is None:
                            g.saturated = False  # No valid prefix anymore
            
            return eliminated
        
        def run_for_K(self, K: int) -> Tuple[List[str], Dict[str, int]]:
            """
            Main algorithm for selecting K winners with candidate-wise RUS and dynamic prefix tightening
            """
            self.audit.log(
                f"K={K}",
                "Starting K-Selection",
                f"Selecting {K} winners from {len(self.candidates)} candidates",
                {"candidates": self.candidates[:10] if len(self.candidates) > 10 else self.candidates}
            )
            
            T = self.compute_T()
            non_satiating_candidates = set()  # Specific candidates marked as non-satiating for this K
            
            iteration = 0
            last_winners = []  # Start with empty set
            last_power = None
            max_iterations = 100
            
            while iteration < max_iterations:
                iteration += 1
                
                # Step 1: Pick provisional winners based on previous winners (for dynamic prefix)
                # and considering non-satiating candidates for tiebreaking
                winners = self.select_provisional_winners(K, iteration, 
                                                         last_winners if iteration > 1 else None,
                                                         non_satiating_candidates)
                
                # Step 2: Apply satiation with candidate-wise RUS
                newly_satiated, found_consensus_triggers = self.apply_satiation(winners, T, non_satiating_candidates)
                
                # Compute newly added consensus triggers (not already known)
                newly_added_triggers = found_consensus_triggers - non_satiating_candidates
                
                # Add newly identified consensus triggers to non-satiating set
                if newly_added_triggers:
                    non_satiating_candidates.update(newly_added_triggers)
                    self.audit.log(
                        f"K={K} ITER-{iteration}",
                        "Non-satiating Candidates Updated",
                        f"Added {newly_added_triggers} to non-satiating set",
                        {"newly_added": list(newly_added_triggers), 
                         "total_non_satiating": list(non_satiating_candidates)}
                    )
                
                # Step 3: Assign power
                power = self.assign_power(winners)
                self.audit.log(
                    f"K={K} ITER-{iteration}",
                    "Power Assignment",
                    "Voter support distribution",
                    {"power": power}
                )
                
                # Step 4: Eliminate zero-power winners
                eliminated = self.eliminate_zero_power(winners, power)
                
                # Check for stabilization - only count NEWLY ADDED triggers as a change
                changed = (winners != last_winners or 
                          power != last_power or 
                          newly_satiated or 
                          bool(newly_added_triggers) or  # Only newly added, not all found
                          eliminated)
                
                if not changed:
                    self.audit.log(
                        f"K={K}",
                        "STABILIZED",
                        f"No changes in iteration {iteration}",
                        {"final_winners": winners, "final_power": power}
                    )
                    break
                
                last_winners = winners
                last_power = power
            
            return winners, power
    
    def run_election_with_audit(title: str, candidates: List[str], groups: List[VoterGroup], 
                               max_K: int, verbose: bool = True):
        """
        Run complete election from K=1 to max_K with detailed auditing
        """
        print("="*80)
        print(f"ELECTION: {title}")
        print("="*80)
        
        if verbose:
            print("\nINITIAL CONFIGURATION:")
            print(f"Candidates: {candidates}")
            print("\nVoter Groups:")
            for i, g in enumerate(groups):
                print(f"  Group {i+1}: {g.n} voters")
                print(f"    Ranking: {' > '.join(g.rank)}")
                print(f"    Approves top {g.a}: {list(g.rank[:g.a])}")
            print()
        
        # Create fresh election (groups will carry satiation forward between K values)
        el = Election(
            candidates=list(candidates),
            groups=groups,  # These will maintain state across K
            tie_order=list(candidates),
            audit=AuditLog(verbose=verbose)
        )
        
        results = []
        for K in range(1, max_K + 1):
            if verbose:
                print(f"\n{'='*60}")
                print(f"SOLVING FOR K={K}")
                print(f"{'='*60}\n")
            
            winners, power = el.run_for_K(K)
            
            results.append({
                "K": K,
                "Winners": winners,
                "Power": power,
                "Power_str": ", ".join(f"{w}:{p}" for w, p in power.items())
            })
            
            if verbose:
                print(f"\nRESULT FOR K={K}:")
                print(f"  Winners: {winners}")
                print(f"  Power: {power}")
        
        return results
    
    # Example scenarios
    def demo_scenario():
        """Run a demonstration scenario"""
        resuls = None
        if True:
            # Scenario: Three factions with compromise candidate U
            candidates = ["A", "B", "C", "U", "D"]
            groups = [
                VoterGroup(34, ["A", "U", "B", "C", "D"], 2),
                VoterGroup(33, ["B", "U", "A", "C", "D"], 2), 
                VoterGroup(33, ["C", "U", "A", "B", "D"], 2),
            ]
            
            results = run_election_with_audit(
                "Three Factions with Universal Compromise",
                candidates,
                groups,
                max_K=3,
                verbose=True
            )
            
            # Summary table
            print("\n" + "="*80)
            print("SUMMARY")
            print("="*80)
            df = pd.DataFrame([{
                "K": r["K"],
                "Winners": ", ".join(r["Winners"]),
                "Power Distribution": r["Power_str"]
            } for r in results])
            print(df.to_string(index=False))
        elif True:
            pass
        
        return results
    
    if __name__ == "__main__":
        demo_scenario()
    
    # -------------------------------
    # More example elections
    # -------------------------------
    
    def scenario_majority_clones_vs_two_minorities(verbose=False):
        """
        Majority (52) spreads support across 3 near-clone heads (A1,A2,A3).
        Two cohesive minorities (28 for B-first, 20 for C-first).
        Stress: clone-packing + whether minorities still seat as K grows.
        Expectation: As K increases, A-bloc locks to one/two A* winners,
        while B and C each capture representation; U (none here) not present.
        """
        candidates = ["A1","A2","A3","B","C","D"]
        groups = [
            VoterGroup(18, ["A1","A2","A3","B","C","D"], 3),
            VoterGroup(17, ["A2","A3","A1","B","C","D"], 3),
            VoterGroup(17, ["A3","A1","A2","B","C","D"], 3),
            VoterGroup(28, ["B","C","A1","A2","A3","D"], 2),
            VoterGroup(20, ["C","B","A1","A2","A3","D"], 2),
        ]
        return run_election_with_audit(
            "Majority Clones vs Two Minorities",
            candidates, groups, max_K=4, verbose=verbose
        )
    
    def scenario_heterogeneous_T(verbose=False):
        """
        Heterogeneous approval cutoffs so median-T matters.
        30 voters approve only top-1; 30 approve top-2; 40 approve top-3.
        Expectation: T=2 (upper median). Compromise M is high but not always seated
        unless supported by multiple blocs; dynamic tightening should peel off approvals.
        """
        candidates = ["A","B","C","M","D","E"]
        groups = [
            VoterGroup(30, ["A","M","B","C","D","E"], 1),
            VoterGroup(30, ["B","M","A","C","D","E"], 2),
            VoterGroup(40, ["C","M","B","A","D","E"], 3),
        ]
        return run_election_with_audit(
            "Heterogeneous T (1/2/3) with Compromise M",
            candidates, groups, max_K=3, verbose=verbose
        )
    
    def scenario_big_majority_plus_consensus_U(verbose=False):
        """
        Big majority (60) vs minority (40), with widely approved compromise U.
        Approvals: Majority approves {A, U}; Minority approves {B, U}.
        Expectation: K=1 likely U; as K grows, blocs lock to A and B and U falls back.
        """
        candidates = ["A","B","U","C","D"]
        groups = [
            VoterGroup(60, ["A","U","B","C","D"], 2),
            VoterGroup(40, ["B","U","A","C","D"], 2),
        ]
        return run_election_with_audit(
            "Big Majority vs Minority with Consensus U",
            candidates, groups, max_K=3, verbose=verbose
        )
    
    def scenario_two_parties_plus_centrist(verbose=False):
        """
        Two parties (45/45) with distinct heads (A,B) and a centrist M broadly approved.
        Small 10-voter group prefers a reformer R (also approves M).
        Expectation: K=1 often M; K=2/3 should seat A and B; R may or may not make it at K=3/4.
        """
        candidates = ["A","B","M","R","D"]
        groups = [
            VoterGroup(45, ["A","M","B","R","D"], 2),
            VoterGroup(45, ["B","M","A","R","D"], 2),
            VoterGroup(10, ["R","M","A","B","D"], 2),
        ]
        return run_election_with_audit(
            "Two Parties + Centrist + Reformer",
            candidates, groups, max_K=4, verbose=verbose
        )
    
    def scenario_clone_sprinkling_attempt(verbose=False):
        """
        Simulate a 'decoy sprinkling' attempt: the 55-voter bloc splits into
        three sub-blocs each ranking a different decoy D* first, all approving their real champion X as well.
        Minority prefers Y and Z. Tests candidate-wise RUS + tightening against seat-packing.
        """
        candidates = ["X","Y","Z","D1","D2","D3","W"]
        groups = [
            VoterGroup(19, ["D1","X","D2","D3","Y","Z","W"], 2),
            VoterGroup(18, ["D2","X","D3","D1","Y","Z","W"], 2),
            VoterGroup(18, ["D3","X","D1","D2","Y","Z","W"], 2),
            VoterGroup(25, ["Y","Z","X","D1","D2","D3","W"], 2),
            VoterGroup(20, ["Z","Y","X","D1","D2","D3","W"], 2),
        ]
        return run_election_with_audit(
            "Clone Sprinkling Attempt (Decoys D1–D3)",
            candidates, groups, max_K=4, verbose=verbose
        )
    
    def scenario_many_seats_party_listish(verbose=False):
        """
        Party-list-ish: A:40, B:35, C:25 with some cross-approval for a shared governance U.
        Test proportionality as K increases (K up to 5).
        """
        candidates = ["A1","A2","B1","B2","C1","U","D"]
        groups = [
            VoterGroup(40, ["A1","U","A2","B1","C1","B2","D"], 2),
            VoterGroup(35, ["B1","U","B2","A1","C1","A2","D"], 2),
            VoterGroup(25, ["C1","U","A1","B1","A2","B2","D"], 2),
        ]
        return run_election_with_audit(
            "Many Seats, Party-list-ish with Shared U",
            candidates, groups, max_K=5, verbose=verbose
        )
    
    def scenario_tie_sensitivity(verbose=False):
        """
        Tight ties across factions; tie order matters.
        Use symmetric 33/33/34 with shared approvals to force frequent ties.
        Verify your tie-break ('unflagged before flagged', then H2H, then fixed).
        """
        candidates = ["A","B","C","M","N"]
        groups = [
            VoterGroup(34, ["A","M","B","C","N"], 2),
            VoterGroup(33, ["B","M","C","A","N"], 2),
            VoterGroup(33, ["C","M","A","B","N"], 2),
        ]
        return run_election_with_audit(
            "Tie Sensitivity (Symmetric 34/33/33)",
            candidates, groups, max_K=3, verbose=verbose
        )
    
    def scenario_median_T_equals_one(verbose=False):
        """
        Force T=1: simple-majority may try to set median approval to 1.
        Everyone approves exactly one (a_i=1), different heads.
        Expectation: behaves close to STV-ish seat allocation by top ranks; 
        dynamic tightening is trivial but RUS can still mute universal names.
        """
        candidates = ["A","B","C","U","D"]
        groups = [
            VoterGroup(40, ["A","U","B","C","D"], 1),
            VoterGroup(35, ["B","U","A","C","D"], 1),
            VoterGroup(25, ["C","U","A","B","D"], 1),
        ]
        return run_election_with_audit(
            "Median T = 1 (All a_i=1)",
            candidates, groups, max_K=3, verbose=verbose
        )
    
    def run_more_examples(verbose=False):
        """
        Run all the example scenarios above.
        Set verbose=True for full step-by-step audits.
        """
        all_results = []
    
        print("\n" + "="*80)
        print("SCENARIO 1: Majority Clones vs Two Minorities")
        print("="*80)
        all_results.append(scenario_majority_clones_vs_two_minorities(verbose=verbose))
    
        print("\n" + "="*80)
        print("SCENARIO 2: Heterogeneous T (1/2/3) with Compromise M")
        print("="*80)
        all_results.append(scenario_heterogeneous_T(verbose=verbose))
    
        print("\n" + "="*80)
        print("SCENARIO 3: Big Majority vs Minority with Consensus U")
        print("="*80)
        all_results.append(scenario_big_majority_plus_consensus_U(verbose=verbose))
    
        print("\n" + "="*80)
        print("SCENARIO 4: Two Parties + Centrist + Reformer")
        print("="*80)
        all_results.append(scenario_two_parties_plus_centrist(verbose=verbose))
    
        print("\n" + "="*80)
        print("SCENARIO 5: Clone Sprinkling Attempt (Decoys D1–D3)")
        print("="*80)
        all_results.append(scenario_clone_sprinkling_attempt(verbose=verbose))
    
        print("\n" + "="*80)
        print("SCENARIO 6: Many Seats, Party-list-ish with Shared U")
        print("="*80)
        all_results.append(scenario_many_seats_party_listish(verbose=verbose))
    
        print("\n" + "="*80)
        print("SCENARIO 7: Tie Sensitivity (Symmetric 34/33/33)")
        print("="*80)
        all_results.append(scenario_tie_sensitivity(verbose=verbose))
    
        print("\n" + "="*80)
        print("SCENARIO 8: Median T = 1 (All a_i=1)")
        print("="*80)
        all_results.append(scenario_median_T_equals_one(verbose=verbose))
    
        # Print compact summaries for each scenario’s last run
        print("\n" + "="*80)
        print("SUMMARY (compact)")
        print("="*80)
        for bundle in all_results:
            # bundle is the list returned by run_election_with_audit (one dict per K)
            df = pd.DataFrame([{
                "K": r["K"],
                "Winners": ", ".join(r["Winners"]),
                "Power": r["Power_str"]
            } for r in bundle])
            print(df.to_string(index=False))
            print("-"*80)
    
        return all_results
    
    
    posted in Voting Theoretic Criteria
  • RE: Idea for truly proportional representation

    @cfrank said in Idea for truly proportional representation:

    @Toby-Pereira I wonder what you think about this, since you have deeper knowledge of PR systems.

    Just to let you know I've seen this, but I'll get back to you in the next few days. For some reason I'm not getting much time to post on here at the moment!

    posted in Voting Theoretic Criteria
  • RE: Idea for truly proportional representation

    @poppeacock here is another toy example, which shows that while seating may be pluralistic/consensus based, the ultimate power-based representation may be strongly majoritarian.

    Example: A “universally approved” compromise seat has ~0 power.

    N = 3 seats, 5 candidates, 100 voters
    • 40: A > M | B C D (approve {A,M})
    • 35: B > M | A C D (approve {B,M})
    • 25: C > B > M > A | D (approve {C,B,M,A})

    Approvals: M=100, A=65, B=60, C=25 → Seated: {M, A, B}.
    Strength:
    • 40 A-first → A
    • 35 B-first → B
    • 25 C-first → among seated {M,A,B} they rank B > M > A → B
    Final: A=40, B=60, M=0.

    The only universally approved candidate M is seated, but powerless, and the majority coalition has monopolized all legitimate majority-based bargaining power through representative B.

    Maybe that’s OK with so few seats? I’m just trying to think through the implications of the system. My guess is that this same kind of power-based majoritarianism may still cause issues even with more seats available. I could be wrong.

    However, it’s important to note that the proportionality/plurality/consensus aspect hinges strongly and primarily on the seating method. Using approval, for example, makes the seats susceptible to strategic nomination of clones, where a majority faction could dominate all seats by approving many clone candidates.

    So the method would need to rely on other more specialized PR-based seating algorithms. In that case, the majoritarian power allocation aspect becomes a questionable design choice, since it seems to potentially undermine the very pluralism that the PR-based seating is meant to achieve.

    The majoritarianism becomes less of an issue though if decisions require supermajorities of power (as it always does).

    SUGGESTED PRELIMINARY ADJUSTMENTS:

    This is out of my wheelhouse, but possibly this could be addressed by removing candidates with zero power, and enabling runners-up to take their seats. And in the approval case, perhaps also allocating approval to the top-seated candidate. This is a suggested direction for using approval:

    1. Select provisional open seat allocations by approval. If there are approval ties, resolve them by a head-to-head match if possible, otherwise by a predetermined sort order of the candidates.
    2. If any voter has their top-ranked candidate provisionally seated, remove all other approval support they give to other candidates, and discount any rankings from their ballot in subsequent head-to-head matches. Allocate provisional power.
    3. If there are any filled seats with zero power, completely remove the provisionally seated candidates with zero power from the election and vacate their seats.
    4. Repeat from step (1) using the updated approval and ranking profiles, until there is no change in the seating or power allocation.

    EDIT: There is a serious issue that remains with using approval even after the adjustments above: A majority could still crowd out seats by coordinating multiple “threads” of ballot with each clone as the head of the thread. It seems that the seating algorithm needs to be something different from straight approval.

    Here’s a thought though—what if the approval seating process was performed with an increasing seat number schedule? So for example, first, there is only one seat, then the election iterates until stability. Then, there are two seats, etc. This would disallow the vulnerability we just discovered, I believe, because the voters whose first choice got represented could no longer contribute to bloc approvals.

    I tested this version out, and it does pretty well.

    UPDATED SUGGESTIONS FOR ADJUSTMENT:

    I assume that we would want to use an approval-based mechanism to determine the seat allocations.

    First, if the total number of seats is N, then to avoid majoritarian seat-packing by bloc approval, rather than a single seat allocation of all seats at once, there should be a sequence of elections for K seats, where K is initialized at 1 and increases incrementally to N.

    The other rules would be as follows:

    1. Select provisional open seat allocations by approval. If there are approval ties, resolve them by a head-to-head match if possible, otherwise by a predetermined sort order of the candidates.
    2. If any voter has their top-ranked candidate provisionally seated, remove all other approval support they give to other candidates, and discount any rankings from their ballot in subsequent head-to-head matches. Allocate provisional power according to your rule (top-ranked seated candidate).
    3. If there are any filled seats with zero power, completely remove the provisionally seated candidates with zero power from the election and vacate their seats.
    4. Repeat from step (1) using the updated approval and ranking profiles, until there is no change in the seating or power allocation.

    This iteration will stabilize the seating for the value of K. Afterwards, proceed by increasing K by 1, maintaining the changes to the ballots that were incurred sequentially (ex: as in step (2), where some approvals and rankings are disregarded), and stopping after stabilization for K=N.

    The "top-rank satiation" principle of ("has their top-ranked candidate provisionally seated") is something that can be exploited by sprinkling in decoy candidates as top-ranks. It requires a pool of many distinct decoys and coordination, but is possible. An alternative is to satiate a voter when any of their approved candidates is seated, but that may "over-satiate" voters. Or, voters could indicate their own satiation thresholds. Or vote on one 😆 For instance, the satiation threshold could be set as the mean or median number of approved candidates. In fact, choosing the median will automatically eliminate the possibility of majoritarian decoy sprinkling.

    However, now the satiation can "stagnate." If all voters are satiated, probably the whole satiation state needs to refresh somehow. Food for thought!

    posted in Voting Theoretic Criteria