Example #1
0
def hare_reallocation(data, tally, winners, quota, weights, rstate=None):
    """
    Re-allocate ranked data by random.
    
    Parameters
    ----------
    data : array shaped (a, b)
        Election voter rankings, from [1 to b].
        Data composed of candidate rankings, with
        
           - Voters as the rows, with `a` total voters
           - Candidates as the columns, with `b` total candidates.
           
        Use 0 to specify unranked (and therefore not to be counted) ballots.  
        
    winners : array shaped (c,)
        The round winners's data column indices
    
    quota : int
        STV winning quota
    rstate : numpy random.RandomState or None (default)
        Random number generating object.
        
    Returns
    -------
    data : array shaped (a, b)
        Election voter rankings, with winning candidates surplus votes transferred
        to runner ups. 
    """

    if rstate is None:
        rstate = np.random.RandomState()

    for ii, winner in enumerate(winners):

        win_voter_locs = data[:, winner] == 1
        win_voter_index = np.flatnonzero(win_voter_locs)

        # vote_num = int(tally[ii])

        # Remove ballots the size of the quota
        remove_index = rstate.choice(
            win_voter_index,
            size=quota,
            replace=False,
        )

        data = np.delete(data, remove_index, axis=0)

        # Zero out the winner
        data[:, winner] = 0

    data = rcv_reorder(data)
    return data, 1.0
Example #2
0
def pairwise_rank_matrix(ranks):
    """Head-to-head vote matrix.
    
    Calculate total votes for a candidate against another candidate given
    ranked voter data.

    Parameters
    ----------
    ranks : array shaped (a, b)
        Election voter rankings, from [1 to b].
        Data composed of candidate rankings, with

           - Voters as the rows
           - Candidates as the columns.

        Use 0 to specify unranked (and therefore not to be counted) ballots.

        - a : number of voters dimension. Voters assign ranks for each candidate.
        - b : number of candidates. A score is assigned for each candidate
              from 0 to b-1.

    Returns
    -------
    out : array shaped (b, b,)
        Vote matrix V[i,j]

        - Total votes for candidate i against candidate j
    """
    data = np.atleast_2d(ranks).copy()
    data = rcv_reorder(data)
    cnum = data.shape[1]

    # For unranked candidates make sure they have extremely high rank.
    data[data == 0] = cnum + 10.
    vote_matrix = np.zeros((cnum, cnum))

    # Get win/loss matrix for each candidate pair
    for i in range(cnum):
        for j in range(i + 1, cnum):
            di = data[:, i]
            dj = data[:, j]

            Vij = np.sum(di < dj)
            Vji = np.sum(dj < di)
            vote_matrix[i, j] = Vij
            vote_matrix[j, i] = Vji
    return vote_matrix
Example #3
0
def gregory_reallocation(data: np.ndarray, tally: np.ndarray,
                         winners: np.ndarray, quota: int, weights: np.ndarray,
                         **kwargs):

    voter_num = len(data)

    for winner in winners:
        win_voter_locs = np.flatnonzero(data[:, winner] == 1)
        win_num = tally[winner]
        surplus_factor = (win_num - quota) / win_num

        factors = np.ones((voter_num, 1))
        factors[win_voter_locs] = surplus_factor
        weights = factors * weights
        data[:, winner] = 0
        pass

    data = rcv_reorder(data)

    return data, weights
