按比例分配(按比例分配)一组值的值

时间:2009-12-18 01:10:52

标签: c# math

我需要根据列表中“基础”值的相对权重编写将在列表中按比例分配值的代码。简单地将“基础”值除以“基础”值的总和,然后将因子乘以原始值以按比例分配工作:

proratedValue = (basis / basisTotal) * prorationAmount;

但是,必须将此计算的结果舍入为整数值。舍入的效果意味着列表中所有项目的proratedValue总和可能与原始prorationAmount不同。

任何人都可以解释如何应用“无损”比例算法,该算法尽可能准确地在列表中按比例分配值,而不会出现舍入误差吗?

6 个答案:

答案 0 :(得分:16)

这里的简单算法草图......

  1. 有一个从0开始的运行总计。
  2. 对于第一项,请按标准“除以总分为基础,然后乘以比例”。
  3. 将运行总计的原始值存储在其他位置,然后在#2中添加刚刚计算的数量。
  4. 将运行总计的旧值和新值四舍五入为整数(不要修改现有值,将它们四舍五入为单独的变量),并采取差异。
  5. 在步骤4中计算的数字是分配给当前基础的值。
  6. 每个基础重复步骤#2-5。
  7. 保证按比例分配的总金额等于输入的比例金额,因为您实际上从未实际修改过运行总计(您只需将其舍入值用于其他计算,不要将其写回)。现在处理整数舍入的问题,因为舍入误差将在运行总计中随时间累加,并最终在另一个方向上跨越舍入阈值。

    基本示例:

    Input basis: [0.2, 0.3, 0.3, 0.2]
    Total prorate: 47
    
    ----
    
    R used to indicate running total here:
    
    R = 0
    
    First basis:
      oldR = R [0]
      R += (0.2 / 1.0 * 47) [= 9.4]
      results[0] = int(R) - int(oldR) [= 9]
    
    Second basis:
      oldR = R [9.4]
      R += (0.3 / 1.0 * 47) [+ 14.1, = 23.5 total]
      results[1] = int(R) - int(oldR) [23-9, = 14]
    
    Third basis:
      oldR = R [23.5]
      R += (0.3 / 1.0 * 47) [+ 14.1, = 37.6 total]
      results[1] = int(R) - int(oldR) [38-23, = 15]
    
    Fourth basis:
      oldR = R [37.6]
      R += (0.2 / 1.0 * 47) [+ 9.4, = 47 total]
      results[1] = int(R) - int(oldR) [47-38, = 9]
    
    9+14+15+9 = 47
    

答案 1 :(得分:11)

TL; DR 算法,准确率最高(+ 20%),速度减慢70%。

在已接受的答案here以及answer中提出的类似性质的蟒蛇问题的推测算法。

测试结果(10,000次迭代)

Algorithm    | Avg Abs Diff (x lowest) | Time (x lowest)     
------------------------------------------------------------------
Distribute 1 | 0.5282 (1.1992)         | 00:00:00.0906921 (1.0000)
Distribute 2 | 0.4526 (1.0275)         | 00:00:00.0963136 (1.0620)
Distribute 3 | 0.4405 (1.0000)         | 00:00:01.1689239 (12.8889)
Distribute 4 | 0.4405 (1.0000)         | 00:00:00.1548484 (1.7074)

方法3的准确率提高了19.9%,执行时间比预期慢70.7%。

分发3

