Reputation: 2143
I have an dictionary of lists of objects as shown below:
IDictionary<string, IList> MyItemDictionary
I work out the percentages by doing a for each through the dictionary with the code below:
IList<double> percentages = new List<double>();
foreach(KeyValuePair<string, IList> pair in MyItemDictionary)
{
double percentage = (100d/totalItemsCount)*pair.Value.Count;
percentages.Add(percentage);
}
Basically I need to process the percentages list and round each percentage to a whole number BUT have them add up to 100. Accuracy is not of the highest importance but a sub 1 percentage i.e. 0.45 needs to be rounded to 1.
Does anyone know how to do this?
Upvotes: 2
Views: 2266
Reputation: 154
This is my C# implementation of the Largest Remainder Method. I have not tested it extensively, but so far so good. There may be some edge cases where it doesn't work.
Here is a sample call:
var unrounded = new List { 1.0M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M, 1.5M };
var rounded = RoundNumberList(unrounded);
/// <summary>
/// Round list of numbers using the Largest Remainder Method. Sum of Numbers should equal 100.
/// </summary>
/// <param name="Numbers">List of decimals numbers to round</param>
/// <returns>Rounded list of integers</returns>
public static List<int> RoundNumberList(List<decimal> Numbers)
{
int sum = 0;
var rounded = new Dictionary<int, decimal>();
for (int i = 0; i < Numbers.Count; i++)
{
rounded.Add(i, Numbers[i]);
sum += (int)Numbers[i];
}
if (sum > 100)
throw new Exception("The sum of all numbers is > 100.");
if (100 - sum > Numbers.Count)
throw new Exception("The sum of all numbers is too low for rounding: " + sum.ToString());
if (sum < 100)
{
// Sort descending by the decimal portion of the number
rounded = rounded.OrderByDescending(n => n.Value-(int)n.Value).ToDictionary(x => x.Key, x => x.Value);
int i = 0;
int diff = 100 - sum;
foreach (var key in rounded.Keys.ToList())
{
rounded[key]++;
i++;
if (i >= diff) break;
}
// Put back in original order and return just integer portion
return rounded.OrderBy(n => n.Key).Select(n => (int)n.Value).ToList();
}
else
{
// Return just integer portion
return rounded.Select(n => (int)n.Value).ToList();
}
}
Upvotes: 3
Reputation: 56772
Take a look at how seats in a parliament are assigned based on votes for proportional representation. I would suggest the largest remainder method, because it uses time proportional to the number of items in your list.
Upvotes: 1
Reputation: 19905
If your percentage list ends up containing 1.5 + 1.5 + .. + 1.5 = 100.0 and you were to round them into 2 + 2 + .. + 2, you would end up with 134 as the total sum (~67 entries), not 100. The way to fix this, is to distribute the error (134-100=34) among the existing percentages. In this case, you would subtract 1 from 34 of the percentages so you end up with a series of 1 + 2 + 1 + 2 + .. + 2 = 100.0.
To find what "every other" means you simply do int(numberOfPercentages / theError) and that should give you the interval.
Also, you must take care not to subtract anything from your sub 1 percentages.
Oh, and in case all your percentages are sub 1, the problem cannot be solved :-(
Upvotes: 3
Reputation: 17020
Can you have an "Other" or "Misc" category? If you have many small percentages, and you round them all up or down, you will accumulate a huge error as wic pointed out. You might need a minmimum threshold beyond which you lump everything together as miscellaneous stuff.
Otherwise, I second wic's idea about summing up the percentages to find the error and then distributing the error over the largest percentages.
Upvotes: 1