Example #4
0
def ____BROKEN_irv(data, numwin=1, num_eliminate=None):
    """
    Calculate winners of an election using Instant Runoff Voting
    
    Parameters
    ----------
    data : array shaped (a, b)
        Election voter rankings, from [1 to b].
        Data composed of candidate rankings, with
        
           - Voters as the rows, with `a` total voters
           - Candidates as the columns, with `b` total candidates.
           
        Use 0 to specify unranked (and therefore not to be counted) ballots.  
        
    numwin : int
        Number of winners
        
    Returns
    --------
    winners : array of shape (numwin,)
        Winning candidates index.
    ties : array of shape (tienum,)
        Tied candidates index for the last round, numbering 'tienum'.
    round_history : array of shape (numwin, b)
        Score summations for each candidate, for each round.        
    data : array of shape (a, b)
        Re-ordered ranking data after losers have been eliminated, retaining 
        winner ranksings.
    """
    data = np.array(data, copy=True)
    candidate_num = data.shape[1]
    if num_eliminate is None:
        numrounds = max(1, candidate_num - numwin)
    else:
        numrounds = num_eliminate

    logger.info('# of rounds = %s', numrounds)
    logger.info('# of winners = %s', numwin)
    # initialize history datas
    round_history = []
    loserbool = np.ones(candidate_num, dtype=bool)

    for i in range(numrounds):
        data2 = data.copy()

        # Set of survivor candidates for each rond
        survived = set()
        num_left = candidate_num - (i + 1)

        # Second loop to check for ties.
        for j in range(1, candidate_num + 1):
            vote_index = (data2 == j)
            vote_count = np.sum(vote_index, axis=0)

            # Retrieve survivors of the round as winnersj. Add to survivors
            winnersj, tiesj = winner_check(vote_count, numwin=num_left)
            survived.update(set(winnersj))
            num_left = num_left - len(winnersj)
            round_history.append(vote_count)

            logger.info('\nRound %d, rank %d' % (i, j))
            logger.info('Counts = %s' % vote_count)
            logger.info('survivors=%s of %s' % (survived, num_left))

            # Break loop if no ties found and continue additional runoff rounds.
            if len(tiesj) == 0:

                # Generate loser by inverting survivors
                loserbool[list(survived)] = False

                # Zero out loser rank data
                data[:, loserbool] = 0
                data = rcv_reorder(data)
                logger.info('losers=%s' % np.where(loserbool)[0])
                break
            else:
                # Zero out winner rank data temporarily for tie checking
                data2[:, winnersj] = 0

    ties = tiesj
    winners = np.sort(list(survived))
    round_history = np.array(round_history)
    logger.info('winners=%s', winners)
    logger.info('ties=%s', ties)

    output = {}
    output['round_history'] = round_history
    output['data'] = data
    return winners, ties, output
