| from __future__ import unicode_literals |
| from __future__ import print_function |
| |
| from math import modf |
| import random as r |
| |
| |
| def _enumerate_with_random(xs): |
| """ List[(key * value)] -> List[(key * value * rand)] |
| |
| like 'enumerate' but with a third argument that can be used as a random |
| tiebreaker for fair sorting |
| |
| @param xs : an iterator of things |
| |
| @return : an iterator of triples consisting of |
| 1) the index into the original iterator |
| 2) the element drawn from the iterator |
| 3) a number in the range [0,1) chosen uniformly at random |
| """ |
| for (k, v) in enumerate(xs): |
| yield (k, v, r.random()) |
| |
| |
| def _normalize_entitlement(entitlement): |
| """normalize a list of entitlements so it has unit sum |
| |
| @param entitlement : a list of constants proportional to the share of |
| the total that the nth item is entitled to. |
| |
| @result : same as entitlement, but normalized to sum to 1. |
| """ |
| s = sum(entitlement) |
| return tuple(x / float(s) for x in entitlement) |
| |
| |
| def descending_fair_sort_indices(xs): |
| """fairly sort an iterator of values in descending order. |
| |
| each item in the iterator is a pair consisting of an index into xs |
| and the original value |
| |
| (4, 5, 6) --> iter([(2, 6), (1, 5), (0, 4)]) |
| |
| @param xs : an iterator of things |
| |
| @return : the indices of xs, but with ties resolved fairly |
| """ |
| for idx, _, _ in sorted( |
| _enumerate_with_random(xs), |
| key=(lambda (_, v, tiebreaker): (v, tiebreaker)), |
| reverse=True): |
| yield idx |
| |
| |
| def _full_partial_remaining(quota, seats): |
| """number of full seats, partial seats, and remaining seats to be filled. |
| |
| given a list of numbers with unit sum (e.g. [0.5, 0.2, 0.3]) |
| perform the first step of fairly allocating a non-negative integer |
| number of seats between them. |
| |
| @param quota : a list of numbers with unit sum |
| |
| @param seats : the number of items to be distributed. |
| |
| @return : a triple containing three things |
| 1) the number of full seats each index is entitled to |
| 2) the number of partial seats each index is entitled to |
| 3) the number of remaining seats that need to be filled from (2) |
| """ |
| full = [] |
| partial = [] |
| must_fill = seats |
| for x in quota: |
| partial_seat, full_seats = modf(x * seats) |
| full.append(full_seats) |
| partial.append(partial_seat) |
| must_fill -= full_seats |
| return full, partial, must_fill |
| |
| |
| def _largest_remainder(entitlement, seats): |
| """distribute stuff according to the largest remainder method. |
| |
| @param entitlement : a not-necessarily-normalized list of numbers |
| representing |
| how many seats/things each index is entitled to. |
| |
| @param seats : the number of seats to distribute |
| |
| @return : a list of integers of the same length as entitlement summing to |
| seats. The allocation of seats is intended to be as close as |
| possible |
| To the original entitlement. |
| """ |
| quota = _normalize_entitlement(entitlement) |
| out, rems, remaining = _full_partial_remaining(quota, seats) |
| indices = descending_fair_sort_indices(rems) |
| for idx in indices: |
| if remaining <= 0: |
| break |
| out[idx] += 1 |
| remaining -= 1 |
| assert sum(out) == seats |
| return out |
| |
| |
| def _indices(xs): |
| """get an iterator of indices over an iterator. |
| |
| Do not materialize the entire iterator first. |
| |
| @param xs : an iterator of things |
| |
| @return : an iterator of indices of the same length as (xs) |
| """ |
| for k, _ in enumerate(xs): |
| yield k |
| |
| |
| def partition(xs, ratio): |
| """take a list of items and a ratio and return two lists. |
| |
| The ratio determines which fraction of the items are transferred. |
| |
| @param xs : a list of things to split between the transfer and retain group. |
| |
| @param ratio : the ratio of things to transfer. |
| |
| @return : a list of two things |
| 1) the elements of xs that are going to be transferred |
| 2) the elements of xs that are going to be retained |
| """ |
| ratios = [ratio, 1.0 - ratio] |
| transfer_count, _ = _largest_remainder(ratios, len(xs)) |
| transfer_count = int(round(transfer_count)) |
| to_transfer_indices = r.sample( |
| population=list(_indices(xs)), k=transfer_count) |
| to_transfer = [] |
| to_retain = [] |
| for k, v in enumerate(xs): |
| if k in to_transfer_indices: |
| to_transfer.append(v) |
| else: |
| to_retain.append(v) |
| return to_transfer, to_retain |