寻找要向下取整的集合中最大的数字,并将其向上取整。

5
正如标题所描述的那样,我有一组对象 - 称它们为“分配” - 包含一个描述和一个数字。集合中的所有数字加起来等于100%,但出于显示目的,有时会四舍五入到整数百分比。在某些极端情况下,当我将数字舍入后,可能会得到99%。
例如:
Description  | Actual | Rounded
===============================
Allocation A | 65.23% | 65% 
Allocation B | 25.40% | 25%
Allocation C | 7.95%  | 8%
Allocation D | 1.42%  | 1%
===============================
Total        | 100%   | 99% (Bad!)

请求的解决方案并不完美,但可以使用。需要找到最高的要舍去的数字,并将其向上取整。在上面的例子中,1.42%在取整后会变成2%。编辑:所谓“最高的要舍去的数字”,是指被舍去的数字最大的那个。因此,在1.42%中,0.42被舍去,而在65.23中,只有0.23被舍去。 现在看一下代码,我有一个类。
public class Allocation
{
  public string Description {get;set;}
  public doubel Percentage {get;set;}
}

这些数据被保存在一个 IEnumerable<Allocation> 中。因此,可能需要使用 LINQ 来确定哪个是要四舍五入的数据。或者更具体地说,如何生成一个新的 IEnumerable<Allocation> 来四舍五入。

如果有任何其他建议,始终使四舍五入的百分比等于 100%,那就更好了!


关于需求和算法的评论:看起来您想确定您的四舍五入值超过或低于100的程度,然后通过将尽可能多的值向另一方向进行四舍五入来调整这些值。在这样做的同时,您希望引入最少的误差。因此,根据您提供的数据,分配B应该向上舍入,因为42是舍入下来的最高小数值。这听起来对吗? - Steven Sudit
哦,还有一件事:由于这些是保留两位小数的财务价值,所以您应该使用“decimal”而不是“double”。 - Steven Sudit
Supercat的回答引出了一个问题,即我们如何定义最小误差。我们是指最小绝对误差还是最小相对误差? - Steven Sudit
1
这里有一个相关的问题:https://dev59.com/VXRA5IYBdhLWcg3w4iF_ - Mnebuerquo
5个回答

3
如ho1所示,给特定行加1并不能解决真正的问题。
考虑以下情况:
3 items evenly divided, 100/3 = 33 ; 33 * 3 = 99 ; Error = -1
7 items evenly divided, 100/7 = 14 ; 14 * 7 = 98 ; Error = -2
66 items evenly divided, 100/66 = 2 ; 2 * 66 = 132 ; Error = 32

这里有一些未经测试的代码,可能能让你接近你需要到达的位置。这里可能会有一个符号错误,请小心。

public class AllocationRoundingWrapper
{
  public Allocation Original {get;set;}
  public double Rounded {get;set;}
  public double IntroducedError()
  {
    return  Rounded - Original.Percentage;
  }
}

  //project the Allocations into Wrappers for rounding efforts.

List<Allocation> source = GetAllocations();

List<AllocationRoundingWrapper> roundingWrappers = source
  .Select(a => new AllocationRoundingWrapper()
  {
    Original = a,
    Rounded = Math.Round(a.Percentage)
  }).ToList();

int error = (int) roundingWrappers.Sum(x => x.IntroducedError());

  //distribute the rounding errors across the
  // items with the absolute largest error.

List<RoundingWrapper> orderedItems = error > 0 ?
  roundingWrappers.OrderByDescending(x => x.IntroducedError()).ToList() :
  roundingWrappers.OrderBy(x => x.IntroducedError()).ToList();

IEnumerator<RoundingWrapper> enumerator = orderedItems.GetEnumerator();

while(error > 0)
{
  enumerator.MoveNext();
  enumerator.Current.Rounded += 1.0;
  error -= 1;
}
while(error < 0)
{
  enumerator.MoveNext();
  enumerator.Current.Rounded -= 1.0;
  error += 1;
}

  //project back into Allocations for the result
List<Allocation> result = roundingWrappers
  .Select(x => new Allocation()
  {
    Description = x.Original.Description,
    Percentage = x.Rounded
  }).ToList();

注意:按引入误差进行排序可能会导致并列的情况。考虑一个有3个项的案例,只有一个项会得到+1……你可以期望该项被一致选择。如果希望在多次运行中获得一致的结果,则需要解决并列情况。

+1,不过,原帖指出实际值保证精确到小数点后两位,因此你的情景是不可能发生的。 - Steven Evers
最差的一个是67等分:将67个项目平均分配,100/67 = 1; 1 * 67 = 67; 错误 = -33 - Ax.
@David B ~ 我的错,当我说+1时,我是指...只是有点延迟 :) 抱歉。另外100/7 = ~14.2857和14.29 * 7 = 100.03。 - Steven Evers
@David B~ 我的理解不是这样的。据我所知,他收到了一组准确到小数点后两位的数字列表(它们总和恰好为100%),他的任务是将它们四舍五入到小数点后0位,使得四舍五入后的数字总和仍然为100%。 - Steven Evers
@SnOrfus,你在技术上是正确的。7*14.29确实不等于100,因此不能适用于对问题的狭义解释。但它仍然是一个解决集合取整问题的有用思维练习。如果这让您感到困扰,请考虑将六个14.29和一个14.26相加。另外 - 请忽略14.29无法在double类型中精确表示的事实。 - Amy B
David,谢谢你的帮助 - 这对我很有帮助。而且与我在晚上思考后得出的结果非常接近。 - Jamiec