Example #5
0
def ___BROKEN_IRV_STV(data,
                      numwin=1,
                      reallocation='hare',
                      maxiter=50,
                      rstate=None):
    """
    Calculate winners of an election using Single Transferable Vote
    
    Parameters
    ----------
    data : array shaped (a, b)
        Election voter rankings, from [1 to b].
        Data composed of candidate rankings, with
        
           - Voters as the rows
           - Candidates as the columns. 
           
        Use 0 to specify unranked (and therefore not to be counted) ballots.  
        
        - a : number of voters dimension. Voters assign ranks for each candidate. 
        - b : number of candidates. A score is assigned for each candidate 
              from 0 to b-1.   
              
    numwin : int
        Number of winners
    reallocation : str
        Vote reallocation algorithm for vote transfer. 
        
        - 'hare' -- randomized transfer of surplus votes.
        
    rstate : None or `numpy.random.RandomState`
        Set to None to be truly random.
        Set RandomState to use deterministic pseudo-random number generator
    
    Returns
    -------
    winners : int array shaped (numwin,)
        Candidate index locations of winners. 
    ties : int array shaped (c,)    
        Candidate index location of tied candidates. 
    history : int array shaped (d, b)
        Vote counting record for each round. 
    """
    if rstate is None:
        rstate = np.random.RandomState

    data = np.atleast_2d(data).copy()
    data = rcv_reorder(data)
    dmax = data.max()

    original = np.copy(data)

    num_candidates = data.shape[1]
    num_voters = data.shape[0]
    quota = droop_quota(num_voters, numwin)

    logger.info('quota = %d' % quota)
    logger.info('quota percent = %7.3f' % (quota / num_voters))

    winner_list = []
    round_results = []
    ties = np.array([])
    i = 0
    while len(winner_list) < numwin:
        i += 1
        if i >= maxiter:
            logger.info('MAX ITERATION OF %s REACHED. TERMINATING' % maxiter)
            break

        # get the votes for the i^th round, shaped (a, b)
        ith_round_votes = (data == 1)

        # total votes for each candidate; array shaped (b); number of candidates
        ith_vote_totals = ith_round_votes.sum(axis=0)
        num_candidates_left = np.sum(ith_vote_totals > 0)

        logger.info("\nSTV Round %d" % i)
        logger.info("# of winners found = %s", len(winner_list))
        logger.info("Candidate vote totals = %s" % ith_vote_totals)
        logger.info("Net for this round = %s" % np.sum(ith_vote_totals))
        logger.info("# of candidates left = %s", num_candidates_left)
        logger.debug('Voter data:\n%s' % data)
        round_results.append(ith_vote_totals)

        # Which candidates have won
        round_winners = np.where(ith_vote_totals >= quota)[0]

        if len(round_winners) > 0:

            # Retrieve ballots of winners.
            for k in round_winners:
                if reallocation == 'hare':
                    surplus = ith_vote_totals[k] - quota

                    winning_ballot_index = np.flatnonzero(data[:, k] == 1)
                    winning_data = data[winning_ballot_index]
                    winning_ballot_num = len(winning_data)
                    num2remove = winning_ballot_num - surplus

                    shuffled_index = winning_ballot_index.copy()
                    rstate.shuffle(shuffled_index)
                    remove_index = shuffled_index[0:num2remove]
                    retain_index = shuffled_index[num2remove:]
                else:
                    raise ValueError(reallocation +
                                     ' not valid reallocation method')

                # Zero out winner votes that are not surplus.
                data[remove_index, :] = 0

                # Zero out the winner and re-order.
                data[:, k] = 0

                # With reorder, 2nd choices can be sorted to 1st choice.
                data = rcv_reorder(data)

                winner_list.append(k)

                logger.info("Winner Found = Candidate #%r" % k)
                logger.info("Surplus Votes = %d" % surplus)
                logger.debug("Winning ballots to remove = %s" % remove_index)
                logger.debug("Winning ballots transfer = %s" % retain_index)
                logger.info('')

        # begin candidate elimination
        else:
            if data.sum() == 0:
                logger.warning(
                    'Everyone has been elimated. No majority found... WARNING!'
                )
            logger.info('No Winners Found. Start IRV Elimination')
            data, loser, ties = IRV_eliminate(data)
            if len(ties) > 1:
                logger.info("Ties found: %s", ties)
                loser = ties[rstate.randint(0, 1)]
                logger.info("Picking a loser at random...")
                data[:, loser] = 0

            logger.info("Candidate %d eliminated." % loser)

    logger.info("WINNERS")
    logger.info(winner_list)

    return np.array(winner_list), ties, np.array(round_results)
