4

I need to subdivide a given amount of items (lets say 10) using a given ratio [0.55, 0.45]. The result here should either be 6:4 or 5:5. The usual approach [0.55*10, 0.45*10] would result in [6, 5] (11, not 10).

Another example: divide 7 using ratio: [0.36, 0.44, 0.07, 0.07, 0.03, 0.03] which ideally should yield something like [3, 3, 1, 0, 0, 0] or [3, 3, 0, 1, 0, 0].

What would be a good approach to this problem?

Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
SecondLemon
  • 953
  • 2
  • 9
  • 18
  • Have you attempted to solve this problem? If you have, please edit your question to include your code and research to show what hasn't worked for you. If you haven't, you should attempt to solve it yourself first and then post the code and research here. It makes your question easier for others to answer too! – SuperBiasedMan Dec 08 '15 at 12:59
  • See this [stackoverflow](http://stackoverflow.com/questions/2356501/how-do-you-round-up-a-number-in-python) and this [python math](https://docs.python.org/2/library/math.html) – Jose Rocha Dec 08 '15 at 13:00
  • I could write something that after doing it as in the first example, ending up with [6,5] would measure if there are more or less in it and then simply, if more, delete one from the highest number or, if less, add to the highest ratio that does not have anything yet. However I was wondering if there is some smart mathematical way to do this. – SecondLemon Dec 08 '15 at 13:03
  • @Jose Rocha How would using the ceil function work? If you look at [0.55*10, 0.45*10] it would result in [6,5] with round or ceil either way. I can't manually define if it should round up or down since the ratio is not always made up of 2 numbers. – SecondLemon Dec 08 '15 at 13:08
  • 1
    "would result in `[6, 5]`" - Nope. "should yield *something like*" - oh come on, don't be sloppy. – Karoly Horvath Dec 08 '15 at 13:09
  • 1
    Go to [repl](https://repl.it/languages/python3) and write `a = [0.55, 0.45]` next `round(a[1]*10)` next `round(a[0]*10)` and it will give `6` and `4`, what can i do more for you? – Jose Rocha Dec 08 '15 at 13:17
  • @Jose Rocha Is this different for python 3 compared to 2.7? `>>> round(0.45*10) 5.0` I guess it is: https://repl.it/B6rG/0 – SecondLemon Dec 08 '15 at 13:23
  • [2.7 round](https://docs.python.org/2/library/functions.html#round) vs [3 round](https://docs.python.org/3/library/functions.html#round) seems to me that they are the same. – Jose Rocha Dec 08 '15 at 13:25
  • Well I don't know what is happening then. See my previous link (https://repl.it/B6rG/0) I get 5.0 and 6.0 for sure (as should be of course). – SecondLemon Dec 08 '15 at 13:28
  • `import math` `a = [0.55, 0.45]` `print math.floor(a[1]*10)` `print math.floor(a[0]*10)` this works for you? – Jose Rocha Dec 08 '15 at 13:30
  • Well then I get 4 and 5 respectively (as expected). So not right. – SecondLemon Dec 08 '15 at 13:31

5 Answers5

3

Here's my try on the matter :) The hardest part being reversing the sort operation and matching it with results... If you don't need to keep the original order of ratios, then you can delete part of the last function.

def scale_ratio(ratios: list) -> list:
    sum_ = sum(ratios)
    return [x/sum_ for x in ratios]

def ratio_breakdown_recursive(x: int, ratios: list) -> list:
    top_ratio = ratios[0]
    part = round(x*top_ratio)
    if x <= part:
        return [x]
    x -= part
    return [part] + ratio_breakdown_recursive(x, scale_ratio(ratios[1:]))


def ratio_breakdown(x: int, ratios: list) -> list:
    sorted_ratio = sorted(ratios, reverse=True)
    assert(round(sum(ratios)) == 1)
    sorted_result = ratio_breakdown_recursive(x, sorted_ratio)
    assert(sum(sorted_result) == x)
    # Now, we have to reverse the sorting and add missing zeros
    sorted_result += [0]*(len(ratios)-len(sorted_result))
    numbered_ratios = [(r, i) for i, r in enumerate(ratios)]
    sorted_numbered_ratios = sorted(numbered_ratios, reverse=True)
    combined = zip(sorted_numbered_ratios, sorted_result)
    combined_unsorted = sorted(combined, key=lambda x: x[0][1])
    unsorted_results = [x[1] for x in combined_unsorted]
    return unsorted_results

Results:

ratio_breakdown(7, [0.36, 0.44, 0.07, 0.07, 0.03, 0.03])
[3, 3, 1, 0, 0, 0]
ratio_breakdown(10, [0.55, 0.45])
[6, 4]
ratio_breakdown(16, [0.16, 0.47, 0.13, 0.24])
[2, 8, 2, 4]

EDIT: That's Python3.

Maciek
  • 3,174
  • 1
  • 22
  • 26
  • Thanks, looks promising. Let my study it for a while and I report back to you. (doesn't matter if it is in 3 or 2.7) – SecondLemon Dec 08 '15 at 13:55
  • I have a bit of a hard time understanding what is going on sometimes but I get the gist of it. However what is happening here (another example): `ratio_breakdown(16, [0.16, 0.47, 0.13, 0.24]) --> [2, 3, 3, 8]` – SecondLemon Dec 08 '15 at 14:03
  • Obviously something wrong :D Let mee see if I can fix it. – Maciek Dec 08 '15 at 14:04
  • I forgot to pass the sorted version of ratios to the recursive function :) Also, changed `math.ceiling` to `round`, it gives nicer results now I guess. – Maciek Dec 08 '15 at 14:12
  • Very nice. You seem to have solved it. A well deserved upvote and accept! Thanks a lot. – SecondLemon Dec 08 '15 at 14:13
  • @Maciek, I found this answer helpful, but needed it in R. I rewrote your answer and posted it as an additional answer for any other **R programmers** that pass by that could benefit from your work. – ScottyJ Feb 20 '23 at 03:17
2

I would suggest you to have another array. I'm beginner in Python.

Here is the code with your example (you can simply adapt it, I'm sure) :

a = [0.36, 0.44, 0.07, 0.07, 0.03, 0.03]

numberItem = 7
remainder = numberItem


b = [0,0,0,0,0,0]

for i in range(0,6):
    b[i] = round(a[i]*numberItem)
    if (b[i] > remainder) or (b[i] == 0):
        b[i] = remainder
        remainder = 0
    else:
            remainder = remainder - b[i]
    print(b[i])

With this bounds, you cannot have more items than stated. It's better if the ratio array is sorted from the bigger to the smaller.

Iggy
  • 53
  • 8
  • Wow... Also very nice. Almost a shame I gave away the answer already. Let me test this a bit. – SecondLemon Dec 08 '15 at 14:15
  • It is also very nice and good. However Maciek's answer seems a bit more acurate. If you look at this example: 16 samples with ratio [0.47, 0.24, 0.16, 0.13] your algorithm returns [8,4,3,1] and Maciek's [8,4,2,2]. A nice approach though! – SecondLemon Dec 08 '15 at 14:21
  • I've seen the Maciek algorithm after, and it seems more complete :-) – Iggy Dec 08 '15 at 14:24
  • Doesn't correctly work for "6" with [0.4, 0.4, 0.2] ratios - it will return [2, 2, 1] and 1 will remain in the remainder. – Oleg Polosin Oct 20 '22 at 00:54
0

Here is a non-recursive NumPy implementation of the @maciek's algorithm:

import numpy as np


def split_integer_into_parts(x: int, ratios: list) -> np.ndarray:

    ratios = np.array(ratios, dtype=float)

    assert x >= 0
    assert (ratios >= 0).all()
    assert ratios.sum() > 0

    # sort ratios
    sort_idx = np.argsort(-ratios)
    ratios = ratios[sort_idx]

    # compute fractions of the remainders
    ratios_cumsum = np.cumsum(ratios[::-1])[::-1]
    fracs = np.divide(ratios, ratios_cumsum, out=np.ones_like(ratios), where=(ratios_cumsum != 0))

    # split integer into parts
    remainder = x
    parts = np.zeros_like(fracs, dtype=int)
    for i, frac in enumerate(fracs):
        parts[i] = round(remainder * frac)
        remainder -= parts[i]

    assert parts.sum() == x

    # unsort parts
    parts = parts[np.argsort(sort_idx)]

    return parts
Oleg Polosin
  • 81
  • 1
  • 5
0

Accepted Answer written in R

I was looking for this question/solution, but I'm working in R, so I have re-written @Maciek's answer above in R for anyone that passes this way. I kept it as close as I could to the original answer in Python.

library(dplyr)

scale_ratio <- function(ratios){
  sum_ <- sum(ratios)
  return(ratios/sum_)
}

ratio_breakdown_recursive <- function(x, ratios){
  top_ratio <- ratios[1]
  part <- round(x*top_ratio)
  if (x <= part){
    return(x)
  }
  x <- (x - part)
  c(part, ratio_breakdown_recursive(x, scale_ratio(ratios[2:length(ratios)])))
}

ratio_breakdown <- function(x, ratios){
  x <- x[1]
  sorted_ratio = sort(ratios, decreasing = TRUE)
  stopifnot(round(sum(ratios)) == 1)
  sorted_result = ratio_breakdown_recursive(x, sorted_ratio)
  stopifnot(sum(sorted_result) == x)
  # Now, we have to reverse the sorting and add missing zeros
  sorted_result <- append(sorted_result, rep(0, length(ratios) - length(sorted_result)))
  numbered_ratios <- data.frame(ratios, seq_along(ratios))
  sorted_numbered_ratios <- arrange(numbered_ratios, desc(ratios))
  combined <- cbind(sorted_numbered_ratios, sorted_result)
  combined_unsorted <- arrange(combined, seq_along.ratios.)
  unsorted_results <- combined_unsorted[,3]
  return(unsorted_results)
}

> ratio_breakdown(7, c(0.36, 0.44,0.07,0.07,0.03,0.03))
[1] 3 3 0 1 0 0
> ratio_breakdown(10, c(0.55,0.45))
[1] 6 4
> ratio_breakdown(16, c(0.16,0.47,0.13,0.24))
[1] 2 8 2 4

I realize the first answer returns a different order than @Maciek's answer, but for my purposes, this worked, and if someone comes along and can improve my R code, be my guest -- I'm happy to learn.

ScottyJ
  • 945
  • 11
  • 16
  • Please don't post answers in a programming language that's not asked for in the question. Instead, post this on an R question that's asking the same question. If one doesn't exist, you should go ahead and post your own question, along with a self-answer. – cigien Feb 20 '23 at 07:06
  • @cigien Works for me. I get points for answering my own post, I think. – ScottyJ Feb 20 '23 at 17:45
0

I would like to suggest another solution. The idea is to first calculate the approx integer subdivision by taking the floor of the float subdivision. The difference between the desired sum and the sum of the approx tells us how many values we need to increment to get the correct result. We sort the values by taking the difference between the floats and the ints. The higher the difference is the closest the value to it's ceil. Finally, we increment as many of the closest values to their ceil as we need to get the correct sum.

Note: I implemented using NumPy, but converting to standard Python should be straight forward.

def split_integer_into_parts(val: int, ratio: np.ndarray) -> np.ndarray:
    ratio = np.asarray(ratio)

    assert val >= 0
    assert np.all(ratio >= 0)
    assert np.round(np.sum(ratio), 5) == 1

    # get float and approx int subdivision
    result_f = ratio * val
    result_i = np.floor(result_f)
    # get difference between float and int
    diff_floor = result_f - result_i
    result_i = result_i.astype(int)  # convert to int
    # difference from approx (#values to increment)
    diff_val = val - np.sum(result_i)
    if diff_val == 0:
        return result_i
    # reverse sort to get highest differences (closest to ceil)
    idx = np.argsort(diff_floor)[::-1]
    # increment as many of the closest as diff from approx
    result_i[idx[:diff_val]] += 1

    assert np.sum(result_i) == val

    return result_i

Results:

split_integer_into_parts(6, [0.4, 0.4, 0.2])
[2 3 1]
split_integer_into_parts(10, [0.45, 0.55])
[4 6]
split_integer_into_parts(16, [0.47, 0.24, 0.16, 0.13])
[7 4 3 2]
split_integer_into_parts(7, [0.36, 0.44, 0.07, 0.07, 0.03, 0.03])
[3 3 0 1 0 0]
codingmonkey87
  • 186
  • 1
  • 4