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

4
这是为了处理股票数据而设计的,数据格式如下:
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个,则前1000个按10元/个的价格购买,然后下一个200个按每个15元的价格购买等等。

我已经在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以使用数据库迭代器,因此我想避免扫描整个数据并在计算答案时停止迭代。 数据已经按价格在数据库中排序。


1
你能发一下你的迭代代码吗?也许没有迭代是不可能的。而且别忘了,在幕后,LINQ 也会在需要时使用迭代 ;) - Youp Bernoulli
1
您的示例数据不清晰,请求的数量100->10与样本数据{10, 1000}不匹配。 - Maslow
1
我已经更新了示例,使其可以编译,但是你能否请发布这个内容:“我已经用C#实现了它”? - Rufus L
@Thomas,你说“是的,我需要100个,这被1000个以10美元的价格提供的数量所覆盖”,但它也被“可用”数量500->以15美元的价格覆盖,你会选择最低价吗? - mshwf
@Johnny 5,我之前稍微更新了一下问题,在底部进行了澄清。 - Thomas
显示剩余10条评论
4个回答

3

这可能是您可以得到的最 Linqy 的内容。它使用了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
    );
}

更新: 我将返回类型从double改为decimal,因为 decimal是货币值的首选类型。

顺便说一下,如果这个函数被频繁地使用相同的数据,并且数据列表很大,可以通过在List<(long, decimal)>中存储累积摘要,并应用BinarySearch来快速查找所需的条目进行优化。然而,它变得更加复杂,我不指望这种优化的先决条件经常出现。


SQL数据库是否支持使用“aggregate”函数? - Ammar
@Ammar 我不知道。但我猜这样复杂的逻辑在SQL中表达起来会很困难。 - Theodor Zoulias

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

1
那种方法行不通,因为需要对数据进行两次扫描;我正在尝试使用LINQ和数据库迭代器,只拉取所需的数量(我们谈论的是大量数据快速变化)。 - Thomas
@Thomas在你的例子中很明显你在从列表中获取数据,如果你想使用数据库迭代器,你应该在最初的问题中展示它。 - johnny 5
@johnny5:这就是目标,我想从DB迭代器中提取数据,以免拉取整个数据。C#代码只是为了说明算法。 - Thomas
@Thomas - 这仍然是对数据的单次遍历。你为什么认为它是两次? - Enigmativity
再次查看后,我认为你是正确的:我担心你需要一次通行来使可枚举和一次通行来进行计算,但结果是使用可枚举计算的,所以是的,你是正确的,应该只需一次通行。 - Thomas

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,并在每个“求和迭代”中减去列表中每个项目的数量。最后除以原始请求的数量。


0

我认为在使用LINQ时,最好的做法是尽量减少在服务器上进行的运行总计算,并在客户端上计算大部分内容,但要尽量减少从服务器下载的数据量。

我假设items已经被投影到了两个最小列(PriceAvailability)。如果没有,可以在从数据库中提取数据到orderedItems之前添加一个Select

// 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;

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