Example #6
0
def ____BROKEN_IRV_eliminate(data):
    """Eliminate a candidate using ranked choice, instant runoff voting.
    
    Parameters
    ----------
    data : array shaped (a, b)
        Election voter rankings, from [1 to b].
        Data composed of candidate rankings, with
        
           - Voters as the rows, with `a` total voters
           - Candidates as the columns, with `b` total candidates.
           
        Use 0 to specify unranked (and therefore not to be counted) ballots.  
        

    Returns
    -------
    data : array shaped (a, b)
        Election voter rankings, but with losing/eliminated candidate's data zeroed. 
    loser : int
        Column integer index of data of losing candidate. 
        
        - lower will be equal to -1 if no candidates can be eliminated.
        - loser will be 0 or larger, if a candidate can be eliminated.
    ties : array shaped (c,)
        Index locations of tied losing candidates. Empty array if no ties. 
    """

    data = np.copy(data)
    num_candidates = data.shape[1]
    num_voters = data.shape[0]

    safe_candidates = np.zeros(num_candidates, dtype=bool)

    # iterate through rankings until a rank is found with no ties.
    for rank in range(1, num_candidates + 1):
        logger.info('Eliminating for rank %s' % rank)
        index = (data == rank)
        vote_totals = index.sum(axis=0)
        if rank == 1:
            # get previously eliminated candidates
            eliminated = np.sum(data, axis=0) == 0
            logger.info('Previously eliminated candidates=%s',
                        np.where(eliminated)[0])

            # Add eliminated to the safe list
            safe_candidates = safe_candidates | eliminated

        logger.info('Vote totals')
        logger.info(vote_totals)

        # Ignore safe candidates
        vote_totals[safe_candidates] = num_voters
        proposed_loser = np.argmin(vote_totals)

        # check for loser ties
        tie_bools = vote_totals[proposed_loser] == vote_totals
        tie_num = np.sum(tie_bools)

        # update safe candidates to include anyone not tied with loser.
        safe_candidates = safe_candidates | (~tie_bools)
        logger.info('Safe & eliminated candidates = %s',
                    np.where(safe_candidates)[0])
        logger.info('Unsafe candidates = %s', np.where(~safe_candidates)[0])

        # If no ties, eliminate
        if tie_num == 1:
            loser = proposed_loser
            logger.info('Eliminating candidate %s using #%s ranking' %
                        (loser, rank))

            # Completely zero out the loser on all ballots.
            data[:, loser] = 0

            # Ensure ranking order is good
            data = rcv_reorder(data)
            return data, loser, np.array([])

        # Continue iteration if ties found.
        elif tie_num > 1:
            logger.info(
                'Elimination tie found for candidates, at ranking #%s' % rank)
            logger.info(np.where(tie_bools)[0])

    logger.info('Tie found with no tie breaker.')
    tie_index = np.where(tie_bools)[0]
    return data, -1, tie_index
Example #7
0
def _prep_data(data):
    data = np.atleast_2d(data).copy()
    data = rcv_reorder(data)
    locs_filled = np.max(data > 0, axis=1)
    data = data[locs_filled]
    return data
