如何使用C#/ LINQ计算加权平均值

时间:2019-06-19 16:58:36

标签: c# linq

这是为了处理库存数据;数据采用以下格式:

public class A
{
    public int Price;
    public int Available;
}

以以下数据为例:

var items = new List<A>
{
    new A { Price = 10, Available = 1000 },
    new A { Price = 15, Available = 500 },
    new A { Price = 20, Available = 2000 },
};

我的查询返回特定数量的平均价格,例如:

  • 如果我请求的数量为100,我的平均价格为10

  • 如果我的需求量为1200,则我以10的价格获取第一个1000,然后以15的价格获取下一个200 等等

我已经在C#中实现了这一点,但是我试图找出是否可以直接使用LINQ与数据库迭代器一起完成。

我得到的价格已经按价格排序,但是我不知道如何解决这个问题。


编辑:

这是代码:

public static double PriceAtVolume(IEnumerable<A> Data, long Volume)
{
    var PriceSum = 0.0;
    var VolumeSum = 0L;

    foreach (var D in Data)
    {
        if (D.Volume < Volume)
        {
            PriceSum += D.Price * D.Volume;
            VolumeSum += D.Volume;
            Volume -= D.Volume;
        }
        else
        {
            PriceSum += D.Price * Volume;
            VolumeSum += Volume;
            Volume = 0;
        }

        if (Volume == 0) break;
    }

    return PriceSum / VolumeSum;
}

和测试代码:

var a = new List<A>
{
    new A { Price = 10, Volume = 1000 },
    new A { Price = 15, Volume = 500 },
    new A { Price = 20, Volume = 2000 }
};

var P0 = PriceAtVolume(a, 100);
var P1 = PriceAtVolume(a, 1200);

说明:

上面我说过我想将其移至LINQ以使用数据库迭代器,因此我想避免扫描整个数据并在计算出答案时停止迭代。数据已经在数据库中按价格排序了。

4 个答案:

答案 0 :(得分:2)

这可能是您可以获得的最多的Linqy。它使用Aggregate方法,特别是Aggregate的三个重载版本中最复杂的一种,它接受三个参数。第一个参数是种子,并使用置零的ValueTuple<long, decimal>进行初始化。第二个参数是累加器函数,具有将种子和当前元素组合为新种子的逻辑。第三个参数采用最终的累积值并将其投影到理想的平均值。

public static decimal PriceAtVolume(IEnumerable<A> data, long requestedVolume)
{
    return data.Aggregate(
        (Volume: 0L, Price: 0M), // Seed
        (sum, item) => // Accumulator function
        {
            if (sum.Volume == requestedVolume)
                return sum; // Goal reached, quick return

            if (item.Available < requestedVolume - sum.Volume)
                return // Consume all of it
                (
                    sum.Volume + item.Available,
                    sum.Price + item.Price * item.Available
                );

            return // Consume part of it (and we are done)
            (
                requestedVolume,
                sum.Price + item.Price * (requestedVolume - sum.Volume)
            );
        },
        sum => sum.Volume == 0M ? 0M : sum.Price / sum.Volume // Result selector
    );
}

更新:我将返回类型从双精度更改为十进制,因为decimal is the preferred type for currency values

如果此函数经常用相同的数据调用,并且数据列表很大,则可以通过将累积的摘要存储在List<(long, decimal)>中并应用BinarySearch来对其进行优化。快速找到所需的条目。但是,它变得很复杂,我不希望优化的前提条件会经常出现。

答案 1 :(得分:0)

您可以做一些事情以按顺序生成商品价格。例如

public class A
{
    public int Price;
    public int Available;
    public IEnumerable<int> Inv => Enumerable.Repeat(Price, Available);
}

var avg1 = items.SelectMany(i => i.Inv).Take(100).Average(); // 10
var avg2 = items.SelectMany(i => i.Inv).Take(1200).Average(); // 10.8333333333333

答案 2 :(得分:0)

这也很好(尽管不是单线的):

private static decimal CalculateWeighedAverage(List<A> amountsAndPrices, int requestedVolume)
{
    int originalRequestedVolume = requestedVolume;

    return (decimal)amountsAndPrices.Sum(amountAndPrice =>
    {
        int partialResult = Math.Min(amountAndPrice.Available, requestedVolume) * amountAndPrice.Price;

        requestedVolume = Math.Max(requestedVolume - amountAndPrice.Available, 0);

        return partialResult;
    }) / originalRequestedVolume;
}

只要请求的数量大于0,就获取价格*的总和,并减去每次“求和迭代”中列表中每个项目的数量。最后除以原始请求的体积。

答案 3 :(得分:0)

我认为,使用LINQ最好的办法是最大程度地减少服务器上运行的总计算量,并在客户端上进行大部分计算,但是最小化从服务器上下载的量。

我假设items已被投影到两个最小列(PriceAvailability)。如果没有,可以先添加Select,然后再将数据从数据库中拉到orderedItems

// find price of last item needed; worst case there won't be one
var lastPriceItem = items.Select(i => new { i.Price, RT = items.Where(it => it.Price <= i.Price).Sum(it => it.Available) }).FirstOrDefault(irt => irt.RT > origReqVol);

// bring over items below that price
var orderedItems = items.OrderBy(i => i.Price).Where(i => i.Price <= lastPriceItem.Price).ToList();
// compute running total on client
var rtItems = orderedItems.Select(i => new {
    Item = i,
    RT = orderedItems.Where(i2 => i2.Price <= i.Price).Sum(i2 => i2.Available)
});

// computer average price
var reqVol = origReqVol;
var ans = rtItems.Select(irt => new { Price = irt.Item.Price, Quantity = Math.Min((reqVol -= irt.Item.Available)+irt.Item.Available, irt.Item.Available) })
                     .Sum(pq => pq.Price * pq.Quantity) / (double)origReqVol;