I was going to offer a solution based on a Bresenham line between the cumulative sum of the input data and the cumulative sum of the proportional output values but (a) it turns out to give the wrong answer - see below - and (b) I believe @tzaman's pointer to Allocate an array of integers proportionally compensating for rounding errors provides a simpler solution than any correction that I could make to the Bresenham method (the proportional()
function is by @Dr. Goulu):
def proportional(nseats,votes):
"""assign n seats proportionaly to votes using Hagenbach-Bischoff quota
:param nseats: int number of seats to assign
:param votes: iterable of int or float weighting each party
:result: list of ints seats allocated to each party
"""
quota=sum(votes)/(1.+nseats) #force float
frac=[vote/quota for vote in votes]
res=[int(f) for f in frac]
n=nseats-sum(res) #number of seats remaining to allocate
if n==0: return res #done
if n<0: return [min(x,nseats) for x in res] # see siamii's comment
#give the remaining seats to the n parties with the largest remainder
remainders=[ai-bi for ai,bi in zip(frac,res)]
limit=sorted(remainders,reverse=True)[n-1]
#n parties with remainter larger than limit get an extra seat
for i,r in enumerate(remainders):
if r>=limit:
res[i]+=1
n-=1 # attempt to handle perfect equality
if n==0: return res #done
raise #should never happen
print (proportional(20,[832, 325, 415, 385, 745]))
print (proportional(20,[414, 918, 860, 978, 438]))
... gives the output:
[6, 2, 3, 3, 6]
[2, 5, 5, 6, 2]
... as required.
Bresenham line (non-)solution
For those who may be interested in the Bresenham line (non-)solution, here it is, based on the code here:
import itertools, operator
def bresenhamLine(x0, y0, x1, y1):
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = x0 < x1 and 1 or -1
sy = y0 < y1 and 1 or -1
err = dx - dy
points = []
x, y = x0, y0
while True:
points += [(x, y)]
if x == x1 and y == y1:
break
e2 = err * 2
if e2 > -dy:
err -= dy
x += sx
if e2 < dx:
err += dx
y += sy
return points
def proportional(n,inp):
cumsum = list(itertools.accumulate(inp))
pts = bresenhamLine(0,0,max(cumsum),n)
yval = [y for x,y in pts]
cumsum2 = [yval[x] for x in cumsum]
res = [cumsum2[0]]
for i,x in enumerate(cumsum2[1:]):
res.append(x-cumsum2[i])
return res
print (proportional(20,[832, 325, 415, 385, 745]))
print (proportional(20,[414, 918, 860, 978, 438]))
... however the output is
[6, 3, 3, 2, 6]
[2, 5, 5, 6, 2]
... which is incorrect because for the second to fourth items in the first list, it assigns "2" to the middle-ranked item rather than the lowest-ranked item. The Hagenbach-Bischoff quota method gets this allocation correct.