Example #8
0
def irv_stv(data,
            numwin=1,
            reallocation='hare',
            weights=1,
            seed=None,
            maxiter=500):
    """
    Calculate winners of an election using Single Transferable Vote
    
    Parameters
    ----------
    data : array shaped (a, b)
        Election voter rankings, from [1 to b].
        Data composed of candidate rankings, with
        
           - Voters as the rows
           - Candidates as the columns. 
           
        Use 0 to specify unranked (and therefore not to be counted) ballots.  
        
        - a : number of voters dimension. Voters assign ranks for each candidate. 
        - b : number of candidates. A score is assigned for each candidate 
              from 0 to b-1.   
              
    numwin : int
        Number of winners
    reallocation : str
        Vote reallocation algorithm for vote transfer. 
        
        - 'hare' -- randomized transfer of surplus votes.
        - 'gregory' -- Weighted transfer of surplus votes.
        
    weights : array shaped (a,), int, or float
        Initial ballot weights, only works on gregory for now. 
        
    seed : int or Nont
        Set pseudo-random number generator for Hare.
    
    Returns
    -------
    winners : int array shaped (numwin,)
        Candidate index locations of winners. 
    ties : int array shaped (c,)    
        Candidate index location of tied candidates. 
    history : int array shaped (d, b)
        Vote counting record for each round. 
    """
    rstate = np.random.RandomState(seed)

    # retrieve number of filled ballots. Omit blank ballots.
    data = _prep_data(data)
    num_ranked, num_candidates = data.shape

    quota = droop_quota(num_ranked, numwin)
    logger.info('\n\n------ STARTING STV ----------------------')
    logger.info('stv droop quota = %s', quota)

    if reallocation == 'hare':
        allocate = hare_reallocation

    elif reallocation == 'gregory':
        allocate = gregory_reallocation

    else:
        raise ValueError(reallocation + ' not valid reallocation method')

    round_history = []
    winners = []
    exhausted = []
    winner_count = 0
    survivor_count = num_candidates
    survivor_bool = np.ones(num_candidates, dtype=bool)

    for ii in range(maxiter):
        # Get this round's winners from votes exceeding quota.
        vote_index = (data == 1)

        # Get tally totals for each candidate * weights
        tally = np.sum(vote_index * weights, axis=0)
        round_history.append(tally)

        winners_ii = np.where(tally >= quota)[0]
        winners = np.append(winners, winners_ii)
        winner_count = len(winners)

        # Get voters who won

        logger.info('\n\nstv round #%d', ii)
        logger.info('stv tally = %s', tally)
        logger.info('stv winners = %s', winners)

        # Break if we've gotten all winners
        if winner_count >= numwin:
            ties = np.array([])
            break

        # If winner found, reallocate surplus votes
        if len(winners_ii) > 0:
            data, weights = allocate(data=data,
                                     tally=tally,
                                     winners=winners_ii,
                                     quota=quota,
                                     weights=weights,
                                     rstate=rstate)
        # Perform instant runoff counting & elimination
        # irv eliminates candidates by zeroing out their rank data.
        else:
            data, exhaustedi, ties, hist = irv_eliminate(data)

            # Randomly eliminate loser if tie found.
            tienum = len(ties)
            tied = tienum > 1

            if tied:
                logger.warning('Ties found for IRV elimination for %s', ties)

                # Check if there are too many ties to complete STV
                if winner_count + tienum > numwin + survivor_count - tienum:
                    logger.warning(
                        'Ties %s too close to winner. Outputting ties', ties)
                    break
                else:
                    jj = rstate.randint(0, tienum)
                    exhaustedi = ties[jj]
                    data[:, exhaustedi] = 0
                    data = rcv_reorder(data)
                    logger.warning('Randomly eliminated ties %s', exhaustedi)

            exhausted.append(exhaustedi)
            exhausted_count = len(exhausted)
            survivor_count = num_candidates - exhausted_count

        ######################################################################
        # check for special condition if survivors equal number of winners left.
        if survivor_count + winner_count <= numwin:

            survivor_bool[np.array(exhausted)] = False
            survivors = np.where(survivor_bool)[0]
            winners = np.append(winners, survivors)

            logger.warning(
                'Too few survivors = %s, %s winners found',
                survivor_count,
                winner_count,
            )
            break

        logger.debug('stv survived # = %s', survivor_count)
        logger.debug('stv exhausted = %s', exhausted)

    winners = winners.astype(int)
    round_history = np.row_stack(round_history)

    output = {}
    output['round_history'] = round_history
    output['quota'] = quota
    return winners, ties, output
Example #9
0
def top2runoff(data, numwin=1):
    """
    Emulate top two runoff using ranked data

    
    Parameters
    ----------
    data : array shaped (a, b)
        Election voter rankings, from [1 to b].
        Data composed of candidate rankings, with
        
           - Voters as the rows, with `a` total voters
           - Candidates as the columns, with `b` total candidates.
           
        Use 0 to specify unranked (and therefore not to be counted) ballots.  
        
    numwin : int
        Number of winners
        
    Returns
    --------
    winners : array of shape (numwin,)
        Winning candidates index.
    ties : array of shape (tienum,)
        Tied candidates index for the last round, numbering 'tienum'.
    output : dict
        talley : array shape (b,)
            Number of transferred votes obtained by candidates before elimination.
       
    """

    ### round #1
    # if numwin > 1:
    #     raise ValueError('Only numwinner=1 supported')

    data = np.array(data)
    candidate_num = data.shape[1]

    vote_index = data == 1
    vote_count = np.sum(vote_index, axis=0)
    winners, ties = winner_check(vote_count, numwin=2)
    winners = np.append(winners, ties)

    loser_bool = np.ones(candidate_num, dtype=bool)
    loser_bool[winners] = False

    # zero out all losers
    data[:, loser_bool] = 0
    data = rcv_reorder(data)

    ### round #2
    vote_index = data == 1
    vote_count2 = np.sum(vote_index, axis=0)
    winners2, ties2 = winner_check(vote_count2, numwin=1)

    output = {}
    output['tally'] = np.maximum(vote_count, vote_count2)
    output['runoff_candidates'] = winners
    output['runoff_tally'] = vote_count2[winners]
    output['first_tally'] = vote_count

    return winners2, ties2, output
