【问题标题】:Proportionately distribute (prorate) a value across a set of values在一组值之间按比例分配(按比例分配)一个值
【发布时间】:2010-12-27 21:30:32
【问题描述】:

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

proratedValue = (basis / basisTotal) * prorationAmount;

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

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

【问题讨论】:

    标签: c# math


    【解决方案1】:

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

    1. 有一个从零开始的运行总计。
    2. 对第一项执行您的标准“除以总基数,然后乘以比例金额”。
    3. 将运行总计的原始值存储在其他位置,然后添加您刚刚在 #2 中计算的金额。
    4. 将累计值的旧值和新值四舍五入为整数(不要修改现有值,将它们四舍五入为单独的变量),然后取其差。
    5. 第 4 步中计算的数字是分配给当前基的值。
    6. 对每个基础重复步骤 #2-5。

    这保证了按比例分配的总金额等于输入的按比例分配金额,因为您从未实际修改运行总计本身(您只取其四舍五入的值用于其他计算,您不会将它们写回)。现在解决了之前整数舍入的问题,因为舍入误差会随着时间的推移在运行总计中累加,并最终将一个值推向另一个方向的舍入阈值。

    基本示例:

    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
    

    【讨论】:

    • 很好的答案!简单可靠:) 我证明了一些数学性质,例如它保证总和并且满足“配额规则”。你知道算法的数学背景吗?我对公平等其他属性很好奇
    • 输入基础:[300,300,300] 总比例:899 它给我的结果是 299+300+299=898,你能即兴发挥吗?即使我被困在这个:(
    【解决方案2】:

    TL;DR 算法具有最佳 (+20%) 可能的准确度,但速度降低了 70%。

    在接受的答案 hereanswer 中对类似性质的 python 问题提出的评估算法。

    测试结果(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. 最大误差增加权重,直到实际分配的数量等于预期数量

    为了准确度而牺牲速度,通过循环不止一次。

    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);
        }
    }
    

    【讨论】:

    • 您如何定义准确性?你的“Avg Abs Diff”是什么?您的准确度是否定义为项目的理想分配(重量[i] * 总计)和实际分配(四舍五入)之间的绝对差异?
    【解决方案3】:

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

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

    然后,我选择 1 和 2 而不是 0 和 3,因为我假设您想要最小化完美分配和整数分配之间的差异。这可能不是您认为的“良好分配”,这是您需要考虑的问题:什么会使分配比另一个更好?
    一种可能的价值函数可能是最小化“总误差”,即您的分配与“完美”、无约束分配之间差异的绝对值之和。
    在我看来,受Branch and Bound 启发的东西可能会奏效,但这并非微不足道。
    假设 Dav 解决方案总是产生一个满足约束的分配(我相信是这种情况),我假设它不能保证给你“最好”的解决方案,“最好的”由你定义的任何距离/适合度量最终采用。我这样做的原因是,这是一个贪心算法,在整数规划问题中,它可以引导你找到真正偏离最优解的解决方案。但是,如果您可以接受“有点正确”的分配,那么我说,去吧!以“最佳方式”进行操作听起来并不简单。
    祝你好运!

    【讨论】:

    • 你说得对,我描述的算法并不总是能产生“最佳”解决方案,例如,最小化“理想”小数值和分配的整数值之间的差异。但是,它保证不会超过分配给每个基的小数值的 +/- 1,这可能是您能以有效方式做的最好的事情。
    • 显然有 2 个人不喜欢我的回答,以至于投了反对票;我很想知道为什么!
    • 我实际上并没有完全理解您概述的问题。但是,当我尝试从 OP 实现算法时,我遇到了同样的问题。我正在尝试拆分(例如,将 100 分成 3 等份)。其中一个必须是 34,另外两个必须是 33。我几乎可以肯定原始算法(至少在上面的 T-SQL 中实现)不能处理这个问题。修改原件,我将撤消我的反对票。
    【解决方案4】:

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

    我对这个算法的预期用途是稍微更具体的应用。而不是使用(@amt / @SumAmt) 计算百分比,如原始问题所示。我有一个固定的 $ 金额,需要根据为每个项目定义的百分比拆分来拆分或分布在多个项目中。拆分百分比总和为 100%,但是,直接乘法通常会导致小数(当强制四舍五入为整数时)不加起来我拆分的总金额。这是问题的核心。

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

    取出 100 美元,以 33.333333% 作为百分比,将其分成 3 种方式。

    使用@jtw 发布的代码(假设这是原始算法的准确实现),会产生为每个项目分配 33 美元的错误答案(导致总和为 99 美元),因此它未通过测试。

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

    • 有一个从 0 开始的运行总计
    • 对于组中的每个项目:
    • 计算未四舍五入的分配量为( [Amount to be Split] * [% to Split] )
    • 计算累积余数为[Remainder] + ( [UnRounded Amount] - [Rounded Amount] )
    • 如果Round( [Remainder], 0 ) &gt; 1 OR当前项是列表中的最后一项,则设置该项的分配=[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
    

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

        【讨论】:

        • 该链接只不过是对问题的(非常简单的)重述。问题是如果分配的内容不是无限可分的,如何进行比例分配。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-02-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多