使用LINQ计算加权平均值

25

我的目标是从一个表中获取加权平均值,基于另一个表的主键。

示例数据:

表1

Key     WEIGHTED_AVERAGE

0200    0

表格2

ForeignKey    Length    Value
0200          105       52
0200          105       60
0200          105       54
0200          105       -1
0200          47        55

我需要根据片段长度计算加权平均值,并忽略-1的值。我知道如何在SQL中实现,但我的目标是使用LINQ来实现。在SQL中,它看起来像这样:

SELECT Sum(t2.Value*t2.Length)/Sum(t2.Length) AS WEIGHTED_AVERAGE
FROM Table1 t1, Table2 t2
WHERE t2.Value <> -1
AND t2.ForeignKey = t1.Key;

我对LINQ还不是很熟悉,很难找出如何进行翻译。希望得到的结果加权平均值大致为55.3。谢谢。

3个回答

68
这是一个用于LINQ的扩展方法。
public static double WeightedAverage<T>(this IEnumerable<T> records, Func<T, double> value, Func<T, double> weight)
{
    if(records == null)
        throw new ArgumentNullException(nameof(records), $"{nameof(records)} is null.");

    int count = 0;
    double valueSum = 0;
    double weightSum = 0;

    foreach (var record in records)
    {
        count++;
        double recordWeight = weight(record);

        valueSum += value(record) * recordWeight;
        weightSum += recordWeight;
    }

    if (count == 0)
        throw new ArgumentException($"{nameof(records)} is empty.");

    if (count == 1)
        return value(records.Single());

    if (weightSum != 0)
        return valueSum / weightSum;
    else
        throw new DivideByZeroException($"Division of {valueSum} by zero.");
}

这非常有用,因为我可以根据同一记录中的另一个字段获取任何数据组的加权平均值。

更新

现在我会检查是否存在除以0的情况,并抛出更详细的异常,而不是返回0。这允许用户捕获异常并根据需要处理。


1
谢谢,非常有帮助。我最终将其变成了一行代码... public static float WeightedAverage<T>(this IEnumerable<T> items, Func<T, float> value, Func<T, float> weight) { return items.Sum(item => value(item) * weight(item)) / items.Sum(weight); } - josefresno
2
我不得不在计算后添加"If weightedSum.AlmostZero() return 0"来防止当所有权重和/或所有值都为零时发生除以零的情况。AlmostZero是一个扩展函数,用于检查double是否为零。 - derdo
请注意,这会枚举两次记录。在许多应用程序中,这是可以接受的,但如果它们从数据库流式传输或以其他方式不可能或昂贵地重新枚举,它将会出错。个人而言,我将“records”键入为IReadOnlyCollection<T>而不是IEnumerable<T>,以向调用者指示他们有责任处理此问题(例如,必要时首先调用.ToList())。 - solublefish

4

如果您确定Table2中的每个外键都有对应的Table1记录,那么您可以通过进行分组来避免连接操作。

在这种情况下,LINQ查询如下所示:

IEnumerable<int> wheighted_averages =
    from record in Table2
    where record.PCR != -1
    group record by record.ForeignKey into bucket
    select bucket.Sum(record => record.PCR * record.Length) / 
        bucket.Sum(record => record.Length);

更新

以下是获取特定foreign_keywheighted_average的方法。

IEnumerable<Record> records =
    (from record in Table2
    where record.ForeignKey == foreign_key
    where record.PCR != -1
    select record).ToList();
int wheighted_average = records.Sum(record => record.PCR * record.Length) /
    records.Sum(record => record.Length);

在获取记录时调用的ToList方法是为了在两个单独的Sum操作中聚合记录时避免执行查询两次。


这将为每个不同的ForeignKey返回一个值。如果您只想要特定的一个ForeignKey的加权平均值,则可以避免使用GroupBy,并仅筛选具有所需外键的记录,然后执行聚合操作。我会编辑我的答案向您展示如何实现。 - Fede

2

(回答上面答案中jsmith的评论)

如果您不希望循环遍历某个集合,可以尝试以下方法:

var filteredList = Table2.Where(x => x.PCR != -1)
 .Join(Table1, x => x.ForeignKey, y => y.Key, (x, y) => new { x.PCR, x.Length });

int weightedAvg = filteredList.Sum(x => x.PCR * x.Length) 
    / filteredList.Sum(x => x.Length);

只是让你知道,我的解决方案假设你想计算加权平均值,其基于外键与第一张表中任何行的键值匹配的一组行。Fede的解决方案将为您获取特定外键的行。因此,请随意选择更合适的解决方案。 - Jimmy W

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