Example #10
0
def irv_eliminate(data):
    """Eliminate a candidate using ranked choice, instant runoff voting.
    
    Parameters
    ----------
    data : array shaped (a, b)
        Election voter rankings, from [1 to b].
        Data composed of candidate rankings, with
        
           - Voters as the rows, with `a` total voters
           - Candidates as the columns, with `b` total candidates.
           
        Use 0 to specify unranked (and therefore not to be counted) ballots.  
        

    Returns
    -------
    data : array shaped (a, b)
        Election voter rankings, but with losing/eliminated candidate's data zeroed. 
    loser : int 
        Column integer index of data of losing candidate. 
        
        - lower will be equal to -1 if no candidates can be eliminated.
        - loser will be 0 or larger, if a candidate can be eliminated.
    ties : array shaped (c,)
        Index locations of tied losing candidates. Empty array if no ties.
        
    history : array shaped (n, b)
        History of each elimination round. Multiple rounds will occur if 
        ties are found during elimination. `n` is number of rounds. 
    """
    data = np.array(data, copy=True)
    candidate_num = data.shape[1]
    start_losers = np.sum(data, axis=0) == 0
    losernum = np.sum(start_losers)
    round_history = []

    logger.debug('irv elimination start.')
    logger.debug('start losers = %s', start_losers)
    logger.debug('# losers = %s', losernum)

    active_bool = np.ones(candidate_num, dtype=bool)
    active_bool[start_losers] = False
    tie_bool = np.zeros(candidate_num, dtype=bool)
    data2 = data.copy()

    # Handle all zeros array
    if np.all(start_losers):
        logger.debug('All zero data input into irv eliminate')
        ties = np.array([], dtype=int)
        loser = -1
        vote_index = (data2 == 1)
        vote_count = np.sum(vote_index, axis=0, dtype='float64')
        round_history.append(vote_count)
        return data, loser, ties, np.array(round_history)

    elif (candidate_num - losernum) == 1:
        logger.debug('All but one candidate already eliminated')
        ties = np.array([], dtype=int)
        loser = -1
        vote_index = (data2 == 1)
        vote_count = np.sum(vote_index, axis=0, dtype='float64')
        round_history.append(vote_count)
        return data, loser, ties, np.array(round_history)

    for j in range(1, candidate_num + 1):

        vote_index = (data2 == j)
        vote_count = np.sum(vote_index, axis=0, dtype='float64')
        round_history.append(vote_count)

        vote_count[~active_bool] = np.nan

        # Negative votes to get loser rather than winner.
        losers, ties = winner_check(-vote_count, numwin=1)
        logger.debug('Eliminating from rank #%d', j)
        logger.debug('\n,%s', data2)
        logger.debug('count=%s', vote_count)
        logger.debug('losers = %s', losers)
        logger.debug('ties = %s', ties)

        if len(ties) == 0:
            loser = losers[0]
            break
        else:
            loser = -1
            tie_bool[ties] = True
            active_bool = tie_bool

    if len(ties) == 0:
        # Zero out loser rank data
        data[:, loser] = 0
        data = rcv_reorder(data)
    return data, loser, ties, np.array(round_history)