3
我建议始终向下取整,如果结果是100-n,则将具有'n'个最大余数的数字四舍五入。这适用于任何数据。尝试先四舍五入到最近的数字,然后再调整结果的方法可能会更加复杂。我认为分配按0.01%舍入后总和为100.00%并不意味着当它们被舍入到最近的0.1%或1%时会发生什么。
另一种方法是使用四舍五入到最近的数字进行初始计算,如果结果不等于100%,则将所有数字除以总百分比,然后再次尝试。因此,如果最终百分比为101%,则将所有(未舍入)数字除以1.01并重复四舍五入和求和序列。这将给出略微不同的结果,您可能会发现更或者不太理想。假设数字为1.3 1.3 1.3 96.1。当舍入时,它们总共为99。将其中一个1.3四舍五入为2将使总和为100,但是四舍五入会使其值失真53%而不是23%;相比之下,将96.1四舍五入为97将表示其值的约0.95%失真(97与96.1)。

我喜欢它。第一个想法是一个干净、易于理解的解决方案,很容易正确实现。 - Steven Sudit
我正在重新阅读第二个想法,虽然它似乎有点复杂,但确实提出了一个很好的观点:将1.4四舍五入为2.0还是将60.3四舍五入为60.1更好?换句话说,我们是在尝试在每个分配基础上最小化失真吗? - Steven Sudit
这里的第二个想法并不适用于所有数据集。使用这些数字尝试一下:[19.6,19.7,19.7,20.5,20.5]它们的总和是100,但四舍五入后它们相加为102。将它们缩小并四舍五入,它们相加为97。 - Mnebuerquo
@Mnebuerquo:说得好。建议的缩放可能会超过预期,并不一定会落入吸引子中。可以通过多次尝试来解决问题,这些尝试不需要完美地命中目标,而是逐步接近它。如果原始数据中存在多个“并列”,那么这种方法可能行不通(例如,如果八个数据项精确地为12.5,则无法将它们缩放以使它们总和为100);在这种情况下,必须回到第一种方法。 - supercat

2

关于如何得到100%,为什么不先运行原始计算一次,看看您能得到多少百分比,然后根据与100%相差的百分点数来确定需要向上或向下舍入多少个数字。

因此,如果您最终得到了97%,则向上舍入3个数字而不是向下。 或者如果您最终得到了102%,则将小数位最低的两个数字(超过0.5)向下舍入而不是向上。


只是为了澄清,它永远只能是99%或100%,确定我有一个99%的情况不是问题,问题是知道在集合中哪个项目要向上舍入而不是向下。 - Jamiec
@Jamiec:说得也对,但是出于好奇,你真的确定它只能是99%或100%吗?你有其他检查方法来确保值不可能是65.47、25.51、7.51和1.51这样的数吗? - Hans Olsson
2
同意ho1的观点。你是否在说只有99,因为这是你遇到的所有情况,还是实际上有业务规则防止这种情况?看一下50.50 + 49.50,以获得101的情况。看一下100%/16,以获得四舍五入=100和未舍入=96的情况。 - Mike M.
好的,为了给这个思考练习增加一点领域知识。这些百分比代表投资组合中资产类别的分配比例。这些以2dp精度表示的百分比最终总是等于100%,这是事实。我的问题是,对于一个特定的功能,我必须先将资产配置四舍五入到0dp的精度,但它仍然必须总和为100%。从逻辑上讲,我认为我只能最终得到99%或100%,但我可能错了。 - Jamiec
@Mike M - 似乎你证明了我错了。正如你的50.50 + 49.50案例所示,我的结果可能在99-101范围内结束。谢谢让我的问题变得更加复杂:) 我现在需要一个人工四舍五入的解决方案。感激不尽!开个玩笑。 - Jamiec

1
 var HighestDown = 
       allocation.Where(a=>Math.Round(a.Percentage) == Math.Floor(a.Percentage)
                 .Max(a=>a.Percentage - Math.Floor(a.Percentage));

  HighestDown.Percentage = Math.Ceiling(HighestDown.Percentage);

  var roundedAllocations = for a in allocation
                           select new Allocation
                                  {
                                       Description = a.Description,
                                       Percentage = Math.Round(a.Percentage)
                                  };

谢谢您的回答,它看起来很有前途,与我在本地尝试的类似,只是您得到的是最高的数字,而不是被舍入最远的数字。请查看我对原始帖子的编辑。 - Jamiec
@Jamiec: 请看我对答案的编辑(PS:我的小名是“Jamie C”) - James Curran
目前 - 我的姓氏是Cohen。我的真名是詹姆斯。我不知道有多少次在电话中被误称为“詹姆斯·科伦”。好恐怖! - Jamiec

0

我认为这可能是你要找的内容。它可能需要进行清理和优化,但在决定将数字四舍五入到另一侧时,它采用了最大距离。

    static List<double> Round2(List<double> actualAllocations)
    {
        List<double> actual = new List<double>();
        actual.AddRange(actualAllocations);

        List<double> rounded = new List<double>();
        foreach (var a in actual)
            rounded.Add(Math.Round(a));

        if (rounded.Sum() == 100)
        {
            return rounded;
        }
        else
        {
            bool roundUp = rounded.Sum() < 100;

            for (int i = 0; i < Math.Abs(100 - rounded.Sum()); i++)
            {
                var index = actual.IndexOf(
                    (from a in actual
                    orderby Math.Abs(Math.Round(a) - a) descending
                    select a).First());

                if (roundUp)
                    actual[index]++;
                else
                    actual[index]--;
            }
        }

        rounded.Clear();
        foreach (var a in actual)
            rounded.Add(Math.Round(a));

        return rounded;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接