尽最大努力在分发金额方面尽可能准确

  1. 正常分配权重
  2. 使用最高错误增加权重,直到实际分配的金额等于预期金额
  3. 通过在循环中进行多次传递,牺牲了准确性。

    public static IEnumerable<int> Distribute3(IEnumerable<double> weights, int amount)
    {
        var totalWeight = weights.Sum();
        var query = from w in weights
                    let fraction = amount * (w / totalWeight)
                    let integral = (int)Math.Floor(fraction)
                    select Tuple.Create(integral, fraction);
    
        var result = query.ToList();
        var added = result.Sum(x => x.Item1);
    
        while (added < amount)
        {
            var maxError = result.Max(x => x.Item2 - x.Item1);
            var index = result.FindIndex(x => (x.Item2 - x.Item1) == maxError);
            result[index] = Tuple.Create(result[index].Item1 + 1, result[index].Item2);
            added += 1;
        }
    
        return result.Select(x => x.Item1);
    }
    

    分发4

    public static IEnumerable<int> Distribute4(IEnumerable<double> weights, int amount)
    {
        var totalWeight = weights.Sum();
        var length = weights.Count();
    
        var actual = new double[length];
        var error = new double[length];
        var rounded = new int[length];
    
        var added = 0;
    
        var i = 0;
        foreach (var w in weights)
        {
            actual[i] = amount * (w / totalWeight);
            rounded[i] = (int)Math.Floor(actual[i]);
            error[i] = actual[i] - rounded[i];
            added += rounded[i];
            i += 1;
        }
    
        while (added < amount)
        {
            var maxError = 0.0;
            var maxErrorIndex = -1;
            for(var e = 0; e  < length; ++e)
            {
                if (error[e] > maxError)
                {
                    maxError = error[e];
                    maxErrorIndex = e;
                }
            }
    
            rounded[maxErrorIndex] += 1;
            error[maxErrorIndex] -= 1;
    
            added += 1;
        }
    
        return rounded;
    }
    

    测试线束

    static void Main(string[] args)
    {
        Random r = new Random();
    
        Stopwatch[] time = new[] { new Stopwatch(), new Stopwatch(), new Stopwatch(), new Stopwatch() };
    
        double[][] results = new[] { new double[Iterations], new double[Iterations], new double[Iterations], new double[Iterations] };
    
        for (var i = 0; i < Iterations; ++i)
        {
            double[] weights = new double[r.Next(MinimumWeights, MaximumWeights)];
            for (var w = 0; w < weights.Length; ++w)
            {
                weights[w] = (r.NextDouble() * (MaximumWeight - MinimumWeight)) + MinimumWeight;
            }
            var amount = r.Next(MinimumAmount, MaximumAmount);
    
            var totalWeight = weights.Sum();
            var expected = weights.Select(w => (w / totalWeight) * amount).ToArray();
    
            Action<int, DistributeDelgate> runTest = (resultIndex, func) =>
                {
                    time[resultIndex].Start();
                    var result = func(weights, amount).ToArray();
                    time[resultIndex].Stop();
    
                    var total = result.Sum();
    
                    if (total != amount)
                        throw new Exception("Invalid total");
    
                    var diff = expected.Zip(result, (e, a) => Math.Abs(e - a)).Sum() / amount;
    
                    results[resultIndex][i] = diff;
                };
    
            runTest(0, Distribute1);
            runTest(1, Distribute2);
            runTest(2, Distribute3);
            runTest(3, Distribute4);
        }
    }
    

答案 2 :(得分:2)

您遇到的问题是定义“可接受的”舍入策略是什么,或者换句话说,您要尝试最小化的是什么。首先考虑这种情况:列表中只有2个相同的项目,并且正在尝试分配3个单元。理想情况下,您希望为每个项目分配相同的金额(1.5),但这显然不会发生。你能做的“最好的”可能是分配1和2,或2和1.所以

  • 每个分配可能有多个解决方案
  • 相同的项目可能无法获得相同的分配

然后,我选择1和2而不是0和3因为我认为你想要的是最小化完美分配和整数分配之间的差异。这可能不是你认为“一个好的分配”,这是一个你需要考虑的问题:什么会使分配比另一个好?? 一个可能的值函数可以是最小化“总误差”,即分配与“完美”,无约束分配之间差异的绝对值之和。
听起来,Branch and Bound启发的东西可以发挥作用,但这并不重要 假设Dav解决方案总是产生满足约束条件的分配(我信任的情况就是这种情况),我认为不能保证给你“最佳”解决方案,“最好”由任何距离/拟合度量定义你最终采用。我的理由是这是一个贪婪的算法,在整数编程中,问题会导致你找到真正偏离最优解的解决方案。但是,如果你可以忍受“有点正确”的分配,那么我说,去吧! “最佳”地做到这一点听起来并不简单 祝你好运!

答案 3 :(得分:2)

确定。我很确定原始算法(如编写的)和发布的代码(如编写的)并不完全回答@Mathias概述的测试用例的邮件。

我对此算法的预期用途是稍微更具体的应用程序。而不是使用原始问题中显示的(@amt / @SumAmt)计算%。我有一个固定的$金额,需要根据为每个项目定义的%拆分进行拆分或分散。拆分%总和为100%,但是,直接乘法通常会导致小数(当被强制舍入到整数$时)不会累加到我分开的总量。这是问题的核心。

我很确定@Dav的原始答案在(如@Mathias描述的)多个切片的舍入值相等的情况下不起作用。原始算法和代码的这个问题可以用一个测试用例来概括:

花费100美元并将其分为3种方式,使用33.333333%作为百分比。

使用@jtw发布的代码(假设这是原始算法的准确实现),给你一个错误的答案:为每个项目分配33美元(导致总计99美元),所以它没有通过测试。

我认为更准确的算法可能是:

  • 运行总计从0开始
  • 对于论坛中的每件商品:
  • 将未舍入的分配金额计算为( [Amount to be Split] * [% to Split] )
  • 将累计余数计算为[Remainder] + ( [UnRounded Amount] - [Rounded Amount] )
  • 如果Round( [Remainder], 0 ) > 1 ,当前项目是列表中的最后一项,则设置项目的分配= [Rounded Amount] + Round( [Remainder], 0 )
  • 其他设置项目的分配= [Rounded Amount]
  • 重复下一项

在T-SQL中实现,它看起来像这样:

-- Start of Code --
Drop Table #SplitList
Create Table #SplitList ( idno int , pctsplit decimal(5, 4), amt int , roundedAmt int )

-- Test Case #1
--Insert Into #SplitList Values (1, 0.3333, 100, 0)
--Insert Into #SplitList Values (2, 0.3333, 100, 0)
--Insert Into #SplitList Values (3, 0.3333, 100, 0)

-- Test Case #2
--Insert Into #SplitList Values (1, 0.20, 57, 0)
--Insert Into #SplitList Values (2, 0.20, 57, 0)
--Insert Into #SplitList Values (3, 0.20, 57, 0)
--Insert Into #SplitList Values (4, 0.20, 57, 0)
--Insert Into #SplitList Values (5, 0.20, 57, 0)

-- Test Case #3
--Insert Into #SplitList Values (1, 0.43, 10, 0)
--Insert Into #SplitList Values (2, 0.22, 10, 0)
--Insert Into #SplitList Values (3, 0.11, 10, 0)
--Insert Into #SplitList Values (4, 0.24, 10, 0)

-- Test Case #4
Insert Into #SplitList Values (1, 0.50, 75, 0)
Insert Into #SplitList Values (2, 0.50, 75, 0)

Declare @R Float
Declare @Results Float
Declare @unroundedAmt Float
Declare @idno Int
Declare @roundedAmt Int
Declare @amt Float
Declare @pctsplit Float
declare @rowCnt int

Select @R = 0
select @rowCnt = 0

-- Define the cursor 
Declare SplitList Cursor For 
Select idno, pctsplit, amt, roundedAmt From #SplitList Order By amt Desc
-- Open the cursor
Open SplitList

-- Assign the values of the first record
Fetch Next From SplitList Into @idno, @pctsplit, @amt, @roundedAmt
-- Loop through the records
While @@FETCH_STATUS = 0

Begin
    -- Get derived Amounts from cursor
    select @unroundedAmt = ( @amt * @pctsplit )
    select @roundedAmt = Round( @unroundedAmt, 0 )

    -- Remainder
    Select @R = @R + @unroundedAmt - @roundedAmt
    select @rowCnt = @rowCnt + 1

    -- Magic Happens!  (aka Secret Sauce)
    if ( round(@R, 0 ) >= 1 ) or ( @@CURSOR_ROWS = @rowCnt ) Begin
        select @Results = @roundedAmt + round( @R, 0 )
        select @R = @R - round( @R, 0 )
    End
    else Begin
        Select @Results = @roundedAmt
    End

    If Round(@Results, 0) <> 0
    Begin
        Update #SplitList Set roundedAmt = @Results Where idno = @idno
    End

    -- Assign the values of the next record
    Fetch Next From SplitList Into @idno, @pctsplit, @amt, @roundedAmt
End

-- Close the cursor
Close SplitList
Deallocate SplitList

-- Now do the check
Select * From #SplitList
Select Sum(roundedAmt), max( amt ), 
case when max(amt) <> sum(roundedamt) then 'ERROR' else 'OK' end as Test 
From #SplitList

-- End of Code --

为测试用例生成最终结果集:

idno   pctsplit   amt     roundedAmt
1      0.3333    100     33
2      0.3333    100     34
3      0.3333    100     33

尽可能接近(我在代码中有几个测试用例),这可以非常优雅地处理所有这些情况。

答案 4 :(得分:1)

这是apportionment问题,有许多已知方法。所有人都有一定的病态:阿拉巴马州悖论,人口悖论,或配额规则的失败。 (Balinski和Young证明没有任何方法可以避免这三种方法。)你可能想要一个遵循引用规则并避免阿拉巴马悖论的方法;人口悖论并不是一个问题,因为不同年份之间每月的天数差别不大。

答案 5 :(得分:0)